SSH Operations
Overview
This runbook documents the complete SSH infrastructure for Domus Digitalis, including:
-
Multi-tier key hierarchy (Vault CA → YubiKey → Software fallback)
-
systemd-managed SSH agent
-
Post-quantum key exchange configuration
-
Per-host key chains with automatic fallback
Key Inventory
| Key File | Type | TTL | Purpose |
|---|---|---|---|
|
Ed25519 + Cert |
8h |
Infrastructure hosts via Vault SSH CA |
|
FIDO2 Resident |
∞ |
YubiKey Nano (always present) |
|
FIDO2 Resident |
∞ |
Primary YubiKey 5C NFC |
|
FIDO2 Resident |
∞ |
Backup YubiKey |
|
Ed25519 |
∞ |
Software fallback (passphrase) |
|
Ed25519 |
∞ |
GitHub only |
|
Ed25519 |
∞ |
GitLab only |
|
Ed25519 |
∞ |
Gitea (NAS) only |
|
RSA 4096 |
∞ |
Azure DevOps (RSA required) |
SSH Config Behavior
Your ~/.ssh/config has these key settings that control agent behavior:
AddKeysToAgent 1h # Auto-add keys to agent for 1 hour on first use
IdentitiesOnly yes # ONLY try keys explicitly listed for each host
ControlMaster auto # Enable connection multiplexing
ControlPersist 600 # Keep master connection alive 10 minutes
AddKeysToAgent Explained
When you SSH to a host, OpenSSH:
-
Reads your config to find which
IdentityFileto use -
Prompts for passphrase (if key is encrypted)
-
Automatically adds the key to the agent for the duration specified
| Scenario | Passphrase Prompt? | Key Lifetime in Agent |
|---|---|---|
First SSH to github.com |
Yes (once) |
1 hour (from |
Second SSH to github.com (within 1h) |
No |
Remaining time from first add |
Manual |
Yes (once) |
1 day (overrides config) |
After 1h expires, SSH again |
Yes (once) |
1 hour (re-added automatically) |
When to Use Manual ssh-add
Use manual ssh-add -t when you want a longer lifetime than the default 1 hour:
# Work session - extend to full day
ssh-add -t 1d ~/.ssh/id_ed25519_github
# Infrastructure work - match Vault cert TTL
ssh-add -t 8h ~/.ssh/id_ed25519_d000
The -t duration overrides the AddKeysToAgent 1h setting for that key.
IdentitiesOnly Explained
With IdentitiesOnly yes, SSH will only try keys explicitly listed in config:
# For github.com, ONLY try id_ed25519_github
Host github.com
IdentityFile ~/.ssh/id_ed25519_github
# For vault-01, try these in order until one works
Host vault-01
IdentityFile ~/.ssh/id_ed25519_vault
CertificateFile ~/.ssh/id_ed25519_vault-cert.pub
IdentityFile ~/.ssh/id_ed25519_sk_rk_d000_nano
IdentityFile ~/.ssh/id_ed25519_sk_rk_d000
IdentityFile ~/.ssh/id_ed25519_d000
Without IdentitiesOnly yes, SSH would try ALL keys in the agent, causing:
-
Unnecessary auth attempts
-
Possible lockouts (too many failed attempts)
-
Slower connections
SSH Agent Management
systemd User Service
The SSH agent runs as a systemd user service, ensuring it starts at login and persists across terminals.
~/.config/systemd/user/ssh-agent.service
systemctl --user status ssh-agent
systemctl --user restart ssh-agent
ls -la /run/user/$(id -u)/ssh-agent.socket
Adding Keys to Agent
ssh-add -t 1d ~/.ssh/id_ed25519_github
ssh-add -t 8h ~/.ssh/id_ed25519_d000
for key in github gitlab codeberg bitbucket gitea; do
ssh-add -t 1d ~/.ssh/id_ed25519_$key 2>/dev/null && echo "Added: $key"
done
ssh-add -t 8h ~/.ssh/id_ed25519_vault
|
The Vault cert ( |
Vault SSH CA Operations
Certificate Renewal
The Vault SSH CA issues 8-hour certificates. Renew with vault-ssh-sign:
vault-ssh-sign
vault write -field=signed_key ssh/sign/domus-client \
public_key=@$HOME/.ssh/id_ed25519_vault.pub \
valid_principals="ansible,evanusmodestus,root" \
>| ~/.ssh/id_ed25519_vault-cert.pub
Verify Certificate
ssh-keygen -Lf ~/.ssh/id_ed25519_vault-cert.pub
ssh-keygen -Lf ~/.ssh/id_ed25519_vault-cert.pub | grep -E "Valid:|Serial:"
ssh-keygen -Lf ~/.ssh/id_ed25519_vault-cert.pub | awk '/Valid:/{
split($5, t, "T");
print "Expires:", t[1], substr(t[2],1,5)
}'
YubiKey Operations
Regenerate Resident Keys
If you need to recover keys from YubiKey:
cd ~/.ssh
ssh-keygen -K
This extracts all resident keys from the inserted YubiKey.
Test YubiKey Key
# Requires touch
ssh -i ~/.ssh/id_ed25519_sk_rk_d000 -o IdentitiesOnly=yes user@host
Multiple YubiKeys
The SSH config specifies fallback order:
IdentityFile ~/.ssh/id_ed25519_sk_rk_d000_nano # Try Nano first
IdentityFile ~/.ssh/id_ed25519_sk_rk_d000 # Then 5C NFC
IdentityFile ~/.ssh/id_ed25519_sk_rk_d000_secondary # Then backup
|
The Nano is always plugged in, so it’s tried first. Keep backup YubiKey in safe location. |
Git Forge Operations
Connection Multiplexing
How It Works
The config enables ControlMaster with persistent sockets:
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
First SSH to a host creates a master connection. Subsequent SSHs reuse it (instant connect, no re-auth).
Troubleshooting
"Permission denied (publickey)"
# 1. Check agent has keys
ssh-add -l
# 2. Test with verbose
ssh -vvv user@host 2>&1 | grep -E "Offering|Trying|Authentication"
# 3. Check key permissions
ls -la ~/.ssh/id_ed25519_*
# 4. Verify correct key for host
ssh -G host | grep -E "identityfile|user|hostname"
"Certificate invalid: not yet valid"
Clock skew between client and server.
sudo systemctl enable --now systemd-timesyncd
timedatectl show | awk -F= '/NTPSynchronized/{print}'
Agent Not Running
# Check status
systemctl --user status ssh-agent
# Restart
systemctl --user restart ssh-agent
# Verify socket
echo $SSH_AUTH_SOCK
ls -la /run/user/$(id -u)/ssh-agent.socket
Wrong Key Used
The IdentitiesOnly yes setting forces SSH to only try keys explicitly listed for that host.
ssh -vvv host 2>&1 | grep "Offering public key"
Quick Reference
| Action | Command |
|---|---|
List loaded keys |
|
Add key (1 day) |
|
Remove key |
|
Remove ALL keys |
|
Renew Vault cert |
|
Check cert validity |
|
Reload Vault key |
|
Restart agent |
|
Debug connection |
|
Close multiplex socket |
|
Recover YubiKey keys |
|
Daily Workflow
Post-Quantum Security
The SSH config enables hybrid post-quantum key exchange:
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,...
This provides protection against future quantum computers while maintaining compatibility with current systems.
|
Post-quantum KEX requires OpenSSH 9.0+ (ideally 9.5+). Older servers will negotiate down to curve25519. |