Architecture
Vault Structure
Section titled “Vault Structure”The vault is a JSONL (JSON Lines) file where each line is a self-contained JSON entry. The append-only format gives you safe concurrent git merges, efficient incremental updates, a complete audit trail, and simple conflict resolution.
File Format
Section titled “File Format”# === VAULT HEADER v1 ===Line 2: Header JSON with metadata and indexes# === VAULT DATA ===Line 4+: Identity entries, secret definitions, and secret valuesExample 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..."}}Header (Line 2)
Section titled “Header (Line 2)”The header provides fast lookup without scanning the entire file:
| Field | Purpose |
|---|---|
version | Format version (currently 1) |
identities | Array of [fingerprint, line] pairs (sorted by line number) |
secrets | Map of secret name to definition line and value lines |
Entry Types
Section titled “Entry Types”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": "..." }}Identity Model
Section titled “Identity Model”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)
Identity Fields
Section titled “Identity Fields”| Field | Description |
|---|---|
fingerprint | 40-character GPG key fingerprint |
uid | User ID (typically email) from GPG key |
algorithm | Key algorithm and size (e.g., rsa4096, ed25519) |
created_at | When identity was added to vault |
signature | Signature proving identity owns the key |
Identity Workflow
Section titled “Identity Workflow”1. User generates GPG key pair2. Public key exported and shared3. Vault admin imports public key to keyring4. Vault admin shares a secret: dotsecenv secret share SECRET_NAME FINGERPRINT5. Identity is auto-added to vault and can now receive shared secretsSecret Model
Section titled “Secret Model”A secret has two components: a definition (name, creator, timestamps) and one or more values (encrypted versions of the actual value).
Why split definition from value?
Section titled “Why split definition from value?”The split lets you keep multiple values per secret (version history), grant or revoke access on a per-value basis, and reconstruct who changed what when from the audit trail.
Namespaces
Section titled “Namespaces”Secrets can be namespaced using :: separator:
prod::DATABASE_URL → Production database URLstaging::DATABASE_URL → Staging database URLapi::JWT_SECRET → API service JWT secretNamespaces give you environment isolation, organizational clarity, and bulk operations like prod::*.
Encrypted Value Structure
Section titled “Encrypted Value Structure”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).
Decryption Flow
Section titled “Decryption Flow”1. Find entry where your fingerprint is in available_to2. Decrypt session key using your GPG private key3. Decrypt AES-256-GCM ciphertext using session key4. Verify signature against originator's public key5. Return plaintext valueAccess Control
Section titled “Access Control”The available_to field tracks who can decrypt each value:
{ "available_to": [ "E60A1740BAEF49284D22EA7D3C376348F0921C59", "ABC123DEF456789012345678901234567890ABCD" ]}Share Operation
Section titled “Share Operation”When you share a secret:
1. Decrypt value using your key2. Generate new random session key3. Re-encrypt value with new session key4. Encrypt session key for all recipients (including new one)5. Sign with your key6. Append new entry to vaultRevoke Operation
Section titled “Revoke Operation”When you revoke access:
1. Decrypt value using your key2. Generate new random session key3. Re-encrypt value with new session key4. Encrypt session key for remaining recipients (excluding revoked)5. Sign with your key6. Append new entry to vaultAppend-Only Design
Section titled “Append-Only Design”The vault never modifies existing entries. Every operation appends:
| Operation | Action |
|---|---|
| Add secret | Append definition + value entry |
| Update secret | Append new value entry |
| Share secret | Append new value entry with more recipients |
| Revoke access | Append new value entry with fewer recipients |
| Add identity | Append identity entry |
Benefits
Section titled “Benefits”- Full history is preserved as an audit trail
- Concurrent edits don’t conflict in git (each operation is its own line)
- A partial write can’t corrupt existing data
- Merging two vaults is concatenate-and-dedupe
Defragmentation
Section titled “Defragmentation”Over time, the vault accumulates obsolete entries. The vault doctor command checks vault health and can defragment it:
dotsecenv vault doctor # interactive, prompts before fixingdotsecenv vault doctor --fix # auto-fix without promptingAfter displaying health checks, doctor offers to defragment (or applies fixes automatically with --fix). Defragmentation removes old value entries (keeping the latest per recipient set), obsolete entries, and duplicates.
Configuration
Section titled “Configuration”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 identity (signed login block, populated by `dotsecenv login <FP>`)login: fingerprint: E60A1740BAEF49284D22EA7D3C376348F0921C59 added_at: "2026-04-25T12:00:00Z" hash: "<sha-256 of 'login:<added_at>:<fingerprint>'>" signature: "<hex-encoded detached GPG signature of hash>"
# Approved algorithms (minimum strength)approved_algorithms: - rsa:3072 - ecdsa:p384 - eddsa:ed25519Config Override
Section titled “Config Override”# CLI flagdotsecenv -c /path/to/config secret get KEY
# Environment variableDOTSECENV_CONFIG=/path/to/config dotsecenv secret get KEYMulti-Vault Support
Section titled “Multi-Vault Support”dotsecenv can work with multiple vaults:
vault: - name: personal path: ~/.config/dotsecenv/vault - name: work path: ~/work/secrets/vaultAccess by name or index:
dotsecenv secret get -v personal DATABASE_PASSWORDdotsecenv secret get -v 2 API_KEY # Index is 1-basedFile Permissions
Section titled “File Permissions”Recommended permissions:
| File | Mode | Notes |
|---|---|---|
| Config | 600 | User read/write only |
| Vault | 600 | User read/write only |
| GPG home | 700 | Directory: user only |
| Private key | 600 | User read/write only |
dotsecenv warns if permissions are too open.
Integration Points
Section titled “Integration Points”Vault files are designed for git:
# Add vault to gitgit add vaultgit commit -m "Add secrets"
# On conflict (rare due to append-only)git merge --strategy-option=ours # or theirs, then re-shareShell Plugins
Section titled “Shell Plugins”The .secenv file syntax:
# Load secret with same name as variableDATABASE_PASSWORD={dotsecenv}
# Load specific secretAPI_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.shData Flow Summary
Section titled “Data Flow Summary”┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 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.