SSH Agent and Key Management¶
SSH is the authentication substrate for git, remote shells, rsync,
scp, sftp, tunneling, and most deployment pipelines. Its configuration
surface is small but deeply consequential: a misconfigured
~/.ssh/config can leak keys between identities, lock you out of
hosts that enforce key-count limits, or silently downgrade a secure
connection. This page establishes a configuration that fails loudly
rather than silently, minimizes passphrase prompts without weakening
security, and cleanly supports multiple identities via host aliases.
Key management philosophy¶
The framework commits to the following defaults:
- Ed25519 keys only — smaller, faster, and cryptographically stronger than RSA at any practical key size. Generate RSA only when a legacy server refuses Ed25519 (rare in 2026).
- One key per identity, not per host — a single work key authenticates you to every work system; a single personal key authenticates you everywhere else.
- Passphrase on every private key — no exceptions. The passphrase
is what makes a leaked
~/.sshuseless to an attacker. - Agent over persisted keys — the private key material stays encrypted at rest; the agent holds decrypted material in memory only.
IdentitiesOnly yes— without this, SSH offers every key in your agent to every host, which breaks authentication on servers that reject after N failed attempts and leaks key fingerprints to servers that don't need them.- No agent forwarding to untrusted hosts — a compromised host with
your forwarded agent can sign authentications for any key you have
loaded. Use
ProxyJumpfor jump-host scenarios instead.
Generating keys¶
Generate a separate key for each identity. Use descriptive filenames and meaningful comments — the comment is written into both the public and private key and is the only way to distinguish keys years later.
# Work key
ssh-keygen -t ed25519 \
-C "dev@springbig.com (work, MBP 16\" M4, 2026-04)" \
-f ~/.ssh/id_ed25519_work
# Personal key
ssh-keygen -t ed25519 \
-C "you@billwoika.com (personal, MBP 16\" M4, 2026-04)" \
-f ~/.ssh/id_ed25519_personal
# Permissions must be strict; SSH refuses to use keys with loose perms
chmod 600 ~/.ssh/id_ed25519_*
chmod 644 ~/.ssh/id_ed25519_*.pub
chmod 700 ~/.ssh
Why the comment format matters
The comment travels with the key. When you look at
authorized_keys on a server two years from now trying to decide
which lines are stale, the you@billwoika.com (personal, MBP 16"
M4, 2026-04) format tells you: which identity, which machine,
when the key was generated. Never use the default comment
(user@hostname at generation time).
Registering public keys¶
The .pub file contents are what you paste into GitHub (Settings >
SSH and GPG keys > New SSH key), GitLab, or a server's
~/.ssh/authorized_keys. For GitHub, add the key twice if you use it
for both authentication and signing — once with the "Authentication
Key" type, once with "Signing Key."
# Copy public key to clipboard (macOS)
pbcopy < ~/.ssh/id_ed25519_work.pub
# Copy to a remote server in one step
ssh-copy-id -i ~/.ssh/id_ed25519_work.pub user@host
The ~/.ssh/config file¶
The SSH config is where host aliases, identity scoping, and connection
options live. The framework's reference config follows a first-match-wins
pattern: specific host blocks come before the general Host * block.
# ── GitHub multi-identity host aliases ────────────────────────
Host github.com-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes # macOS: store passphrase in Keychain
Host github.com-personal
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
# ── Work infrastructure ──────────────────────────────────────
Host bastion
HostName bastion.springbig.internal
User your-username
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
# Internal hosts via bastion — no agent forwarding
Host *.springbig.internal
User your-username
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
ProxyJump bastion # Jump through bastion; key stays local
# ── Global defaults (must come LAST) ─────────────────────────
Host *
IdentitiesOnly yes
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
ControlMaster auto
ControlPath ~/.ssh/control/%C
ControlPersist 10m
HashKnownHosts yes
StrictHostKeyChecking accept-new
UpdateHostKeys yes
UseKeychain yes
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519_personal
ControlPath needs the directory to exist
The ~/.ssh/control/ directory is not created automatically — SSH
will silently skip multiplexing. The framework's bootstrap.sh
creates it, and conf.d/70-tools.zsh checks for it on every shell
start. The %C substitution hashes user, host, port, and address
into a short filename — safer than the older %h-%p-%r form
because it avoids the Unix socket path-length limit.
ssh-agent — native, keychain-backed (macOS)¶
macOS ships a launchd-managed ssh-agent that runs per-user and is
always available at $SSH_AUTH_SOCK without any shell activation.
Combined with UseKeychain yes, passphrases are stored in the
Keychain, and you never type a passphrase after the initial key load.
# Load a key and save its passphrase to the Keychain (once per key)
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_work
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_personal
# Verify
ssh-add -l
With AddKeysToAgent yes in the SSH config, the first use of a key
also triggers an agent load and a Keychain save — you can skip the
ssh-add step and just git push, answer the passphrase prompt once,
and be done.
1Password SSH agent (recommended alternative)¶
If you already use 1Password, the 1Password SSH agent is a substantial
upgrade. It stores private keys inside the vault (end-to-end encrypted,
synced across devices), requires Touch ID for every use, integrates
with git commit signing, and eliminates the passphrase workflow. On a
machine using the 1Password agent, your ~/.ssh directory contains
no private key material at all.
Enabling the agent¶
In the 1Password desktop app: Settings > Developer > Use the SSH agent. Point SSH at it:
# In ~/.ssh/config, under Host *:
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
# Linux: IdentityAgent ~/.1password/agent.sock
The IdentityFile directives still matter — they tell SSH which public
key to present, and the agent looks up the matching private key in the
vault.
Commit signing via 1Password¶
# In ~/.config/git/work.config (and personal.config)
[gpg "ssh"]
allowedSignersFile = ~/.config/git/allowed_signers
# macOS
program = "/Applications/1Password.app/Contents/MacOS/op-ssh-sign"
# Linux: program = "/opt/1Password/op-ssh-sign"
Touch ID on every operation
This is both the selling point and the main tradeoff. Every git
push, every ssh, every git commit (if signing is on) triggers
a Touch ID prompt. For daily workflows this is the right security
posture. For high-volume scripts or CI, use short-lived personal
access tokens or agent-less CI runners.
Connection multiplexing¶
ControlMaster auto with ControlPersist 10m reuses a single
TCP connection for up to 10 minutes of subsequent ssh/scp/sftp
invocations to the same host, dropping second-and-later connection
latency to effectively zero. This is the single biggest
perceived-latency improvement for anyone who runs SSH in a loop.
To close a multiplexed session explicitly: ssh -O exit <host>. To
see active control sockets: ls ~/.ssh/control/.
Host aliases for multi-identity use¶
The github.com-work and github.com-personal host entries are the
key mechanism. SSH treats them as distinct hosts with distinct identity
files; the git URL rewrites from the
git configuration then route clones
through the correct alias based on repository path.
The end-to-end flow:
- Clone
~/work/springbig/api— git seesincludeIf "gitdir:~/work/", loadswork.config work.configrewritesgithub.com:springbig/togithub.com-work:springbig/- SSH resolves
github.com-worktogithub.com+ work identity file - The correct key is presented; the commit is signed with the work key
ProxyJump vs. agent forwarding¶
For reaching internal hosts through a bastion, always use
ProxyJump rather than agent forwarding (-A or
ForwardAgent yes):
Agent forwarding creates a socket on the bastion that anyone with root access can use to authenticate as you to any host your agent has keys for. ProxyJump tunnels the TCP connection through the bastion without exposing the agent — your keys never leave your machine.
Security checklist¶
- Ed25519 keys with passphrases
-
IdentitiesOnly yesinHost * - No agent forwarding;
ProxyJumpfor bastions -
StrictHostKeyChecking accept-new(prompt once, then strict) -
HashKnownHosts yes(obscures hostnames in known_hosts) - Private key permissions 600,
.sshdirectory 700 - Keys registered with GitHub as both Authentication and Signing