Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws): add tls ca provision #5097

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type Config struct {
AWSZoneTagFilter []string
AWSAssumeRole string
AWSProfiles []string
AWSSkipTLSVerify bool
AWSAssumeRoleExternalID string `secure:"yes"`
AWSBatchChangeSize int
AWSBatchChangeSizeBytes int
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/tlsutils/tlsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 29 additions & 1 deletion provider/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package aws

import (
"context"
"crypto/tls"
"fmt"
"net/http"
"strings"
Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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,
hjoshi123 marked this conversation as resolved.
Show resolved Hide resolved
tls.VersionTLS13)

if err != nil {
return awsv2.Config{}, fmt.Errorf("instantiating TLS config: %w", err)
}

httpClient := &http.Client{}
if tlsConfig != nil {
hjoshi123 marked this conversation as resolved.
Show resolved Hide resolved
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]
Expand Down
228 changes: 180 additions & 48 deletions provider/aws/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,59 +17,191 @@ limitations under the License.
package aws

import (
"context"
"os"
"testing"
"context"
hjoshi123 marked this conversation as resolved.
Show resolved Hide resolved
"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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

rootCertDER, err := x509.CreateCertificate(rand.Reader, &rootCertTemplate, &rootCertTemplate, &rootKey.PublicKey, rootKey)
if err != nil {
return nil, err
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

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()
}
Loading