Skip to main content

rosec SSH Agent

rosecd includes a built-in SSH agent and a FUSE virtual filesystem that expose SSH keys from your unlocked vault — without ever writing private key material to disk.

Overview

When rosecd starts, it:

  1. Binds an SSH agent socket at $XDG_RUNTIME_DIR/rosec/agent.sock
  2. Mounts a FUSE filesystem at $XDG_RUNTIME_DIR/rosec/ssh/

The FUSE filesystem is read-only and entirely in-memory. Its contents update automatically when the vault is re-synced or a provider is locked/unlocked. No private keys are ever written to disk.

Both FUSE mounts can be disabled independently via the [service] section of config.toml:

[service]
ssh_fuse = false # disable the SSH FUSE mount at $XDG_RUNTIME_DIR/rosec/ssh/
totp_fuse = false # disable the TOTP FUSE mount at $XDG_RUNTIME_DIR/rosec/totp/

Both default to true. Disabling ssh_fuse also disables the generated config.d/ SSH snippets, since they reference paths inside the mount.

TOTP has its own FUSE mount and CLI surface — see TOTP.

FUSE Filesystem Layout

$XDG_RUNTIME_DIR/rosec/ssh/
├── keys/
│ ├── by-name/ # keyed by vault item name
│ │ ├── My GitHub Key.pub
│ │ └── Production Server.pub
│ ├── by-fingerprint/ # keyed by SHA-256 fingerprint
│ │ ├── SHA256_xxxxxxxxxxxxxx.pub
│ │ └── SHA256_yyyyyyyyyyyyyy.pub
│ └── by-host/ # keyed by mapped hostname
│ ├── github.com.pub
│ ├── _star.prod.example.com.pub # * replaced with _star
│ └── bastion.internal.pub
├── config.d/ # SSH config snippets
│ ├── my_github_key.conf
│ └── production_server.conf
└── allowed_signers # synthesised — see "Git signature verification"

All .pub files contain the OpenSSH wire-format public key (the same line you would put in ~/.ssh/authorized_keys).

Hooking it up to OpenSSH

Two lines in ~/.ssh/config are enough to wire rosec into your existing SSH workflow:

Include /run/user/1000/rosec/ssh/config.d/*

Host *
IdentityAgent /run/user/1000/rosec/agent.sock

Replace 1000 with your UID (id -u) — or substitute ${XDG_RUNTIME_DIR} if your ssh build supports it.

What each line does:

  • Include …/config.d/* pulls in per-item snippets generated from any vault item that has an ssh_host attribute. Each snippet binds the matching Host pattern to its specific key + IdentityAgent. The directory lives on the FUSE mount, so the snippets stay in lock-step with your vault — nothing to regenerate manually.
  • Host * IdentityAgent … is the catch-all. It points everything else at the rosec agent socket so any key in the agent (whether or not it has ssh_host mappings) is offered for hosts not covered by a snippet.

Place the Host * block after the Include line. SSH applies per-parameter first-match-wins, so the snippets' IdentityFile / IdentitiesOnly settings need to be seen first.

You can also export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/rosec/agent.sock" in your shell profile if you'd rather not edit ~/.ssh/config. The env var and the config-based approach are interchangeable; pick whichever you prefer. Per-host snippets work either way.

Quick check that everything's wired up:

SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/rosec/agent.sock" ssh-add -l

SSH Key Sources

rosec discovers SSH keys from any vault item regardless of type. Two sources are supported:

Native SSH Key items (type sshkey)

Vault backends that have a first-class SSH key type (e.g. Bitwarden's native SSH Key item) expose the key directly. rosec uses the private_key field for signing and derives the public key automatically.

KeePassXC SSH Agent integration

The keepassxc-file provider parses entries that have KeePassXC's built-in SSH Agent integration enabled — i.e. a KeeAgent.settings field plus a binary attachment containing an OpenSSH private key file. Encrypted keys are decrypted using the entry's Password field; the unencrypted PEM is then handed to the SSH agent. See providers/keepassxc-file.md for the per-entry setup.

PEM keys in notes, passwords, or hidden custom fields

rosec scans the notes, password, and hidden custom.* fields of all vault items for PEM-encoded private keys. The following headers are recognised:

-----BEGIN OPENSSH PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
-----BEGIN EC PRIVATE KEY-----
-----BEGIN DSA PRIVATE KEY-----
-----BEGIN ENCRYPTED PRIVATE KEY-----

This means you can store SSH keys as a Secure Note or as a hidden custom field on a Login item and they will be discovered automatically — no special item type required.

Host Mapping with ssh_host

The most powerful feature of the rosec SSH agent is automatic SSH config generation based on vault item fields.

How it works

Add one or more text custom fields named ssh_host (or ssh-host) to any vault item that contains an SSH key. Each field value is an OpenSSH Host pattern:

Field nameField valueEffect
ssh_hostgithub.comMaps this key to github.com
ssh_host*.prod.example.comMaps this key to all prod hosts
ssh_hostbastion.internalMaps this key to the bastion

Multiple ssh_host fields on the same item are all honoured — each generates its own Host block in the config snippet.

A single field can also contain newline-separated host patterns, which is equivalent to multiple fields:

github.com
*.prod.example.com
bastion.internal

Setting the SSH user with ssh_user

Add a text custom field named ssh_user (or ssh-user) to set the User directive in the generated Host blocks:

Field nameField valueEffect
ssh_userdeployAdds User deploy to each Host block

Only the first ssh_user field is used if multiple are present.

Generated config snippets

For each vault item that has at least one ssh_host field, rosec generates a .conf file in config.d/. The filename is derived from the item name using the following normalisation rules:

  • Converted to lowercase
  • Spaces and punctuation replaced with _
  • Leading . replaced with _dot
  • * replaced with _star

Example: "My Server One.two"my_server_one_two.conf

A generated snippet looks like:

# Auto-generated by rosec — do not edit
# Source: My GitHub Key (last updated: 2025-12-01T10:30:00Z)

Host github.com
User git
IdentityFile /run/user/1000/rosec/ssh/keys/by-name/My GitHub Key.pub
IdentityAgent /run/user/1000/rosec/agent.sock
IdentitiesOnly yes

Host gh-alt.example.com
User git
IdentityFile /run/user/1000/rosec/ssh/keys/by-name/My GitHub Key.pub
IdentityAgent /run/user/1000/rosec/agent.sock
IdentitiesOnly yes
  • User is only emitted when the vault item has a ssh_user custom field.
  • IdentitiesOnly yes prevents ssh from offering other keys to that host.
  • IdentityAgent ensures the correct agent socket is used even if SSH_AUTH_SOCK points elsewhere.
  • IdentityFile points to a .pub file — ssh reads it to identify which key to request from the agent; the private key never leaves the agent.

Conflict resolution

If multiple vault items claim the same ssh_host pattern, the item with the most recent revision_date wins. Older items have that specific Host block omitted from their snippet. If an item loses all its host entries to newer items, its .conf file still exists but contains only a header comment explaining the conflict.

This ensures generated configs are deterministic and consistent across vault re-syncs.

Wildcard patterns in filenames

OpenSSH Host patterns use * and ? as wildcards. Since * is illegal in FUSE filenames (it would trigger shell glob expansion), the by-host/ directory substitutes * with _star and ? with _qmark:

Host patternFilename in by-host/
github.comgithub.com.pub
*.prod.example.com_star.prod.example.com.pub
192.168.?.1192.168._qmark.1.pub

The config.d/ snippets use the original pattern in the Host line — the substitution only applies to FUSE filenames.

Git signature verification (allowed_signers)

rosec also synthesises an allowed_signers file at the root of the FUSE mount so ssh-keygen -Y verify (and git, via gpg.ssh.allowedSignersFile) can validate signed commits against the same keys it signs them with — no separate file to maintain.

Tag a key you sign with by adding a custom field to its vault item:

Field nameField value
ssh_signing_principal (or ssh-signing-principal)you@example.com

Multiple principals are supported (one per line in a single field, or duplicate fields). Each (principal × public-key) pair becomes one line in the synthesised file, in the format ssh-keygen -Y verify expects:

you@example.com namespaces="git" ssh-ed25519 AAAA... comment

Point git at the file once:

git config --global gpg.ssh.allowedSignersFile "$XDG_RUNTIME_DIR/rosec/ssh/allowed_signers"

After that, git log --show-signature reports Good "git" signature for <principal> for every commit signed by a key you've tagged.

Scoping (important). Only keys with at least one principal land in allowed_signers. A server-access key sitting in rosec untagged contributes nothing to verification trust — so a leaked deploy key cannot spoof commits unless you've explicitly opted it in to a git identity.

Fail-closed. If rosec is locked or the FUSE mount is unavailable, allowed_signers is unreadable / empty. Git's behaviour in that case is "no key trusted", which is the right default — better to mark a real signature as untrusted than to trust the wrong key.

Signing confirmation

By default, signing operations are silent (like standard ssh-agent). To require interactive confirmation before a key is used for signing, add a text custom field to the vault item:

Field nameField value
ssh_confirm (or ssh-confirm)true

When this field is present with the value true, rosec will launch rosec-prompt in confirmation mode each time a signing request arrives for that key. The prompt shows the key fingerprint and vault item name, and the user must explicitly confirm or deny the operation.

  • GUI mode (Wayland/X11): a dialog appears with Confirm / Cancel buttons.
  • TTY fallback: the terminal shows the key details and asks Allow / Deny [y/N]:.
  • Headless (no display, no TTY): the sign operation is allowed with a warning logged. This preserves functionality for automated / headless environments.

Multi-Provider Support

The SSH agent is provider-agnostic. Any Provider implementation that returns vault items with discoverable SSH key material (native SSH key type, or PEM content in notes/fields) will have those keys automatically included.

This means SSH keys from Bitwarden PM, Bitwarden SM, 1Password (future), Proton Pass (future), or any other provider are all surfaced through the same agent socket and FUSE filesystem without provider-specific configuration.

Security Notes

  • Private keys are never written to disk. The FUSE filesystem exposes only public keys (.pub files). Private key material lives exclusively in the agent's in-memory key store, which is zeroized on lock or process exit.
  • The agent socket is owned by your user and has mode 0600. No other user can connect to it.
  • Locking the provider locks the agent. When a provider is locked (e.g. due to the autolock timer or rosec provider lock), its keys are immediately removed from the agent and from the FUSE filesystem.
  • Encrypted PEM keys (BEGIN ENCRYPTED PRIVATE KEY) are supported; the passphrase is prompted interactively the first time the key is loaded.

Troubleshooting

Keys not appearing

  1. Check that the provider is unlocked: rosec provider list
  2. Check that the FUSE mount is active: ls "$XDG_RUNTIME_DIR/rosec/ssh/keys/by-name/"
  3. For PEM keys, verify the PEM header is one of the recognised formats listed above

config.d/ snippet not generated

The snippet is only generated for items that have at least one ssh_host (or ssh-host) custom field. The field must be a text type (not hidden). Verify the field name is exactly ssh_host or ssh-host (case-sensitive — SSH_Host will not work).

Multiple items claiming the same host

Run rosec ssh list to see which item currently owns each host pattern and when each item was last updated. The most recently updated item wins.

FUSE mount fails

Ensure fusermount3 is installed and that your user is in the fuse group (or that /etc/fuse.conf contains user_allow_other).

# Debian/Ubuntu
sudo apt install fuse3
sudo usermod -aG fuse "$USER"

TOTP .code files not appearing

  1. Check that totp_fuse = true (the default) is set in [service].
  2. Verify the provider is unlocked: rosec provider list.
  3. Confirm the item has a TOTP seed: rosec search rosec:totp=true.
  4. Check the mount is active: ls "$XDG_RUNTIME_DIR/rosec/totp/by-name/".