SSH & Key Management
Volume 1 — Core Developer Skills
SSH (Secure Shell) is how you authenticate to GitHub, remote servers, and tunneled resources without typing a password every time. This chapter covers how it actually works, how to set it up correctly the first time, and how to fix it when it breaks.
1. How SSH Authentication Works
SSH key auth is based on a public/private key pair. The private key never leaves your machine; the public key is what you hand out to services like GitHub.
The private key is only ever used locally to answer the challenge — it's never transmitted. This is why losing a public key is a non-event, but losing (or leaking) a private key means treating it as compromised immediately.
2. Generating a Key Pair
ssh-keygen -t ed25519 -C "pete.carroll@traxsgroup.com"
You'll be prompted for a save location (default ~/.ssh/id_ed25519 is fine for a primary key) and a passphrase. Set a passphrase. Without one, anyone who gets a copy of your private key file can use it directly — the passphrase is a second factor protecting the key at rest.
Key type — use Ed25519 unless something specifically requires RSA:
| Type | Notes |
|---|---|
ed25519 | Modern default. Shorter keys, faster, at least as secure as RSA-4096. Use this unless told otherwise. |
rsa -b 4096 | Older, still supported everywhere. Needed only for legacy systems that don't support Ed25519. |
ssh-keygen -t rsa -b 4096 -C "pete@traxsgroup.com" # only if Ed25519 isn't supported by the target system
This produces two files:
~/.ssh/id_ed25519 # PRIVATE key — never share, never commit, never email
~/.ssh/id_ed25519.pub # PUBLIC key — safe to share, this is what goes on GitHub/servers
3. File Permissions Matter
SSH refuses to use key files with overly permissive permissions — this is a built-in safeguard, not a bug.
chmod 700 ~/.ssh # only you can access the .ssh directory at all
chmod 600 ~/.ssh/id_ed25519 # only you can read/write the private key
chmod 644 ~/.ssh/id_ed25519.pub # public key can be world-readable, it's meant to be shared
If you ever see Permissions 0644 for '~/.ssh/id_ed25519' are too open, that's SSH telling you the private key file itself is readable by other users on the machine — fix it with chmod 600 immediately.
4. ssh-agent: Unlocking Your Key Once Per Session
Typing your passphrase on every single connection gets old fast. ssh-agent holds your decrypted key in memory for the session so you only unlock it once.
eval "$(ssh-agent -s)" # start the agent for this shell session
ssh-add ~/.ssh/id_ed25519 # add your key, prompts once for the passphrase
ssh-add -l # list keys currently loaded in the agent
ssh-add -D # remove all keys from the agent
On macOS, keep the key loaded across reboots by storing the passphrase in the system Keychain:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
And add this to ~/.ssh/config so future terminal sessions pick the key up automatically without re-running ssh-add:
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
5. Adding Your Public Key to GitHub and Azure DevOps
cat ~/.ssh/id_ed25519.pub
Copy the full output (starts with ssh-ed25519, ends with your comment/email) and paste it in:
- GitHub: Settings → SSH and GPG keys → New SSH key
- Azure DevOps: User settings → SSH public keys → New Key
Verify the connection works before relying on it:
ssh -T git@github.com
A successful response looks like Hi <username>! You've successfully authenticated... — note that this always reports "you cannot access via shell" even on success, because GitHub only allows Git operations over SSH, not an interactive shell. That message is expected, not an error.
6. The SSH Config File
~/.ssh/config lets you define per-host shortcuts and settings instead of typing full flags every time.
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
Host github-traxs
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_traxs
IdentitiesOnly yes
Host roundtrip-prod
HostName api.roundtrips.app
User azureuser
IdentityFile ~/.ssh/id_ed25519
Port 22
With this in place:
ssh roundtrip-prod # instead of ssh -i ~/.ssh/id_ed25519 azureuser@api.roundtrips.app
git clone github-traxs:Traxs-dev/RoundTripAPI.git # uses the traxs identity specifically
IdentitiesOnly yes is what makes multiple-identity setups actually work — without it, SSH may try your default key first regardless of which IdentityFile you specified, and you'll authenticate as the wrong account.
7. known_hosts and Host Verification
The first time you connect to a new host, SSH asks you to confirm its fingerprint:
The authenticity of host 'github.com (140.82.113.4)' can't be established.
ED25519 key fingerprint is SHA256:...
Are you sure you want to continue connecting (yes/no)?
Typing yes adds that host's public key to ~/.ssh/known_hosts. On every future connection, SSH checks the server's key against this record.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Do not blindly clear known_hosts and reconnect when you see this. It usually means the server was legitimately rebuilt (a new VM, a reinstalled OS) — but it's also exactly what a machine-in-the-middle attack looks like. Confirm out-of-band (check the Azure Portal for a VM rebuild, ask whoever manages the box) before removing the old entry with ssh-keygen -R hostname and reconnecting.
8. Copying Files: scp and rsync
scp report.txt user@server:/home/user/ # copy a local file to a remote server
scp user@server:/var/log/app.log ./ # copy a remote file to your local machine
scp -r localdir user@server:/remote/path/ # copy a directory recursively
For anything larger or repeated, rsync over SSH is faster and resumable — it only transfers what's changed:
rsync -avz -e ssh localdir/ user@server:/remote/path/
9. Agent Forwarding
Agent forwarding lets a remote server use your local SSH agent to authenticate onward — useful when you SSH into a build box that then needs to git pull from GitHub using your key, without ever copying your private key onto that box.
ssh -A user@buildserver
Or persistently in ~/.ssh/config:
Host buildserver
ForwardAgent yes
Use this deliberately, not by default. Anyone with root on the intermediate server can, while your session is active, make requests through your forwarded agent as if they were you. Only enable it for hosts you trust as much as your own machine.
10. Port Forwarding (Tunneling)
Port forwarding routes traffic through an SSH connection — useful for reaching something that isn't publicly exposed, like a database only reachable from inside a VNet.
# Local forwarding: reach a remote-only resource as if it were on localhost
ssh -L 5432:internal-db-host:5432 user@jumpbox
# now localhost:5432 on your machine routes to internal-db-host:5432 through jumpbox
# Remote forwarding: expose something on your machine to the remote side
ssh -R 8080:localhost:3000 user@server
# Dynamic forwarding: turns the SSH connection into a local SOCKS proxy
ssh -D 1080 user@jumpbox
Local forwarding (-L) is the one you'll actually reach for most often — e.g., connecting DataGrip to a database that's firewalled to only accept connections from inside Azure, by tunneling through a VM that does have access.
11. Hardening Basics (Developer Side)
- Always set a passphrase on private keys — a key without one is equivalent to a password saved in plaintext.
- One key per context where practical (personal GitHub vs. Traxs GitHub vs. servers) so a single leaked key has limited blast radius.
- Never email, Slack, or commit a private key file — if one ever ends up somewhere it shouldn't, rotate it immediately: generate a new pair, add the new public key everywhere the old one was trusted, then remove the old public key from every service and delete the old private key.
- Prefer Ed25519 over RSA for new keys — smaller attack surface, faster, and there's no practical downside on any modern service.
- Set
IdentitiesOnly yesper-host once you have more than one key, so you're never accidentally authenticating with the wrong identity.
12. Troubleshooting Playbook
| Symptom | Likely cause | What to do |
|---|---|---|
Permission denied (publickey) | Server doesn't have your public key, or the wrong private key is being offered | ssh -v user@host to see which key was tried; confirm the matching public key is actually installed on the target |
Could not open a connection to your authentication agent | ssh-agent isn't running in this shell | eval "$(ssh-agent -s)", then ssh-add your key again |
| Works in one terminal, not a new one | Agent doesn't persist across terminal sessions by default | Add AddKeysToAgent yes (and UseKeychain yes on macOS) to ~/.ssh/config |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED | Server was rebuilt, or a genuine MITM concern | Verify out-of-band first, then ssh-keygen -R hostname before reconnecting |
| Wrong GitHub account used for a clone/push | Default key offered instead of the intended one | Add explicit Host aliases with IdentitiesOnly yes in ~/.ssh/config, as shown in Section 6 |
git@github.com: Permission denied (publickey) on git pull/push | Repo remote URL uses SSH but no matching key is loaded, or you're using the wrong Host alias | ssh -T git@github.com to test auth directly, separate from any specific repo |
| Port forward "works" but nothing connects on the forwarded port | Wrong local/remote port, or the target service isn't listening where expected | Double-check -L localport:targethost:targetport ordering; confirm the target service is actually up |
| Passphrase prompt appears every single time despite using the agent | Key was never actually ssh-add-ed after the last reboot | ssh-add -l to confirm it's loaded; re-add if the list is empty |
| Private key suspected leaked | Committed accidentally, shared over an insecure channel, or a machine was compromised | Treat as compromised immediately: generate a new key pair, replace the public key everywhere, revoke the old one — do not wait to "see if it's used" |
13. Quick Reference
| Category | Command | What it does |
|---|---|---|
| Keys | ssh-keygen -t ed25519 -C "email" | Generate a new key pair |
| Keys | chmod 600 ~/.ssh/id_ed25519 | Lock down private key permissions |
| Agent | eval "$(ssh-agent -s)" | Start the agent for this session |
| Agent | ssh-add ~/.ssh/id_ed25519 | Load a key into the agent |
| Agent | ssh-add -l | List keys currently loaded |
| Verify | ssh -T git@github.com | Test GitHub SSH authentication |
| Debug | ssh -v user@host | Verbose connection, shows which key was offered |
| Config | IdentitiesOnly yes | Force use of only the specified key for a host |
| Copy | scp file user@host:/path | Copy a file to a remote host |
| Copy | rsync -avz -e ssh src/ user@host:/dst/ | Efficient, resumable directory copy |
| Tunnel | ssh -L 5432:dbhost:5432 user@jumpbox | Local port forward |
| Forwarding | ForwardAgent yes | Let a remote host use your local agent |
| Hosts | ssh-keygen -R hostname | Remove a stale host key entry |
Part of the Traxs Engineering Handbook — Volume 1: Core Developer Skills. Companion chapters in this volume: Bash Reference, Git Reference, Git Troubleshooting.