diff --git a/docs/flags.md b/docs/flags.md index 44b0d0b575..f0d21376ea 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -66,6 +66,7 @@ | `--aws-profile=` | When using the AWS provider, name of the profile to use | | `--aws-assume-role=""` | When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional) | | `--aws-assume-role-external-id=""` | When using the AWS API and assuming a role then specify this external ID` (optional) | +| `--[no-]aws-skip-tls-verify` | When using TLS with the aws provider, disable verification of any TLS certificates | | `--aws-batch-change-size=1000` | When using the AWS provider, set the maximum number of changes that will be applied in each batch. | | `--aws-batch-change-size-bytes=32000` | When using the AWS provider, set the maximum byte size that will be applied in each batch. | | `--aws-batch-change-size-values=1000` | When using the AWS provider, set the maximum total record values that will be applied in each batch. | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 9e8e740eaa..5ce4af6b27 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -88,6 +88,7 @@ type Config struct { AWSZoneTagFilter []string AWSAssumeRole string AWSProfiles []string + AWSSkipTLSVerify bool AWSAssumeRoleExternalID string `secure:"yes"` AWSBatchChangeSize int AWSBatchChangeSizeBytes int @@ -493,6 +494,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("aws-profile", "When using the AWS provider, name of the profile to use").Default("").StringsVar(&cfg.AWSProfiles) app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole) app.Flag("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID) + app.Flag("aws-skip-tls-verify", "When using TLS with the aws provider, disable verification of any TLS certificates").BoolVar(&cfg.AWSSkipTLSVerify) app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize) app.Flag("aws-batch-change-size-bytes", "When using the AWS provider, set the maximum byte size that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSizeBytes)).IntVar(&cfg.AWSBatchChangeSizeBytes) app.Flag("aws-batch-change-size-values", "When using the AWS provider, set the maximum total record values that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSizeValues)).IntVar(&cfg.AWSBatchChangeSizeValues) diff --git a/pkg/tlsutils/tlsconfig.go b/pkg/tlsutils/tlsconfig.go index 5275fad135..06346d2894 100644 --- a/pkg/tlsutils/tlsconfig.go +++ b/pkg/tlsutils/tlsconfig.go @@ -47,7 +47,7 @@ func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, m if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" { return nil, errors.New("either both cert and key or none must be provided") } - var certificates []tls.Certificate + certificates := make([]tls.Certificate, 0) if certPath != "" { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { diff --git a/provider/aws/config.go b/provider/aws/config.go index 5908150e77..aa692c4037 100644 --- a/provider/aws/config.go +++ b/provider/aws/config.go @@ -18,6 +18,7 @@ package aws import ( "context" + "crypto/tls" "fmt" "net/http" "strings" @@ -31,6 +32,7 @@ import ( "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/pkg/apis/externaldns" + "sigs.k8s.io/external-dns/pkg/tlsutils" ) // AWSSessionConfig contains configuration to create a new AWS provider. @@ -39,6 +41,10 @@ type AWSSessionConfig struct { AssumeRoleExternalID string APIRetries int Profile string + TLSCAFilePath string + TLSCertPath string + TLSCertKeyPath string + TLSSkipVerify bool } func CreateDefaultV2Config(cfg *externaldns.Config) awsv2.Config { @@ -47,6 +53,10 @@ func CreateDefaultV2Config(cfg *externaldns.Config) awsv2.Config { AssumeRole: cfg.AWSAssumeRole, AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, APIRetries: cfg.AWSAPIRetries, + TLSCertPath: cfg.TLSClientCert, + TLSCertKeyPath: cfg.TLSClientCertKey, + TLSCAFilePath: cfg.TLSCA, + TLSSkipVerify: cfg.AWSSkipTLSVerify, }, ) if err != nil { @@ -68,6 +78,10 @@ func CreateV2Configs(cfg *externaldns.Config) map[string]awsv2.Config { AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, APIRetries: cfg.AWSAPIRetries, Profile: profile, + TLSCertPath: cfg.TLSClientCert, + TLSCertKeyPath: cfg.TLSClientCertKey, + TLSCAFilePath: cfg.TLSCA, + TLSSkipVerify: cfg.AWSSkipTLSVerify, }, ) if err != nil { @@ -80,11 +94,25 @@ func CreateV2Configs(cfg *externaldns.Config) map[string]awsv2.Config { } func newV2Config(awsConfig AWSSessionConfig) (awsv2.Config, error) { + tlsConfig, err := tlsutils.NewTLSConfig(awsConfig.TLSCertPath, awsConfig.TLSCertKeyPath, awsConfig.TLSCAFilePath, "", awsConfig.TLSSkipVerify, + tls.VersionTLS13) + + if err != nil { + return awsv2.Config{}, fmt.Errorf("instantiating TLS config: %w", err) + } + + httpClient := &http.Client{} + if tlsConfig != nil { + httpClient.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + } + defaultOpts := []func(*config.LoadOptions) error{ config.WithRetryer(func() awsv2.Retryer { return retry.AddWithMaxAttempts(retry.NewStandard(), awsConfig.APIRetries) }), - config.WithHTTPClient(instrumented_http.NewClient(&http.Client{}, &instrumented_http.Callbacks{ + config.WithHTTPClient(instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{ PathProcessor: func(path string) string { parts := strings.Split(path, "/") return parts[len(parts)-1] diff --git a/provider/aws/config_test.go b/provider/aws/config_test.go index 00b3b46aac..309d67950b 100644 --- a/provider/aws/config_test.go +++ b/provider/aws/config_test.go @@ -17,59 +17,191 @@ limitations under the License. package aws import ( - "context" - "os" - "testing" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "os" + "testing" + "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_newV2Config(t *testing.T) { - t.Run("should use profile from credentials file", func(t *testing.T) { - // setup - credsFile, err := prepareCredentialsFile(t) - defer os.Remove(credsFile.Name()) - require.NoError(t, err) - os.Setenv("AWS_SHARED_CREDENTIALS_FILE", credsFile.Name()) - defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") - - // when - cfg, err := newV2Config(AWSSessionConfig{Profile: "profile2"}) - require.NoError(t, err) - creds, err := cfg.Credentials.Retrieve(context.Background()) - - // then - assert.NoError(t, err) - assert.Equal(t, "AKID2345", creds.AccessKeyID) - assert.Equal(t, "SECRET2", creds.SecretAccessKey) - }) - - t.Run("should respect env variables without profile", func(t *testing.T) { - // setup - os.Setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") - os.Setenv("AWS_SECRET_ACCESS_KEY", "topsecret") - defer os.Unsetenv("AWS_ACCESS_KEY_ID") - defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") - - // when - cfg, err := newV2Config(AWSSessionConfig{}) - require.NoError(t, err) - creds, err := cfg.Credentials.Retrieve(context.Background()) - - // then - assert.NoError(t, err) - assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", creds.AccessKeyID) - assert.Equal(t, "topsecret", creds.SecretAccessKey) - }) + t.Run("should use profile from credentials file", func(t *testing.T) { + // setup + credsFile, err := prepareCredentialsFile(t) + defer os.Remove(credsFile.Name()) + require.NoError(t, err) + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", credsFile.Name()) + defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") + + // when + cfg, err := newV2Config(AWSSessionConfig{Profile: "profile2"}) + require.NoError(t, err) + creds, err := cfg.Credentials.Retrieve(context.Background()) + + // then + assert.NoError(t, err) + assert.Equal(t, "AKID2345", creds.AccessKeyID) + assert.Equal(t, "SECRET2", creds.SecretAccessKey) + }) + + t.Run("should respect env variables without profile", func(t *testing.T) { + // setup + os.Setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") + os.Setenv("AWS_SECRET_ACCESS_KEY", "topsecret") + defer os.Unsetenv("AWS_ACCESS_KEY_ID") + defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") + + // when + cfg, err := newV2Config(AWSSessionConfig{}) + require.NoError(t, err) + creds, err := cfg.Credentials.Retrieve(context.Background()) + + // then + assert.NoError(t, err) + assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", creds.AccessKeyID) + assert.Equal(t, "topsecret", creds.SecretAccessKey) + }) + + t.Run("should use tls config", func(t *testing.T) { + os.Setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") + os.Setenv("AWS_SECRET_ACCESS_KEY", "topsecret") + defer os.Unsetenv("AWS_ACCESS_KEY_ID") + defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") + + certPEMMap, err := generateSelfSignedCert(t) + fmt.Println(certPEMMap) + require.NoError(t, err) + + cfg, err := newV2Config(AWSSessionConfig{ + TLSCertPath: certPEMMap["clientCertPath"], + TLSCertKeyPath: certPEMMap["clientKeyPath"], + TLSCAFilePath: certPEMMap["rootCAPath"], + TLSSkipVerify: false, + }) + require.NoError(t, err) + creds, err := cfg.Credentials.Retrieve(context.Background()) + assert.NoError(t, err) + assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", creds.AccessKeyID) + assert.Equal(t, "topsecret", creds.SecretAccessKey) + }) } func prepareCredentialsFile(t *testing.T) (*os.File, error) { - credsFile, err := os.CreateTemp("", "aws-*.creds") - require.NoError(t, err) - _, err = credsFile.WriteString("[profile1]\naws_access_key_id=AKID1234\naws_secret_access_key=SECRET1\n\n[profile2]\naws_access_key_id=AKID2345\naws_secret_access_key=SECRET2\n") - require.NoError(t, err) - err = credsFile.Close() - require.NoError(t, err) - return credsFile, err + credsFile, err := os.CreateTemp("", "aws-*.creds") + require.NoError(t, err) + _, err = credsFile.WriteString("[profile1]\naws_access_key_id=AKID1234\naws_secret_access_key=SECRET1\n\n[profile2]\naws_access_key_id=AKID2345\naws_secret_access_key=SECRET2\n") + require.NoError(t, err) + err = credsFile.Close() + require.NoError(t, err) + return credsFile, err +} + +func certTemplate(keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, isCA bool) (x509.Certificate, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return x509.Certificate{}, err + } + + return x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: isCA, + BasicConstraintsValid: true, + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + Subject: pkix.Name{ + CommonName: "Test CA", + Organization: []string{"Test Org"}, + }, + }, nil +} + +func generateSelfSignedCert(t *testing.T) (map[string]string, error) { + t.Helper() + rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + rootCertTemplate, err := certTemplate(x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature|x509.KeyUsageCertSign, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, true) + if err != nil { + return nil, err + } + + rootCertDER, err := x509.CreateCertificate(rand.Reader, &rootCertTemplate, &rootCertTemplate, &rootKey.PublicKey, rootKey) + if err != nil { + return nil, err + } + + rootCert, err := x509.ParseCertificate(rootCertDER) + if err != nil { + return nil, err + } + + certTemplate, err := certTemplate(x509.KeyUsageDigitalSignature, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, false) + if err != nil { + return nil, err + } + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + certDER, err := x509.CreateCertificate(rand.Reader, &certTemplate, rootCert, &certKey.PublicKey, rootKey) + if err != nil { + return nil, err + } + + rootCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCertDER}) + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + keyPKCS1, err := x509.MarshalECPrivateKey(certKey) + if err != nil { + return nil, err + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyPKCS1}) + + rootCAPath := fileToTempLocation(t, "rootCA*.pem", rootCertPEM) + clientCertPath := fileToTempLocation(t, "clientCert*.pem", certPEM) + clientKeyPath := fileToTempLocation(t, "clientKey*.pem", keyPEM) + + return map[string]string{ + "rootCAPath": rootCAPath, + "clientCertPath": clientCertPath, + "clientKeyPath": clientKeyPath, + }, nil +} + +func fileToTempLocation(t *testing.T, pattern string, data []byte) string { + t.Helper() + tmpFile, err := os.CreateTemp("", pattern) + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + + defer func(tmpFile *os.File) { + err := tmpFile.Close() + if err != nil { + t.Fatalf("failed to close temp file: %v", err) + } + }(tmpFile) + + if _, err := tmpFile.Write(data); err != nil { + t.Fatalf("failed to write data to temp file: %v", err) + } + + return tmpFile.Name() }