The Death of Keeper¶
How secrets work now — and what you never have to do again. 2026-07-03 · live today across the VerticalScope pod: dealdesk, contentgen, moderation, and all six MCP servers. This is the company standard going forward — every team is expected to switch immediately. Keeper AND PrivateBin both shut down 2026-07-17.**
What died¶
Until this week, getting an app running locally meant someone sending you a Keeper
export of env files, which you pasted into… wherever your copy of the app happened
to read from. There wasn't even a consistent file: some setups loaded .env.local,
some .env, some both in either order, in different directories per repo — so the
same export landed in different places on every laptop, secrets got mixed into
committed-defaults files, and two people with "the same" Keeper export could still
run different configs. No audit trail, no rotation story, silent drift between
laptops, and onboarding depended on finding the person with the freshest copy.
And that was just one laptop. Multiply it across four environments — local,
stg, uat, prod — and herding secrets became genuinely impossible: every env var existed
in up to four places (your .env.local, the Keeper export, per-env Key Vaults,
per-env Terraform), each rotated on its own schedule, none authoritative. Deploys
failed on values that were right locally and stale in stg; a promotion to uat
could silently run on last month's credential; and nobody could answer "where is
the current value of X?" without checking every copy. The failure mode wasn't
leaking secrets — it was never knowing which one was real.
And Keeper wasn't even alone: secrets also traveled by PrivateBin paste links. We were paying for two subscriptions whose shared job was moving credentials around outside any system of record. Both are going away.
That workflow is retired. No Keeper env files. No PrivateBin links. No pasting secrets. No asking a teammate for "the file."
What replaced it¶
Azure Key Vault is the single source of truth — the same per-org vault the
deployed apps already use (vsstguseast2kvvs01; dev = stg for VerticalScope).
One tool hydrates your local secrets from it:
az login # once per machine
# connect the VPN (the vault firewall requires it)
make env # hydrates .env.local from Key Vault. That's onboarding.
Re-run make env any time — after a rotation, on a new machine, whenever.
make env-check tells you if you've drifted without writing anything.
The three-file model (one loader, same everywhere)¶
The file chaos is gone too: every app loads env the same way, through the shared
loader in altaforge-libraries, with one precedence order — OS env → .env.local
→ .env — from a known directory per repo. Same files, same order, every repo,
every laptop, every environment.
| File | Committed? | Contains |
|---|---|---|
.env |
✅ yes | non-secret defaults — URLs, model ids, toggles, ports |
env.secrets.toml |
✅ yes | the secrets contract: every secret the app needs, mapped to its Key Vault name (replaces .env.example, which is deleted) |
.env.local |
❌ gitignored | your machine's copy — a fenced block the tool owns, plus your personal overrides |
The tool (altaforge.envsync, in altaforge-libraries) only ever rewrites the
fenced block in .env.local:
# --- BEGIN kv-managed (altaforge.envsync) — do not edit inside this block ---
# hydrated 2026-07-03T04:50:21Z from vsstguseast2kvvs01
ARIZE_API_KEY=ak-…
# --- END kv-managed ---
Anything you write outside the fence — port tweaks, tracing toggles, even a deliberate override of a managed secret — survives every re-hydration, byte for byte. (An override below the block wins, dotenv-style; the tool tells you it's shadowed rather than fighting you.)
The contract file¶
Each repo commits an env.secrets.toml — dealdesk's, in full:
[vault]
name = "vsstguseast2kvvs01"
[secrets]
ALTAFORGE_LLM_API_KEY = { kv = "af-llm-api-key", doc = "shared AF-LLM gateway key (all VS apps)" }
DECKGEN_LLM_API_KEY = { kv = "dd--deckgen-llm-api-key", doc = "deck-gen's own gateway key" }
ARIZE_API_KEY = { kv = "arize-api-key", doc = "Arize ingest key — org Vertical Scope / space dev" }
ARIZE_DEVELOPER_KEY = { kv = "shared--arize-developer-key", doc = "Arize GraphQL/API key" }
ANTHROPIC_API_KEY = { kv = "shared--anthropic-api-key", optional = true, doc = "direct Anthropic runs only" }
That's the whole surface: which vault, which env var, which KV secret, one doc
line each. Adding a secret = have it seeded into Key Vault + add one line here,
in a PR. Seeding goes through your Dev Manager or DevOps — developers hold
read-only access and cannot write to the vault. Reviewers see exactly what secrets an app consumes; nobody reviews values,
ever. optional = true means "skip quietly if not seeded yet"; type = "file"
writes the value to a gitignored path (for things like service-account JSON) and
points the env var at it.
Env vars are for things that vary¶
While you're writing the manifest, apply the same audit to .env itself.
Environment variables are for things that differ per environment; secrets
are for secrets. That's the whole list. A tuning parameter nobody has ever
changed, a "configurable" timeout with one value in every environment, a
feature flag that shipped years ago, a constant wearing an env-var costume —
none of that belongs in a file. Put it in code, as a named constant, next to
the thing that uses it.
The litmus test, per line:
Is someone really going to change this after the app is deployed?
If no — it's a constant; delete the line. If yes but it's the same in every
environment — it's still a constant. Only "yes, and per environment" earns a
line in .env, and only a credential earns one in the manifest. Every line you
don't have is a line that can't drift, can't be misconfigured in stg, and never
needs an answer to "wait, what does this one do?"
The rule that made the manifests small¶
Apps never hold MCP-domain credentials. A secret lives with the thing that uses it.
Auditing consumers shrank every manifest: dealdesk's HubSpot token and BigQuery
SA key were dead config — fetch_hubspot/fetch_bigquery call the hubspot/
vsanalytics MCPs, which hold their own credentials server-side (Key Vault via
Terraform in deployment, their own env locally). contentgen went further: the
in-process WordPress adapter was deleted, so all WordPress passwords moved
to the wordpress-mcp server — the app that writes articles holds zero WP
credentials. Even the demo WordPress now ships with the MCP
(client-verticalscope/mcp/wordpress/demo, make demo-up, non-secret
admin/admin default).
Final manifests: dealdesk 5 · contentgen 5 · moderation 3 — every entry verified app-consumed.
Naming, access, and failure modes¶
- Vault = org boundary. Same secret names exist in the AltaML and VS vaults
with different values (e.g. each org's own HubSpot portal key). Never copy a
credential across that boundary.
make env --vault <other>hydrates another flavor with no manifest change. - Names: pre-existing KV names are kept (
af-llm-api-key); new ones follow<app>--<kebab>orshared--<kebab>. List the vault before inventing a name — duplicates are how we got two dead secrets to clean up. - Access: developers get read-only (
Key Vault Secrets User). Seeding and rotating (envsync --seed) need write RBAC, which only Dev Managers and DevOps hold — ask them to seed; don't request write access. The tool confirms per secret, never prints a value, and never overwrites without--force. - Errors are instructions: not logged in → "run
az login"; firewall 403 → "connect to the VPN"; RBAC 403 → ask your Dev Manager to add you tokv-secrets-readers-<org>(e.g.kv-secrets-readers-verticalscope) — a one-command group add, no role assignments needed; missing secret → names the exact KV entry that needs seeding.
FAQ¶
Where did .env.example go? Deleted. Its two jobs are covered: non-secret
docs live in the committed .env; the secret list is env.secrets.toml.
I got "Access denied … Key Vault Secrets User"? You're not in the org's
reader group yet. Ask your Dev Manager to add you to kv-secrets-readers-<org>
— read-only, scoped to that org's pod, done in one command.
What if I need a secret that isn't in the vault? Ask your Dev Manager or DevOps to seed it, then add the manifest line in a PR. If it's for an MCP, it's the MCP's secret — it goes in the MCP's config, not your app's manifest.
Rotation? Rotate in Key Vault; everyone runs make env; deployed apps pick
it up via Terraform. One place.
I found a Keeper env file / an old PrivateBin link. Delete it. If it holds a value the vault doesn't have, that's a seeding gap — raise it with your Dev Manager or DevOps, don't re-circulate the file or the link.
This is the standard — adopt it now¶
VerticalScope is the reference implementation, apps and MCP servers alike: the apps hydrate their few own secrets from the vault; every MCP holds its own domain credentials server-side (Key Vault via Terraform when deployed, its own env locally) — HubSpot, BigQuery, WordPress, XenForo. No app carries a credential that belongs to a service it merely calls.
Every team switches to this immediately — Keeper and PrivateBin are both being shut down on 2026-07-17, two weeks from today. Any secret still living only in a Keeper export or a PrivateBin paste on that date is gone with it, so migrate now, not on day 13. The recipe for an existing repo:
uv add "altaforge[envsync]"(the tool ships in altaforge-libraries).- Write
env.secrets.toml— one line per secret, checking each has a real consumer in the app first (dead entries were half of what we cleaned up; anything an MCP or downstream service uses belongs to it, not to you). - Get the vault seeded — your Dev Manager or DevOps runs
envsync --seedfrom a known-good.env.local(list the vault first, reuse existing names, never overwrite blindly). - Load env through the shared loader in altaforge-libraries — this is
mandatory, not stylistic; it's what guarantees the same files, same
precedence (OS env →
.env.local→.env), same behavior everywhere. No hand-rolled dotenv calls, no pydanticenv_file:
from altaforge.core.sanity.env import load_env
load_env(BASE_DIR) # once, at startup, before anything reads os.environ
.env.example, point your README at the
three-line onboarding above:
.PHONY: env env-check
env: ## Hydrate .env.local secrets from Key Vault (az login + VPN)
uv run python -m altaforge.envsync
env-check: ## Report secret drift without writing (non-zero exit on drift)
uv run python -m altaforge.envsync --check
Run from the directory holding env.secrets.toml (prefix with
cd backend && if your app lives in a subdir; --manifest PATH overrides).
6. Delete your Keeper files.
Or skip the manual steps entirely: point Claude at this document, the
client-verticalscope* repos, and your repo — and say "Go!" The VS repos are
the worked examples of every step above; Claude will write your manifest
(checking real consumers), wire the make targets, kill your .env.example, and
hand your Dev Manager the exact seed command. Only the vault seeding itself
needs a human with write RBAC.
Think this is a big hurdle and you can't get it done in two weeks? Book 30 minutes with me. In that meeting you'll open the Claude Code CLI, I'll walk you through the steps, and we will be done inside the 30 minutes.