diff --git a/go.mod b/go.mod index 8de50e9e9..43cf7b477 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,20 @@ require ( github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect github.com/DataDog/sketches-go v1.4.5 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect + github.com/aws/smithy-go v1.21.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect diff --git a/go.sum b/go.sum index 124e6f149..5839a06ea 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,42 @@ github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/Y github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= +github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= +github.com/aws/aws-sdk-go-v2/config v1.27.39 h1:FCylu78eTGzW1ynHcongXK9YHtoXD5AiiUqq3YfJYjU= +github.com/aws/aws-sdk-go-v2/config v1.27.39/go.mod h1:wczj2hbyskP4LjMKBEZwPRO1shXY+GsQleab+ZXT2ik= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= +github.com/aws/aws-sdk-go-v2/credentials v1.17.37 h1:G2aOH01yW8X373JK419THj5QVqu9vKEwxSEsGxihoW0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3 h1:W2M3kQSuN1+FXgV2wMv1JMWPxw/37wBN87QHYDuTV0Y= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3/go.mod h1:WyLS5qwXHtjKAONYZq/4ewdd+hcVsa3LBu77Ow5uj3k= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 h1:VzudTFrDCIDakXtemR7l6Qzt2+JYsVqo2MxBPt5k8T8= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.3/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= +github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= +github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= diff --git a/plugin/dnssec/README.md b/plugin/dnssec/README.md index 00766a1e3..f586ca9ff 100644 --- a/plugin/dnssec/README.md +++ b/plugin/dnssec/README.md @@ -16,7 +16,7 @@ This plugin can only be used once per Server Block. ~~~ dnssec [ZONES... ] { - key file KEY... + key file|aws_secretsmanager KEY... cache_capacity CAPACITY } ~~~ @@ -49,6 +49,26 @@ used. * generated public key `Kexample.org+013+45330.key` * generated private key `Kexample.org+013+45330.private` +* `key aws_secretsmanager` indicates that **KEY** secret(s) should be read from AWS Secrets Manager. Secret + names or ARNs may be used. After generating the keys as described in the `key file` section, you can + store them in AWS Secrets Manager using the following AWS CLI v2 command: + + ```sh + aws secretsmanager create-secret --name "Kexample.org.+013+45330" \ + --description "DNSSEC keys for example.org" \ + --secret-string "$(jq -n --arg key "$(cat Kexample.org.+013+45330.key)" \ + --arg private "$(cat Kexample.org.+013+45330.private)" \ + '{key: $key, private: $private}')" + ``` + + This command reads the contents of the `.key` and `.private` files, constructs a JSON object, and stores it + as a new secret in AWS Secrets Manager with the specified name and description. CoreDNS will then fetch + the key data from AWS Secrets Manager when using the `key aws_secretsmanager` directive. + + [AWS SDK for Go V2](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) is used + for authentication with AWS Secrets Manager. Make sure the provided AWS credentials have the necessary + permissions (e.g., `secretsmanager:GetSecretValue`) to access the specified secrets in AWS Secrets Manager. + * `cache_capacity` indicates the capacity of the cache. The dnssec plugin uses a cache to store RRSIGs. The default for **CAPACITY** is 10000. @@ -75,6 +95,18 @@ example.org { } ~~~ +Sign responses for `example.org` with the key stored in AWS Secrets Manager under the secret name +"Kexample.org.+013+45330". + +~~~ +example.org { + dnssec { + key aws_secretsmanager Kexample.org.+013+45330 + } + whoami +} +~~~ + Sign responses for a kubernetes zone with the key "Kcluster.local+013+45129.key". ~~~ diff --git a/plugin/dnssec/dnskey.go b/plugin/dnssec/dnskey.go index 161db9471..cfe59845a 100644 --- a/plugin/dnssec/dnskey.go +++ b/plugin/dnssec/dnskey.go @@ -1,16 +1,21 @@ package dnssec import ( + "context" "crypto" "crypto/ecdsa" "crypto/rsa" + "encoding/json" "errors" "os" "path/filepath" + "strings" "time" "github.com/coredns/coredns/request" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/miekg/dns" "golang.org/x/crypto/ed25519" ) @@ -23,6 +28,12 @@ type DNSKEY struct { tag uint16 } +// SecretKeyData represents the structure of the DNS keys stored in AWS Secrets Manager. +type SecretKeyData struct { + Key string `json:"key"` + Private string `json:"private"` +} + // ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other // utilities. It adds ".key" for the public key and ".private" for the private key. func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { @@ -63,6 +74,69 @@ func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: nil, tag: 0}, errors.New("no private key found") } +// ParseKeyFromAWSSecretsManager retrieves and parses a DNSSEC key pair from AWS Secrets Manager. +func ParseKeyFromAWSSecretsManager(secretID string) (*DNSKEY, error) { + // Load the AWS SDK configuration + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, err + } + + // Create a Secrets Manager client + client := secretsmanager.NewFromConfig(cfg) + + // Retrieve the secret value + input := &secretsmanager.GetSecretValueInput{ + SecretId: &secretID, + } + result, err := client.GetSecretValue(context.TODO(), input) + if err != nil { + return nil, err + } + + // Parse the secret string into SecretKeyData + var secretData SecretKeyData + err = json.Unmarshal([]byte(*result.SecretString), &secretData) + if err != nil { + return nil, err + } + + // Parse the public key + rr, err := dns.NewRR(secretData.Key) + if err != nil { + return nil, err + } + dk, ok := rr.(*dns.DNSKEY) + if !ok { + return nil, errors.New("invalid public key format") + } + + // Parse the private key + p, err := dk.ReadPrivateKey(strings.NewReader(secretData.Private), secretID) + if err != nil { + return nil, err + } + + // Create the DNSKEY structure + var s crypto.Signer + var tag uint16 + switch key := p.(type) { + case *rsa.PrivateKey: + s = key + tag = dk.KeyTag() + case *ecdsa.PrivateKey: + s = key + tag = dk.KeyTag() + case ed25519.PrivateKey: + s = key + tag = dk.KeyTag() + default: + return nil, errors.New("unsupported key type") + } + + return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: s, tag: tag}, nil +} + // getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true. func (d Dnssec) getDNSKEY(state request.Request, zone string, do bool, server string) *dns.Msg { keys := make([]dns.RR, len(d.keys)) diff --git a/plugin/dnssec/setup.go b/plugin/dnssec/setup.go index 7820e93a7..9a87c41c0 100644 --- a/plugin/dnssec/setup.go +++ b/plugin/dnssec/setup.go @@ -141,6 +141,19 @@ func keyParse(c *caddy.Controller) ([]*DNSKEY, error) { } keys = append(keys, k) } + } else if value == "aws_secretsmanager" { + ks := c.RemainingArgs() + if len(ks) == 0 { + return nil, c.ArgErr() + } + + for _, k := range ks { + k, err := ParseKeyFromAWSSecretsManager(k) + if err != nil { + return nil, err + } + keys = append(keys, k) + } } return keys, nil }