diff --git a/iam/role/role.go b/iam/role/role.go index 387497f..dd6d36e 100644 --- a/iam/role/role.go +++ b/iam/role/role.go @@ -102,7 +102,7 @@ func main() { } stackProps.setDefaults() if err := stackProps.validateProps(); err != nil { - logrus.Fatalf("invalid stack properties: %w", err) + logrus.Fatalf("invalid stack properties: %v", err) } common.AppendScopedTags(app, stackProps.Tags) diff --git a/kms/key/.dockerignore b/kms/key/.dockerignore new file mode 100644 index 0000000..b3265a2 --- /dev/null +++ b/kms/key/.dockerignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# go.sum should be committed +!go.sum + +# CDK asset staging directory +.cdk.staging +cdk.out + +.git diff --git a/kms/key/.gitignore b/kms/key/.gitignore new file mode 100644 index 0000000..92fe1ec --- /dev/null +++ b/kms/key/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# go.sum should be committed +!go.sum + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/kms/key/Acornfile b/kms/key/Acornfile new file mode 100644 index 0000000..318c198 --- /dev/null +++ b/kms/key/Acornfile @@ -0,0 +1,144 @@ +name: "AWS KMS Key" +description: "AWS Key Management Service (KMS) Key" +info: localData.info +readme: "./README.md" + +args: { + // Name of the Key to create. The default is auto-generated. + keyName: "" + // The ARN of the principal that will be allowed to manage the key as an admin. Optional. You can specify AWS accounts, IAM users, Federated SAML users, IAM roles, and specific assumed-role sessions. + adminArn: "" + // Extra tags to place on the created Key. Optional. + tags: {} + // Alias for the Key. Optional. + keyAlias: "" + // Description for the Key. Optional. + description: "Acorn created KMS Key" + // Whether to enable the Key to be used. Default true. + enabled: true + // Whether to enable automatic rotation of the Key. Default false. + enableKeyRotation: false + // Type of Key to create. Options are "SYMMETRIC_DEFAULT", "RSA_2048", "RSA_3072", "RSA_4096", "ECC_NIST_P256", "ECC_NIST_P384", "ECC_NIST_P521", "ECC_SECG_P256K1", "HMAC_224", "HMAC_256", "HMAC_384", and "HMAC_512". Default is "SYMMETRIC_DEFAULT". + keySpec: "SYMMETRIC_DEFAULT" + // The usage for the Key. Options are "ENCRYPT_DECRYPT", "SIGN_VERIFY", and "GENERATE_VERIFY_HMAC". Each keySpec is only compatible with certain keyUsages - see README for more info. Default is "ENCRYPT_DECRYPT". + keyUsage: "ENCRYPT_DECRYPT" + // The time (in days) that must pass after key deletion is requested before the key is deleted. Default is 7. Minimum is 7. Maximum is 30. + pendingWindowDays: 7 + // AWS IAM policy to attach to the Key. Optional. + keyPolicy: {} +} + +services: key: { + name: "AWS KMS Key" + generated: job: "apply" + consumer: permissions: rules: [{ + apiGroups: ["aws.acorn.io"] + verbs: [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair", + "kms:GenerateDataKeyPairWithoutPlaintext", + "kms:GenerateMac", + "kms:GenerateRandom", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:GetPublicKey", + "kms:ListAliases", + "kms:ListGrants", + "kms:ListKeyPolicies", + "kms:ListResourceTags", + "kms:ListRetirableGrants", + "kms:ReEncryptFrom", + "kms:ReEncryptTo", + "kms:Sign", + "kms:Verify", + "kms:VerifyMac", + ] + resources: ["*"] + }] +} + +jobs: apply: { + build: { + context: "." + additionalContexts: { + common: "../../libs" + } + } + files: "/app/config.json": std.toJSON(args) + env: { + CDK_DEFAULT_ACCOUNT: "@{secrets.aws-context.account-id}" + CDK_DEFAULT_REGION: "@{secrets.aws-context.aws-region}" + VPC_ID: "@{secrets.aws-context.vpc-id}" + ACORN_ACCOUNT: "@{acorn.account}" + ACORN_NAME: "@{acorn.name}" + ACORN_PROJECT: "@{acorn.project}" + ACORN_EXTERNAL_ID: "@{acorn.externalId}" + } + events: ["create", "update", "delete"] + permissions: rules: [{ + apiGroup: "aws.acorn.io" + verbs: [ + "cloudformation:DescribeStacks", + "cloudformation:CreateChangeSet", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStackEvents", + "cloudformation:DescribeStackResources", + "cloudformation:ExecuteChangeSet", + "cloudformation:PreviewStackUpdate", + "cloudformation:UpdateStack", + "cloudformation:GetTemplateSummary", + "cloudformation:DeleteStack", + "kms:*", + ] + resources: ["*"] + }, { + apiGroup: "aws.acorn.io" + verbs: [ + "ec2:DescribeAvailabilityZones", + "ec2:DescribeVpcs", + "ec2:DescribeSubnets", + "ec2:DescribeRouteTables", + ] + resources: ["*"] + }, { + apiGroup: "api.acorn.io" + verbs: [ + "create", + ] + resources: ["events"] + }] +} + +secrets: "aws-context": { + name: "AWS Context" + external: "context://aws" + type: "opaque" + data: { + "account-id": "" + "vpc-id": "" + "aws-region": "" + } +} + +localData: info: """ +Key ARN: @{services.key.data.arn} +Key Alias: \(args.keyAlias) +Key Description: \(args.description) +Key Spec: \(args.keySpec) +Key Usage: \(args.keyUsage) + +Example usage: + +```typescript +services: kmskey: external: "@{acorn.name}" + +containers: app: { + build: context: "./" + consumes: ["kmskey"] + env: KEY_ARN: "@{@{service.}kmskey.data.arn}" +} +``` +""" diff --git a/kms/key/Dockerfile b/kms/key/Dockerfile new file mode 100644 index 0000000..aaab75b --- /dev/null +++ b/kms/key/Dockerfile @@ -0,0 +1,21 @@ +FROM cgr.dev/chainguard/go as build + +WORKDIR /src/kms/key +COPY --from=common . ../../libs +COPY . . + +RUN --mount=type=cache,target=/root/go/pkg \ + --mount=type=cache,target=/root/.cache/go-build \ + go build -o key . + +FROM ghcr.io/acorn-io/aws/utils/cdk-runner:v0.6.0 as cdk-runner +FROM cgr.dev/chainguard/wolfi-base +RUN apk add -U --no-cache nodejs bash busybox jq && \ + apk del --no-cache wolfi-base apk-tools +RUN npm install -g aws-cdk +WORKDIR /app +COPY ./cdk.json ./ +COPY ./scripts ./scripts +COPY --from=cdk-runner /cdk-runner . +COPY --from=build /src/kms/key/key . +CMD [ "/app/cdk-runner" ] diff --git a/kms/key/README.md b/kms/key/README.md new file mode 100644 index 0000000..b4a6f06 --- /dev/null +++ b/kms/key/README.md @@ -0,0 +1,100 @@ +# KMS Key Service Acorn + +This Service Acorn creates a CloudFormation stack containing the given KMS Key. + +## Limitations + +Currently, this Service Acorn only supports adding a single ARN as an admin for the key. + +## Usage + +### Running the Acorn + +``` +acorn run ghcr.io/acorn-io/aws/kms/key:v0.1.0 \ + --key-name="my-key" \ + --key-alias="my-key" \ + --admin-arn="" \ + --description="Example key for encryption and decryption" \ + --key-spec="RSA_4098" \ + --key-usage="ENCRYPT_DECRYPT" \ + --pending-window-days=10 \ + --key-policy @policy.json +``` + +### Using the service in an Acornfile + +```cue +services: key: { + image: "ghcr.io/acorn-io/aws/kms/key:v0.1.0" + serviceArgs: { + keyName: "my-key" + keyAlias: "my-key" + adminArn: "" + description: "Example key for encryption and decryption" + keySpec: "RSA_4098" + keyUsage: "ENCRYPT_DECRYPT" + pendingWindowDays: 10 + tags: "my-tag": "my-tag-value" + + // This is an example policy: + keyPolicy: { + Version: "2012-10-07" + Statement: [ + { + Effect: "Allow" + Principal: AWS: "arn:aws:iam:::root" + Action: "kms:*" + Resource: "*" + }, + ] + } + } +} + +containers: mycontainer: { + image: "" + consumes: ["key"] + env: KEY_ARN: "@{services.key.data.arn}" +} + +``` + +### Arguments + +| Name | Description | Required | Default | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------| +| `--key-name` | The name of the key in the CloudFormation stack. | No | (generated) | +| `--key-alias` | The alias (friendly name) to give to the key. | No | (none) | +| `--admin-arn` | The ARN of a user to set as the administrator of the key. You can specify AWS accounts, IAM users, Federated SAML users, IAM roles, and specific assumed-role sessions. | No | (none) | +| `--description` | Description to attach to the key. | No | "Acorn created KMS Key" | +| `--key-spec` | The type of key to create. | Yes | `SYMMETRIC_DEFAULT` | +| `--key-usage` | The usage of the key. Each key spec only supports certain usages. See table below for details. | Yes | `ENCRYPT_DECRYPT` | +| `--pending-window-days` | The time (in days) that must pass after key deletion is requested before the key is deleted. Must be between 7 and 30 (inclusive) | Yes | 7 | +| `--key-policy` | The key policy to attach to the key. This must be in JSON format. | No | (created by AWS) | +| `--tags` | Tags to attach to the key. | No | (none) | + +#### Key Specs and Usages + +| Key Spec | Supported Key Usages | +|---------------------|----------------------------------| +| `SYMMETRIC_DEFAULT` | `ENCRYPT_DECRYPT` | +| `RSA_2048` | `ENCRYPT_DECRYPT`, `SIGN_VERIFY` | +| `RSA_3072` | `ENCRYPT_DECRYPT`, `SIGN_VERIFY` | +| `RSA_4096` | `ENCRYPT_DECRYPT`, `SIGN_VERIFY` | +| `ECC_NIST_P256` | `SIGN_VERIFY` | +| `ECC_NIST_P384` | `SIGN_VERIFY` | +| `ECC_NIST_P521` | `SIGN_VERIFY` | +| `ECC_SECG_P256K1` | `SIGN_VERIFY` | +| `HMAC_224` | `GENERATE_VERIFY_MAC` | +| `HMAC_256` | `GENERATE_VERIFY_MAC` | +| `HMAC_384` | `GENERATE_VERIFY_MAC` | +| `HMAC_512` | `GENERATE_VERIFY_MAC` | + +Source: https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdk/v2/awskms@v2.96.0#KeySpec + +### Outputs + +| Name | Description | +|-------|-----------------------------| +| `arn` | The ARN of the created key. | diff --git a/kms/key/cdk.json b/kms/key/cdk.json new file mode 100644 index 0000000..dbb204b --- /dev/null +++ b/kms/key/cdk.json @@ -0,0 +1,4 @@ +{ + "app": "./key", + "versionReporting": false +} diff --git a/kms/key/go.mod b/kms/key/go.mod new file mode 100644 index 0000000..f8fd58d --- /dev/null +++ b/kms/key/go.mod @@ -0,0 +1,30 @@ +module github.com/acorn-io/aws/kms/key + +go 1.21.1 + +require ( + github.com/acorn-io/services/aws/libs/common v0.0.0 + github.com/aws/aws-cdk-go/awscdk/v2 v2.96.0 + github.com/aws/aws-sdk-go-v2 v1.21.0 + github.com/aws/constructs-go/constructs/v10 v10.2.70 + github.com/aws/jsii-runtime-go v1.88.0 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 +) + +replace github.com/acorn-io/services/aws/libs/common v0.0.0 => ../../libs/common + +require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.200 // indirect + github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2 // indirect + github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/yuin/goldmark v1.4.13 // indirect + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/tools v0.13.0 // indirect +) diff --git a/kms/key/go.sum b/kms/key/go.sum new file mode 100644 index 0000000..a649f22 --- /dev/null +++ b/kms/key/go.sum @@ -0,0 +1,71 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/aws/aws-cdk-go/awscdk/v2 v2.96.0 h1:fIotQYKA9PxXdFQ1XT/xk/VMVAtgJKhMATCfRjO+Hic= +github.com/aws/aws-cdk-go/awscdk/v2 v2.96.0/go.mod h1:0HMIUTNhy228wx8J/O5sHPCE+yWP0kiomWjmCctSnRY= +github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= +github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= +github.com/aws/constructs-go/constructs/v10 v10.2.70 h1:CuKeOwf27CzGUt8XxOZStFSOVZ7An5XpCzxvqUk8zW4= +github.com/aws/constructs-go/constructs/v10 v10.2.70/go.mod h1:Jnh2jtqYQBjifA5+03aJmnIItEcjqAgMBJ8iZpFjNRE= +github.com/aws/jsii-runtime-go v1.88.0 h1:1qJ9Ane+oxTt1c3xkpDE5YIv53MzKDKYcAr70lAxT/Q= +github.com/aws/jsii-runtime-go v1.88.0/go.mod h1:rIisDqmDOviluOfibmnvjr1I9u2EFcbWLWUjOXy9/Ms= +github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.200 h1:CwkS78cin4h5A3IaDcL69GrBI1HgTEB/xtECTf1luCc= +github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.200/go.mod h1:sx6+u9s3UHyhm9BGrkGdQgNA0Ni5ekbJ9hW2Gupvoy0= +github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2 h1:k+WD+6cERd59Mao84v0QtRrcdZuuSMfzlEmuIypKnVs= +github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2/go.mod h1:CvFHBo0qcg8LUkJqIxQtP1rD/sNGv9bX3L2vHT2FUAo= +github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.1 h1:MBBQNKKPJ5GArbctgwpiCy7KmwGjHDjUUH5wEzwIq8w= +github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.1/go.mod h1:/2WiXEft9s8ViJjD01CJqDuyJ8HXBjhBLtK5OvJfdSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kms/key/kms.go b/kms/key/kms.go new file mode 100644 index 0000000..c903a1a --- /dev/null +++ b/kms/key/kms.go @@ -0,0 +1,83 @@ +package main + +import ( + "github.com/acorn-io/aws/kms/key/props" + "github.com/acorn-io/services/aws/libs/common" + "github.com/aws/aws-cdk-go/awscdk/v2" + "github.com/aws/aws-cdk-go/awscdk/v2/awsiam" + "github.com/aws/aws-cdk-go/awscdk/v2/awskms" + "github.com/aws/constructs-go/constructs/v10" + "github.com/aws/jsii-runtime-go" + "github.com/sirupsen/logrus" +) + +func NewKMSKeyStack(scope constructs.Construct, id string, props *props.KMSKeyStackProps) (awscdk.Stack, error) { + var sprops awscdk.StackProps + if props != nil { + sprops = props.StackProps + } + stack := awscdk.NewStack(scope, &id, &sprops) + + keySpec, keyUsage, err := props.GetKeySpecAndUsage() + if err != nil { + return nil, err + } + + keyProps := &awskms.KeyProps{ + Enabled: jsii.Bool(props.Enabled), + EnableKeyRotation: jsii.Bool(props.EnableKeyRotation), + KeySpec: keySpec, + KeyUsage: keyUsage, + PendingWindow: awscdk.Duration_Days(jsii.Number(props.PendingWindowDays)), + + // Hardcode this to `DESTROY` in order to prevent the user from leaving behind a KMS key that they can't delete. + RemovalPolicy: awscdk.RemovalPolicy_DESTROY, + } + + // Set optional properties + if len(props.KeyAlias) > 0 { + keyProps.Alias = jsii.String(props.KeyAlias) + } + if len(props.Description) > 0 { + keyProps.Description = jsii.String(props.Description) + } + if len(props.AdminArn) > 0 { + keyProps.Admins = &[]awsiam.IPrincipal{awsiam.NewArnPrincipal(jsii.String(props.AdminArn))} + } + if len(props.KeyPolicy) > 0 { + keyProps.Policy = awsiam.PolicyDocument_FromJson(props.KeyPolicy) + } + + kmsKey := awskms.NewKey(stack, jsii.String(props.KeyName), keyProps) + + awscdk.NewCfnOutput(stack, jsii.String("KMSKeyArn"), &awscdk.CfnOutputProps{ + Value: kmsKey.KeyArn(), + }) + + return stack, nil +} + +func main() { + defer jsii.Close() + + app := common.NewAcornTaggedApp(nil) + stackProps := &props.KMSKeyStackProps{ + StackProps: *common.NewAWSCDKStackProps(), + } + + if err := common.NewConfig(stackProps); err != nil { + logrus.Fatal(err) + } + stackProps.SetDefaults() + if err := stackProps.ValidateProps(); err != nil { + logrus.Fatalf("invalid stack properties: %s", err) + } + + common.AppendScopedTags(app, stackProps.Tags) + + if _, err := NewKMSKeyStack(app, "kmsKeyStack", stackProps); err != nil { + logrus.Fatal(err) + } + + app.Synth(nil) +} diff --git a/kms/key/props/props.go b/kms/key/props/props.go new file mode 100644 index 0000000..234c77d --- /dev/null +++ b/kms/key/props/props.go @@ -0,0 +1,106 @@ +package props + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/aws/aws-cdk-go/awscdk/v2" + "github.com/aws/aws-cdk-go/awscdk/v2/awskms" + "github.com/aws/aws-sdk-go-v2/aws/arn" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +type KMSKeyStackProps struct { + StackProps awscdk.StackProps + Tags map[string]string `json:"tags"` + KeyName string `json:"keyName"` + AdminArn string `json:"adminArn"` + KeyAlias string `json:"keyAlias"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + EnableKeyRotation bool `json:"enableKeyRotation"` + KeySpec string `json:"keySpec"` + KeyUsage string `json:"keyUsage"` + PendingWindowDays int `json:"pendingWindowDays"` + KeyPolicy map[string]interface{} `json:"keyPolicy"` +} + +// Source: https://pkg.go.dev/github.com/aws/aws-cdk-go/awscdk/v2/awskms@v2.96.0#KeySpec +var validKeySpecsAndUsages = map[string][]awskms.KeyUsage{ + "SYMMETRIC_DEFAULT": {awskms.KeyUsage_ENCRYPT_DECRYPT}, + "RSA_2048": {awskms.KeyUsage_ENCRYPT_DECRYPT, awskms.KeyUsage_SIGN_VERIFY}, + "RSA_3072": {awskms.KeyUsage_ENCRYPT_DECRYPT, awskms.KeyUsage_SIGN_VERIFY}, + "RSA_4096": {awskms.KeyUsage_ENCRYPT_DECRYPT, awskms.KeyUsage_SIGN_VERIFY}, + "ECC_NIST_P256": {awskms.KeyUsage_SIGN_VERIFY}, + "ECC_NIST_P384": {awskms.KeyUsage_SIGN_VERIFY}, + "ECC_NIST_P521": {awskms.KeyUsage_SIGN_VERIFY}, + "ECC_SECG_P256K1": {awskms.KeyUsage_SIGN_VERIFY}, + "HMAC_224": {awskms.KeyUsage_GENERATE_VERIFY_MAC}, + "HMAC_256": {awskms.KeyUsage_GENERATE_VERIFY_MAC}, + "HMAC_384": {awskms.KeyUsage_GENERATE_VERIFY_MAC}, + "HMAC_512": {awskms.KeyUsage_GENERATE_VERIFY_MAC}, +} + +func (ksp *KMSKeyStackProps) SetDefaults() { + if ksp.KeyName == "" { + ksp.KeyName = os.Getenv("ACORN_EXTERNAL_ID") + } + if ksp.KeySpec == "" { + ksp.KeySpec = "SYMMETRIC_DEFAULT" + } + if ksp.KeyUsage == "" { + ksp.KeyUsage = "ENCRYPT_DECRYPT" + } + if ksp.PendingWindowDays == 0 { + ksp.PendingWindowDays = 7 + } + if ksp.Description == "" { + ksp.Description = "Acorn created KMS Key" + } +} + +func (ksp *KMSKeyStackProps) ValidateProps() error { + var errs []error + if len(ksp.AdminArn) > 0 { + if _, err := arn.Parse(ksp.AdminArn); err != nil { + errs = append(errs, fmt.Errorf("failed to parse adminArn: %w", err)) + } + } + if _, _, err := ksp.GetKeySpecAndUsage(); err != nil { + errs = append(errs, err) + } + if ksp.PendingWindowDays < 7 || ksp.PendingWindowDays > 30 { + errs = append(errs, fmt.Errorf("pendingWindowDays must be between 7 and 30 (inclusive)")) + } + return errors.Join(errs...) +} + +func (ksp *KMSKeyStackProps) GetKeySpecAndUsage() (awskms.KeySpec, awskms.KeyUsage, error) { + var kmsUsage awskms.KeyUsage + switch ksp.KeyUsage { + case "ENCRYPT_DECRYPT": + kmsUsage = awskms.KeyUsage_ENCRYPT_DECRYPT + case "SIGN_VERIFY": + kmsUsage = awskms.KeyUsage_SIGN_VERIFY + case "GENERATE_VERIFY_MAC": + kmsUsage = awskms.KeyUsage_GENERATE_VERIFY_MAC + default: + return "", "", fmt.Errorf("invalid key usage: %s", ksp.KeyUsage) + } + + if usages, ok := validKeySpecsAndUsages[ksp.KeySpec]; ok { + if slices.Contains(usages, kmsUsage) { + return awskms.KeySpec(ksp.KeySpec), kmsUsage, nil + } + + var supportedUsages []string + for _, u := range usages { + supportedUsages = append(supportedUsages, string(u)) + } + return "", "", fmt.Errorf("invalid key usage %s for key spec: %s, supported usages: %s", ksp.KeyUsage, ksp.KeySpec, strings.Join(supportedUsages, ", ")) + } + return "", "", fmt.Errorf("invalid key spec %s, supported key specs: %s", ksp.KeySpec, strings.Join(maps.Keys(validKeySpecsAndUsages), ", ")) +} diff --git a/kms/key/props/props_test.go b/kms/key/props/props_test.go new file mode 100644 index 0000000..afa1f6c --- /dev/null +++ b/kms/key/props/props_test.go @@ -0,0 +1,88 @@ +package props + +import ( + "strings" + "testing" +) + +func TestPropsValidation(t *testing.T) { + tests := []struct { + name string + props KMSKeyStackProps + errContains string + }{ + { + name: "valid", + props: KMSKeyStackProps{ + AdminArn: "arn:aws:iam:us-east-2:123456789012:root", + KeySpec: "RSA_3072", + KeyUsage: "ENCRYPT_DECRYPT", + PendingWindowDays: 10, + }, + }, + { + name: "invalid adminArn", + props: KMSKeyStackProps{ + AdminArn: "invalid", + KeySpec: "RSA_3072", + KeyUsage: "ENCRYPT_DECRYPT", + PendingWindowDays: 10, + }, + errContains: "failed to parse adminArn", + }, + { + name: "invalid keySpec", + props: KMSKeyStackProps{ + AdminArn: "arn:aws:iam:us-east-2:123456789012:root", + KeySpec: "INVALID", + KeyUsage: "ENCRYPT_DECRYPT", + PendingWindowDays: 10, + }, + errContains: "invalid key spec INVALID", + }, + { + name: "invalid keyUsage", + props: KMSKeyStackProps{ + AdminArn: "arn:aws:iam:us-east-2:123456789012:root", + KeySpec: "RSA_3072", + KeyUsage: "INVALID", + PendingWindowDays: 10, + }, + errContains: "invalid key usage: INVALID", + }, + { + name: "keySpec does not support keyUsage", + props: KMSKeyStackProps{ + AdminArn: "arn:aws:iam:us-east-2:123456789012:root", + KeySpec: "SYMMETRIC_DEFAULT", + KeyUsage: "GENERATE_VERIFY_MAC", + PendingWindowDays: 10, + }, + errContains: "invalid key usage GENERATE_VERIFY_MAC for key spec: SYMMETRIC_DEFAULT", + }, + { + name: "invalid pendingWindowDays", + props: KMSKeyStackProps{ + AdminArn: "arn:aws:iam:us-east-2:123456789012:root", + KeySpec: "RSA_3072", + KeyUsage: "ENCRYPT_DECRYPT", + PendingWindowDays: 5, + }, + errContains: "pendingWindowDays must be between 7 and 30 (inclusive)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.props.ValidateProps(); err != nil { + if tt.errContains == "" { + t.Errorf("unexpected error: %s", err) + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("expected error to contain %q, got %q", tt.errContains, err) + } + } else if tt.errContains != "" { + t.Errorf("expected error to contain %q, got nil", tt.errContains) + } + }) + } +} diff --git a/kms/key/scripts/service.sh b/kms/key/scripts/service.sh new file mode 100755 index 0000000..d92f9d0 --- /dev/null +++ b/kms/key/scripts/service.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +if [ ! -f outputs.json ]; then + echo "outputs.json file not found!" + exit 1 +fi + +# Render Output +arn=$(jq -r '.[] | select(.OutputKey=="KMSKeyArn") | .OutputValue' outputs.json) + +cat > /run/secrets/output<