Skip to content
Danylo Ohurtsov

Project

Secrets Manager

A self-hosted secrets manager for teams. Credentials stay on infrastructure you control, every value is encrypted and versioned, and every access leaves a record.

Why it exists

Every team accumulates credentials — database passwords, API keys, service tokens. They tend to drift into the wrong places: a .env file passed over chat, a shared password vault never designed for machines, a spreadsheet nobody has opened in a year. The failure is rarely dramatic. It's quieter than that: at some point, no one can say with confidence who has access to what, or who last read a production credential.

Secrets Manager exists to make those questions answerable. It's self-hosted, so the credentials never leave infrastructure the team already owns, and open source, so the security model can be read rather than taken on trust.

What a team gets from it

  • Credentials live on your own servers — no third party holds them, nothing to license
  • Every value is versioned, so a bad change is one rollback away
  • Access is granted per environment — staging access is not production access
  • Automation gets its own accounts and tokens, kept separate from people's
  • Every time a secret is read, it's recorded: who, what, and when
  • Permission to manage a project is separate from permission to read its secrets

How secrets are stored

Someone holding a full copy of the database still cannot read a single value.

Every secret value gets its own randomly generated 32-byte data key. The value is encrypted with that key using AES-256-GCM, and the data key is then itself encrypted with a master key held outside the database. Plaintext is never written to disk.

The part worth pausing on is what each ciphertext is bound to. The authenticated data includes the organization it belongs to, which ties the encrypted value cryptographically to its tenant. Move a ciphertext into a different organization, project, environment, or version context and integrity verification fails outright — no plaintext is returned. Cross-tenant access isn't a permission check that has to be written correctly every time; it's a cryptographic dead end.

Rolling a secret back doesn't copy the old ciphertext forward. The server decrypts the target version and writes a fresh one under the current context, so history stays honest. Master keys rotate through a dedicated admin operation that re-wraps the data keys without touching the encrypted values themselves.

Who can see what

Running a project and reading its credentials are two different permissions, and the system treats them that way.

An organization owner can create projects, add environments, invite people, and issue access. What an owner cannot do is reveal a secret value — that takes an explicit grant, issued as its own deliberate decision. The same holds at the very top: the platform administrator runs the instance, not a skeleton key to every team's data. There is no bypass built into the hierarchy.

Grants are scoped either to a whole project or to a single environment, and carry individual capabilities for revealing, creating, updating, deleting, and rolling back. A developer can see which secrets exist without seeing what they contain — enough to work, not enough to walk away with production access.

Standing up to attack

Password-guessing at scale is the ordinary way accounts fall, so the defence works in two directions at once.

Login and signup are rate-limited by IP address and, independently, by the email being attempted. One account under attack stays throttled even when the attempts come from a hundred different addresses; a single address stays throttled even as it cycles through different emails.

Because the counter keys on what was submitted rather than on a database lookup, real and non-existent accounts land in the same bucket — so the response never reveals whether an account exists. The counters run in Redis as an atomic operation shared across every server instance. If Redis goes down, throttling doesn't quietly switch off: it falls back to per-process counting and says so in the logs. The limits stay enforced, just no longer cluster-wide.

A record that can't be quietly skipped

An audit log is only worth something if it cannot fail in silence.

Security-critical actions are audited fail-closed. The audit entry is written inside the same database transaction as the action itself, so if the record can't be written, the change is rolled back along with it.

Revealing a secret is logged before the value is decrypted. If the audit log is unavailable, no plaintext is returned and the request fails. An action that cannot be recorded does not happen.

Using secrets without copying them around

Developers and CI pipelines can run against real credentials without ever keeping a copy.

The CLI fetches an environment's secrets and injects them directly into a process:

secrets run -e <environment> -- npm run start

No file is written, nothing lands in a repository, and revoking the token cuts access immediately. Where the caller lacks permission for a particular secret, it's skipped with a warning — rather than quietly injecting an empty value and letting the application fail somewhere further downstream. Local CLI configuration is written with restrictive permissions, reapplied on every write so a pre-existing loose file gets tightened rather than trusted.

Built like production software

Backend
NestJS 11, Prisma 7, PostgreSQL 16, Redis 7, AES-256-GCM envelope encryption, helmet with an explicit, narrow Content-Security-Policy.
Frontend
React 19, Vite, TypeScript, Tailwind CSS v4, Radix primitives.
CLI
TypeScript, commander, Node child processes.

Every change runs through continuous integration: unit tests, linting, and builds across all three packages, plus an end-to-end suite against isolated PostgreSQL and Redis instances. The encryption key used in that run is generated inside the job and never printed to a log.

Known limits and what's next

Stated openly, because a security tool that only advertises its strengths is harder to trust than one that names its edges.

Browser sessions to cookies.
Session tokens currently live in browser storage; moving them to httpOnly, Secure, SameSite cookies removes a class of client-side exposure.
Soft-delete policy.
Recoverable deletion and compliance-grade purge are presently the same operation. They shouldn't be.
Redis hardening.
Redis backs both the token cache and rate-limit storage, so a production deployment wants authentication, TLS, and network isolation around it.
Security-focused end-to-end suite.
Dedicated coverage for tenant isolation, audit scoping, service account boundaries, session revocation, and rate limiting.
Sourcegithub.com/DanyloOhurtsov/secrets-managerSelf-hostedOpen source