SSH Agent¶
The ssh-agent namespace brokers SSH key operations between the shed microVM and the host machine.
How It Works¶
shed-ext-ssh-agent runs inside the VM as a systemd service, listening on a Unix domain socket at /run/shed-extensions/ssh-agent.sock. The SSH_AUTH_SOCK environment variable points all SSH clients to this socket.
When an SSH client (git, ssh, scp) requests a key operation, shed-ext-ssh-agent translates it into a message bus request and forwards it to the host agent for processing.
Operations¶
| Operation | Description | Touch ID |
|---|---|---|
list |
Return available public keys | No |
sign |
Sign challenge data with specified key | Configurable |
Message Format¶
Request¶
{
"id": "0192b3a4-...",
"namespace": "ssh-agent",
"type": "request",
"payload": {
"operation": "sign",
"public_key": "ssh-ed25519 AAAA... user@host",
"data": "<base64-encoded challenge>",
"flags": 0
}
}
Response¶
{
"id": "0192b3a4-...",
"namespace": "ssh-agent",
"type": "response",
"payload": {
"format": "ssh-ed25519",
"blob": "<base64-encoded signature>",
"rest": ""
}
}
Error¶
{
"id": "0192b3a4-...",
"namespace": "ssh-agent",
"type": "response",
"payload": {
"error": "key not found",
"code": "KEY_NOT_FOUND"
}
}
Host-Side Backend¶
The host agent auto-detects the best signing strategy for request-time signing (the per-VM ssh-agent operations above):
- Agent-forward (default): If
SSH_AUTH_SOCKexists on the host, proxies sign requests to the developer's existing SSH agent (Secretive, 1Password, ssh-agent, etc.) - Local-keys (fallback): Reads keys directly from
~/.ssh/. Only passphrase-less key files work in this mode — an encrypted key is skipped. Use agent-forward (which handles passphrase/Keychain/hardware keys via your agent) for protected keys.
Override via config:
Secure-mode token bootstrap¶
When the shed server runs in auth.mode: secure, the host agent mints its own
short-lived API token over the server's reserved _bootstrap SSH channel (rather
than reading a pasted credentials_token). It does this by invoking your
system ssh client, so the SSH identity is resolved exactly the way
shed server add and shed attach resolve it — from your agent, the macOS
Keychain, a 1Password/Secretive IdentityAgent, a hardware key, or
~/.ssh/config. A passphrase-protected or agent-only key works; no
unencrypted key file is required, and the host agent never reads private key
material itself.
The server's host key is pinned in ~/.shed/known_hosts (written by
shed server add) and ssh verifies it with StrictHostKeyChecking=yes against
that file as the sole trust root. A confirmed host-key change is treated as a
possible MITM and fails closed permanently for that server; a missing pin is a
non-terminal "run shed server add" error.
Running under brew services (launchd)¶
The agent runs as a per-user LaunchAgent. macOS exposes the Apple Keychain
ssh-agent to that launchd session automatically, so a key added with
ssh-add --apple-use-keychain works out of the box. 1Password and Secretive,
however, usually export SSH_AUTH_SOCK only from your shell rc files, which the
launchd agent does not inherit — so the daemon may not see them. If a mint fails
and shed-host-agent status (or the log) reports no SSH identity available,
point ssh at the agent explicitly in ~/.ssh/config (the recommended fix):
Host my-server # the SSH host the agent dials — see `shed server list`
IdentityAgent ~/.1password/agent.sock # or your Secretive socket
# or pin a specific key instead:
# IdentityFile ~/.ssh/id_ed25519
# IdentitiesOnly yes
Alternatives: launchctl setenv SSH_AUTH_SOCK <socket>, or add SSH_AUTH_SOCK
to the service's launchd plist.
The
Hostblock must match the actual server host the agent dials (as inshed server list), not an invented alias — the bootstrap connects to that host directly.
If your allowlisted identity lives in an agent holding many keys, ssh can
exhaust the server's per-connection attempt cap before offering it; pin the one
key with IdentityFile + IdentitiesOnly yes, or raise auth.ssh.max_auth_tries
on the server. A touch-required (FIDO/hardware) key is a poor fit for the
unattended, periodic mint — the daemon can't satisfy the touch — so keep a
non-touch key on the server's allowlist. Note that ~/.ssh/config is read by an
unattended daemon, so a global ProxyCommand/Match will run on each mint.
Touch ID Approval¶
Sign operations can require interactive approval on the host. Set ssh.approval.enabled: true to gate every sign request behind a macOS authentication prompt (see Configuration for all fields).
When enabled, ssh.approval.method selects the authentication factor:
method |
LocalAuthentication policy | Behavior |
|---|---|---|
biometrics-or-password (default) |
LAPolicyDeviceOwnerAuthentication |
Touch ID, Apple Watch, or account password. Works in clamshell mode (lid closed) and on Macs without a fingerprint sensor. |
biometrics |
LAPolicyDeviceOwnerAuthenticationWithBiometrics |
Touch ID only. Fails when no biometric is available. |
biometrics-or-password is the default so approval works out of the box on a closed-lid MacBook with an external display, on a Mac mini/Studio, and on any setup without a Touch ID Magic Keyboard.
The ssh.approval.policy field (per-request, per-session, per-shed) controls how often the prompt appears, and session_ttl sets how long an approval is cached. LAPolicyDeviceOwnerAuthentication also has its own macOS-managed grace period that can briefly skip the prompt; this is independent of session_ttl, and the two compose without conflict.
Timeouts¶
Credential requests use a 3-second timeout. On timeout, shed-ext-ssh-agent logs an actionable error:
Startup Health Check¶
On startup, shed-ext-ssh-agent publishes a ping to the ssh-agent namespace. If no response arrives within 2 seconds, it logs a warning but continues starting (so it's ready when the host agent comes up).