Multi-environment vaults
A working pattern for teams that need to keep development, staging, and production secrets cryptographically separate, with FIPS 186-5 algorithm enforcement applied to all three.
The strategy in three lines
Section titled “The strategy in three lines”- One vault file per environment.
vault-dev,vault-staging,vault-prodare independent files in your application repo. Each has its own recipient list. The same secret key (DATABASE_URL) in each vault holds a different value. That is what makes the vaults environment-specific. - Different recipient subsets per vault. Developers are listed on dev (and maybe staging). Operations is listed on staging and production. Production typically excludes developers entirely. The union of identities across vaults equals your team; no single vault holds the union.
- One FIPS 186-5 policy fragment, machine-wide. Drop
00-corp-fips.yamlinto/etc/dotsecenv/policy.d/on every developer laptop, CI runner, and bastion. Allow-list fields union across fragments, so teams that want stricter limits add a50-team.yamlrather than rewriting the corporate baseline.
Identity patterns
Section titled “Identity patterns”dotsecenv stores one login.fingerprint per config file. Where you put the keyring (and which fingerprint you log in with) is what gives you per-environment isolation. Three real-world patterns, roughly in order of operational overhead:
Pattern 1: One machine per environment
Section titled “Pattern 1: One machine per environment”The cleanest separation. Each runner (CI job, deploy bastion, build node) holds exactly one secret key, in its only $GNUPGHOME. No switching needed at the shell level; the machine itself is the boundary. The Team Vault Setup tutorial documents the multi-vault --all flag pattern that makes this ergonomic for the initial bootstrap.
Pick this when you can dedicate hardware (or a runner image) per environment and you trust the machine boundary.
Pattern 2: GNUPGHOME-switching on a single machine
Section titled “Pattern 2: GNUPGHOME-switching on a single machine”A developer laptop holds multiple keyrings under different $GNUPGHOME paths. Shell aliases route every command to the correct keyring:
alias dse-dev='GNUPGHOME=~/.gnupg-dev dotsecenv'alias dse-staging='GNUPGHOME=~/.gnupg-staging dotsecenv'The runnable example (examples/06-multi-environment-fips/) uses this pattern because it works in one shell without faking a multi-machine deployment.
Pick this when developers need to operate against more than one environment but the org has not (yet) dedicated machines per env.
Pattern 3: One identity, recipient-set partitioning
Section titled “Pattern 3: One identity, recipient-set partitioning”A single person holds one identity. Different vaults list different subsets of the team on their recipient lists. Access is controlled by which vault you share a secret into, not by which key you hold. The Share a Secret tutorial walks the recipient-set mechanics.
Pick this when “different identities per environment” is overkill for your threat model and the per-vault recipient list is sufficient isolation. For example, when the same developer reads dev and staging from the same laptop.
The patterns are not mutually exclusive. Production CI runners often use pattern 1; the same team’s developer laptops use pattern 2.
FIPS 186-5 vs FIPS 140-3
Section titled “FIPS 186-5 vs FIPS 140-3”These are two different standards and dotsecenv treats them differently. Both matter for a regulated deployment.
| Standard | Governs | Where it lives |
|---|---|---|
| FIPS 186-5 | Approved digital-signature algorithms | approved_algorithms policy fragment + user config allow-list |
| FIPS 140-3 | Validated cryptographic module | Build-time flag GOFIPS140=v1.26.0 on go build |
FIPS 186-5 is the runtime concern. At every identity create, vault write, and policy validate call, dotsecenv refuses algorithms outside the union of the policy allow-list and the user’s own narrower list. The fragment in examples/06-multi-environment-fips/policy.d/00-corp-fips.yaml encodes the FIPS 186-5 approved set: ECC P-384/P-521, EdDSA Ed25519/Ed448, RSA ≥ 3072.
FIPS 140-3 is the build-time concern. The cryptographic primitives themselves (AES-256-GCM, SHA-384/512, the signature operations) come from the Go cryptographic module. Pinning that module to a validated version requires building the binary with GOFIPS140=v1.26.0. See Compliance: Module Locking for the full discussion of validated environments and the build flag.
A regulated deployment usually wants both: the policy fragment for algorithm enforcement, plus a validated binary pinned into every machine and runner image.
CI enforcement
Section titled “CI enforcement”The companion ci.yml in the example directory shows a GitHub Actions layout that:
- Validates the policy fragment once per push with
dotsecenv policy validate. The job fails fast on any of the distinct exit codes (1 for empty allow-list, 2 for malformed fragment, 8 for insecure permissions). - Fans out a matrix job over
dev/staging/production. Each matrix entry imports a different repo-secret-stored private key (DOTSECENV_GPG_PRIVATE_KEY_DEVand friends) and points at the matching vault file. - Runs
dotsecenv vault doctor --jsonper environment before anysecret getcall, so a fragmented or stale vault fails the job before it can leak partial state.
The pattern works the same on GitLab CI, Buildkite, CircleCI, and self-hosted Jenkins. The only platform-specific pieces are the secret-storage syntax and the runner image.
See also
Section titled “See also”- Example:
examples/06-multi-environment-fips/— runnable demo with the 3x3 access matrix. - Example:
examples/04-policy-directory/— the policy.d fragment story without the multi-environment overlay. - Tutorial: Team Vault Setup — multi-vault initialization with the
--allflag. - Tutorial: Share a Secret — recipient-set partitioning on a single vault.
- Concept: Compliance — full FIPS 186-5 and FIPS 140-3 discussion.
- Concept: Security Policies — policy.d directory contract, merge semantics, exit codes.