Merge pull request #661 from splashx/master

[ACME] Add RFC2136 DNS Provider (2nd attempt)
This commit is contained in:
jetstack-bot 2018-09-12 09:11:48 +01:00 committed by GitHub
commit feb589feb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 848 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@ -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-"]};})();
(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-"]};})();

View File

@ -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
========

View File

@ -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,31 @@ 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.
// Note: FQDN is not a valid value, only IP.
Nameserver string `json:"nameserver"`
// The name of the secret containing the TSIG value.
// If ``tsigKeyName`` is defined, this field is required.
// +optional
TSIGSecret SecretKeySelector `json:"tsigSecretSecretRef"`
// The TSIG Key name configured in the DNS.
// If ``tsigSecretSecretRef`` is defined, this field is required.
// +optional
TSIGKeyName string `json:"tsigKeyName"`
// The TSIG Algorithm configured in the DNS supporting RFC2136. Used only
// when ``tsigSecretSecretRef`` and ``tsigKeyName`` are defined.
// Supported 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"`

View File

@ -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

View File

@ -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,42 @@ 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"), ""))
} else {
if _, err := rfc2136.ValidNameserver(p.RFC2136.Nameserver); err != nil {
el = append(el, field.Invalid(fldPath.Child("rfc2136", "nameserver"), "", "Nameserver invalid. Check the documentation for details."))
}
}
if len(p.RFC2136.TSIGAlgorithm) > 0 {
present := false
for _, b := range rfc2136.GetSupportedAlgorithms() {
if b == strings.ToUpper(p.RFC2136.TSIGAlgorithm) {
present = true
}
}
if !present {
el = append(el, field.NotSupported(fldPath.Child("rfc2136", "tsigAlgorithm"), "", rfc2136.GetSupportedAlgorithms()))
}
}
if len(p.RFC2136.TSIGKeyName) > 0 {
el = append(el, ValidateSecretKeySelector(&p.RFC2136.TSIGSecret, fldPath.Child("rfc2136", "tsigSecretSecretRef"))...)
}
if len(ValidateSecretKeySelector(&p.RFC2136.TSIGSecret, fldPath.Child("rfc2136", "tsigSecretSecretRef"))) == 0 {
if len(p.RFC2136.TSIGKeyName) <= 0 {
el = append(el, field.Required(fldPath.Child("rfc2136", "tsigKeyName"), ""))
}
}
}
}
if numProviders == 0 {
el = append(el, field.Required(fldPath, "at least one provider must be configured"))
}

View File

@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/rfc2136"
)
var (
@ -403,6 +404,110 @@ 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 invalid nameserver": {
cfg: &v1alpha1.ACMEIssuerDNS01Config{
Providers: []v1alpha1.ACMEIssuerDNS01Provider{
{
Name: "a name",
RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{
Nameserver: "dns.example.com",
},
},
},
},
errs: []*field.Error{
field.Invalid(providersPath.Index(0).Child("rfc2136", "nameserver"), "", "Nameserver invalid. Check the documentation for details."),
},
},
"rfc2136 provider using case-camel in algorithm": {
cfg: &v1alpha1.ACMEIssuerDNS01Config{
Providers: []v1alpha1.ACMEIssuerDNS01Provider{
{
Name: "a name",
RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{
Nameserver: "127.0.0.1",
TSIGAlgorithm: "HmAcMd5",
},
},
},
},
errs: []*field.Error{},
},
"rfc2136 provider using unsupported algorithm": {
cfg: &v1alpha1.ACMEIssuerDNS01Config{
Providers: []v1alpha1.ACMEIssuerDNS01Provider{
{
Name: "a name",
RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{
Nameserver: "127.0.0.1",
TSIGAlgorithm: "HAMMOCK",
},
},
},
},
errs: []*field.Error{
field.NotSupported(providersPath.Index(0).Child("rfc2136", "tsigAlgorithm"), "", rfc2136.GetSupportedAlgorithms()),
},
},
"rfc2136 provider TSIGKeyName provided but no TSIGSecret": {
cfg: &v1alpha1.ACMEIssuerDNS01Config{
Providers: []v1alpha1.ACMEIssuerDNS01Provider{
{
Name: "a name",
RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{
Nameserver: "127.0.0.1",
TSIGKeyName: "some-name",
},
},
},
},
errs: []*field.Error{
field.Required(providersPath.Index(0).Child("rfc2136", "tsigSecretSecretRef", "name"), "secret name is required"),
field.Required(providersPath.Index(0).Child("rfc2136", "tsigSecretSecretRef", "key"), "secret key is required"),
},
},
"rfc2136 provider TSIGSecret provided but no TSIGKeyName": {
cfg: &v1alpha1.ACMEIssuerDNS01Config{
Providers: []v1alpha1.ACMEIssuerDNS01Provider{
{
Name: "a name",
RFC2136: &v1alpha1.ACMEIssuerDNS01ProviderRFC2136{
Nameserver: "127.0.0.1",
TSIGSecret: validSecretKeyRef,
},
},
},
},
errs: []*field.Error{
field.Required(providersPath.Index(0).Child("rfc2136", "tsigKeyName"), ""),
},
},
"multiple providers configured": {
cfg: &v1alpha1.ACMEIssuerDNS01Config{
Providers: []v1alpha1.ACMEIssuerDNS01Provider{

View File

@ -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.
@ -280,6 +282,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)
}
@ -299,6 +324,7 @@ func NewSolver(ctx *controller.Context) *Solver {
route53.NewDNSProvider,
azuredns.NewDNSProviderCredentials,
acmedns.NewDNSProviderHostBytes,
rfc2136.NewDNSProviderCredentials,
},
}
}

View File

@ -0,0 +1,224 @@
/*
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"
"reflect"
"sort"
"strings"
"time"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
"github.com/miekg/dns"
)
var supportedAlgorithms = map[string]string{
"HMACMD5": dns.HmacMD5,
"HMACSHA1": dns.HmacSHA1,
"HMACSHA256": dns.HmacSHA256,
"HMACSHA512": dns.HmacSHA512,
}
// Returns a slice of all the supported algorithms
// It should contain all listed in https://tools.ietf.org/html/rfc4635#section-2
// but miekd/dns supports only supportedAlgorithms(keys)
func GetSupportedAlgorithms() []string {
keys := reflect.ValueOf(supportedAlgorithms).MapKeys()
strkeys := make([]string, len(keys))
for i := 0; i < len(keys); i++ {
strkeys[i] = keys[i].String()
}
sort.Strings(strkeys)
return strkeys
}
// This function make a valid nameserver as per RFC2136
func ValidNameserver(nameserver string) (string, error) {
if nameserver == "" {
return "", fmt.Errorf("RFC2136 nameserver missing")
}
// SplitHostPort Behavior
// namserver host port err
// 8.8.8.8 "" "" missing port in address
// 8.8.8.8: "8.8.8.8" "" <nil>
// 8.8.8.8.8:53 "8.8.8.8" 53 <nil>
// nameserver.com "" "" missing port in address
// nameserver.com: "nameserver.com" "" <nil>
// nameserver.com:53 "nameserver.com" 53 <nil>
// :53 "" 53 <nil>
host, port, err := net.SplitHostPort(nameserver)
if err != nil {
if strings.Contains(err.Error(), "missing port") {
host = nameserver
}
}
if port == "" {
port = "53"
}
if host != "" {
if ipaddr := net.ParseIP(host); ipaddr == nil {
return "", fmt.Errorf("RFC2136 nameserver must be a valid IP Address, not %v", host)
}
} else {
return "", fmt.Errorf("RFC2136 nameserver has no IP Address defined, %v", nameserver)
}
return nameserver, nil
}
// 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) {
d := &DNSProvider{}
if validNameserver, err := ValidNameserver(nameserver); err != nil {
return nil, err
} else {
d.nameserver = validNameserver
}
if len(tsigKeyName) > 0 && len(tsigSecret) > 0 {
d.tsigKeyName = tsigKeyName
d.tsigSecret = tsigSecret
}
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
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
}

View File

@ -0,0 +1,322 @@
/*
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 TestRFC2136InvalidNameserverFQDN(t *testing.T) {
_, err := NewDNSProviderCredentials("nameserver.com", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.Error(t, err)
}
func TestRFC2136InvalidNameserverFQDNWithPort(t *testing.T) {
_, err := NewDNSProviderCredentials("nameserver.com:53", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.Error(t, err)
}
func TestRFC2136InvalidNameserverFQDNWithPort2(t *testing.T) {
_, err := NewDNSProviderCredentials("nameserver.com:", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.Error(t, err)
}
func TestRFC2136NamserverWithoutPort(t *testing.T) {
_, err := NewDNSProviderCredentials("127.0.0.1", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.NoError(t, err)
}
func TestRFC2136NamserverWithoutPort2(t *testing.T) {
_, err := NewDNSProviderCredentials("127.0.0.1:", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.NoError(t, err)
}
func TestRFC2136NamserverWithPort(t *testing.T) {
_, err := NewDNSProviderCredentials("127.0.0.1:53", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.NoError(t, err)
}
func TestRFC2136NamserverWithPortNoIP(t *testing.T) {
_, err := NewDNSProviderCredentials(":53", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.Error(t, err)
}
func TestRFC2136NamserverEmpty(t *testing.T) {
_, err := NewDNSProviderCredentials("", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.Error(t, err)
}
func TestRFC2136NamserverIPInvalid(t *testing.T) {
_, err := NewDNSProviderCredentials("900.65.3.64", "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret)
assert.Error(t, err)
}
func TestRFC2136NamserverIPInvalid2(t *testing.T) {
_, err := NewDNSProviderCredentials(":", "", 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 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
}
}

View File

@ -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
}