Bean & Bark: secrets without the spreadsheet
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.
- 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:
# 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:
"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?
kanject env --env dev 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:
kanject test --pull-env --env dev ⚠ 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.
- Three env forms, picked per value: plain (baked verbatim),
param:KEY(SSM under/beanandbark-orders/dev/),secret:NAME#json-key(Secrets Manager) — registered withadd 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 envis local-only and masks reference values by default;--show-valuesis for local terminals, never CI.kanject test --pull-envpulls a stage's real env into process memory only — KANCLI044 warns you a SecureString is in the process, which is the point. Pull fromdev, notprod.
PriceFeed__ApiKey from?kanject test --pull-env --env dev and see KANCLI044. What happened?dev uses the new key?Show solution
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.kanject aws deploy --env dev kanject add env Pricing:Feed:Currency GBP --env dev. What key lands in the stage file, and what does the kanject env table display?Show solution
Pricing__Feed__Currency — add 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.