Security Notes
Threat model: the server is trusted
Encryption at rest protects stored secrets when the data directory leaks — a stolen backup or disk is useless without each user's SSH agent. It does not protect against a malicious or compromised server:
- The server handles plaintext secrets in memory during every
getandset. - Key derivation happens server-side, so a compromised server could retain derived keys.
- Agent forwarding (
-A) lets the server request signatures from your agent for the duration of the session. A malicious server could use that to authenticate as you to other hosts.
Run keyhole only on a host you trust. Consider loading your key with ssh-add -c, so your agent asks for confirmation on every signature request.
Ed25519 only
RSA and ECDSA keys are rejected at authentication. Ed25519 signatures are deterministic, which is required for reproducible key derivation. RSA signatures are probabilistic — the same input produces different signatures each time — so they cannot be used to derive stable encryption keys.
Path isolation
Secrets are namespaced per user; one user cannot access another's secrets. Paths containing .., ., or any component starting with . are rejected. Control characters and invalid UTF-8 are rejected in paths and vault names, preventing terminal escape injection through shared vault listings. Paths starting with __ are reserved for internal key-derivation namespaces.
Secret size limit
Secrets are capped at 64 KB.
Agent required for get/set
If the SSH agent is not forwarded, the server returns an error immediately rather than hanging.
No shell
The server accepts only structured commands; there is no shell access.
Vault key wrapping
Vault keys are individually wrapped per member using their SSH agent signature, so revoking a member does not require re-encrypting all vault secrets.
Revocation and key rotation
vault revoke removes access immediately but does not change the vault key: a revoked member who saved the key while they had access could still decrypt vault ciphertexts obtained out-of-band (for example from a leaked backup). Run vault rotate after revoking to re-encrypt the vault under a fresh key — see Vaults.
Two-phase vault invite
Invite tokens wrap the vault key with a temporary HKDF-derived key; on accept, the vault key is re-wrapped with the member's agent key. The HKDF info parameter includes the vault name and target username for domain separation. See Encryption for details.
Invite expiration
Both user invite codes and vault invite tokens expire after 72 hours. Expired invites are rejected, as are invite files with unreadable timestamps. User invite codes are consumed atomically using a filesystem rename to prevent race conditions.
Invite tokens in command arguments
register and vault accept take their codes as command arguments, which typically land in your local shell history. Codes are single-use and short-lived, but if that matters in your environment, prefix the command with a space (with HISTCONTROL=ignorespace) or clear your history afterwards.
Username enumeration
Authentication never reveals whether a username exists — unknown users and wrong keys are treated identically, and registration errors are uniform. One deliberate exception: the holder of a valid, unconsumed invite code can detect whether a username is taken by attempting to register it.
Server secret
The server secret must be at least 64 characters. The server will refuse to start with a shorter value. On first run, keyhole auto-generates a 64-character alphanumeric secret; if you provide your own via the config file, it must meet the same minimum length.
DEPRECATED
KEYHOLE_SERVER_SECRET is deprecated and will be removed in a future release. Environment variables are visible through /proc, ps, and are inherited by child processes. Use the server_secret field in your HCL config file (with 0600 permissions) or let keyhole auto-generate the secret file in the data directory instead.
DANGER
Store server_secret somewhere safe and separate from the data directory. Without it, every stored secret is permanently inaccessible.
