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 (like Monday's 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=orders-db.dev.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 + set PriceFeed__RefreshMinutes in kanject-cli/stages/dev.json + SSM /beanandbark-orders/dev/Database/ConnectionString (SecureString) + stage env reference Database__ConnectionString =param:Database__ConnectionString + stage env reference PriceFeed__ApiKey =secret:beanandbark/dev/price-feed#apiKey add env and add secret only edit the stage file; add param also writes the value to SSM, under the stage's path. 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 ││ Database:ConnectionString │ ssm (param) │ param:Database__ConnectionString ││ PriceFeed:ApiKey │ secrets-manager │ ●●●●●●●●●● ││ PriceFeed:RefreshMinutes │ stage env (plain) │ 15 │╰───────────────────────────┴───────────────────┴──────────────────────────────────╯ Parameter Store path: /beanandbark-orders/dev/ (resolved at deploy) Only the Secrets Manager row is masked. The param: row prints its reference as written, and the plain values are in git anyway. And note what this command didn't do: it never called AWS. kanject env reads the stage file, so every row is a value or a reference — never a resolved secret. --show-values prints the masked row as written too. Resolution against SSM and Secrets Manager happens at deploy, or when you explicitly pull, below.
Resolved once, at deploy
Redeploy with kanject aws deploy --env dev and watch the second rung:
── Deploy stage dev ─────────────────────────────────────────────────────────── sync deps (no deps) 00:00:00 resolve env 4 keys 00:00:02 prepare artifacts done 00:00:00 write deploy config done 00:00:00 render provider artifacts done 00:00:01 deploy Lambda ok 00:01:38 promote revision 2 → live 00:01:41 record ledger s3 snapshot written 00:01:42╭───────────────────────────┬──────────────────────┬─────────────╮│ .NET key │ Source │ Value │├───────────────────────────┼──────────────────────┼─────────────┤│ ASPNETCORE_ENVIRONMENT │ StagePlain │ Development ││ Database:ConnectionString │ ProviderConfigSecure │ ●●●●●●●●●● ││ PriceFeed:ApiKey │ Vault │ ●●●●●●●●●● ││ PriceFeed:RefreshMinutes │ StagePlain │ 15 │╰───────────────────────────┴──────────────────────┴─────────────╯ ✓ Deployed dev-beanandbark-orders (Lambda) @ revision 2 resolve env 4 keys is the one moment references become values. The CLI loads every parameter under /beanandbark-orders/dev/, resolves each param: and secret: reference, and bakes the resolved strings into the Lambda's function config before publishing version 2. 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. Baking is the default; --bake-secrets false skips it for a service that loads its own config at startup. (CLI 1.0.36 to 1.0.50 skipped it unless you passed --bake-secrets.)
The table masks the SecureString and the Secrets Manager value; --show-values would print them in cleartext, so keep that to your own terminal. And the ledger keeps you honest here too: revision 2's entry stores a SHA-256 of every resolved env value — so you can tell two revisions' config apart 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 attach an IDE to:
kanject test --http --pull-env --env dev ── Local test (WebApi) ───────────────────────────────────────────────────────── Runner: dotnet run URL: http://localhost:5000 Env: dev ⚠ Pulling 4 values into the test process: 1 from SSM (SecureString), 1 fromSecrets Manager, 2 from stage env. → --pull-env resolved 1 SSM SecureString value into the local process.KANCLI044 Then dotnet run takes over and serves the API on localhost:5000. --pull-env resolves the stage's references live — same resolver the deploy uses — and hands the values to that process's environment. They never touch disk: normal kanject test runs preserve IDE files and write no retrieved value to a cache or log. Explicit IDE setup is a separate action; see Local Development for native Debug/F5 setup from CLI 1.0.67. 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 attached IDE. That's exactly why you're here, and exactly why you pull from dev, not prod.
Two minutes with the IDE attached finds it: the connection string's host is the one from the spreadsheet — the dev database moved to a new host in the spring, and nobody updated the cell. Re-run the add param with the current connection string, redeploy, and /orders serves real orders — revision 3, in the ledger, with a new hash for Database__ConnectionString.
- 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 the function config by default — the runtime never calls SSM or Secrets Manager, and rotating a secret requires a redeploy to take effect.
kanject envreads the stage file only: it shows values and references, never resolved secrets, and masks thesecret:rows.kanject test --pull-envpulls a stage's real env into the test process only — KANCLI044 warns you a SecureString is in the process, which is the point. Pull fromdev, notprod.
PriceFeed__ApiKey from?kanject test --http --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 a changed hash for that key 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 turns : and / separators into the __ form .NET configuration binds from. The kanject env table displays it back as Pricing:Feed:Currency, the .NET config-key spelling. Same key, two faces. (Keys starting with KANJECT_ are refused: that namespace belongs to the tool.)