diff --git a/main.go b/main.go index 5c3bac76e7..0a8ad9d391 100644 --- a/main.go +++ b/main.go @@ -365,7 +365,15 @@ func main() { p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) } case "rfc2136": - p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, nil) + tlsConfig := rfc2136.TLSConfig{ + UseTLS: cfg.RFC2136UseTls, + SkipTLSVerify: cfg.RFC2136SkipTLSVerify, + CAFilePath: cfg.TLSCA, + ClientCertFilePath: cfg.TLSClientCert, + ClientCertKeyFilePath: cfg.TLSClientCertKey, + ServerName: "", + } + p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, tlsConfig, nil) case "ns1": p, err = ns1.NewNS1Provider( ns1.NS1Config{ diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 029224092a..aae0dcfa39 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -187,6 +187,8 @@ type Config struct { RFC2136TAXFR bool RFC2136MinTTL time.Duration RFC2136BatchChangeSize int + RFC2136UseTls bool + RFC2136SkipTLSVerify bool NS1Endpoint string NS1IgnoreSSL bool NS1MinTTLSeconds int @@ -341,6 +343,8 @@ var defaultConfig = &Config{ RFC2136TAXFR: true, RFC2136MinTTL: 0, RFC2136BatchChangeSize: 50, + RFC2136UseTls: false, + RFC2136SkipTLSVerify: false, NS1Endpoint: "", NS1IgnoreSSL: false, TransIPAccountName: "", @@ -569,6 +573,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("rfc2136-kerberos-password", "When using the RFC2136 provider with GSS-TSIG, specify the password of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosPassword).StringVar(&cfg.RFC2136KerberosPassword) app.Flag("rfc2136-kerberos-realm", "When using the RFC2136 provider with GSS-TSIG, specify the realm of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosRealm).StringVar(&cfg.RFC2136KerberosRealm) app.Flag("rfc2136-batch-change-size", "When using the RFC2136 provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.RFC2136BatchChangeSize)).IntVar(&cfg.RFC2136BatchChangeSize) + app.Flag("rfc2136-use-tls", "When using the RFC2136 provider, communicate with name server over tls").BoolVar(&cfg.RFC2136UseTls) + app.Flag("rfc2136-skip-tls-verify", "When using TLS with the RFC2136 provider, disable verification of any TLS certificates").BoolVar(&cfg.RFC2136SkipTLSVerify) // Flags related to TransIP provider app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName) diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index 27da01c69b..0a197f19d8 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -18,6 +18,7 @@ package rfc2136 import ( "context" + "crypto/tls" "fmt" "net" "strconv" @@ -32,6 +33,7 @@ import ( log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/pkg/tlsutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) @@ -53,6 +55,7 @@ type rfc2136Provider struct { axfr bool minTTL time.Duration batchChangeSize int + tlsConfig TLSConfig // options specific to rfc3645 gss-tsig support gssTsig bool @@ -66,6 +69,16 @@ type rfc2136Provider struct { actions rfc2136Actions } +// TLSConfig is comprised of the TLS-related fields necessary if we are using DNS over TLS +type TLSConfig struct { + UseTLS bool + SkipTLSVerify bool + CAFilePath string + ClientCertFilePath string + ClientCertKeyFilePath string + ServerName string +} + // Map of supported TSIG algorithms var tsigAlgs = map[string]string{ "hmac-sha1": dns.HmacSHA1, @@ -81,7 +94,7 @@ type rfc2136Actions interface { } // NewRfc2136Provider is a factory function for OpenStack rfc2136 providers -func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, gssTsig bool, krb5Username string, krb5Password string, krb5Realm string, batchChangeSize int, actions rfc2136Actions) (provider.Provider, error) { +func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, gssTsig bool, krb5Username string, krb5Password string, krb5Realm string, batchChangeSize int, tlsConfig TLSConfig, actions rfc2136Actions) (provider.Provider, error) { secretAlgChecked, ok := tsigAlgs[secretAlg] if !ok && !insecure && !gssTsig { return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg) @@ -91,6 +104,10 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k krb5Realm = strings.ToUpper(zoneName) } + if tlsConfig.UseTLS { + tlsConfig.ServerName = host + } + r := &rfc2136Provider{ nameserver: net.JoinHostPort(host, strconv.Itoa(port)), zoneName: dns.Fqdn(zoneName), @@ -104,6 +121,7 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k axfr: axfr, minTTL: minTTL, batchChangeSize: batchChangeSize, + tlsConfig: tlsConfig, } if actions != nil { r.actions = actions @@ -200,6 +218,15 @@ func (r rfc2136Provider) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Env t.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret} } + c, err := makeClient(r) + if err != nil { + return nil, fmt.Errorf("error setting up TLS: %v", err) + } + conn, err := c.Dial(a) + if err != nil { + return nil, fmt.Errorf("failed to connect for transfer: %v", err) + } + t.Conn = conn return t.In(m, r.nameserver) } @@ -385,7 +412,10 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error { } log.Debugf("SendMessage") - c := new(dns.Client) + c, err := makeClient(r) + if err != nil { + return fmt.Errorf("error setting up TLS: %v", err) + } if !r.insecure { if r.gssTsig { @@ -405,8 +435,6 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error { } } - c.Net = "tcp" - resp, _, err := c.Exchange(msg, r.nameserver) if err != nil { if resp != nil && resp.Rcode != dns.RcodeSuccess { @@ -439,3 +467,33 @@ func chunkBy(slice []*endpoint.Endpoint, chunkSize int) [][]*endpoint.Endpoint { return chunks } + +func makeClient(r rfc2136Provider) (result *dns.Client, err error) { + c := new(dns.Client) + + if r.tlsConfig.UseTLS { + log.Debug("RFC2136 Connecting via TLS") + c.Net = "tcp-tls" + tlsConfig, err := tlsutils.NewTLSConfig( + r.tlsConfig.ClientCertFilePath, + r.tlsConfig.ClientCertKeyFilePath, + r.tlsConfig.CAFilePath, + r.tlsConfig.ServerName, + r.tlsConfig.SkipTLSVerify, + // Per RFC9103 + tls.VersionTLS13, + ) + if err != nil { + return nil, err + } + if tlsConfig.NextProtos == nil { + // Per RFC9103 + tlsConfig.NextProtos = []string{"dot"} + } + c.TLSConfig = tlsConfig + } else { + c.Net = "tcp" + } + + return c, nil +} diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go index 38220e042f..6fef352068 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -18,7 +18,9 @@ package rfc2136 import ( "context" + "crypto/tls" "fmt" + "os" "strings" "testing" "time" @@ -95,7 +97,18 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo } func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) { - return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, stub) + tlsConfig := TLSConfig{ + UseTLS: false, + SkipTLSVerify: false, + CAFilePath: "", + ClientCertFilePath: "", + ClientCertKeyFilePath: "", + } + return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub) +} + +func createRfc2136TLSStubProvider(stub *rfc2136Stub, tlsConfig TLSConfig) (provider.Provider, error) { + return NewRfc2136Provider("rfc2136-host", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub) } func extractAuthoritySectionFromMessage(msg fmt.Stringer) []string { @@ -130,6 +143,157 @@ func TestRfc2136GetRecordsMultipleTargets(t *testing.T) { assert.Equal(t, 0, len(recs[0].ProviderSpecific), "expected no provider specific config") } +func TestRfc2136TLSConfig(t *testing.T) { + stub := newStub() + + caFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX.crt") + assert.NoError(t, err) + defer os.Remove(caFile.Name()) + _, err = caFile.Write([]byte( + `-----BEGIN CERTIFICATE----- +MIH+MIGxAhR2n1aQk0ONrQ8QQfa6GCzFWLmTXTAFBgMrZXAwITELMAkGA1UEBhMC +REUxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMzEwMjQwNzI5NDNaGA8yMTIzMDkz +MDA3Mjk0M1owITELMAkGA1UEBhMCREUxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUG +AytlcAMhAA1FzGJXuQdOpKv02SEl7SIA8SP8RVRI0QTi1bUFiFBLMAUGAytlcANB +ADiCKRUGDMyafSSYhl0KXoiXrFOxvhrGM5l15L4q82JM5Qb8wv0gNrnbGTZlInuv +ouB5ZN+05DzKCQhBekMnygQ= +-----END CERTIFICATE----- +`)) + + tlsConfig := TLSConfig{ + UseTLS: true, + SkipTLSVerify: false, + CAFilePath: caFile.Name(), + ClientCertFilePath: "", + ClientCertKeyFilePath: "", + } + + provider, err := createRfc2136TLSStubProvider(stub, tlsConfig) + assert.NoError(t, err) + + rawProvider := provider.(*rfc2136Provider) + + client, err := makeClient(*rawProvider) + assert.NoError(t, err) + + assert.Equal(t, "tcp-tls", client.Net) + assert.Equal(t, false, client.TLSConfig.InsecureSkipVerify) + assert.Equal(t, "rfc2136-host", client.TLSConfig.ServerName) + assert.Equal(t, uint16(tls.VersionTLS13), client.TLSConfig.MinVersion) + assert.Equal(t, []string{"dot"}, client.TLSConfig.NextProtos) +} + +func TestRfc2136TLSConfigNoVerify(t *testing.T) { + stub := newStub() + + caFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX.crt") + assert.NoError(t, err) + defer os.Remove(caFile.Name()) + _, err = caFile.Write([]byte( + `-----BEGIN CERTIFICATE----- +MIH+MIGxAhR2n1aQk0ONrQ8QQfa6GCzFWLmTXTAFBgMrZXAwITELMAkGA1UEBhMC +REUxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMzEwMjQwNzI5NDNaGA8yMTIzMDkz +MDA3Mjk0M1owITELMAkGA1UEBhMCREUxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUG +AytlcAMhAA1FzGJXuQdOpKv02SEl7SIA8SP8RVRI0QTi1bUFiFBLMAUGAytlcANB +ADiCKRUGDMyafSSYhl0KXoiXrFOxvhrGM5l15L4q82JM5Qb8wv0gNrnbGTZlInuv +ouB5ZN+05DzKCQhBekMnygQ= +-----END CERTIFICATE----- +`)) + + tlsConfig := TLSConfig{ + UseTLS: true, + SkipTLSVerify: true, + CAFilePath: caFile.Name(), + ClientCertFilePath: "", + ClientCertKeyFilePath: "", + } + + provider, err := createRfc2136TLSStubProvider(stub, tlsConfig) + assert.NoError(t, err) + + rawProvider := provider.(*rfc2136Provider) + + client, err := makeClient(*rawProvider) + assert.NoError(t, err) + + assert.Equal(t, "tcp-tls", client.Net) + assert.Equal(t, true, client.TLSConfig.InsecureSkipVerify) + assert.Equal(t, "rfc2136-host", client.TLSConfig.ServerName) + assert.Equal(t, uint16(tls.VersionTLS13), client.TLSConfig.MinVersion) + assert.Equal(t, []string{"dot"}, client.TLSConfig.NextProtos) +} + +func TestRfc2136TLSConfigClientAuth(t *testing.T) { + stub := newStub() + + caFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX.crt") + assert.NoError(t, err) + defer os.Remove(caFile.Name()) + _, err = caFile.Write([]byte( + `-----BEGIN CERTIFICATE----- +MIH+MIGxAhR2n1aQk0ONrQ8QQfa6GCzFWLmTXTAFBgMrZXAwITELMAkGA1UEBhMC +REUxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMzEwMjQwNzI5NDNaGA8yMTIzMDkz +MDA3Mjk0M1owITELMAkGA1UEBhMCREUxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUG +AytlcAMhAA1FzGJXuQdOpKv02SEl7SIA8SP8RVRI0QTi1bUFiFBLMAUGAytlcANB +ADiCKRUGDMyafSSYhl0KXoiXrFOxvhrGM5l15L4q82JM5Qb8wv0gNrnbGTZlInuv +ouB5ZN+05DzKCQhBekMnygQ= +-----END CERTIFICATE----- +`)) + + certFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX-client.crt") + assert.NoError(t, err) + defer os.Remove(certFile.Name()) + _, err = certFile.Write([]byte( + `-----BEGIN CERTIFICATE----- +MIIBfDCCAQICFANNDjPVDMTPm63C0jZ9M3H5I7GJMAoGCCqGSM49BAMCMCExCzAJ +BgNVBAYTAkRFMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjMxMDI0MDcyMTU1WhgP +MjEyMzA5MzAwNzIxNTVaMCExCzAJBgNVBAYTAkRFMRIwEAYDVQQDDAlsb2NhbGhv +c3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQj7rjkeUEvjBT++IBMnIWgmI9VIjFx +4VUGFmzPEawOckdnKW4fBdePiItsgePDVK4Oys5bzfSDhl6aAPCe16pwvljB7yIm +xLJ+ytWk7OV/s10cmlaczrEtNeUjV1X9MTMwCgYIKoZIzj0EAwIDaAAwZQIwcZl8 +TrwwsyX3A0enXB1ih+nruF8Q9f9Rmm2pNcbEv24QIW/P2HGQm9qfx4lrYa7hAjEA +goRP/fRfTTTLwLg8UBpUAmALX8A8HBSBaUlTTQcaImbcwU4DRSbv5JEA8tM1mWrA +-----END CERTIFICATE----- +`)) + + keyFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX-client.key") + assert.NoError(t, err) + defer os.Remove(keyFile.Name()) + _, err = keyFile.Write([]byte( + `-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD5B+aPE+TuHCvW1f7L +U8jEPVXHv1fvCR8uBSsf1qdPo929XGpt5y5QfIGdW3NUeHWhZANiAAQj7rjkeUEv +jBT++IBMnIWgmI9VIjFx4VUGFmzPEawOckdnKW4fBdePiItsgePDVK4Oys5bzfSD +hl6aAPCe16pwvljB7yImxLJ+ytWk7OV/s10cmlaczrEtNeUjV1X9MTM= +-----END PRIVATE KEY----- +`)) + + tlsConfig := TLSConfig{ + UseTLS: true, + SkipTLSVerify: false, + CAFilePath: caFile.Name(), + ClientCertFilePath: certFile.Name(), + ClientCertKeyFilePath: keyFile.Name(), + } + + provider, err := createRfc2136TLSStubProvider(stub, tlsConfig) + log.Infof("provider, err is: %s", err) + assert.NoError(t, err) + + rawProvider := provider.(*rfc2136Provider) + + client, err := makeClient(*rawProvider) + log.Infof("client, err is: %v", client) + log.Infof("client, err is: %s", err) + assert.NoError(t, err) + + assert.Equal(t, "tcp-tls", client.Net) + assert.Equal(t, false, client.TLSConfig.InsecureSkipVerify) + assert.Equal(t, "rfc2136-host", client.TLSConfig.ServerName) + assert.Equal(t, uint16(tls.VersionTLS13), client.TLSConfig.MinVersion) + assert.Equal(t, []string{"dot"}, client.TLSConfig.NextProtos) +} + func TestRfc2136GetRecords(t *testing.T) { stub := newStub() err := stub.setOutput([]string{