SSH Agent Forwarding¶
SSH agent forwarding lets every agent machine authenticate to borg repositories using SSH keys held exclusively on the Assimilate server. No SSH private keys need to be distributed to or stored on agent machines.
Overview¶
In a typical multi-host borg setup each backup machine needs its own copy of the SSH private key that authorises access to the borg repository server. This creates a key-management burden and a larger attack surface — a compromised agent machine exposes the repository key.
Assimilate eliminates this by relaying the SSH agent protocol over WebSocket. The server holds the key; agents borrow signing operations from it on demand. From borg's perspective it is talking to a local SSH agent — the relay is transparent.
See Architecture for the high-level component diagram.
How It Works¶
sequenceDiagram
participant Borg as borg process
participant Agent as Assimilate Agent
participant UDS as Temp Unix Socket<br/>(SSH_AUTH_SOCK)
participant Server as Assimilate Server
participant LocalAgent as Server SSH Agent<br/>(SSH_AUTH_SOCK)
participant Repo as Borg Repo Host
Agent->>UDS: Create temp socket in $TMPDIR/assimilate-XXXX/agent.sock
Agent->>Agent: Set SSH_AUTH_SOCK → temp socket path
Agent->>Borg: Spawn borg with SSH_AUTH_SOCK injected
Borg->>UDS: Connect (SSH agent protocol)
Agent->>Server: Open WebSocket /ws/ssh-agent/{hostname}
Agent->>Server: Send token as first WebSocket message
Server->>Server: Verify token via bcrypt hash lookup
Server->>LocalAgent: Connect to server's SSH_AUTH_SOCK
loop SSH agent protocol relay
Borg->>UDS: SSH agent request bytes
UDS->>Agent: Read bytes from Unix socket
Agent->>Server: Forward as WebSocket binary frame
Server->>LocalAgent: Write to server's Unix socket
LocalAgent->>Server: SSH agent response bytes
Server->>Agent: Forward as WebSocket binary frame
Agent->>UDS: Write to Unix socket
UDS->>Borg: SSH agent response
end
Borg->>Repo: SSH connection (signed with server's key)
Repo->>Borg: Authorised — backup proceeds
Before each backup the agent creates a temporary Unix domain socket in a private directory under $TMPDIR. For every connection borg makes to that socket the agent opens a new WebSocket to the server's relay endpoint at /ws/ssh-agent/{hostname} and sends its token as the first message. The server authenticates the request by verifying the token against the bcrypt hash stored in the database, then connects to its own SSH_AUTH_SOCK and relays bytes bidirectionally between the WebSocket and the local agent socket. When the backup finishes the temporary socket directory is removed automatically.
Server Setup¶
Systemd¶
Load the borg SSH private key into an ssh-agent on the server host and make SSH_AUTH_SOCK available to the Assimilate service process.
# Start an ssh-agent and load the key
eval $(ssh-agent)
ssh-add /path/to/borg_ed25519_key
# Print the socket path — you will need it below
echo $SSH_AUTH_SOCK
Add the socket path to the systemd unit file:
[Service]
Environment=SSH_AUTH_SOCK=/run/user/1000/gnupg/S.gpg-agent.ssh
If you use gpg-agent as your SSH agent the socket is typically at /run/user/<uid>/gnupg/S.gpg-agent.ssh. For a standalone ssh-agent started at login the path is printed by echo $SSH_AUTH_SOCK in the same session.
Warning
The socket path must be readable by the user the Assimilate service runs as. If the service runs as a dedicated system user, ensure that user has access to the socket or use a system-level agent (e.g. ssh-agent started in the service unit itself).
After updating the unit file reload and restart the service:
systemctl daemon-reload
systemctl restart assimilate
Docker¶
The official Docker image manages its own Ed25519 key pair — no host SSH agent or key mounting is required.
services:
server:
image: assimilate-server
volumes:
- ssh_keys:/app/ssh
volumes:
ssh_keys:
On first start the container generates an Ed25519 key pair in the ssh_keys volume, loads it into a container-local ssh-agent, and sets SSH_AUTH_SOCK automatically. The public key is:
- Printed to container logs at startup (
docker compose logs server | grep "Public key") - Visible in the admin UI under System → SSH Public Key
Add the public key to ~/.ssh/authorized_keys on the borg repository host to authorise backups.
Note
The ssh_keys volume persists the key pair across container restarts. Do not delete it — if the key is lost you will need to re-authorise the new key on every borg repository host.
Agent Setup¶
Note
No configuration is required on the agent. SSH agent forwarding is automatic.
The agent uses the same BORG_SERVER_URL and BORG_AGENT_TOKEN environment variables it already needs to connect to the server. Before each backup it attempts to establish the SSH relay. If the relay succeeds, SSH_AUTH_SOCK is injected into the borg subprocess environment. If it fails for any reason the backup continues without forwarding and borg falls back to whatever SSH keys are available locally on the agent machine.
Assimilate records each repository server's SSH host key and sends that pin to assigned agents. Before running borg, the agent writes the pin to a private temporary known_hosts file and uses StrictHostKeyChecking=yes. The connection fails if the repository server presents a different key, and the temporary file is removed after the operation.
Borg Repository Authorization¶
The borg repository host must trust the server's SSH public key. Retrieve the public key and add it to the authorised keys file on the repository host.
# Retrieve the server's public key (run on the server or via the admin UI)
ssh-add -L
On the borg repository host:
echo "<paste public key here>" >> ~/.ssh/authorized_keys
For tighter security, restrict the key to borg operations only using a command= prefix in authorized_keys:
command="borg serve --restrict-to-path /backup/repos",restrict ssh-ed25519 AAAA... assimilate-server
Warning
The restrict keyword disables port forwarding, agent forwarding, and X11 forwarding for this key. Combined with command=, it limits the key to running borg serve only — recommended for production repository hosts.
Failure Modes¶
| Condition | Behaviour |
|---|---|
SSH_AUTH_SOCK not set on server |
Relay endpoint closes immediately; agent logs a warning; backup proceeds without forwarding |
| Server's ssh-agent socket unreachable | Same as above — relay closes, backup falls back to local keys |
| Token authentication fails | Server closes the WebSocket; agent logs a warning; backup proceeds without forwarding |
| Network interruption during relay | Active borg SSH connection drops; borg retries according to its own retry logic |
| Key not authorised on repo host | SSH authentication fails; borg exits with an error; backup is marked failed |
| Temp socket directory creation fails | Agent logs an error; backup proceeds without forwarding |
In all cases except the last two the backup can still succeed if the agent machine has its own SSH key authorised on the repository host.
Troubleshooting¶
Socket not found / SSH_AUTH_SOCK not set
The server process does not have SSH_AUTH_SOCK in its environment. Check the systemd unit file or Docker configuration. For systemd, verify with:
systemctl show assimilate --property=Environment
For Docker, check container logs:
docker compose logs server | grep -i ssh
Key not authorised on repository host¶
Verify the server's public key is in ~/.ssh/authorized_keys on the repo host:
ssh-add -L # on the server — copy this output
cat ~/.ssh/authorized_keys # on the repo host — confirm it is present
Test the connection manually from the server:
ssh -i /path/to/borg_key borg@repo-host echo ok
Repository host key rotated¶
If the repository host was reprovisioned or its SSH host key was intentionally replaced, backups fail until Assimilate refreshes the repository configuration. Save the repository again after verifying the new host-key fingerprint through a trusted channel; Assimilate records the currently presented key and distributes the new pin to assigned agents.
If the failure persists after the rotation, verify you are reaching the intended repository host and that no jump host or DNS override is pointing agents at the wrong machine.
WebSocket connection refused or firewall blocking¶
The agent connects to the server on the same port and URL used for the main WebSocket connection. If the main agent connection works but SSH forwarding does not, check that the /ws/ssh-agent/ path is not blocked by a reverse proxy or firewall rule.
For nginx, ensure the proxy passes WebSocket upgrade headers:
location /ws/ {
proxy_pass http://assimilate;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Backup succeeds but uses wrong key¶
If the backup succeeds but you suspect it is using a local key rather than the server's key, check the agent logs for a line like ssh relay: failed to connect to relay or SSH_AUTH_SOCK not set. If forwarding is working correctly the agent logs will show ssh relay connection opened before the backup starts.