Skip to content

Architecture

The vault is a JSONL (JSON Lines) file where each line is a self-contained JSON entry. This append-only format enables:

  • Safe concurrent git merges
  • Efficient incremental updates
  • Complete audit trail
  • Simple conflict resolution
# === VAULT HEADER v1 ===
Line 2: Header JSON with metadata and indexes
# === VAULT DATA ===
Line 4+: Identity entries, secret definitions, and secret values

Example structure:

{"version":1,"identities":[["ABC123",4],["DEF456",5]],"secrets":{"DATABASE_URL":{"secret":6,"values":[7]}}}
{"type":"identity","data":{"fingerprint":"ABC123","uid":"alice@example.com","algorithm":"RSA","algorithm_bits":4096}}
{"type":"identity","data":{"fingerprint":"DEF456","uid":"bob@example.com","algorithm":"EdDSA","curve":"Ed25519"}}
{"type":"secret","data":{"key":"DATABASE_URL","signed_by":"ABC123","added_at":"2025-01-01T00:00:00Z"}}
{"type":"value","secret":"DATABASE_URL","data":{"available_to":["ABC123","DEF456"],"value":"encrypted..."}}

The header provides fast lookup without scanning the entire file:

FieldPurpose
versionFormat version (currently 1)
identitiesArray of [fingerprint, line] pairs (sorted by line number)
secretsMap of secret name to definition line and value lines

Identity Entry:

{
"type": "identity",
"data": {
"fingerprint": "E60A1740BAEF49284D22EA7D3C376348F0921C59",
"uid": "alice@example.com",
"algorithm": "rsa4096",
"created_at": "2025-01-01T00:00:00Z",
"signature": "..."
}
}

Secret Definition:

{
"type": "secret",
"data": {
"name": "DATABASE_PASSWORD",
"created_by": "E60A1740BAEF49284D22EA7D3C376348F0921C59",
"created_at": "2025-01-01T00:00:00Z",
"namespace": null,
"signature": "..."
}
}

Secret Value:

{
"type": "value",
"secret": "DATABASE_PASSWORD",
"data": {
"available_to": ["E60A1740...", "DEF456..."],
"value": "base64-encoded-encrypted-blob",
"created_by": "E60A1740...",
"created_at": "2025-01-01T00:00:00Z",
"signature": "..."
}
}

An identity represents a user who can access secrets. Each identity is:

  • A GPG key (public + private)
  • Identified by fingerprint (40-character hex)
  • Stored in the vault (public key info only)
FieldDescription
fingerprint40-character GPG key fingerprint
uidUser ID (typically email) from GPG key
algorithmKey algorithm and size (e.g., rsa4096, ed25519)
created_atWhen identity was added to vault
signatureSignature proving identity owns the key
1. User generates GPG key pair
2. Public key exported and shared
3. Vault admin imports public key to keyring
4. Vault admin shares a secret: dotsecenv secret share SECRET_NAME FINGERPRINT
5. Identity is auto-added to vault and can now receive shared secrets

A secret has two components:

  1. Secret Definition — Name, creator, timestamps
  2. Secret Values — Encrypted versions of the actual value

This separation enables:

  • Multiple values per secret (version history)
  • Different access per value (sharing/revoking)
  • Audit trail of who changed what when

Secrets can be namespaced using :: separator:

prod::DATABASE_URL → Production database URL
staging::DATABASE_URL → Staging database URL
api::JWT_SECRET → API service JWT secret

Benefits:

  • Environment isolation
  • Organizational clarity
  • Bulk operations (prod::*)

dotsecenv uses RFC 9580 (OpenPGP) with mandatory AEAD encryption. Each secret value is a blob containing:

┌─────────────────────────────────────────┐
│ Encrypted Value Blob │
├─────────────────────────────────────────┤
│ AES-256-GCM nonce (96 bits) │
│ AES-256-GCM ciphertext │
│ AES-256-GCM auth tag (128 bits) │
│ GPG-encrypted session key (per-user) │
│ Detached GPG signature (FIPS 186-5) │
└─────────────────────────────────────────┘

The symmetric encryption follows NIST SP 800-38D (GCM mode), while signatures comply with FIPS 186-5 (Digital Signature Standard).

1. Find entry where your fingerprint is in available_to
2. Decrypt session key using your GPG private key
3. Decrypt AES-256-GCM ciphertext using session key
4. Verify signature against originator's public key
5. Return plaintext value

The available_to field tracks who can decrypt each value:

{
"available_to": [
"E60A1740BAEF49284D22EA7D3C376348F0921C59",
"ABC123DEF456789012345678901234567890ABCD"
]
}

When you share a secret:

1. Decrypt value using your key
2. Generate new random session key
3. Re-encrypt value with new session key
4. Encrypt session key for all recipients (including new one)
5. Sign with your key
6. Append new entry to vault

When you revoke access:

1. Decrypt value using your key
2. Generate new random session key
3. Re-encrypt value with new session key
4. Encrypt session key for remaining recipients (excluding revoked)
5. Sign with your key
6. Append new entry to vault

The vault never modifies existing entries—only appends:

OperationAction
Add secretAppend definition + value entry
Update secretAppend new value entry
Share secretAppend new value entry with more recipients
Revoke accessAppend new value entry with fewer recipients
Add identityAppend identity entry
  1. Audit trail — Full history preserved
  2. Git-friendly — No conflicts from concurrent edits
  3. Crash-safe — Partial writes don’t corrupt existing data
  4. Simple merge — Just concatenate and dedupe

Over time, the vault accumulates obsolete entries. The vault doctor command checks vault health and can defragment it:

Terminal window
dotsecenv vault doctor

After displaying health checks, doctor offers to defragment if needed. This removes:

  • Old value entries (keeps latest per recipient set)
  • Obsolete entries
  • Duplicate entries

Configuration lives at ~/.config/dotsecenv/config (or $XDG_CONFIG_HOME/dotsecenv/config):

# Vault file path(s)
vault:
- ~/.config/dotsecenv/vault
- ~/work/project/secrets/vault
# Active user fingerprint
fingerprint: E60A1740BAEF49284D22EA7D3C376348F0921C59
# Approved algorithms (minimum strength)
approved_algorithms:
- rsa:3072
- ecdsa:p384
- eddsa:ed25519
# Strict mode: treat warnings as errors
strict: false
Terminal window
# CLI flag
dotsecenv -c /path/to/config secret get KEY
# Environment variable
DOTSECENV_CONFIG=/path/to/config dotsecenv secret get KEY

dotsecenv can work with multiple vaults:

vault:
- name: personal
path: ~/.config/dotsecenv/vault
- name: work
path: ~/work/secrets/vault

Access by name or index:

Terminal window
dotsecenv secret get -v personal DATABASE_PASSWORD
dotsecenv secret get -v 2 API_KEY # Index is 1-based

Recommended permissions:

FileModeNotes
Config600User read/write only
Vault600User read/write only
GPG home700Directory: user only
Private key600User read/write only

dotsecenv warns if permissions are too open.


Vault files are designed for git:

Terminal window
# Add vault to git
git add vault
git commit -m "Add secrets"
# On conflict (rare due to append-only)
git merge --strategy-option=ours # or theirs, then re-share

The .secenv file syntax:

Terminal window
# Load secret with same name as variable
DATABASE_PASSWORD={dotsecenv}
# Load specific secret
API_KEY={dotsecenv/prod::API_KEY}
# Plain values (not encrypted)
DATABASE_HOST=localhost
# GitHub Actions example
- name: Setup GPG
run: echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --import
# Install dotsecenv in your GitHub workflow
- uses: dotsecenv/dotsecenv@v0
- name: Get secrets
run: |
export API_KEY=$(dotsecenv secret get API_KEY)
./deploy.sh

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User Input │ ──▶ │ dotsecenv │ ──▶ │ Vault │
│ (stdin) │ │ CLI │ │ (JSONL) │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ GPG │
│ (Encrypt/ │
│ Decrypt) │
└─────────────┘

All cryptographic operations go through GPG. dotsecenv orchestrates the workflow and manages the vault format.