Skip to content

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> or shared--<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 to kv-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:

  1. uv add "altaforge[envsync]" (the tool ships in altaforge-libraries).
  2. 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).
  3. Get the vault seeded — your Dev Manager or DevOps runs envsync --seed from a known-good .env.local (list the vault first, reuse existing names, never overwrite blindly).
  4. 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 pydantic env_file:

from altaforge.core.sanity.env import load_env

load_env(BASE_DIR)  # once, at startup, before anything reads os.environ
5. Add the make targets, delete .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.