From 41111f78793f926ea670938a17928ab904a3077e Mon Sep 17 00:00:00 2001 From: splashx Date: Thu, 6 Sep 2018 16:34:50 +0200 Subject: [PATCH] patch with rfc2136 Signed-off-by: splashx --- .../output/reference/api-docs/index.html | 58 +++- .../output/reference/api-docs/navData.js | 2 +- docs/reference/issuers/acme/dns01.rst | 13 + pkg/apis/certmanager/v1alpha1/types_issuer.go | 28 +- .../v1alpha1/zz_generated.deepcopy.go | 26 ++ pkg/apis/certmanager/validation/issuer.go | 21 ++ .../certmanager/validation/issuer_test.go | 42 +++ pkg/issuer/acme/dns/dns.go | 26 ++ pkg/issuer/acme/dns/rfc2136/rfc2136.go | 188 ++++++++++++ pkg/issuer/acme/dns/rfc2136/rfc2136_test.go | 283 ++++++++++++++++++ pkg/issuer/acme/dns/util_test.go | 5 + 11 files changed, 689 insertions(+), 3 deletions(-) create mode 100644 pkg/issuer/acme/dns/rfc2136/rfc2136.go create mode 100644 pkg/issuer/acme/dns/rfc2136/rfc2136_test.go diff --git a/docs/generated/reference/output/reference/api-docs/index.html b/docs/generated/reference/output/reference/api-docs/index.html index c2526ed24..8b73fcc4a 100755 --- a/docs/generated/reference/output/reference/api-docs/index.html +++ b/docs/generated/reference/output/reference/api-docs/index.html @@ -11,7 +11,7 @@ - +

    cert-manager

    @@ -500,6 +500,10 @@ Appears In: +rfc2136
    ACMEIssuerDNS01ProviderRFC2136 + + + route53
    ACMEIssuerDNS01ProviderRoute53 @@ -744,6 +748,57 @@ Appears In: +

    ACMEIssuerDNS01ProviderRFC2136 v1alpha1

    + + + + + + + + + + + + + + + +
    GroupVersionKind
    certmanagerv1alpha1ACMEIssuerDNS01ProviderRFC2136
    +

    ACMEIssuerDNS01ProviderRFC2136 is a structure containing the configuration for RFC2136 DNS

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    nameserver
    string
    The IP address of the DNS supporting RFC2136. Required.
    tsigAlgorithm
    string
    The TSIG Algorithm configured in the DNS supporting RFC2136. Acceptable values are (case-insensitive): HMACMD5 (default), HMACSHA1, HMACSHA256 or HMACSHA512
    tsigKeyName
    string
    The TSIG Key name configured in the DNS. If tsigKeyName is not defined, tsigSecretSecretRef is ignored
    tsigSecretSecretRef
    SecretKeySelector
    The name of the secret containing the TSIG value. If tsigSecretSecretRef is not defined, tsigKey is ignored.

    ACMEIssuerDNS01ProviderRoute53 v1alpha1

    @@ -1618,6 +1673,7 @@ Appears In:
  • ACMEIssuerDNS01ProviderAzureDNS v1alpha1
  • ACMEIssuerDNS01ProviderCloudDNS v1alpha1
  • ACMEIssuerDNS01ProviderCloudflare v1alpha1
  • +
  • ACMEIssuerDNS01ProviderRFC2136 v1alpha1
  • ACMEIssuerDNS01ProviderRoute53 v1alpha1
  • VaultAppRole v1alpha1
  • VaultAuth v1alpha1
  • diff --git a/docs/generated/reference/output/reference/api-docs/navData.js b/docs/generated/reference/output/reference/api-docs/navData.js index 15ceef3fe..f857d64bc 100644 --- a/docs/generated/reference/output/reference/api-docs/navData.js +++ b/docs/generated/reference/output/reference/api-docs/navData.js @@ -1 +1 @@ -(function(){navData = {"toc":[{"section":"-strong-field-definitions-strong-","subsections":[{"section":"vaultissuer-v1alpha1"},{"section":"vaultauth-v1alpha1"},{"section":"vaultapprole-v1alpha1"},{"section":"time-v1"},{"section":"statusdetails-v1"},{"section":"statuscause-v1"},{"section":"status-v1"},{"section":"selfsignedissuer-v1alpha1"},{"section":"secretkeyselector-v1alpha1"},{"section":"ownerreference-v1"},{"section":"objectreference-v1alpha1"},{"section":"objectmeta-v1"},{"section":"listmeta-v1"},{"section":"issuercondition-v1alpha1"},{"section":"initializers-v1"},{"section":"initializer-v1"},{"section":"http01solverconfig-v1alpha1"},{"section":"domainsolverconfig-v1alpha1"},{"section":"dns01solverconfig-v1alpha1"},{"section":"certificatecondition-v1alpha1"},{"section":"certificateacmestatus-v1alpha1"},{"section":"caissuer-v1alpha1"},{"section":"acmeorderstatus-v1alpha1"},{"section":"acmeorderchallenge-v1alpha1"},{"section":"acmeissuerhttp01config-v1alpha1"},{"section":"acmeissuerdns01providerroute53-v1alpha1"},{"section":"acmeissuerdns01providercloudflare-v1alpha1"},{"section":"acmeissuerdns01providerclouddns-v1alpha1"},{"section":"acmeissuerdns01providerazuredns-v1alpha1"},{"section":"acmeissuerdns01providerakamai-v1alpha1"},{"section":"acmeissuerdns01provideracmedns-v1alpha1"},{"section":"acmeissuerdns01provider-v1alpha1"},{"section":"acmeissuerdns01config-v1alpha1"},{"section":"acmeissuer-v1alpha1"},{"section":"acmecertificateconfig-v1alpha1"}]},{"section":"-strong-old-api-versions-strong-","subsections":[]},{"section":"issuer-v1alpha1","subsections":[]},{"section":"clusterissuer-v1alpha1","subsections":[]},{"section":"certificate-v1alpha1","subsections":[]},{"section":"-strong-cert-manager-strong-","subsections":[]}],"flatToc":["vaultissuer-v1alpha1","vaultauth-v1alpha1","vaultapprole-v1alpha1","time-v1","statusdetails-v1","statuscause-v1","status-v1","selfsignedissuer-v1alpha1","secretkeyselector-v1alpha1","ownerreference-v1","objectreference-v1alpha1","objectmeta-v1","listmeta-v1","issuercondition-v1alpha1","initializers-v1","initializer-v1","http01solverconfig-v1alpha1","domainsolverconfig-v1alpha1","dns01solverconfig-v1alpha1","certificatecondition-v1alpha1","certificateacmestatus-v1alpha1","caissuer-v1alpha1","acmeorderstatus-v1alpha1","acmeorderchallenge-v1alpha1","acmeissuerhttp01config-v1alpha1","acmeissuerdns01providerroute53-v1alpha1","acmeissuerdns01providercloudflare-v1alpha1","acmeissuerdns01providerclouddns-v1alpha1","acmeissuerdns01providerazuredns-v1alpha1","acmeissuerdns01providerakamai-v1alpha1","acmeissuerdns01provideracmedns-v1alpha1","acmeissuerdns01provider-v1alpha1","acmeissuerdns01config-v1alpha1","acmeissuer-v1alpha1","acmecertificateconfig-v1alpha1","-strong-field-definitions-strong-","-strong-old-api-versions-strong-","issuer-v1alpha1","clusterissuer-v1alpha1","certificate-v1alpha1","-strong-cert-manager-strong-"]};})(); \ No newline at end of file +(function(){navData = {"toc":[{"section":"-strong-field-definitions-strong-","subsections":[{"section":"vaultissuer-v1alpha1"},{"section":"vaultauth-v1alpha1"},{"section":"vaultapprole-v1alpha1"},{"section":"time-v1"},{"section":"statusdetails-v1"},{"section":"statuscause-v1"},{"section":"status-v1"},{"section":"selfsignedissuer-v1alpha1"},{"section":"secretkeyselector-v1alpha1"},{"section":"ownerreference-v1"},{"section":"objectreference-v1alpha1"},{"section":"objectmeta-v1"},{"section":"listmeta-v1"},{"section":"issuercondition-v1alpha1"},{"section":"initializers-v1"},{"section":"initializer-v1"},{"section":"http01solverconfig-v1alpha1"},{"section":"domainsolverconfig-v1alpha1"},{"section":"dns01solverconfig-v1alpha1"},{"section":"certificatecondition-v1alpha1"},{"section":"certificateacmestatus-v1alpha1"},{"section":"caissuer-v1alpha1"},{"section":"acmeorderstatus-v1alpha1"},{"section":"acmeorderchallenge-v1alpha1"},{"section":"acmeissuerhttp01config-v1alpha1"},{"section":"acmeissuerdns01providerroute53-v1alpha1"},{"section":"acmeissuerdns01providerrfc2136-v1alpha1"},{"section":"acmeissuerdns01providercloudflare-v1alpha1"},{"section":"acmeissuerdns01providerclouddns-v1alpha1"},{"section":"acmeissuerdns01providerazuredns-v1alpha1"},{"section":"acmeissuerdns01providerakamai-v1alpha1"},{"section":"acmeissuerdns01provideracmedns-v1alpha1"},{"section":"acmeissuerdns01provider-v1alpha1"},{"section":"acmeissuerdns01config-v1alpha1"},{"section":"acmeissuer-v1alpha1"},{"section":"acmecertificateconfig-v1alpha1"}]},{"section":"-strong-old-api-versions-strong-","subsections":[]},{"section":"issuer-v1alpha1","subsections":[]},{"section":"clusterissuer-v1alpha1","subsections":[]},{"section":"certificate-v1alpha1","subsections":[]},{"section":"-strong-cert-manager-strong-","subsections":[]}],"flatToc":["vaultissuer-v1alpha1","vaultauth-v1alpha1","vaultapprole-v1alpha1","time-v1","statusdetails-v1","statuscause-v1","status-v1","selfsignedissuer-v1alpha1","secretkeyselector-v1alpha1","ownerreference-v1","objectreference-v1alpha1","objectmeta-v1","listmeta-v1","issuercondition-v1alpha1","initializers-v1","initializer-v1","http01solverconfig-v1alpha1","domainsolverconfig-v1alpha1","dns01solverconfig-v1alpha1","certificatecondition-v1alpha1","certificateacmestatus-v1alpha1","caissuer-v1alpha1","acmeorderstatus-v1alpha1","acmeorderchallenge-v1alpha1","acmeissuerhttp01config-v1alpha1","acmeissuerdns01providerroute53-v1alpha1","acmeissuerdns01providerrfc2136-v1alpha1","acmeissuerdns01providercloudflare-v1alpha1","acmeissuerdns01providerclouddns-v1alpha1","acmeissuerdns01providerazuredns-v1alpha1","acmeissuerdns01providerakamai-v1alpha1","acmeissuerdns01provideracmedns-v1alpha1","acmeissuerdns01provider-v1alpha1","acmeissuerdns01config-v1alpha1","acmeissuer-v1alpha1","acmecertificateconfig-v1alpha1","-strong-field-definitions-strong-","-strong-old-api-versions-strong-","issuer-v1alpha1","clusterissuer-v1alpha1","certificate-v1alpha1","-strong-cert-manager-strong-"]};})(); \ No newline at end of file diff --git a/docs/reference/issuers/acme/dns01.rst b/docs/reference/issuers/acme/dns01.rst index a05686df3..1c3ad5f41 100644 --- a/docs/reference/issuers/acme/dns01.rst +++ b/docs/reference/issuers/acme/dns01.rst @@ -142,6 +142,19 @@ Akamai FastDNS name: akamai-dns key: accessToken +RFC2136 +======== + +.. code-block:: yaml + + rfc2136: + nameserver: 192.168.0.1 + tsigKeyName: myzone-tsig + tsigAlgorithm: HMACMD5 + tsigSecretSecretRef: + name: my-secret + key: tsigkey + ACME-DNS ======== diff --git a/pkg/apis/certmanager/v1alpha1/types_issuer.go b/pkg/apis/certmanager/v1alpha1/types_issuer.go index e9503f976..1e3e5b4c1 100644 --- a/pkg/apis/certmanager/v1alpha1/types_issuer.go +++ b/pkg/apis/certmanager/v1alpha1/types_issuer.go @@ -16,7 +16,9 @@ limitations under the License. package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // +genclient // +genclient:nonNamespaced @@ -150,6 +152,7 @@ type ACMEIssuerDNS01Provider struct { Route53 *ACMEIssuerDNS01ProviderRoute53 `json:"route53,omitempty"` AzureDNS *ACMEIssuerDNS01ProviderAzureDNS `json:"azuredns,omitempty"` AcmeDNS *ACMEIssuerDNS01ProviderAcmeDNS `json:"acmedns,omitempty"` + RFC2136 *ACMEIssuerDNS01ProviderRFC2136 `json:"rfc2136,omitempty"` } // ACMEIssuerDNS01ProviderAkamai is a structure containing the DNS @@ -204,6 +207,29 @@ type ACMEIssuerDNS01ProviderAcmeDNS struct { AccountSecret SecretKeySelector `json:"accountSecretRef"` } +// ACMEIssuerDNS01ProviderRFC2136 is a structure containing the +// configuration for RFC2136 DNS +type ACMEIssuerDNS01ProviderRFC2136 struct { + // The IP address of the DNS supporting RFC2136. Required. + Nameserver string `json:"nameserver"` + + // The name of the secret containing the TSIG value. + // If ``tsigSecretSecretRef`` is not defined, ``tsigKey`` is ignored. + // +optional + TSIGSecret SecretKeySelector `json:"tsigSecretSecretRef"` + + // The TSIG Key name configured in the DNS. If ``tsigKeyName`` is not defined, + // ``tsigSecretSecretRef`` is ignored + // +optional + TSIGKeyName string `json:"tsigKeyName"` + + // The TSIG Algorithm configured in the DNS supporting RFC2136. Acceptable + // values are (case-insensitive): ``HMACMD5`` (default), ``HMACSHA1``, + // ``HMACSHA256`` or ``HMACSHA512`` + // +optional + TSIGAlgorithm string `json:"tsigAlgorithm"` +} + // IssuerStatus contains status information about an Issuer type IssuerStatus struct { Conditions []IssuerCondition `json:"conditions"` diff --git a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go index 8bd1786b8..01ad33bee 100644 --- a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go @@ -162,6 +162,15 @@ func (in *ACMEIssuerDNS01Provider) DeepCopyInto(out *ACMEIssuerDNS01Provider) { **out = **in } } + if in.RFC2136 != nil { + in, out := &in.RFC2136, &out.RFC2136 + if *in == nil { + *out = nil + } else { + *out = new(ACMEIssuerDNS01ProviderRFC2136) + **out = **in + } + } return } @@ -262,6 +271,23 @@ func (in *ACMEIssuerDNS01ProviderCloudflare) DeepCopy() *ACMEIssuerDNS01Provider return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ACMEIssuerDNS01ProviderRFC2136) DeepCopyInto(out *ACMEIssuerDNS01ProviderRFC2136) { + *out = *in + out.TSIGSecret = in.TSIGSecret + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ACMEIssuerDNS01ProviderRFC2136. +func (in *ACMEIssuerDNS01ProviderRFC2136) DeepCopy() *ACMEIssuerDNS01ProviderRFC2136 { + if in == nil { + return nil + } + out := new(ACMEIssuerDNS01ProviderRFC2136) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ACMEIssuerDNS01ProviderRoute53) DeepCopyInto(out *ACMEIssuerDNS01ProviderRoute53) { *out = *in diff --git a/pkg/apis/certmanager/validation/issuer.go b/pkg/apis/certmanager/validation/issuer.go index f252ef85c..5fb62c6a1 100644 --- a/pkg/apis/certmanager/validation/issuer.go +++ b/pkg/apis/certmanager/validation/issuer.go @@ -17,6 +17,10 @@ limitations under the License. package validation import ( + "strings" + + "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/rfc2136" + "k8s.io/apimachinery/pkg/util/validation/field" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" @@ -202,6 +206,23 @@ func ValidateACMEIssuerDNS01Config(iss *v1alpha1.ACMEIssuerDNS01Config, fldPath el = append(el, field.Required(fldPath.Child("acmedns", "host"), "")) } } + if p.RFC2136 != nil { + if numProviders > 0 { + el = append(el, field.Forbidden(fldPath.Child("rfc2136"), "may not specify more than one provider type")) + } else { + numProviders++ + // Nameserver is the only required field for RFC2136 + if len(p.RFC2136.Nameserver) == 0 { + el = append(el, field.Required(fldPath.Child("rfc2136", "nameserver"), "")) + } + if len(p.RFC2136.TSIGAlgorithm) > 0 { + _, ok := rfc2136.SupportedAlgorithms[strings.ToUpper(p.RFC2136.TSIGAlgorithm)] + if !ok { + el = append(el, field.Forbidden(fldPath.Child("rfc2136", "tsigSecretSecretRef"), "")) + } + } + } + } if numProviders == 0 { el = append(el, field.Required(fldPath, "at least one provider must be configured")) } diff --git a/pkg/apis/certmanager/validation/issuer_test.go b/pkg/apis/certmanager/validation/issuer_test.go index 3d17beaa1..b6ba497c8 100644 --- a/pkg/apis/certmanager/validation/issuer_test.go +++ b/pkg/apis/certmanager/validation/issuer_test.go @@ -403,6 +403,48 @@ func TestValidateACMEIssuerDNS01Config(t *testing.T) { }, errs: []*field.Error{}, }, + "valid rfc2136 config": { + cfg: &v1alpha1.ACMEIssuerDNS01Config{ + Providers: []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "a name", + RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{ + Nameserver: "127.0.0.1", + }, + }, + }, + }, + errs: []*field.Error{}, + }, + "missing rfc2136 required field": { + cfg: &v1alpha1.ACMEIssuerDNS01Config{ + Providers: []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "a name", + RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{}, + }, + }, + }, + errs: []*field.Error{ + field.Required(providersPath.Index(0).Child("rfc2136", "nameserver"), ""), + }, + }, + "rfc2136 provider using non-supported algorithm": { + cfg: &v1alpha1.ACMEIssuerDNS01Config{ + Providers: []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "a name", + RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{ + Nameserver: "127.0.0.1", + TSIGAlgorithm: "HAMMOCK", + }, + }, + }, + }, + errs: []*field.Error{ + field.Forbidden(providersPath.Index(0).Child("rfc2136", "tsigSecretSecretRef"), ""), + }, + }, "multiple providers configured": { cfg: &v1alpha1.ACMEIssuerDNS01Config{ Providers: []v1alpha1.ACMEIssuerDNS01Provider{ diff --git a/pkg/issuer/acme/dns/dns.go b/pkg/issuer/acme/dns/dns.go index 820173e00..a54e0e8cd 100644 --- a/pkg/issuer/acme/dns/dns.go +++ b/pkg/issuer/acme/dns/dns.go @@ -33,6 +33,7 @@ import ( "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/azuredns" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/clouddns" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/cloudflare" + "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/rfc2136" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/route53" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" ) @@ -56,6 +57,7 @@ type dnsProviderConstructors struct { route53 func(accessKey, secretKey, hostedZoneID, region string, ambient bool, dns01Nameservers []string) (*route53.DNSProvider, error) azureDNS func(clientID, clientSecret, subscriptionID, tenentID, resourceGroupName, hostedZoneName string, dns01Nameservers []string) (*azuredns.DNSProvider, error) acmeDNS func(host string, accountJson []byte, dns01Nameservers []string) (*acmedns.DNSProvider, error) + rfc2136 func(nameserver, tsigAlgorithm, tsigKeyName, tsigSecret string) (*rfc2136.DNSProvider, error) } // Solver is a solver for the acme dns01 challenge. @@ -265,6 +267,29 @@ func (s *Solver) solverForIssuerProvider(issuer v1alpha1.GenericIssuer, provider if err != nil { return nil, fmt.Errorf("error instantiating acmedns challenge solver: %s", err) } + case providerConfig.RFC2136 != nil: + var secret string + if len(providerConfig.RFC2136.TSIGSecret.Name) > 0 { + tsigSecret, err := s.secretLister.Secrets(resourceNamespace).Get(providerConfig.RFC2136.TSIGSecret.Name) + if err != nil { + return nil, fmt.Errorf("error getting rfc2136 service account: %s", err.Error()) + } + secretBytes, ok := tsigSecret.Data[providerConfig.RFC2136.TSIGSecret.Key] + if !ok { + return nil, fmt.Errorf("error getting rfc2136 secret key: key '%s' not found in secret", providerConfig.RFC2136.TSIGSecret.Key) + } + secret = string(secretBytes) + } + + impl, err = s.dnsProviderConstructors.rfc2136( + providerConfig.RFC2136.Nameserver, + string(providerConfig.RFC2136.TSIGAlgorithm), + providerConfig.RFC2136.TSIGKeyName, + secret, + ) + if err != nil { + return nil, fmt.Errorf("error instantiating rfc2136 challenge solver: %s", err.Error()) + } default: return nil, fmt.Errorf("no dns provider config specified for provider %q", providerName) } @@ -282,6 +307,7 @@ func NewSolver(ctx *controller.Context) *Solver { route53.NewDNSProvider, azuredns.NewDNSProviderCredentials, acmedns.NewDNSProviderHostBytes, + rfc2136.NewDNSProviderCredentials, }, } } diff --git a/pkg/issuer/acme/dns/rfc2136/rfc2136.go b/pkg/issuer/acme/dns/rfc2136/rfc2136.go new file mode 100644 index 000000000..3beb32cd0 --- /dev/null +++ b/pkg/issuer/acme/dns/rfc2136/rfc2136.go @@ -0,0 +1,188 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge +// using the rfc2136 dynamic update. +// This code was adapted from lego: +// https://github.com/xenolf/lego + +package rfc2136 + +import ( + "fmt" + "net" + "os" + "strings" + "time" + + "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" + "github.com/miekg/dns" +) + +// SupportedAlgorithms should refer to https://tools.ietf.org/html/rfc4635#section-2 +// but miekd/dns supports only the ones below +var SupportedAlgorithms = map[string]string{ + "HMACMD5": dns.HmacMD5, + "HMACSHA1": dns.HmacSHA1, + "HMACSHA256": dns.HmacSHA256, + "HMACSHA512": dns.HmacSHA512, +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface that +// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver. +type DNSProvider struct { + nameserver string + tsigAlgorithm string + tsigKeyName string + tsigSecret string +} + +// NewDNSProvider returns a DNSProvider instance configured for rfc2136 +// dynamic update. Configured with environment variables: +// RFC2136_NAMESERVER: Network address in the form "host" or "host:port". +// RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5). +// See https://github.com/miekg/dns/blob/master/tsig.go for supported values. +// RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration. +// RFC2136_TSIG_SECRET: Secret key payload. +// To disable TSIG authentication, leave the RFC2136_TSIG* variables unset. +func NewDNSProvider() (*DNSProvider, error) { + nameserver := os.Getenv("RFC2136_NAMESERVER") + tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM") + tsigKeyName := os.Getenv("RFC2136_TSIG_KEY_NAME") + tsigSecret := os.Getenv("RFC2136_TSIG_SECRET") + return NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKeyName, tsigSecret) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for rfc2136 dynamic update. To disable TSIG +// authentication, leave the TSIG parameters as empty strings. +// nameserver must be a network address in the form "IP" or "IP:port". +func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKeyName, tsigSecret string) (*DNSProvider, error) { + + if nameserver == "" { + return nil, fmt.Errorf("RFC2136 nameserver missing") + } + + // Append the default DNS port if none is specified. + if _, _, err := net.SplitHostPort(nameserver); err != nil { + if strings.Contains(err.Error(), "missing port") { + host := nameserver + if ipaddr := net.ParseIP(host); ipaddr != nil { + nameserver = net.JoinHostPort(host, "53") + } else { + return nil, fmt.Errorf("RFC2136 nameserver must be a valid IP Address, not %v", nameserver) + } + } else { + return nil, err + } + } + + d := &DNSProvider{ + nameserver: nameserver, + } + + if tsigAlgorithm == "" { + tsigAlgorithm = dns.HmacMD5 + } else { + if value, ok := SupportedAlgorithms[strings.ToUpper(tsigAlgorithm)]; ok { + tsigAlgorithm = value + } else { + return nil, fmt.Errorf("The algorithm '%v' is not supported", tsigAlgorithm) + + } + } + + d.tsigAlgorithm = tsigAlgorithm + + if len(tsigKeyName) > 0 && len(tsigSecret) > 0 { + d.tsigKeyName = tsigKeyName + d.tsigSecret = tsigSecret + } + + return d, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS +// propagation. 300s (5m) is usually a default time for TTL in DNS +func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { + return 300 * time.Second, 5 * time.Second +} + +// Present creates a TXT record using the specified parameters +func (r *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl, err := util.DNS01Record(domain, keyAuth, strings.Fields(r.nameserver)) + if err != nil { + return err + } + return r.changeRecord("INSERT", fqdn, value, ttl) +} + +// CleanUp removes the TXT record matching the specified parameters +func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value, ttl, err := util.DNS01Record(domain, keyAuth, strings.Fields(r.nameserver)) + if err != nil { + return err + } + return r.changeRecord("REMOVE", fqdn, value, ttl) +} + +func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { + // Find the zone for the given fqdn + zone, err := util.FindZoneByFqdn(fqdn, []string{r.nameserver}) + if err != nil { + return err + } + + // Create RR + rr := new(dns.TXT) + rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)} + rr.Txt = []string{value} + rrs := []dns.RR{rr} + + // Create dynamic update packet + m := new(dns.Msg) + m.SetUpdate(zone) + switch action { + case "INSERT": + // Always remove old challenge left over from who knows what. + m.RemoveRRset(rrs) + m.Insert(rrs) + case "REMOVE": + m.Remove(rrs) + default: + return fmt.Errorf("Unexpected action: %s", action) + } + + // Setup client + c := new(dns.Client) + c.SingleInflight = true + // TSIG authentication / msg signing + if len(r.tsigKeyName) > 0 && len(r.tsigSecret) > 0 { + m.SetTsig(dns.Fqdn(r.tsigKeyName), r.tsigAlgorithm, 300, time.Now().Unix()) + c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKeyName): r.tsigSecret} + } + + // Send the query + reply, _, err := c.Exchange(m, r.nameserver) + if err != nil { + return fmt.Errorf("DNS update failed: %v", err) + } + if reply != nil && reply.Rcode != dns.RcodeSuccess { + return fmt.Errorf("DNS update failed. Server replied: %s", dns.RcodeToString[reply.Rcode]) + } + + return nil +} diff --git a/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go b/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go new file mode 100644 index 000000000..611ff412a --- /dev/null +++ b/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go @@ -0,0 +1,283 @@ +/* +Copyright 2018 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge +// using the rfc2136 dynamic update. +// This code was adapted from lego: +// https://github.com/xenolf/lego + +package rfc2136 + +import ( + "fmt" + "net" + "strings" + "sync" + "testing" + "time" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +var ( + rfc2136TestDomain = "123456789.www.example.com" + rfc2136TestKeyAuth = "123d==" + rfc2136TestValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" + rfc2136TestFqdn = "_acme-challenge.123456789.www.example.com." + rfc2136TestZone = "example.com." + rfc2136TestTsigKeyName = "example.com." + rfc2136TestTTL = 60 + rfc2136TestTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA==" +) + +var reqChan = make(chan *dns.Msg, 10) + +func TestRFC2136CanaryLocalTestServer(t *testing.T) { + dns.HandleFunc("example.com.", serverHandlerHello) + defer dns.HandleRemove("example.com.") + + server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) + if err != nil { + t.Fatalf("Failed to start test server: %v", err) + } + defer server.Shutdown() + + c := new(dns.Client) + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeTXT) + r, _, err := c.Exchange(m, addrstr) + if err != nil || len(r.Extra) == 0 { + t.Fatalf("Failed to communicate with test server: %v", err) + } + txt := r.Extra[0].(*dns.TXT).Txt[0] + if txt != "Hello world" { + t.Error("Expected test server to return 'Hello world' but got: ", txt) + } +} + +func TestRFC2136ServerSuccess(t *testing.T) { + dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) + defer dns.HandleRemove(rfc2136TestZone) + + server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) + + if err != nil { + t.Fatalf("Failed to start test server: %v", err) + } + defer server.Shutdown() + + provider, err := NewDNSProviderCredentials(addrstr, "", "", "") + if err != nil { + t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) + } + if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil { + t.Errorf("Expected Present() to return no error but the error was -> %v", err) + } +} + +func TestRFC2136ServerError(t *testing.T) { + dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr) + defer dns.HandleRemove(rfc2136TestZone) + + server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) + if err != nil { + t.Fatalf("Failed to start test server: %v", err) + } + defer server.Shutdown() + + provider, err := NewDNSProviderCredentials(addrstr, "", "", "") + if err != nil { + t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) + } + if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err == nil { + t.Errorf("Expected Present() to return an error but it did not.") + } else if !strings.Contains(err.Error(), "NOTZONE") { + t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string but it did not.") + } +} + +func TestRFC2136TsigClient(t *testing.T) { + dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) + defer dns.HandleRemove(rfc2136TestZone) + + server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", true) + if err != nil { + t.Fatalf("Failed to start test server: %v", err) + } + defer server.Shutdown() + + provider, err := NewDNSProviderCredentials(addrstr, "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) + if err != nil { + t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) + } + if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil { + t.Errorf("Expected Present() to return no error but the error was -> %v", err) + } +} + +func TestRFC2136InvalidNameserver(t *testing.T) { + _, err := NewDNSProviderCredentials("dns01.example.org", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) + assert.Error(t, err) +} + +func TestRFC2136DefaultTSIGAlgorithm(t *testing.T) { + provider, err := NewDNSProviderCredentials("127.0.0.1:0", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) + if err != nil { + assert.Equal(t, provider.tsigAlgorithm, dns.HmacMD5, "Default TSIG must match") + } +} + +func TestRFC2136InvalidTSIGAlgorithm(t *testing.T) { + _, err := NewDNSProviderCredentials("127.0.0.1:0", "HAMMOCK", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) + assert.Error(t, err) +} + +func TestRFC2136NamserverWithoutPort(t *testing.T) { + _, err := NewDNSProviderCredentials("127.0.0.1", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) + assert.NoError(t, err) +} + +func TestRFC2136ValidUpdatePacket(t *testing.T) { + dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest) + defer dns.HandleRemove(rfc2136TestZone) + + server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) + if err != nil { + t.Fatalf("Failed to start test server: %v", err) + } + defer server.Shutdown() + + txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", rfc2136TestFqdn, rfc2136TestTTL, rfc2136TestValue)) + rrs := []dns.RR{txtRR} + m := new(dns.Msg) + m.SetUpdate(rfc2136TestZone) + m.RemoveRRset(rrs) + m.Insert(rrs) + //expectstr := m.String() + //expect, err := m.Pack() + if err != nil { + t.Fatalf("Error packing expect msg: %v", err) + } + + provider, err := NewDNSProviderCredentials(addrstr, "", "", "") + if err != nil { + t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) + } + + if err := provider.Present(rfc2136TestDomain, "", rfc2136TestValue); err != nil { + t.Errorf("Expected Present() to return no error but the error was -> %v", err) + } + + assert.NoError(t, err) + //rcvMsg := <-reqChan + //rcvMsg.Id = m.Id + //actual, err := rcvMsg.Pack() + //if err != nil { + // t.Fatalf("Error packing actual msg: %v", err) + //} + + //if !bytes.Equal(actual, expect) { + // tmp := new(dns.Msg) + // if err := tmp.Unpack(actual); err != nil { + // t.Fatalf("Error unpacking actual msg: %v", err) + // } + // t.Errorf("Expected msg:\n%s", expectstr) + // t.Errorf("Actual msg:\n%v", tmp) + //} +} + +func runLocalDNSTestServer(listenAddr string, tsig bool) (*dns.Server, string, error) { + pc, err := net.ListenPacket("udp", listenAddr) + if err != nil { + return nil, "", err + } + server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour} + if tsig { + server.TsigSecret = map[string]string{rfc2136TestTsigKeyName: rfc2136TestTsigSecret} + } + + waitLock := sync.Mutex{} + waitLock.Lock() + server.NotifyStartedFunc = waitLock.Unlock + + go func() { + server.ActivateAndServe() + pc.Close() + }() + + waitLock.Lock() + return server, pc.LocalAddr().String(), nil +} + +func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + m.Extra = make([]dns.RR, 1) + m.Extra[0] = &dns.TXT{ + Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, + Txt: []string{"Hello world"}, + } + w.WriteMsg(m) +} + +func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone)) + m.Answer = []dns.RR{soaRR} + } + + if t := req.IsTsig(); t != nil { + if w.TsigStatus() == nil { + // Validated + m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix()) + } + } + + w.WriteMsg(m) +} + +func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetRcode(req, dns.RcodeNotZone) + w.WriteMsg(m) +} + +func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone)) + m.Answer = []dns.RR{soaRR} + } + + if t := req.IsTsig(); t != nil { + if w.TsigStatus() == nil { + // Validated + m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix()) + } + } + + w.WriteMsg(m) + if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET { + // Only talk back when it is not the SOA RR. + reqChan <- req + } +} diff --git a/pkg/issuer/acme/dns/util_test.go b/pkg/issuer/acme/dns/util_test.go index bb24980df..a5bd8fe68 100644 --- a/pkg/issuer/acme/dns/util_test.go +++ b/pkg/issuer/acme/dns/util_test.go @@ -28,6 +28,7 @@ import ( "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/azuredns" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/clouddns" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/cloudflare" + "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/rfc2136" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/route53" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" ) @@ -159,6 +160,10 @@ func newFakeDNSProviders() *fakeDNSProviders { f.call("acmedns", host, accountJson, dns01Nameservers) return nil, nil }, + rfc2136: func(nameserver, tsigAlgorithm, tsigKeyName, tsigSecret string) (*rfc2136.DNSProvider, error) { + f.call("rfc2136", nameserver, tsigAlgorithm, tsigKeyName, tsigSecret) + return nil, nil + }, } return f }