Skip to content

Architecture

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.

# === 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: a definition (name, creator, timestamps) and one or more values (encrypted versions of the actual 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.

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

Namespaces give you environment isolation, organizational clarity, and bulk operations like 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. Every operation 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
  • 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

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

Terminal window
dotsecenv vault doctor # interactive, prompts before fixing
dotsecenv vault doctor --fix # auto-fix without prompting

After 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 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:ed25519
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.