# 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.

**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

> **Tuesday, 10:40am — the ticket:** **Nadia**: *"Tunde says the API works but it's serving the sample orders 😅 can you point it at the real dev database? also the price-feed people sent the API key — forwarding it here. do NOT put that in the repo, we've been burned before 🙏"*

## 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-is
kanject 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: reference
kanject 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"
}
```

> **Write keys however your team does:** `add env` normalizes separators for you: `PriceFeed:RefreshMinutes`, `PriceFeed/RefreshMinutes`, and `PriceFeed__RefreshMinutes` all store as `PriceFeed__RefreshMinutes` — the double-underscore form .NET configuration binds from. One reservation: `KANJECT_`-prefixed keys are refused; that namespace belongs to the tool.

## 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.

> **Baked means readable — and rotation means redeploy:** Two consequences of baking. First: anyone with `lambda:GetFunctionConfiguration` on the function can read the resolved values — treat that permission as sensitive. Second: rotating a secret in Secrets Manager changes **nothing** on a running stage until the next `kanject aws deploy` re-resolves it. Rotation isn't done until you've redeployed every stage that references the 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.

> **🎯 The key never touched git:** Count where the API key has been: Secrets Manager, the Lambda's function config, and — for one debugging session — your process memory. Not the repo, not a stage file, not a log line, not the ledger. The DM is deleted, the spreadsheet is retired, and every future value follows the same three-form rule without a meeting.

**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 yourself**

**1. At runtime, where does the deployed Lambda read `PriceFeed__ApiKey` from?**

- Its own function config — the value was baked at deploy ✓ — references resolve exactly once, during the deploy's `resolve env` phase. The function reads a plain environment variable; no SSM or Secrets Manager call ever happens at runtime.
- Secrets Manager, on each cold start — that's the pattern kanject deliberately avoids — runtime lookups add latency, IAM surface, and a failure mode on every cold start. The resolver runs at deploy, not in your function.
- The committed `stages/dev.json` — the stage file holds the *reference* (`secret:…#apiKey`), never the value — that's what makes it safe to commit.

**2. You run `kanject test --pull-env --env dev` and see KANCLI044. What happened?**

- SecureString values were resolved into your process memory — a warning, and the run continues ✓ — KANCLI044 is informational by design: memory-only, never written to disk, but visible to `printenv` or an attached IDE. The exit code is unchanged.
- The secrets were written to `launchSettings.json` — never — pulled values exist only in the spawned process's memory. Kanject writes no resolved value to disk, which is the guarantee the warning is reminding you about.
- Resolution failed and the run aborted — that's KANCLI045, and it is a real error. 044 means resolution *succeeded* — it's the heads-up, not the failure.

**Try it yourself**

**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?

_Hint:_ When was the old value resolved, and where does the running function actually read from?

_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?

_Hint:_ One normalization on the way in, one prettification on the way out.

_Solution:_ Stored as `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.

> **Where next?:** The API is real now — real database, real key, three revisions in the ledger. Which sets up the series' worst day: [The bad Friday deploy](https://www.kanject.com/docs/cli-bean-bark-rollback/) — a shipped bug, and the calm two-command recovery the ledger makes possible. For the `test` / `env` / `--pull-env` machinery you used today, the full reference is [Local Development](https://www.kanject.com/docs/cli-local-dev/).

---
_Source: https://www.kanject.com/docs/cli-bean-bark-secrets/ · Kanject Docs_
