SSH Cheatsheet
Last updated 11th May 2026

SSH Cheatsheet

What it is

SSH (Secure Shell) is an encrypted protocol for remote login, file transfer, and port forwarding. For deployment workflows it's the transport layer almost everything else rides on — git push over SSH, rsync over SSH, scp, deploy-time SSH commands, and the post-deploy hooks that DeployHQ runs on your servers all use it.

This sheet covers the commands you actually run when connecting to servers, managing keys, tunneling, and wiring SSH into a deploy pipeline — plus the deployment-workflow patterns that matter once you're juggling keys across CI, multiple hosts, and team members.

Quick reference

Connecting

ssh user@host                                       # default port 22
ssh -p 2222 user@host                               # non-default port
ssh -i ~/.ssh/deploy_ed25519 user@host              # specific key
ssh user@host 'uptime'                              # one-shot remote command
ssh -t user@host 'sudo systemctl restart api'      # force TTY (needed for sudo prompts)
ssh -v user@host                                    # verbose; -vvv for debug
ssh -q user@host                                    # quiet (suppress banners)
ssh -N user@host                                    # no remote command — for tunnels only

Key generation and management

ssh-keygen -t ed25519 -C "deploy@myproject"         # modern default; small + fast
ssh-keygen -t ed25519 -f ~/.ssh/deploy_ed25519      # write to a specific path
ssh-keygen -t rsa -b 4096 -C "fallback"             # only when ed25519 isn't supported
ssh-keygen -p -f ~/.ssh/id_ed25519                  # change passphrase on existing key
ssh-keygen -y -f ~/.ssh/id_ed25519                  # print public key from a private key
ssh-keygen -lf ~/.ssh/id_ed25519.pub                # show fingerprint
ssh-keygen -R old-host.example.com                  # remove host from known_hosts
ssh-keyscan -t ed25519 host.example.com >> ~/.ssh/known_hosts   # pre-populate fingerprint

Public key authentication

ssh-copy-id user@host                               # appends id to ~/.ssh/authorized_keys
ssh-copy-id -i ~/.ssh/deploy_ed25519.pub user@host  # specific key
cat ~/.ssh/id_ed25519.pub | ssh user@host \
  'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
                                                    # manual install when ssh-copy-id absent

~/.ssh/config essentials

# ~/.ssh/config — per-user; overrides /etc/ssh/ssh_config
Host prod
    HostName prod.example.com
    User deploy
    Port 2222
    IdentityFile ~/.ssh/deploy_ed25519
    IdentitiesOnly yes                              # don't try every key in agent
    ServerAliveInterval 60
    ServerAliveCountMax 3

Host bastion
    HostName bastion.example.com
    User ops
    IdentityFile ~/.ssh/ops_ed25519

Host prod-internal
    HostName 10.0.1.42
    User deploy
    ProxyJump bastion                               # tunnel through the bastion

Host github.com
    User git
    IdentityFile ~/.ssh/github_ed25519
    IdentitiesOnly yes

File transfer

scp file.tar user@host:/tmp/                        # single file → remote
scp -r ./build user@host:/var/www/                  # recursive
scp user@host:/var/log/app.log ./                   # remote → local
scp -P 2222 -i ~/.ssh/deploy_ed25519 file user@host:~/   # non-default port + key

sftp user@host                                      # interactive transfer session
sftp> put local.txt
sftp> get remote.txt
sftp> bye

Port forwarding (tunnels)

ssh -L 5432:localhost:5432 user@host                # local → remote (DB on prod)
ssh -L 5432:db.internal:5432 user@bastion           # local → remote-of-remote
ssh -R 8080:localhost:80 user@host                  # expose local service to remote
ssh -D 1080 user@host                               # SOCKS5 proxy via remote
ssh -fNL 5432:localhost:5432 user@host              # background tunnel (-f) with no command (-N)

ssh-agent and key forwarding

eval "$(ssh-agent -s)"                              # start the agent in current shell
ssh-add ~/.ssh/deploy_ed25519                       # load key (prompts for passphrase once)
ssh-add -l                                          # list loaded keys
ssh-add -L                                          # list loaded public keys
ssh-add -d ~/.ssh/deploy_ed25519                    # remove a key
ssh-add -D                                          # remove ALL keys
ssh-add -t 3600 ~/.ssh/deploy_ed25519               # auto-expire after 1 hour

ssh -A user@host                                    # forward agent (use sparingly — see below)

Inspection and debugging

ssh -v user@host                                    # show key negotiation, auth steps
ssh -G user@host                                    # show resolved config (effective settings)
ssh -o LogLevel=DEBUG3 user@host                    # max verbosity
nc -zv host 22                                      # is port 22 open?
ssh -T git@github.com                               # test GitHub SSH auth (no shell)
sudo journalctl -u ssh -f                           # tail sshd logs on the remote

Deployment workflows

Deploy keys vs personal keys

A personal key belongs to a human and travels with their laptop, agent, and possibly multiple machines. A deploy key is a single-purpose key that lives on a CI runner or production server and authorises only one repository or host.

Mixing them is the most common SSH mistake in deploy pipelines. Personal keys leaking into CI mean any compromise of that runner is a compromise of every repo the human can access.

Rules that hold up: - Deploy keys are read-only by default, scoped to a single repo or host. Generate one per repo or per environment, never one master key. - Deploy keys never travel. They're created on the host that uses them (CI runner, production box) and the private half never moves. - Personal keys never live on servers. Use them to push to GitHub from your laptop and to log into shared servers via your own account — that's it.

# on a fresh CI runner: generate a per-repo deploy key
ssh-keygen -t ed25519 -f ~/.ssh/deploy_myproject -N "" -C "ci-deploy-key:myproject"

# upload the public half to GitHub: Settings → Deploy keys → Add deploy key
cat ~/.ssh/deploy_myproject.pub

In DeployHQ, this is automated — when you connect a server, DeployHQ generates a unique SSH key per project and you add the public half to your server's ~/.ssh/authorized_keys. The private half never leaves DeployHQ.

~/.ssh/config as deployment infrastructure

A neglected ~/.ssh/config is one of the highest-ROI files on a developer machine. With it tuned:

  • ssh prod instead of ssh -p 2222 -i ~/.ssh/deploy_ed25519 deploy@prod.example.com
  • ssh prod-db runs through the bastion automatically — no manual -J
  • git clone myorg:repo uses an org-specific key
  • Deploy scripts become portable; you change one file when DNS or ports move

Production pattern:

# ~/.ssh/config — checked into a private dotfiles repo, not the project repo

Host bastion
    HostName bastion.prod.example.com
    User ops
    IdentityFile ~/.ssh/ops_ed25519
    IdentitiesOnly yes

Host prod-*
    User deploy
    IdentityFile ~/.ssh/deploy_ed25519
    IdentitiesOnly yes
    ProxyJump bastion
    ServerAliveInterval 60

Host prod-web-1
    HostName 10.0.1.10
Host prod-web-2
    HostName 10.0.1.11
Host prod-db
    HostName 10.0.2.5

# multi-account git
Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/personal_ed25519
    IdentitiesOnly yes
Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/work_ed25519
    IdentitiesOnly yes

IdentitiesOnly yes is the line most tutorials skip — without it, SSH offers every key in your agent before falling back to the one you specified. Production servers that throttle on auth failure will lock you out for 5 minutes after the third wrong key.

Agent forwarding: when to use it, when to avoid it

ssh -A (agent forwarding) lets a session on a remote host use the keys loaded in your local agent. The classic legitimate use is git pull from a private repo on a server where you don't want the deploy key to have repo-write access:

ssh -A deploy@staging
# inside the session — uses your local agent
git pull origin main

The catch: the remote host can use any key in your agent for any SSH connection it can reach, while you're connected. If that host is compromised, an attacker has a window to authenticate as you anywhere your agent has access.

Mitigations that actually work: - Use a separate agent socket per session: ssh -o "ForwardAgent=yes" -A user@host only on hosts you trust. - Load only the key you need with ssh-add before the session, then ssh-add -D after. - Auto-expire keys: ssh-add -t 3600 ~/.ssh/deploy_ed25519 removes the key after an hour. - Prefer ProxyJump (set up above) over agent forwarding when you're just hopping through a bastion — ProxyJump doesn't expose your agent to the bastion at all.

Server-side hardening for production hosts

A production server running sshd defaults is a server with too much attack surface. The minimum set:

# /etc/ssh/sshd_config — on every production host

Port 2222                                           # not security, but cuts log noise from drive-by scans
PermitRootLogin no
PasswordAuthentication no                           # keys only
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AllowUsers deploy ops                               # explicit allowlist
LoginGraceTime 30
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowAgentForwarding no                             # only enable per-host if you actually need it
PermitTTY yes

After editing, validate before reloading — a broken sshd_config can lock you out:

sudo sshd -t                                        # syntax check; non-zero exit = don't reload
sudo systemctl reload ssh                           # apply without dropping existing sessions

Always test from a second SSH session before disconnecting the first. A new session uses the new config; the existing session keeps the old one. If the new session works, the old one was your insurance policy.

Key rotation without lockout

Keys leak — laptops get stolen, ex-employees keep copies, CI runners get compromised. The only safe rotation playbook is one that adds the new key before removing the old one.

# 1. generate the replacement
ssh-keygen -t ed25519 -f ~/.ssh/deploy_ed25519_new -C "deploy@myproject:rotated-2026-q2"

# 2. install the NEW key alongside the old one on every host
ssh-copy-id -i ~/.ssh/deploy_ed25519_new.pub deploy@prod-web-1
ssh-copy-id -i ~/.ssh/deploy_ed25519_new.pub deploy@prod-web-2

# 3. verify the new key works (separate session — keep the old session alive)
ssh -i ~/.ssh/deploy_ed25519_new deploy@prod-web-1 'whoami'

# 4. switch deploy pipelines / CI / DeployHQ to the new key
#    (in DeployHQ: Servers → <server> → SSH key → upload new public key)

# 5. only after a few successful deploys: remove the old key from authorized_keys
ssh deploy@prod-web-1
sed -i.bak '/old-fingerprint-or-comment/d' ~/.ssh/authorized_keys

# 6. archive or destroy the old private key
mv ~/.ssh/deploy_ed25519 ~/.ssh/deploy_ed25519.retired-2026-q2

Schedule rotation on a calendar — quarterly for shared keys, immediately on staff turnover. Don't wait for an incident.


Common errors and fixes

Error Cause Fix
Permission denied (publickey) Server didn't accept any of the keys you offered Run ssh -v user@host and look at "Offering public key" lines. Either the right key isn't in ~/.ssh/config/agent, or the public half isn't in the server's ~/.ssh/authorized_keys. Set IdentitiesOnly yes to stop offering wrong keys.
Permission denied (publickey,password) after key was working ~/.ssh permissions changed on the server ls -ld ~/.ssh && ls -l ~/.ssh/authorized_keys on the remote. Required: ~/.ssh = 700, authorized_keys = 600, both owned by the user. SSH refuses to use them otherwise.
Host key verification failed The host's key changed since the last connection Either the host was reinstalled (legitimate — ssh-keygen -R host to remove old fingerprint), or it's a MITM attack (verify out-of-band before accepting).
Too many authentication failures Agent offered too many keys; server hit its MaxAuthTries cap Set IdentitiesOnly yes for that Host in ~/.ssh/config, or ssh -o IdentitiesOnly=yes -i ~/.ssh/specific_key user@host.
Connection refused sshd isn't running, port is wrong, or firewall is blocking Check the port: nc -zv host 22. SSH into a different host on the same network to confirm it's not your local network. On the remote: sudo systemctl status ssh.
Agent admitted failure to sign using the key Agent has the wrong key, or no keys at all ssh-add -l to list. If empty, ssh-add ~/.ssh/your_key. If the wrong key, ssh-add -d the wrong one.
Bad owner or permissions on ~/.ssh/config World/group write-permission on the config chmod 600 ~/.ssh/config && chmod 700 ~/.ssh.
kex_exchange_identification: read: Connection reset by peer TCP RST during handshake — usually rate-limiting or fail2ban Wait 10–15 minutes (the typical ban duration) and retry from a different IP if needed. Tune your ~/.ssh/config to use the correct key/port so you don't trip MaxAuthTries.
git@github.com: Permission denied (publickey) from CI Personal key isn't on the runner; deploy key isn't configured Generate a per-repo deploy key on the runner, add the public half as a GitHub deploy key. Never copy your personal key to CI.
Hangs at "debug1: pledge: network" Server is up but the network path is dropping packets, or ServerAliveInterval isn't set ssh -o ServerAliveInterval=30 .... If hangs persist mid-session, the route is unstable — try mosh for high-latency or flaky links.

Companion: full DeployHQ workflow over SSH

Every DeployHQ deployment is built on SSH:

  1. DeployHQ generates a unique SSH key per project. You install the public half into your server's ~/.ssh/authorized_keys for the deploy user.
  2. On every push, DeployHQ runs your build pipeline, then connects over SSH to ship the release.
  3. Atomic deployments handle the symlink swap so each release lands in a fresh directory and goes live in one filesystem operation — no half-deployed state.
  4. Pre-deploy and post-deploy SSH hooks run your custom commands (composer install --no-dev, php artisan migrate --force, systemctl reload nginx) inside the same SSH session.

Setup walkthroughs: the deploy from GitHub guide covers the GitHub side; the deploy from GitLab guide covers GitLab. Both end with the SSH key installation step.

If your team has agency clients spread across a dozen hosts with different SSH ports, users, and key requirements, the ~/.ssh/config pattern above is the single biggest workflow improvement you can make — and DeployHQ's for-agencies workflow handles the same fragmentation server-side.


  • Docker cheatsheet — for the deploy hosts where ssh lands on docker compose pull && up -d.
  • rsync cheatsheet — rsync-over-SSH is the most common file-based deploy transport.
  • curl cheatsheet — for the post-deploy smoke tests that confirm the SSH-triggered release is actually serving traffic.
  • Bash scripting cheatsheet — for the local and remote scripts that wrap your SSH commands with proper error handling.

Ship over SSH with DeployHQ

DeployHQ gives every project its own SSH key, atomic releases, and one-click rollback — over SSH, with no agent forwarding required and no shared keys. Start a free trial or browse the pricing tiers.

Need help? Email support@deployhq.com or follow us on @deployhq on X.