Security Design
This document describes PocketProtector’s security architecture, threat model, and considerations for agent and automation use.
Theory of operation
PocketProtector’s protected.yaml file consists of key domains at
the root level. Each domain stores data encrypted by a keypair:
The public key is stored in plaintext, so that anyone may encrypt and add a new secret.
The private key is encrypted with each owner’s passphrase. The owners are known as key custodians, and their private keys are protected by passphrases.
This is a two-key encryption scheme built on NaCl’s SealedBox
(Curve25519 + XSalsa20-Poly1305). Passphrases are processed through
Argon2id key derivation.
File structure
The protected.yaml file is a self-contained YAML document:
dev:
meta:
public-key: <base64>
owners:
alice@example.com: <encrypted-private-key>
tom@example.com: <encrypted-private-key>
secret-db-password: <encrypted-value>
secret-api-key: <encrypted-value>
prod:
meta:
public-key: <base64>
owners:
tom@example.com: <encrypted-private-key>
secret-db-password: <encrypted-value>
key-custodians:
alice@example.com:
pwdkm: <base64>
tom@example.com:
pwdkm: <base64>
audit-log:
- "2026-01-01T00:00:00Z -- created key custodian tom@example.com"
- "2026-01-01T00:00:01Z -- created domain dev with owner tom@example.com"
All state PocketProtector needs to operate is included in this file. The
file is designed for git diff, git blame, and git log.
audit-log entries are unauthenticated free-form strings: anyone
with write access to protected.yaml can add, edit, or remove
them. list-audit-log is informational only; VCS history
(git log protected.yaml) is the authoritative record of who
changed what (AUDIT-INTEG-001).
Cryptographic details
Key derivation: Argon2id via PyNaCl. Three modes:
hard(default):OPSLIMIT_SENSITIVE,MEMLIMIT_MODERATE– ~0.8s, 256 MBfast:OPSLIMIT_INTERACTIVE,MEMLIMIT_INTERACTIVE– ~0.1s, 64 MBraw: No KDF. A 256-bit random key is used directly. Format:P<64 hex chars>P. Format validation checks shape, not entropy; key strength depends on its randomness source, so use the built-inos.urandom-backed generator rather than hand-crafted hex.
Stored v1 KDF costs are checked when the file is loaded, before any key derivation. The maximum
opslimitis 4 and the maximummemlimitis 1,073,741,824 bytes (1 GiB), matching PyNaCl’sargon2id.OPSLIMIT_SENSITIVEandargon2id.MEMLIMIT_SENSITIVE. Exceeding either ceiling raisesPPError; costs are never silently clamped because that would change the derived key.For a trusted file that intentionally uses higher costs, set
PPROTECT_TRUST_KDF_PARAMS=1before loading it. Only1,true, oryes(case-insensitive) enables trusting; any other value, including0andfalse, leaves the ceilings active for both CLI and library callers. Leave it unset for untrusted files, and ensure sufficient memory and CPU are available before deriving a key with trusted higher costs.Salt construction (accepted as-is): the effective Argon2id salt for each custodian is
sha512(salt ‖ custodian name)[:16]– 16 bytes, the RFC 9106 salt width, carrying 64 random bits (os.urandom(8)) plus the per-custodian name. Salts require uniqueness, not secrecy; this construction is deterministic per custodian name by design, and the audit finding (KDF-SALT-001) is accepted rather than changed.Encryption: NaCl
SealedBox(Curve25519 public key encryption)Secret storage: Each secret is encrypted with the domain’s public key. Only domain owners (who hold the private key, encrypted under their passphrase) can decrypt.
Versioned binary format: Key material is prefixed with a version byte (v0, v1, v2) for forward compatibility. Custodian
pwdkmpayloads must be exactly 41 bytes for v0/v2 or 49 bytes for v1 after Base64 decoding; both truncated payloads and trailing bytes raisePPError.
Threat model
Assumed attacker capability: Read access to protected.yaml. This
could happen because a developer’s laptop is compromised, GitHub
credentials are compromised, or git history is accidentally pushed to a
public repo.
What the attacker gets: Environment and secret names, and which secrets are used in which environments. The names and structure are visible; values are not.
What the attacker cannot do: Decrypt any secret values without the correct custodian passphrase.
Write access: PocketProtector does not provide write protection.
Write access control is delegated to the VCS (git permissions, signed
commits, branch protection). Neither the file nor individual entries are
signed, since the security model assumes an attacker does not have write
access. Note that write subcommands (add-secret, update-secret,
rm-secret, rm-domain, rm-owner) require no credentials by
design; an attacker with write access can substitute a domain’s
meta.public-key so that secrets added afterwards encrypt to the
attacker (existing secrets stay safe). See the README FAQ,
“Securing Write Access” (UNAUTH-MUT-001, PUBKEY-SUB-001).
The file is designed for use alongside VCS tools:
git log protected.yaml– view change historygit blame protected.yaml– see who changed whatSigned commits are a particularly good complement
Supply chain
Direct runtime dependencies are attrs, PyNaCl, ruamel.yaml, schema, and
face, plus vendored boltons code (pocket_protector/_vendor,
BSD-licensed, copied verbatim from boltons 25.0.0 – the exact
artifact an OSTIF audit reviewed). pocket_protector, face, and boltons
share a maintainer; downstream users should weigh that trust decision
(DEP-FACE-001). CI actions are SHA-pinned and the lockfile records
package hashes. face’s --flagfile (an arbitrary file read,
pre-auth) is disabled at the root Command; a face upgrade changing
that default must be caught in review.
Passphrase security by domain
Passphrase security will depend on the domain:
A development domain may set the passphrase as an environment variable or hardcode it in a configuration file. The risk is low – dev secrets are typically non-sensitive.
A production domain would likely require manual entry of an authorized release engineer, or use AWS/GCP/Heroku key management solutions to inject the passphrase.
This layered approach lets teams balance security with convenience for each environment.
Agent and automation security
PocketProtector is commonly used in CI/CD pipelines and increasingly alongside AI coding agents. In these contexts, secret hygiene matters more than usual.
PPROTECT_ENABLE_DEBUG (1/true/yes, case-insensitive)
drops the CLI into pdb.post_mortem() on unhandled exceptions. It
is for interactive debugging only; never enable it in automation, where
it would hang the process at a debugger prompt.
In-repo agent-instruction files (.omp/skills/**) are maintainer
tooling that runs shell commands at release time. Treat changes to them
as release-critical in code review (SUPPLY-OMP-001).
Credential injection: safest to weakest
pprotect exec (safest): Decrypts a domain and injects secrets as env vars into a child process. The custodian passphrase is scrubbed from the child environment. Secrets exist only in the child process memory, never on disk or in the parent env.
pprotect exec --domain prod -- ./myapp --flag arg
–passphrase-file from a restricted mount: Store the passphrase on a tmpfs or Docker secret mount with
0400permissions. Keeps the passphrase off the command line and out of the process environment.pprotect decrypt-domain prod --passphrase-file /run/secrets/pp_pass
PPROTECT_PASSPHRASE env var (simplest): The classic option but not the safest. Readable by any subprocess, including AI agents, build scripts, and debug tooling.
Security note on pprotect exec
An agent or process that can run arbitrary commands could call
pprotect decrypt-domain directly. exec reduces accidental
exposure (logged output, env dumps, process listings), not adversarial
exfiltration by a fully compromised agent. Defense in depth still
applies: restrict filesystem access, use scoped custodians, and audit
the protected.yaml change log.
What PocketProtector is not
PocketProtector manages static deploy-time secrets – database passwords, API keys, TLS certificates. It is not a runtime credential manager.
Explicit non-goals:
Network daemon / SaaS mode – serverless is the value prop
Time-limited credentials – no clock-based expiry; use
pprotect execto limit secret lifetime to a processPer-secret access control – domains are the access boundary
MCP server mode – use
pprotect execto inject secrets into MCP server processes at startupOutput redaction – PocketProtector does not scan subprocess output for leaked secret values
Repo leak scanning – PocketProtector does not scan source files for accidentally committed secrets