66. pepsi-keys

Manage the end-to-end key store.

66.1. Role

pepsi-keys is the operator’s tool for Pepsi’s end-to-end cryptography key store: pepsi.crypto_identity (the key pairs of the addresses we serve, private half included and wrapped at rest), pepsi.peer_key (remote correspondents’ cached public keys and certificates) and pepsi.ca_trust (the CA certificates inbound S/MIME chains are validated against). It connects through the shared [pepsi-postgres] section and reads the [pepsi] CRYPTO_* options and the [pepsi-crypto] section. It is not a stage. Reference: pepsi-keys(1); the design is explained in Key management.

It is one of the standalone binaries, outside the unified pepsi executable, for two reasons: it is the only program that generates key material (folding it in would link key generation and certificate issuance into every stage worker), and a site may install it setuid so users can manage their own identities — a bit a shared multi-call binary cannot carry.

66.2. Features

  • identity list / show — what material exists for an address: protocol, purpose, status, whether it is the primary for its capability, whether it is published, whether the private half is present, purged or was never held (client for the user’s own MUA key, whose private half lives in their mail client), and — in show — the custody, whether it is the address’s public face and its key-server state.

  • identity generate — create key material and store it. OpenPGP produces one transferable key (Ed25519 by default, RSA optional); S/MIME produces a signing and an encryption certificate, each self-signed for immediate use, or one certificate with both key usages under CRYPTO_SMIME_SHARED_KEY. --csr additionally emits a PKCS#10 request over each key, so a real CA can issue a replacement without re-keying.

  • identity import / export / csr — store material generated elsewhere (an OpenPGP public key imported without --private is registered as the user’s own MUA key), write material back out in its own binary form (--pem for X.509), or build a certificate request over an identity’s existing key.

  • identity revoke / retire / forget-private — the lifecycle. Revocation and the periodic retire sweep destroy private signing material and retain decryption material; revoke --upload publishes a revocation certificate to the key server first, since that must happen while the signing key still exists. forget-private is the explicit, confirmed (-y to skip the prompt) way to destroy a decryption key.

  • identity set-primary / publish / unpublish / delete — choose the material used for new messages, opt an address in or out of publication (publish can also upload to a key server), or remove a row outright.

  • peer list / show / import / pin / unpin / refresh / prune / remove — the cached peer keys. refresh re-validates keys that already exist and never discovers one for an address that has none; prune deletes expired keys and aged-out discovery requests (--dry-run to look first). show applies the matching rule (an exact address always beats a hand-entered *@domain fallback); import stores a key with source manual, the only source permitted to name a whole domain.

  • ca list / add / enable / disable / remove — the S/MIME trust anchors. A certificate without basicConstraints CA:TRUE is refused, since no chain could ever end at it. Being accepted here is not the same as being usable: an anchor’s own nameConstraints, pathLenConstraint and policy constraints bind every chain through it (RFC 5937), and a CA carrying a critical extension Pepsi cannot process is skipped during path building (RFC 5280 §4.2).

  • wrap rotate — re-wrap every stored private key under a new key-encryption key, incrementally and with --dry-run to look first.

  • dns — print the OPENPGPKEY (RFC 7929) and SMIMEA (RFC 8162) records that publish an address, under the hashed owner names those standards define. Only worth publishing in a DNSSEC-signed zone, which the output says.

66.3. Privilege model

Reaching a private key takes two independently guarded things: the pepsi-crypto database role — the only one granted the crypto_identity.private_wrapped column, every other service role holding a column-level grant that omits it — and the key-encryption key, which lives only in secrets.d/pepsi-crypto.secret. A stolen database yields ciphertext; the secret alone yields nothing.

pepsi-keys assumes that role only for the duration of each database call: started as root it becomes the account (root has no database role of its own) and returns afterwards. PostgreSQL peer authentication keys off the effective uid, not the gid, so a setgid bit would grant the group that guards the secret fragment but not the database identity.

The binary ships without a setuid bit, unlike pepsi-whitelist: one program able to open every private key in a deployment is a much larger prize than a single whitelist table. A site that wants users to manage their own identities installs it setuid pepsi-crypto; identities are owned by the passwd login their address resolves to, and an unprivileged caller of the setuid binary may see and change only their own. Without the bit every caller connects as themselves and the database grants alone decide, so making it setuid is a site decision rather than a code change. Peer keys and trust anchors are deployment-wide and operator-only, listing included.

66.4. See also

Key management, pepsi-setup, pepsi-whitelist, pepsi-settings, Architecture, pepsi-keys(1), pepsi.conf(5).