Skip to content

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 get and set.
  • 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.