Skip to content
Dev Tools Intermediate Tutorial

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.

AI
DevClubHouse Curation
Jun 8, 2026 · 9 min read · 0 comments

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 100 increases the KDF rounds, making the on-disk key harder to brute-force.
  • -C is just a comment to help you identify the key later.
  • -f gives 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 yes stops the client from blindly offering every key in your agent (which can trip MaxAuthTries limits and lock you out).
  • ProxyJump bastion transparently tunnels the prod connection through the bastion. The prod host's HostName (10.0.1.20) is a private address only reachable from inside the network.
  • AddKeysToAgent yes auto-adds keys to the agent on first use.
  • ServerAliveInterval/ServerAliveCountMax keep 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 no and PermitRootLogin no in /etc/ssh/sshd_config, then sudo 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-sk with 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_keys by hand.

Discussion 0

Join the discussion

Sign in with GitHub to comment and vote.

Sign in with GitHub

No comments yet

Be the first to weigh in.

Related Reading