Skip to main content

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:

TypeNotes
ed25519Modern default. Shorter keys, faster, at least as secure as RSA-4096. Use this unless told otherwise.
rsa -b 4096Older, 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 yes per-host once you have more than one key, so you're never accidentally authenticating with the wrong identity.

12. Troubleshooting Playbook

SymptomLikely causeWhat to do
Permission denied (publickey)Server doesn't have your public key, or the wrong private key is being offeredssh -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 agentssh-agent isn't running in this shelleval "$(ssh-agent -s)", then ssh-add your key again
Works in one terminal, not a new oneAgent doesn't persist across terminal sessions by defaultAdd AddKeysToAgent yes (and UseKeychain yes on macOS) to ~/.ssh/config
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGEDServer was rebuilt, or a genuine MITM concernVerify out-of-band first, then ssh-keygen -R hostname before reconnecting
Wrong GitHub account used for a clone/pushDefault key offered instead of the intended oneAdd explicit Host aliases with IdentitiesOnly yes in ~/.ssh/config, as shown in Section 6
git@github.com: Permission denied (publickey) on git pull/pushRepo remote URL uses SSH but no matching key is loaded, or you're using the wrong Host aliasssh -T git@github.com to test auth directly, separate from any specific repo
Port forward "works" but nothing connects on the forwarded portWrong local/remote port, or the target service isn't listening where expectedDouble-check -L localport:targethost:targetport ordering; confirm the target service is actually up
Passphrase prompt appears every single time despite using the agentKey was never actually ssh-add-ed after the last rebootssh-add -l to confirm it's loaded; re-add if the list is empty
Private key suspected leakedCommitted accidentally, shared over an insecure channel, or a machine was compromisedTreat 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

CategoryCommandWhat it does
Keysssh-keygen -t ed25519 -C "email"Generate a new key pair
Keyschmod 600 ~/.ssh/id_ed25519Lock down private key permissions
Agenteval "$(ssh-agent -s)"Start the agent for this session
Agentssh-add ~/.ssh/id_ed25519Load a key into the agent
Agentssh-add -lList keys currently loaded
Verifyssh -T git@github.comTest GitHub SSH authentication
Debugssh -v user@hostVerbose connection, shows which key was offered
ConfigIdentitiesOnly yesForce use of only the specified key for a host
Copyscp file user@host:/pathCopy a file to a remote host
Copyrsync -avz -e ssh src/ user@host:/dst/Efficient, resumable directory copy
Tunnelssh -L 5432:dbhost:5432 user@jumpboxLocal port forward
ForwardingForwardAgent yesLet a remote host use your local agent
Hostsssh-keygen -R hostnameRemove 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.