Connect to Your Production Server Securely from Your Dev Machine
Stop typing passwords and raw IP addresses. Set up SSH key authentication, a clean ~/.ssh/config with host aliases and jump hosts, and agent forwarding done safely.
What you'll build
By the end of this tutorial you'll log into a production server with a single short command (ssh prod), authenticate with an Ed25519 key instead of a password, route through a bastion/jump host using ProxyJump, and use agent forwarding correctly so you never copy a private key onto a remote machine.
Prerequisites
- A local dev machine running macOS (Ventura or later) or Linux with OpenSSH 8.1+. Check with
ssh -V. - Windows users: use WSL2 or Git Bash; the OpenSSH client behaves the same there.
- Shell access to a production host (and optionally a bastion host) with the ability to add a line to
~/.ssh/authorized_keys, or a sysadmin who can do it for you. - The hostnames/IPs and usernames for your servers.
Apple Silicon vs Intel makes no difference here — the OpenSSH client is identical. macOS does add Keychain integration, which we'll use below.
Step 1 — Generate a modern SSH key
Use Ed25519. It's fast, short, and secure. Only fall back to RSA (-t rsa -b 4096) if you must talk to an ancient server that lacks Ed25519 support.
ssh-keygen -t ed25519 -a 100 -C "yourname@devmachine" -f ~/.ssh/id_ed25519_prod
-a 100increases the KDF rounds, making the on-disk key harder to brute-force.-Cis just a comment to help you identify the key later.-fgives the key a descriptive filename so you can keep separate keys per environment.
Always set a passphrase when prompted. The passphrase protects the private key if your laptop is stolen; the agent (next step) means you only type it once per session.
This creates two files: the private key id_ed25519_prod (never share or copy this) and the public key id_ed25519_prod.pub (safe to distribute).
Step 2 — Load the key into ssh-agent
The agent holds your decrypted key in memory so you aren't re-prompted for the passphrase on every connection.
macOS (stores the passphrase in Keychain):
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_prod
Linux:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_prod
Confirm the key is loaded:
ssh-add -l
You should see one line with 256 (the Ed25519 key size) and your comment.
Step 3 — Install your public key on the server
The easiest path is ssh-copy-id, which is bundled with modern OpenSSH (including macOS):
ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub deploy@203.0.113.10
It'll ask for your password one last time, then append your public key to the server's ~/.ssh/authorized_keys.
If ssh-copy-id isn't available, do it manually:
cat ~/.ssh/id_ed25519_prod.pub | ssh deploy@203.0.113.10 \
'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
Step 4 — Write a tidy ~/.ssh/config
This is where the quality-of-life gains live. Create or edit ~/.ssh/config:
# Defaults applied to every host
Host *
AddKeysToAgent yes
IdentitiesOnly yes
ServerAliveInterval 60
ServerAliveCountMax 3
# The bastion / jump host (the only box exposed to the internet)
Host bastion
HostName 198.51.100.5
User jump
IdentityFile ~/.ssh/id_ed25519_prod
# Production app server, reached *through* the bastion
Host prod
HostName 10.0.1.20
User deploy
IdentityFile ~/.ssh/id_ed25519_prod
ProxyJump bastion
Key points:
IdentitiesOnly yesstops the client from blindly offering every key in your agent (which can tripMaxAuthTrieslimits and lock you out).ProxyJump bastiontransparently tunnels theprodconnection through the bastion. The prod host'sHostName(10.0.1.20) is a private address only reachable from inside the network.AddKeysToAgent yesauto-adds keys to the agent on first use.ServerAliveInterval/ServerAliveCountMaxkeep idle sessions from being silently dropped.
On macOS, add UseKeychain yes under Host * to read the passphrase from Keychain automatically.
ProxyJump vs ForwardAgent
| Approach | What it does | When to use |
|---|---|---|
ProxyJump |
Tunnels through the bastion; your key is never used on the bastion itself | Default choice for reaching internal hosts |
ForwardAgent |
Exposes your local agent socket on the remote host | Only when you must git pull/ssh from the remote, and only on hosts you fully trust |
Prefer ProxyJump. It keeps the private key material entirely on your laptop.
Step 5 — Lock down permissions
OpenSSH refuses to use files that are too open. Fix permissions once:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_prod ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519_prod.pub
Step 6 — Agent forwarding, done safely
If you genuinely need to authenticate from prod to a third system (e.g. clone a private Git repo on the server), forward the agent per-command rather than globally:
ssh -A prod
Or scope it in config to a single trusted host:
Host prod
HostName 10.0.1.20
User deploy
ProxyJump bastion
ForwardAgent yes
The caveat: anyone with root on the forwarded host can use your agent to impersonate you for the duration of the session. Never set ForwardAgent yes under Host *, and never forward to a host you don't control. A safer alternative for Git is to use a deploy key on the server or ProxyJump plus running Git locally.
Verify it works
Connect using just the alias:
ssh prod
You should land on the production shell without a password prompt (you may be asked for the key passphrase once if it isn't cached). Confirm where you are:
hostname && whoami
To see the connection path and prove the jump host is being used, run verbose mode:
ssh -v prod
Look for lines mentioning Connecting to 198.51.100.5 (the bastion) followed by the channel to 10.0.1.20, and Authenticated to ... using "publickey". If you see publickey, password auth was never used.
Troubleshooting
Permission denied (publickey)
The server didn't accept your key. Verify the public key is actually in the remote ~/.ssh/authorized_keys, that ssh-add -l lists your key locally, and that file permissions are correct (Step 5). Run ssh -v prod and check which key is being offered.
Bad owner or permissions on ~/.ssh/config
The config or key file is group/world-writable. Re-run the chmod commands in Step 5. The config must be 600 (or at most 644) and owned by your user.
Too many authentication failures
Your agent is offering many keys and hitting the server's MaxAuthTries. Add IdentitiesOnly yes and an explicit IdentityFile to the host block (already in our config above) so only the right key is tried.
Jump host connects but prod times out
The bastion can't reach the prod private IP, or a firewall/security group blocks it. Test from the bastion directly: ssh bastion, then nc -vz 10.0.1.20 22. Fix the internal routing/security-group rule rather than exposing prod publicly.
Next steps
- Harden the server's sshd: set
PasswordAuthentication noandPermitRootLogin noin/etc/ssh/sshd_config, thensudo systemctl reload ssh. Keep a working key session open while you test, so you don't lock yourself out. - Short-lived certificates: for teams, replace static keys with an SSH certificate authority (e.g. HashiCorp Vault's SSH secrets engine or Smallstep
step-ca) so access expires automatically. - Hardware-backed keys: generate
ssh-keygen -t ed25519-skwith a FIDO2 security key so the private key can't be exfiltrated from disk. - Audit access centrally with a bastion that logs sessions (e.g. Teleport) instead of distributing
authorized_keysby hand.
Discussion 0
Join the discussion
Sign in with GitHub to comment and vote.
No comments yet
Be the first to weigh in.