MSB
All posts

DevOps Secrets Manager: One Place for the Keys

A secrets platform with envelope encryption, per-vault roles, and a CLI that injects credentials into any command, reviewed for what it gets right and where the pieces still disagree.

The problem it goes after

Every team eventually has credentials living in the wrong places: a database password pasted into a chat thread, a production .env file passed around by hand, an API key that nobody remembers rotating. DevOps Secrets Manager is my attempt at the tool that replaces that habit. It gives a team one place to store secrets, organized the way deployments actually are, with a clear record of who read what.

The work is split into vaults, usually one per service, and each vault holds environments such as development, staging, and production. A secret is a key and a value inside one environment, so the same DATABASE_URL can point somewhere different in each.

How it works

There are three pieces around a PostgreSQL database. A Go REST API owns the data and every security decision. A React and TypeScript web console is where people browse vaults, add secrets, and manage who has access. A Rust CLI brings the same vaults to the terminal, where most of the actual use happens.

The encryption uses an envelope design. A master key, supplied to the API as an environment variable and never stored in the database, encrypts a separate data key for each vault. That vault key encrypts every secret value with AES-256-GCM. The database only ever holds ciphertext, and a single vault's key can be replaced without touching every other vault.

Access is role-based. Each person has one of five roles on a vault: owner, admin, developer, on-call, or viewer. Viewers can see which secrets exist but can't read their values; on-call engineers can read values but not change them; only owners can delete a vault. Reading a value is deliberately its own action in the interface, with a notice that it's being logged and a view that hides the value again after thirty seconds.

The CLI is what makes it practical day to day. It can list vaults and environments, set a value, pull an environment into a .env file, or run a command with the secrets injected directly into that process's environment, so they never have to touch disk at all.

What works well

  • The encryption boundary is in the right place. The API decrypts only on an explicit request, and what's stored is useless without the master key, which lives outside the database.
  • Revealing is a considered action rather than a default. The audit notice and auto-hide make reading a production credential feel like the deliberate step it is.
  • Sessions are handled carefully for a personal project: short-lived access tokens, refresh tokens that are replaced every time they're used, and bcrypt for passwords.
  • Running a command with injected secrets is the feature that would actually change a team's habits, since it removes the reason to keep a local .env copy at all.

Where it falls short

Setting it up for a screenshot surfaced the project's weakest point: the pieces had drifted apart. The vault handlers depended on a membership table that no database migration created, so vaults couldn't be created or listed until I added it. The Docker image referenced a configuration file that didn't exist, and environment variables set by Docker Compose weren't being read. Smaller mismatches between what the API returned and what the web console expected showed up as empty counts and an "Invalid Date". I've since fixed those, but they came from the same gap.

That gap is still visible in a few places. The audit log records every creation, reveal, and update correctly, but the web console expects a paginated, enriched response the API doesn't produce, so the audit page and the dashboard's recent activity stay empty. There's no way yet to invite a teammate into an organization, which limits vault membership to people already in it. And the README says tokens are signed with RS256, while the code uses a shared-secret HS256 signature.

The root cause is a missing contract. The API has an OpenAPI description, but nothing checks the web console or the CLI against it, and the API handlers have almost no tests beyond the encryption code. With three clients built separately, that's exactly how a missing table can slip through.

What I'd do next

The first step is to make the contract enforceable: generate the web console's API types from the OpenAPI specification and add integration tests that run the real API against a real database, so a missing migration fails a build instead of a demo. After that come the features a team would need before trusting it with production credentials: organization invites, an audit view that works end to end, and key rotation for the master key.

Key takeaways

  • Envelope encryption keeps a leaked database from being a leaked set of credentials, and makes per-vault keys cheap.
  • Making "reveal" an explicit, audited action changes how casually secrets get read.
  • Three clients over one API only work if the API contract is written down and tested, not assumed.