@clef-sh/cdk
v0.5.1
Published
AWS CDK L2 constructs for delivering Clef-managed secrets to AWS resources
Readme
@clef-sh/cdk
AWS CDK L2 constructs for delivering Clef-managed secrets into AWS-native resources at deploy time. One construct call, one explicit IAM grant, no agent to run, no app-code changes.
npm install @clef-sh/cdkPeer deps: aws-cdk-lib ^2.100, constructs ^10. For KMS-envelope
identities (recommended), also install @aws-sdk/client-kms.
What's included
| Construct | Use case |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ClefArtifactBucket | Deliver the encrypted envelope to S3 for the Clef agent (or any VCS-compatible client) to fetch and decrypt at runtime. |
| ClefSecret | Unwrap the envelope at deploy time and store the plaintext in AWS Secrets Manager. Consumers read via the native ASM SDK / ECS secret injection — no Clef agent required. |
| ClefParameter | Unwrap the envelope at deploy time and store the plaintext in an SSM Parameter Store parameter. One construct = one parameter. Consumers read via ssm:GetParameter. |
Quick start
S3 delivery (any identity type)
import { ClefArtifactBucket } from "@clef-sh/cdk";
const artifact = new ClefArtifactBucket(this, "ApiSecrets", {
identity: "api-gateway",
environment: "production",
});
// Consumer wiring — explicit, reviewer-visible
artifact.grantRead(agentLambda); // s3:GetObject
artifact.envelopeKey?.grantDecrypt(agentLambda); // kms:Decrypt (KMS identities only)Artifact signing (KMS asymmetric)
Pass the ARN of an asymmetric KMS key (key spec ECC_NIST_P256,
algorithm ECDSA_SHA_256) as signingKeyArn to sign the envelope at
cdk synth time. The construct provisions a deploy-time
kms:GetPublicKey lookup and exposes the public key as a CFN token, so
consumers can wire CLEF_AGENT_VERIFY_KEY without ever holding key
bytes themselves.
The recommended pattern uses the static helper
ClefArtifactBucket.signingKeyArnFromAlias, which resolves the alias's
region/account at synth via kms.Key.fromLookup and rewrites the result
back into alias-ARN form. KMS resolves the alias on every Sign /
GetPublicKey call, so rotating the underlying key needs no stack changes.
import { ClefArtifactBucket } from "@clef-sh/cdk";
const artifact = new ClefArtifactBucket(this, "ApiSecrets", {
identity: "api-gateway",
environment: "production",
signingKeyArn: ClefArtifactBucket.signingKeyArnFromAlias(this, "alias/clef-signing"),
});
artifact.grantRead(agentLambda);
artifact.envelopeKey?.grantDecrypt(agentLambda);
artifact.bindVerifyKey(agentLambda); // adds CLEF_AGENT_VERIFY_KEY env varThe helper requires AWS credentials with kms:DescribeKey at first
synth; subsequent synths read from cdk.context.json. Commit
cdk.context.json so CI synths are deterministic and don't need AWS
credentials at synth time. If the alias is later re-targeted to a key
in a different account or region, run cdk context --reset and
re-synth. (Re-targeting within the same account/region is transparent
to the cache — KMS resolves the alias at runtime.)
If you already have an ARN string in hand (from a platform-stack output, an SSM parameter you read in a config file, etc.), pass it directly:
signingKeyArn: "arn:aws:kms:us-east-1:111122223333:alias/clef-signing",signingKeyArn must be a literal string at synth — CFN tokens are
rejected. Signing happens in a subprocess outside the CFN graph, so a
token resolved at deploy time is too late. Same-stack new kms.Key(...)
and cross-stack token references will throw with a clear error.
Provision the signing key in a long-lived platform stack. The principal
running cdk synth (developer laptop or CI role) needs kms:Sign on
the key — the construct does not auto-grant.
Direct key ARNs (...:key/abcd-...) work too but couple the stack to
a specific key generation; the alias form is preferred unless you have
a reason to pin.
Only KMS asymmetric signing is supported via the CDK constructs. Ed25519
signing remains available through the CLI (clef pack --signing-key)
but is intentionally not driven by CLEF_SIGNING_KEY in CDK synth — the
construct prop is the only signal that signing is wanted, so a stray env
var in CI cannot silently activate signing the user did not declare.
Signing applies only to ClefArtifactBucket. ClefSecret and
ClefParameter unwrap the envelope at deploy time and write plaintext
into AWS Secrets Manager / SSM, so the signed envelope never crosses a
runtime trust boundary in those flows.
AWS Secrets Manager delivery (KMS-envelope identities)
import { ClefSecret } from "@clef-sh/cdk";
// Single-value secret — connection string, API token, webhook URL, etc.
const dbUrl = new ClefSecret(this, "DbUrl", {
identity: "api-gateway",
environment: "production",
shape: "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/app",
});
// JSON-shaped secret — multiple fields, mapping Clef keys to app-expected names
const config = new ClefSecret(this, "Config", {
identity: "api-gateway",
environment: "production",
shape: {
dbHost: "${DATABASE_HOST}",
apiKey: "${API_KEY}",
region: "us-east-1", // literal
},
});
dbUrl.grantRead(apiLambda);
config.grantRead(apiLambda);Each new ClefSecret(…) provisions one ASM secret, same as native
secretsmanager.Secret. The pack-helper is memoized per
(manifest, identity, environment), so multiple instances for the same
identity share a single pack invocation at synth.
SSM Parameter Store delivery (KMS-envelope identities)
import { ClefParameter } from "@clef-sh/cdk";
const dbUrl = new ClefParameter(this, "DbUrl", {
identity: "api-gateway",
environment: "production",
shape: "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/app",
});
const stripe = new ClefParameter(this, "StripeKey", {
identity: "api-gateway",
environment: "production",
shape: "${STRIPE_SECRET_KEY}",
});
dbUrl.grantRead(apiLambda);
stripe.grantRead(paymentsLambda);ClefParameter is single-value only (SSM holds one value per parameter),
so shape is required and must be a string template. Defaults to
SecureString with the AWS-managed alias/aws/ssm at-rest key. Shares
the unwrap Lambda and per-deploy KMS grant lifecycle with ClefSecret.
Existing Lambdas using SecretsManagerClient.GetSecretValue or ECS
services using Secret.fromSecretsManager(secret, "FIELD") keep working
unchanged — the construct shapes ASM to match what your app already reads.
How ClefSecret stays secure
Conventional "unwrap Lambda" designs leave a long-lived Lambda with
persistent kms:Decrypt in your account — an attractive target for anyone
who pivots into the runtime. ClefSecret uses per-deploy KMS grants
instead:
- The unwrap Lambda's role has no baseline
kms:Decrypt. - On each deploy, a sibling CloudFormation Custom Resource calls
kms:CreateGrantscoped toGranteePrincipal = unwrap-roleandOperations = [Decrypt]. Authority lasts for one invocation. - When the envelope revision changes (every
cdk deploy), CloudFormation replaces the grant resource —RevokeGrantruns on the previous grant before the new one takes over.
Between deploys the Lambda is cold and, on its own, cannot decrypt anything. Ciphertext in stack resource properties is safe at rest (same threat model Clef assumes everywhere else).
Synth-time validation
Shape templates are validated before CloudFormation sees them. Typos
surface at cdk synth with the field name (or <value> for string
shape), the bad reference, a did-you-mean suggestion, and the full list of
valid Clef keys for that identity/environment. No broken deploys, no
rollback dances.
Requirements at synth time
sopsbinary onPATH, or bundled via the matching@clef-sh/sops-*platform package (installed automatically with@clef-sh/cli).- Age credentials —
CLEF_AGE_KEYorCLEF_AGE_KEY_FILEenv var pointing at the user's age private key. Used to decrypt source SOPS files during the pack step. - AWS credentials — required for KMS-envelope identities; the synth
calls
kms:Encryptto wrap a fresh DEK into the envelope. Standard AWS SDK credential resolution (AWS_PROFILE, env vars, instance role, etc.).
Full docs
- Construct reference & guide: clef.sh/cdk
- Quickstart: clef.sh/guide/cdk
- Source & issues: github.com/clef-sh/clef
License
MIT
