DEV Community

Cover image for AWS CDK + Clef: Shift secrets policy and governance left
Clef.sh
Clef.sh

Posted on

AWS CDK + Clef: Shift secrets policy and governance left

A ~10-minute, copy-paste tutorial that takes you from an empty directory to a fully working Clef setup, then deploys the production secrets to AWS Secrets Manager via the @clef-sh/cdk constructs.

By the end, you'll have:

  • A clef.yaml with two namespaces (database, payments) across two environments (dev, production)
  • All four matrix cells populated with demo secrets, encrypted with SOPS
  • A service identity (app) whose production envelope is protected by AWS KMS
  • A CloudFormation stack with three ClefSecrets, each holding a Clef-managed value in AWS Secrets Manager — readable by your app via the standard ASM SDK, with no Clef agent at runtime

Steps 1–3 work fully offline. Steps 4–5 deploy real AWS resources and require working AWS credentials.

Prerequisites

  • Node.js 20+
  • AWS account + credentials for steps 4–5 only. Standard SDK resolution applies (AWS_PROFILE, env vars, SSO, etc.). The KMS key, an unwrap Lambda, and three Secrets Manager secrets will be created in the account/region your credentials resolve to. All resources are tagged clef-quick-start so you can find and remove them.
  • Git — Clef is git-native, and the tutorial commits the initial state after clef init so you can see exactly what each subsequent step adds. Cloning this repo (per the setup step below) gives you a git working tree already.
  • Shell — commands below are written for a POSIX shell (macOS/Linux Terminal, WSL, or Git Bash on Windows). PowerShell works too; the only block that needs a different syntax is the variable derivation in step 4, where a PowerShell variant is shown alongside.

Setup

git clone https://github.com/clef-sh/quick-start.git
cd quick-start
npm install
Enter fullscreen mode Exit fullscreen mode

npm install puts the clef CLI under node_modules/.bin. The tutorial uses npx clef, but you can also install it globally with npm i -g @clef-sh/cli if you'd rather drop the prefix.

Verify the install:

npx clef doctor
Enter fullscreen mode Exit fullscreen mode

You should see green checks for node, sops, and git (if present).


Step 1 — Initialise Clef

Create the manifest and the encrypted matrix in one shot, using the age backend (no AWS account needed yet):

npx clef init \
  --namespaces database,payments \
  --environments dev,production \
  --backend age \
  --non-interactive
Enter fullscreen mode Exit fullscreen mode

What just happened:

  • clef.yaml now declares your namespaces, environments, and the age recipient that owns this repo.
  • .clef/config.yaml records the local age private key location (stored in your OS keychain by default).
  • secrets/database/{dev,production}.enc.yaml and secrets/payments/{dev,production}.enc.yaml were created — each one is a valid SOPS file with no keys yet.
  • .clefignore and .gitattributes were written so the SOPS merge driver picks up *.enc.yaml.

Take a look:

cat clef.yaml
tree secrets
Enter fullscreen mode Exit fullscreen mode

Commit the initial state. Clef is git-native and the matrix files are SOPS-encrypted, so they're safe to commit even once you start adding values:

git add clef.yaml .clef .clefignore .gitattributes secrets
git commit -m "Initialise Clef"
Enter fullscreen mode Exit fullscreen mode

Step 2 — Populate the matrix

Set your first secret with an inline value:

npx clef set database/dev DB_HOST localhost
Enter fullscreen mode Exit fullscreen mode

Now set one with hidden input — Clef prompts and never echoes the value to the terminal or writes it to disk:

npx clef set database/dev DB_PASSWORD
# Value: ********
Enter fullscreen mode Exit fullscreen mode

Confirm it landed:

npx clef get database/dev DB_PASSWORD
Enter fullscreen mode Exit fullscreen mode

To populate the rest of the matrix in one go, paste this block:

npx clef set database/dev        DB_USER       dev_user
npx clef set database/production DB_HOST       db.prod.internal
npx clef set database/production DB_USER       app
npx clef set database/production DB_PASSWORD   --random
npx clef set payments/dev        STRIPE_KEY    sk_test_demo
npx clef set payments/dev        WEBHOOK_URL   https://dev.example.com/webhooks/stripe
npx clef set payments/production STRIPE_KEY    --random
npx clef set payments/production WEBHOOK_URL   https://example.com/webhooks/stripe
Enter fullscreen mode Exit fullscreen mode

--random generates a cryptographically random placeholder and marks the key as pending — Clef tracks placeholders so you can find them later with clef lint.

Step 3 — Explore the matrix

Compare environments side by side:

npx clef diff database dev production
Enter fullscreen mode Exit fullscreen mode

Validate the whole repo — schema compliance, matrix completeness, SOPS integrity:

npx clef lint
Enter fullscreen mode Exit fullscreen mode

Open the local web UI to browse the matrix visually:

npx clef ui
Enter fullscreen mode Exit fullscreen mode

This binds to 127.0.0.1:7777 only. From the UI you can edit secrets with masked values, diff environments, and run lint with one click. Press Ctrl-C to stop the server when you're done.

Step 4 — Provision KMS and migrate production

Steps 4 and 5 use AWS. Make sure your credentials are set:

aws sts get-caller-identity
Enter fullscreen mode Exit fullscreen mode

The @clef-sh/cdk ClefSecret and ClefParameter constructs require KMS-envelope identities — they need a customer KMS key to wrap each pack's data encryption key. The infra/ directory ships two CDK stacks for this:

  • QuickStartKms — provisions the KMS key plus the alias alias/clef-quick-start.
  • QuickStartApp — deploys three ClefSecrets into AWS Secrets Manager. We deploy this in step 5.

The KMS stack uses a fixed alias, so we can compute its ARN up front:

ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_REGION:-$(aws configure get region)}
KMS_ARN="arn:aws:kms:${REGION}:${ACCOUNT}:alias/clef-quick-start"
Enter fullscreen mode Exit fullscreen mode

PowerShell equivalent:

$ACCOUNT = aws sts get-caller-identity --query Account --output text
$REGION  = if ($env:AWS_REGION) { $env:AWS_REGION } else { aws configure get region }
$KMS_ARN = "arn:aws:kms:${REGION}:${ACCOUNT}:alias/clef-quick-start"
Enter fullscreen mode Exit fullscreen mode

Create the app service identity:

npx clef service create app \
  --runtime \
  --namespaces database,payments \
  --kms-env production=aws:$KMS_ARN
Enter fullscreen mode Exit fullscreen mode

Scoped to both namespaces, with KMS-envelope encryption on production. dev stays on age — fine, since the CDK stack only deploys production secrets. --runtime keeps clef from re-encrypting your matrix files with a new shared age key; the CDK pack-helper handles encryption itself at synth time.

Bootstrap CDK (if needed) and deploy the KMS stack:

cd infra
npx cdk bootstrap
npx cdk deploy QuickStartKms --outputs-file ./kms-outputs.json
cd ..
Enter fullscreen mode Exit fullscreen mode

Now re-encrypt the production matrix cells with KMS (dev stays on age):

npx clef migrate-backend \
  --aws-kms-arn $KMS_ARN \
  --environment production
Enter fullscreen mode Exit fullscreen mode

clef migrate-backend decrypts each */production.enc.yaml cell with your age key and re-encrypts it under the new KMS key, in place. Verify with clef lint — the production cells should now show the new backend.

Step 5 — Deploy secrets to AWS Secrets Manager

Take a look at infra/lib/app-stack.ts to see the ClefSecret calls. Each construct synthesises one Secrets Manager secret, with the value computed at deploy time from the encrypted Clef matrix.

The shape property is a template with {{name}} placeholders, and refs binds each placeholder to a (namespace, key) pair in the matrix. Namespace and key stay as separate fields rather than collapsing into a single token, which keeps DB_USER from database distinct from any future DB_USER in another namespace this identity spans. shape also accepts an object literal — see PaymentsConfig in app-stack.ts for a JSON-shaped secret. The unwrap happens inside a synth-time pack step plus a CloudFormation Custom Resource at deploy — see How ClefSecret stays secure for the per-deploy KMS grant model.

Deploy:

cd infra
npx cdk deploy QuickStartApp -c app=true
cd ..
Enter fullscreen mode Exit fullscreen mode

The -c app=true flag opts the app stack into synth — see the comment in infra/bin/infra.ts for why we gate it.

Once the stack settles, list the new secrets:

aws secretsmanager list-secrets \
  --filters Key=tag-value,Values=clef-quick-start \
  --query 'SecretList[].Name'
Enter fullscreen mode Exit fullscreen mode

Read one back the way your application would:

aws secretsmanager get-secret-value \
  --secret-id clef-quick-start/database-url \
  --query SecretString --output text
Enter fullscreen mode Exit fullscreen mode

That value was never typed in plaintext at deploy time — it was reconstructed from secrets/database/production.enc.yaml inside the synth pack step, wrapped under your KMS key, and unwrapped exactly once by the per-deploy grant.

Step 6 — Connect Clef Cloud (optional)

So far everything is local: rotation due-dates, schema rules, lint warnings — they live in .clef/policy.yaml and only fire when you run clef lint or clef policy check. To enforce them across a team, you push the repo and let the Clef Cloud bot watch it on every PR.

clef init already scaffolded two files for this:

  • .clef/policy.yaml — declares rotation cadence per namespace, schema requirements, allowed backends, and any custom policy rules.
  • .github/workflows/clef-compliance.yml — a GitHub Actions workflow that runs clef policy check on each PR, writes compliance.json, and uploads it as the workflow artifact the bot reads.

Install the GitHub App and link this repo to a Clef Cloud workspace:

npx clef cloud init
Enter fullscreen mode Exit fullscreen mode

This authenticates you via GitHub OAuth (device flow), installs the Clef GitHub App on the repository, and registers the workspace. The CLI is non-destructive — .clef/policy.yaml is left alone if it already exists.

Once installed, the bot will:

  • post a status check on every PR summarising rotation overdue counts, schema violations, and pending placeholders,
  • block merges that violate .clef/policy.yaml (configurable per rule), and
  • populate the Cloud dashboard with the compliance history of the repo.

The dashboard won't show data until you actually push and let CI run. Compliance is computed from the compliance.json artifact produced by .github/workflows/clef-compliance.yml, so:

git add .clef/policy.yaml .github/workflows/clef-compliance.yml
git commit -m "Enable Clef Cloud"
git push
Enter fullscreen mode Exit fullscreen mode

Open a PR (even a no-op one) to trigger the workflow, then check the dashboard. The bot's status check will appear on the PR and the dashboard tile for this repo will fill in once the workflow finishes.

This is how governance and policy enforcement come into play: policy.yaml is the spec, the workflow is the enforcement point, and the bot is the cross-team visibility layer. Local devs get the same checks via clef lint / clef policy check, so violations surface long before review.

What you just built

Step back for a second — in the last ~10 minutes you stood up:

  • A central, version-controlled source of truth for secrets. Every value lives in secrets/<ns>/<env>.enc.yaml, encrypted with SOPS, diffable in git, reviewable in PRs.
  • Per-environment encryption with a clean handoff to AWS. dev rides on age for friction-free local work; production is sealed with your own KMS key. The CDK constructs deliver those values into AWS Secrets Manager so applications keep using the standard aws-sdk — no Clef binary, no agent, no sidecar.
  • Rotation and schema tracking with a path to enforcement. clef lint already flags pending placeholders and policy violations locally; once Clef Cloud is connected, the same checks run on every PR and the dashboard tracks rotation health across repositories.

If you swap the demo --random placeholders for real values, point a real service at the secrets via the AWS SDK, and add a schema for each namespace, the same pattern scales straight from this tutorial to a production setup.

Cleaning up

This repo is meant to be thrown away. To remove the AWS resources you just deployed:

cd infra
npx cdk destroy QuickStartApp QuickStartKms
Enter fullscreen mode Exit fullscreen mode

Then rm -rf the directory or re-clone if you want to run through the tutorial again.

Where to go next

If you hit anything that didn't work, please open an issue — the goal is for this tutorial to run cleanly for everyone.

Top comments (1)

Collapse
 
clef_sh profile image
Clef.sh

Looking forward to any comments from people trying the tutorial. Join the discord if you have issues: discord.gg/qCDPZjsbrW.