Bean & Bark: secrets without the spreadsheet

View .md

The Orders API is live on dev — returning sample data. The real database connection string currently lives in a spreadsheet cell, and the wholesale price-feed API key just arrived in a DM. Neither can go in git, and neither can stay where it is. This chapter is the config discipline the whole series leans on: three kinds of value, one place they're declared, and exactly one moment they're resolved.

You'll learn
  • Tell the three env value forms apart — plain, param: (SSM), secret: (Secrets Manager) — and pick the right one per value
  • Register each with kanject add env / add param --secure / add secret
  • Say precisely when references resolve (at deploy — never at runtime) and what that means for rotation
  • Reproduce a stage-only bug on your laptop with kanject test --pull-env, and read the KANCLI044 warning like a pro

Three kinds of value

Every entry in a stage's env map is one of three forms — and the form is implicit in the value itself. A plain value is baked verbatim (fine for ASPNETCORE_ENVIRONMENT). A param:KEY reference is fetched from SSM Parameter Store under the stage's parameter path. A secret:NAME#json-key reference pulls one key out of a Secrets Manager secret. One add command per form:

bash
# A plain value — fine in git, baked as-iskanject add env PriceFeed__RefreshMinutes 15 --env dev# The DB connection string — written to SSM as a SecureString,# bound in the stage file as a param: referencekanject add param Database__ConnectionString \  "Host=dev-db.beanandbark.internal;Port=5432;..." --env dev --secure# The price-feed API key — already lives in Secrets Manager;# this only registers the binding (add --value to write it too)kanject add secret PriceFeed__ApiKey beanandbark/dev/price-feed#apiKey --env dev

All three land in stages/dev.json — which stays committable, because the sensitive entries are references, not values:

json
"env": {  "ASPNETCORE_ENVIRONMENT": "Development",  "PriceFeed__RefreshMinutes": "15",  "Database__ConnectionString": "param:Database__ConnectionString",  "PriceFeed__ApiKey": "secret:beanandbark/dev/price-feed#apiKey"}

Look, but don't leak

Before trusting any of it — a guess. kanject env prints the stage's env table. Which of the four rows show their value, and which are masked?

bash
kanject env --env dev
text
Env  stage dev ─────────────────────────────────────────────────  .NET config key             Source              Value  ASPNETCORE_ENVIRONMENT      stage env (plain)   Development  PriceFeed:RefreshMinutes    stage env (plain)   15  Database:ConnectionString   ssm (param)         ●●●●●●●●●●  PriceFeed:ApiKey            secrets-manager     ●●●●●●●●●●  Parameter Store path: /beanandbark-orders/dev/  (resolved at deploy)

The two references are masked; the two plain values aren't — they're in git anyway. And note what this command didn't do: it never called AWS. kanject env reads the stage file locally; resolution against SSM and Secrets Manager happens at deploy (or when you explicitly pull, below). --show-values unmasks the table — in a local terminal, never in CI logs.

Resolved once, at deploy

Redeploy (kanject aws deploy --env dev) and watch the ladder's second rung: resolve env 4 keys. That's the one moment references become values — the CLI calls SSM and Secrets Manager, then bakes the resolved strings into the Lambda's function config. At runtime your code reads plain environment variables; it never talks to SSM or Secrets Manager, so there's no per-request lookup, no extra IAM on the function, and nothing to time out during a cold start.

The ledger keeps you honest here too: revision 2's snapshot stores a SHA-256 of every resolved env value — so you can prove the config changed between two revisions without the ledger ever containing a secret.

The bug that only happens on dev

Wednesday morning: /orders on dev returns 500s. On your machine everything passes — because your local run uses your local config, and dev uses the resolved references. You need dev's actual env, in a process you can put a breakpoint in:

bash
kanject test --pull-env --env dev
text
  ⚠  Pulling 4 values into the test process: 1 from SSM (SecureString),     1 from Secrets Manager, 2 from stage env.     →  --pull-env resolved 1 SSM SecureString value(s) into the local        process.  KANCLI044

--pull-env resolves the stage's references live — same resolver the deploy uses — and injects the values into the test process's memory. Nothing touches disk: not launchSettings.json, not a cache, not a log. The summary counts values by source without printing a single name or value, and KANCLI044 is a warning, not an error: it's telling you a SecureString is now visible to anything that can read your process env — printenv, or an IDE attached to the process. That's exactly why you're here, and exactly why you pull from dev, not prod.

Two minutes with a breakpoint finds it: the SSM parameter still holds the placeholder host from Monday's scaffold. Re-run the add param with the real connection string, redeploy, and /orders serves real orders — revision 3, in the ledger, with fresh env hashes.

Recap
  • Three env forms, picked per value: plain (baked verbatim), param:KEY (SSM under /beanandbark-orders/dev/), secret:NAME#json-key (Secrets Manager) — registered with add env / add param --secure / add secret.
  • References resolve once, at deploy, and are baked into function config — the runtime never calls SSM or Secrets Manager, and rotating a secret requires a redeploy to take effect.
  • kanject env is local-only and masks reference values by default; --show-values is for local terminals, never CI.
  • kanject test --pull-env pulls a stage's real env into process memory only — KANCLI044 warns you a SecureString is in the process, which is the point. Pull from dev, not prod.
Before the key rotates — check yourself2
At runtime, where does the deployed Lambda read PriceFeed__ApiKey from?
You run kanject test --pull-env --env dev and see KANCLI044. What happened?
Try it yourself 2
1 Rotate the price-feed key
The price-feed vendor rotates the API key in Secrets Manager on Friday. What — if anything — do you have to do so dev uses the new key?
When was the old value resolved, and where does the running function actually read from?
Show solution
Redeploy. The old key was baked into the function config at the last deploy, so the running stage keeps using it until kanject aws deploy --env dev re-resolves the reference. Rotation isn't complete until every stage referencing the secret has redeployed — and each redeploy leaves a ledger entry with changed env hashes as the receipt.
bash
kanject aws deploy --env dev
2 Predict the stored key
A teammate runs kanject add env Pricing:Feed:Currency GBP --env dev. What key lands in the stage file, and what does the kanject env table display?
One normalization on the way in, one prettification on the way out.
Show solution
Stored as Pricing__Feed__Currencyadd env normalizes : and / separators to the __ form .NET binds from. The kanject env table displays it back as Pricing:Feed:Currency, the .NET config-key spelling. Same key, two faces.
Was this page helpful?