ssh
Log into and run commands on a remote machine securely
ssh opens an encrypted connection to another machine and gives you a shell on it, or runs a
single command there and streams the output back. It carries logins to servers, it is what
scp and rsync run over, it is what Git's git@github.com: URLs use, and every "tunnel a
port through a jump host" trick is built on it.
An encrypted pipe with a shell on the end
ssh user@host does three things in order: negotiate an encrypted channel, prove your identity
to the server (and, less obviously, prove the server's identity to you), then attach that channel
to either an interactive shell or a single remote command:
ssh user@deb1.example.com # interactive login shell
ssh user@deb1.example.com uptime # run one command, print its output, exit
Everything else ssh does, port forwarding, agent forwarding, X11 forwarding, is that same
encrypted channel carrying something other than a shell. Once you see forwarding as "another use
of the tunnel" rather than a separate feature, the -L/-R/-D flags stop looking arbitrary.
Two ways in: passwords and keys
Password authentication asks the remote machine to check a secret you type. Key-based
authentication asks it to check that you hold the private half of a key pair whose public
half is already listed in ~/.ssh/authorized_keys on the server; nothing secret ever crosses the
wire. ssh-keygen -t ed25519 generates a pair (Ed25519 is the current recommended type: smaller
and faster to verify than RSA, with no key-size decision to get wrong), and ssh-copy-id installs
the public half on a server you can still log into by password.
Keys can be encrypted at rest with a passphrase, which is what ssh-agent is for: unlock
a key once per session, hand it to the agent, and every subsequent ssh or scp call asks the
agent for a signature instead of prompting you again. If a private key's permissions are too
open (readable by group or other), ssh refuses to use it outright rather than risk a key that
anyone else on the box could read.
A connection that drops takes everything running under it with it, which is worth arranging for before it happens rather than after: keep a program running after you log out.
The config file: stop retyping flags
~/.ssh/config maps a short alias to a real hostname, user, port, and key, so ssh deb1 can mean
ssh -p 2222 -i ~/.ssh/id_ed25519 user@deb1.example.com. Entries are Host blocks read top to
bottom; the first matching value for a given setting wins, which is why host-specific blocks
belong above a catch-all Host * block, not below it. ssh -G <host> prints the fully resolved
configuration for a host without connecting, which is the fastest way to check why ssh is
using the port, key, or jump host it's using.
Host keys prove the server is the one you saw last time
Password or key checks prove who you are; a host key check proves who the server is. The
first time you connect to a new host, ssh shows a fingerprint and asks you to confirm it, then
remembers it (hashed, by default) in ~/.ssh/known_hosts. Every later connection compares the
server's key against that saved copy and refuses to continue if it's changed, which is exactly
what happens if someone is intercepting the connection, or, far more often in practice, if a
server was rebuilt or a hosting provider recycled an IP address. ssh-keygen -R <host> removes a
stale entry after you've confirmed the change is legitimate.
Forwarding sends something other than a shell down the tunnel
-L local:host:remote opens a port on your machine that tunnels to a port reachable from the
server's side, useful for reaching a database that only listens on a remote machine's loopback
interface. ss -ltn on the server shows such a service bound to 127.0.0.1
rather than to 0.0.0.0. -R runs the same idea backwards, exposing a port on your machine to
the server. -D
turns ssh into a SOCKS proxy, routing arbitrary traffic through the connection without picking a
single destination port up front. All three need the connection to stay open, so they're normally
combined with -N (no remote command) or -f (background after connecting).
Reusing one connection for everything
Every fresh ssh connection repeats the full handshake, which is the noticeable delay before a
prompt or scp transfer starts. ControlMaster/ControlPath/ControlPersist in the config file
let a second ssh (or scp, or rsync -e ssh) to the same host reuse an already-open connection
instead of negotiating a new one, cutting a login from a full round trip to almost instant.
Reading a failure
ssh exits 255 for a connection-level failure (unresolvable host, refused port, rejected key,
timeout) and passes through whatever the remote command exited with otherwise, so a script
checking ssh host cmd's exit code has to know which kind of failure it's looking at. See
Exit codes and error handling for handling that
distinction. When the reason for a 255 isn't obvious, -v (repeat up to -vvv) prints the
handshake step by step, showing exactly where it stalled.
Sample files used on this page
Every example below was run against these files. Recreate them to follow along.
~/.ssh/config the host block the -G and connection-sharing examples resolve against
Host deb1
HostName deb1.example.com
User user
Port 2222
IdentityFile ~/.ssh/id_ed25519
~/.ssh/known_hosts two saved host keys, for the lookup and removal examples - host keys are public, so nothing secret is shown
|1|54lfM/zb+rQclYh4kZPZwbDAJXY=|0nF4Tq2N1xiwJDS540Be7ylsvKU= ssh-ed25519
deb1.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOo
Connecting and running commands
The two shapes every ssh invocation takes: an interactive shell, or one command with its output streamed back.
Log into a remote machine
ssh user@deb1.example.com
Opens an interactive shell on the remote machine. Requires a real, reachable host; the address here is a placeholder.
Log in as the current local user
ssh deb1.example.com
With no user@ prefix, ssh uses your local username as the remote username.
Run a single command and exit
ssh user@deb1.example.com uptime
Anything after the host is executed remotely instead of starting a shell; ssh prints its output and exits with the remote command's exit status.
Connect on a non-default port
ssh -p 2222 user@deb1.example.com
-p overrides the default port 22. Common on boxes that move sshd off the default port to cut down on automated scan noise.
Check the installed ssh version
ssh -V
Prints the OpenSSH and OpenSSL versions in use, useful when a flag doesn't behave as documented and you suspect an older build. Note that both are printed on standard error, so ssh -V | grep OpenSSL finds nothing and ssh -V 2>&1 | grep OpenSSL works.
Show output
Your output will differ: the two version numbers and the OpenSSL release date are whatever your Debian ships
OpenSSH_10.0p2 Debian-7+deb13u4, OpenSSL 3.5.6 7 Apr 2026
Run a command that needs a real shell (quoting)
ssh user@deb1.example.com "cd /var/log && ls -la"
Quote multi-part commands as one string; otherwise ssh only sends the first word remotely and the rest gets interpreted locally as ssh's own arguments.
Pipe local input into a remote command
cat report.csv | ssh user@deb1.example.com "cat >> /var/log/reports.log"
Stdin is forwarded over the same connection, so a local pipeline can feed a remote command directly without an intermediate file.
Force a pseudo-terminal for a command that expects one
ssh -t user@deb1.example.com "sudo systemctl status ssh"
ssh only allocates a pseudo-terminal automatically for an interactive login. A command that needs one anyway, such as sudo prompting for a password or an interactive top, needs -t stated explicitly when run as a one-off.
Key-based authentication
Generating a key pair, installing the public half on a server, and letting an agent hold the unlocked key for the session.
Generate an Ed25519 key pair
ssh-keygen -t ed25519 -C "user@deb1" -f ./id_ed25519_test -N "" | head -3
-t ed25519 picks the current recommended key type (smaller and faster to verify than RSA). -C sets a comment, conventionally an identifying label, stored alongside the public key. -f names the output file and -N "" sets an empty passphrase; run without them, ssh-keygen prompts for both. It also prints the new key's fingerprint and a randomart image, which differ for every key generated.
Show output
Generating public/private ed25519 key pair.
./id_ed25519_test already exists.
Overwrite (y/n)?
View the public key
cat ~/.ssh/id_ed25519.pub
The public key is the line you share: it's safe to post anywhere, paste into a server's authorized_keys, or hand to a Git hosting provider.
Show output
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE/UVquqwrnITn1udSbK8/Vp2jejSDOUnYfQPsPIcM/T user@deb1
Show a key's fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub
Prints a short hash of the key instead of the full public key text, handy for visually comparing two keys without pasting the whole thing.
Show output
256 SHA256:EpmQiJDQS3h8FvlWEXRLoxPebOnp/F5nMGf59V9hYEs user@deb1 (ED25519)
Generate an RSA key at a specific size
ssh-keygen -t rsa -b 4096 -C "user@deb1"
RSA is still widely supported by older servers and appliances that don't understand Ed25519. -b 4096 sets the bit length; RSA's default of 3072 is acceptable, but 4096 is the common convention for a long-lived key.
Copy a public key to a server's authorized_keys
ssh-copy-id user@deb1.example.com
Appends your default public key to the remote account's ~/.ssh/authorized_keys, creating the file and directory with correct permissions if they don't exist. Needs a working login (password or an already-trusted key) to install the new one. Requires a real reachable host to verify.
Copy a specific, non-default key
ssh-copy-id -i ~/.ssh/id_ed25519_work.pub user@deb1.example.com
-i points at a .pub file explicitly, for accounts where you keep separate keys per server or per role rather than one default key for everything.
Start ssh-agent and add a key
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-agent -s starts the agent and prints shell variables that later ssh calls in the same session use to find it; eval applies them. ssh-add unlocks the key (prompting for its passphrase once) and hands it to the agent.
Show output
Agent pid 2961
Identity added: id_ed25519 (user@deb1)
List keys currently held by the agent
ssh-add -l
Confirms which keys the agent will offer, without exposing the private key material itself.
Show output
256 SHA256:EpmQiJDQS3h8FvlWEXRLoxPebOnp/F5nMGf59V9hYEs user@deb1 (ED25519)
See the error when no agent is running
ssh-add -l
Without SSH_AUTH_SOCK set (no agent started, or the variable lost in a new shell), ssh-add can't reach an agent at all.
Show output
Could not open a connection to your authentication agent.
Remove all keys from the agent
eval "$(ssh-agent -s)" >/dev/null
ssh-add ~/.ssh/id_ed25519 2>/dev/null
ssh-add -D
Clears every identity the agent is holding, without stopping the agent process itself. Useful before switching to a different set of keys mid-session.
Show output
All identities removed.
Change a key's passphrase
ssh-keygen -p -f ~/.ssh/id_ed25519
-p re-encrypts an existing private key with a new passphrase in place, without generating a new key pair or touching the public key or any authorized_keys entry that already references it.
Refuse a private key with overly open permissions
ssh-keygen -y -f ./id_ed25519_test
A private key readable by group or other (here deliberately set to 644) is rejected outright rather than used. chmod 600 on the key file is the fix.
Show output
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for './id_ed25519_test' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "./id_ed25519_test": bad permissions
Fix a private key's permissions
chmod 600 ~/.ssh/id_ed25519
Owner read/write only, nobody else. Restores a key ssh had refused for being too open. See File permissions explained for what the digits mean.
The ~/.ssh/config file
Aliasing a host, its port, user, and key so a full connection collapses to a short name.
Write a basic host alias
Host deb1
HostName deb1.example.com
User user
Port 2222
IdentityFile ~/.ssh/id_ed25519
Saved in ~/.ssh/config. After this, ssh deb1 expands to the full connection details; scp and rsync -e ssh honour the same file.
Set defaults that apply to every host
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
A catch-all Host * block sets fallback values. Order matters: the first matching value for a setting wins, so host-specific blocks must come above a catch-all, not below it.
Show the fully resolved config for a host
ssh -G deb1
Prints every setting ssh would actually use for deb1 after merging all matching Host blocks, without opening a connection. The fastest way to check why ssh is using a particular port, key, or jump host.
Show output
Pseudo-terminal will not be allocated because stdin is not a terminal.
host deb1
user user
hostname deb1.example.com
port 2222
addressfamily any
batchmode no
canonicalizefallbacklocal yes
canonicalizehostname false
checkhostip no
compression no
controlmaster false
enablesshkeysign no
clearallforwardings no
exitonforwardfailure no
fingerprinthash SHA256
forwardx11 no
forwardx11trusted yes
gatewayports no
gssapiauthentication yes
gssapikeyexchange no
gssapidelegatecredentials no
gssapitrustdns no
gssapirenewalforcesrekey no
gssapikexalgorithms gss-group14-sha256-,gss-group16-sha512-,gss-nistp256-sha256-,gss-curve25519-sha256-,gss-group14-sha1-,gss-gex-sha1-
hashknownhosts yes
hostbasedauthentication no
identitiesonly no
kbdinteractiveauthentication yes
nohostauthenticationforlocalhost no
passwordauthentication yes
permitlocalcommand no
proxyusefdpass no
pubkeyauthentication true
requesttty auto
sessiontype default
stdinnull no
forkafterauthentication no
streamlocalbindunlink no
stricthostkeychecking ask
tcpkeepalive yes
tunnel false
verifyhostkeydns false
visualhostkey no
updatehostkeys true
enableescapecommandline no
canonicalizemaxdots 1
connectionattempts 1
forwardx11timeout 1200
numberofpasswordprompts 3
serveralivecountmax 3
serveraliveinterval 0
requiredrsasize 1024
obscurekeystroketiming yes
ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr
hostkeyalgorithms ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256
hostbasedacceptedalgorithms ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256
kexalgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
casignaturealgorithms ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256
loglevel INFO
macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1
securitykeyprovider internal
pubkeyacceptedalgorithms ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256
xauthlocation /usr/bin/xauth
identityfile ~/.ssh/id_ed25519
canonicaldomains none
globalknownhostsfile /etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2
userknownhostsfile /home/user/.ssh/known_hosts /home/user/.ssh/known_hosts2
sendenv LANG
sendenv LC_*
sendenv COLORTERM
sendenv NO_COLOR
logverbose none
channeltimeout none
permitremoteopen any
addkeystoagent false
forwardagent no
connecttimeout none
tunneldevice any:any
canonicalizePermittedcnames none
controlpersist no
escapechar ~
ipqos ef cs1
rekeylimit 0 0
streamlocalbindmask 0177
syslogfacility USER
Use an alternate config file
ssh -F ~/.ssh/config-work deb1
-F points ssh at a different config file entirely, for keeping personal and work host lists separate.
Route through a jump host
Host internal
HostName 10.0.0.5
User user
ProxyJump deb1.example.com
ProxyJump (or ssh -J deb1.example.com internal) connects through an intermediate host first, useful for machines only reachable from inside a private network. Confirmed by resolving with ssh -G internal, which reports proxyjump deb1.example.com.
Disable host key checking for one throwaway host
Host scratch-vm
HostName 192.0.2.50
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Skips host key verification and avoids polluting your real known_hosts, appropriate for a disposable VM you rebuild constantly. Never do this for a host that holds anything real: it removes ssh's protection against a spoofed or swapped server.
Set a default identity file for every host
Host *
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
IdentitiesOnly yes restricts ssh to the keys explicitly listed instead of also offering every key an agent happens to be holding, avoiding a host locking you out after too many rejected keys.
Send environment variables to the remote session
Host deb1
SendEnv LANG LC_*
Forwards local environment variables matching the pattern, if the server's sshd_config has a matching AcceptEnv entry; most servers don't accept arbitrary variables by default.
Host keys and known_hosts
How ssh verifies the server is who it claims to be, and what to do when that check legitimately fails.
See the confirmation prompt for a first-time host
ssh -o StrictHostKeyChecking=ask user@newhost.example.com
The default behaviour: on a host ssh has never seen, it prints the key's fingerprint and asks for confirmation before continuing. Requires a real, unrecognised host to trigger; this is the default even without the explicit flag.
Accept a new host key automatically
ssh -o StrictHostKeyChecking=no user@deb1.example.com
Skips the confirmation prompt and accepts whatever key the server presents, adding it straight to known_hosts. Convenient for scripted, one-off connections to freshly provisioned hosts; never use it for a host whose identity matters, since it also silently accepts a key that's changed for the wrong reasons.
Query known_hosts for a saved entry
ssh-keygen -F github.com
Looks up a host's saved key without connecting anywhere. known_hosts entries are hashed by default (HashKnownHosts yes on Debian), so the file itself doesn't reveal which hosts you've connected to at a glance; -F decodes the lookup.
Show output
# Host github.com found: line 1
|1|54lfM/zb+rQclYh4kZPZwbDAJXY=|0nF4Tq2N1xiwJDS540Be7ylsvKU= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
Remove a stale host key
ssh-keygen -R deb1.example.com
Deletes a host's entry from known_hosts after you've confirmed a key change is legitimate (server rebuilt, hosting provider recycled the IP), so the next connection re-prompts instead of refusing outright. Keeps a .old backup of the file it edited.
Show output
# Host deb1.example.com found: line 2
/home/user/.ssh/known_hosts updated.
Original contents retained as /home/user/.ssh/known_hosts.old
Pre-populate known_hosts without logging in
ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts
Fetches a host's public key directly, the same way provisioning scripts avoid an interactive first-connection prompt. Only trustworthy over a connection you already trust (a private network, a host you just built yourself): it has no way to verify the key it receives, so it's exactly as blind as StrictHostKeyChecking=no for that one lookup.
Show output
# github.com:22 SSH-2.0-dad0df6
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
See what a changed host key looks like
ssh user@deb1.example.com
When a saved key no longer matches, ssh refuses the connection outright with a loud warning and exits without prompting, unlike the first-connection case. Genuine output from a real mismatch depends on the specific host; treat the warning as a stop sign, not something to route around with -o StrictHostKeyChecking=no out of habit.
Copying files over ssh
scp and rsync both reuse an ssh connection rather than needing a separate file-transfer server. See the full recipe for more: Copy files between two machines over SSH.
Copy a file to a remote host
scp report.pdf user@deb1.example.com:/home/user/backups/
Uses the same authentication and host key checking as ssh. The trailing slash means "into this directory," keeping the original filename.
Copy a file from a remote host
scp user@deb1.example.com:/var/log/syslog ./syslog-backup
Reversing the source and destination pulls a file down instead of pushing one up.
Copy recursively, preserving an ssh config alias
scp -r project/ deb1:/home/user/
-r copies a whole directory tree. Using the deb1 alias from ~/.ssh/config instead of the full user@host form works for scp exactly as it does for ssh.
Sync a tree with rsync over ssh
rsync -avz -e ssh project/ deb1:/home/user/project/
-e ssh is usually redundant (ssh is rsync's default remote shell) but useful for pinning a specific port or key: -e "ssh -p 2222". rsync only transfers what changed, unlike scp's full re-copy.
Copy directly between two remote hosts
scp user@deb1.example.com:/data/export.csv user@deb2.example.com:/data/
By default the data still routes through your local machine unless the server supports -3 mode; without a real pair of reachable hosts this can't be verified here, but the syntax is standard scp usage.
Port forwarding
Using the encrypted connection to carry traffic other than a shell.
Forward a local port to a service reachable from the remote side
ssh -L 8080:localhost:80 user@deb1.example.com
-L local_port:target_host:target_port opens localhost:8080 on your machine and tunnels connections to it through to target_host:target_port as seen from the remote server, useful for reaching a database or admin panel that only listens on the server's own loopback interface. Needs a real reachable target to demonstrate; the tunnel itself carries no output of its own.
Forward a local port without opening a shell
ssh -N -L 8080:localhost:80 user@deb1.example.com
-N tells ssh not to run any remote command at all, just hold the tunnel open. Combine with -f to background it after the connection is established.
Expose a local service on the remote host
ssh -R 9000:localhost:3000 user@deb1.example.com
The reverse of -L: a connection to port 9000 on the remote host tunnels back to port 3000 on your machine. Useful for letting a colleague, or a remote webhook, reach something only running on your laptop. The remote sshd needs GatewayPorts configured to expose this beyond the server's own loopback.
Run ssh as a SOCKS proxy
ssh -D 1080 user@deb1.example.com
-D port turns ssh into a dynamic SOCKS proxy on that local port; pointing a browser or another tool's proxy setting at localhost:1080 routes its traffic through the remote host, without picking a single destination port up front the way -L requires.
Background a forwarding-only connection
ssh -fN -L 5432:localhost:5432 user@deb1.example.com
-f backgrounds ssh right after authentication instead of holding the foreground, combined with -N for a tunnel with no shell attached. The classic incantation for "forward this one database port and get out of the way."
Check a forwarded port is actually listening locally
ss -tlnp | grep 8080
Confirms the local end of an -L tunnel bound successfully before you spend time debugging the application on the other end of it.
Multiplexing and performance
Reusing one authenticated connection instead of repeating the handshake for every ssh, scp, or rsync call to the same host.
Enable connection sharing for a host
Host deb1
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
The first connection to deb1 becomes the "master" and opens a control socket at ControlPath; later connections to the same host reuse it instead of renegotiating. ControlPersist 600 keeps the master alive for 10 minutes after the last session closes, ready for the next one. Confirmed the paths resolve correctly with ssh -G deb1, which reports the expanded controlpath.
Show output
controlmaster auto
controlpath /home/user/.ssh/sockets/user@deb1.example.com-22
controlpersist 600
Create the socket directory multiplexing needs
mkdir -p ~/.ssh/sockets
ControlPath doesn't create its parent directory automatically; the first connection attempt fails with a cryptic error if ~/.ssh/sockets doesn't already exist.
Check whether a master connection is active
ssh -O check deb1
-O check asks an existing control socket whether it's alive without opening a new connection. Needs a real multiplexed session already running to return anything meaningful.
Close a multiplexed master connection
ssh -O exit deb1
-O exit tells the control master to shut down, closing every session still riding on it. Useful after changing a key or a config value, which a live multiplexed connection would otherwise carry on using.
Agent and X11 forwarding
Extending the local session's identity or display to the remote end.
Forward the local agent to a remote host
ssh -A user@deb1.example.com
Lets commands run on the remote host authenticate onward using keys held by your local agent, without copying any private key to the server, useful for hopping from one server to a second one behind it. Only forward an agent to hosts you trust: anyone with root on the remote machine while the forwarding is active can request signatures from your agent.
Forward agent for one command only
ssh -A user@deb1.example.com "git pull"
Limits the exposure window from the previous example to the lifetime of a single remote command instead of an open-ended interactive session.
Forward a graphical application's display
ssh -X user@deb1.example.com
-X forwards X11 so a GUI application started remotely displays on your local screen. Requires a local X server and an X11-enabled sshd on the remote end to show anything; the connection itself is the only part verifiable without both.
Trust the forwarded display fully
ssh -Y user@deb1.example.com
-Y is -X without the extra security restrictions X11SecurityExtension normally applies. Only worth the trade-off for a specific application that breaks under the restricted mode.
Debugging a connection
Working out why ssh isn't behaving the way the config file suggests it should.
Get a step-by-step trace of the connection
ssh -v user@deb1.example.com
-v prints each stage as it happens: config file lines read, key exchange, which keys were offered and in what order, and where the process stopped. The first thing to try when a connection fails for a reason that isn't obvious.
Show output
debug1: OpenSSH_10.0p2 Debian-7+deb13u4, OpenSSL 3.5.6 7 Apr 2026
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files
debug1: /etc/ssh/ssh_config line 21: Applying options for *
debug1: Connecting to deb1.example.com port 22.
Increase verbosity further
ssh -vvv user@deb1.example.com
Each extra v (up to three) adds more detail, down to individual protocol messages. -vvv is usually more than you need; start with -v and escalate only if that doesn't show the problem.
See why a host is unreachable at all
ssh -o BatchMode=yes -o ConnectTimeout=3 nonexistent.invalid echo hi
BatchMode=yes stops ssh from ever prompting (for a password or a host key confirmation), so a script gets a clean failure instead of hanging. This is a real, unresolvable hostname, so the error and exit code below are genuine.
Show output
ssh: Could not resolve hostname nonexistent.invalid: Name or service not known
Check the exit code after a batch-mode failure
ssh -o BatchMode=yes -o ConnectTimeout=3 nonexistent.invalid echo hi
echo "exit=$?"
255 specifically means ssh itself failed to connect or authenticate, distinct from any exit code the remote command might have returned had it actually run.
Show output
ssh: Could not resolve hostname nonexistent.invalid: Name or service not known
exit=255
Test key-based auth against a known real host
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -T git@github.com
GitHub's ssh endpoint replies with an identity check even without a matching key, so it's a genuinely useful way to confirm ssh can reach some real server and negotiate a protocol, isolating whether a problem is network-level or specific to your own target host. -T disables the pseudo-terminal since no shell is expected.
Show output
git@github.com: Permission denied (publickey).
Scripting and hardening patterns
ssh calls that show up inside larger scripts, and config choices that reduce risk on a shared or internet-facing key.
Run a remote command and capture its output
result=$(ssh user@deb1.example.com "df -h /")
Standard command substitution works over ssh exactly as it does locally, since the remote command's stdout is just ssh's own stdout.
Check whether a host is reachable before proceeding
if ssh -o BatchMode=yes -o ConnectTimeout=3 user@deb1.example.com true; then
echo "reachable"
fi
true as the remote command does nothing but succeed, so the whole check reduces to "did ssh manage to connect and authenticate," reported as a plain exit code ready for an if.
Run the same command across several hosts
for host in deb1 deb2 deb3; do
echo "== $host =="
ssh -o BatchMode=yes "$host" 'uptime'
done
A simple fan-out loop. Each iteration is a fresh connection unless multiplexing is configured for these hosts; for more than a handful of hosts, a proper tool (Ansible, pssh) tracks failures per host far better than a bare loop.
List the key exchange and cipher algorithms this build supports
ssh -Q cipher
-Q queries ssh's own compiled-in capabilities, here symmetric ciphers offered during negotiation. Also accepts key, mac, kex, and others.
Show output
3des-cbc
aes128-cbc
aes192-cbc
aes256-cbc
aes128-ctr
aes192-ctr
aes256-ctr
aes128-gcm@openssh.com
aes256-gcm@openssh.com
chacha20-poly1305@openssh.com
Restrict a shared account key to one command
command="/usr/local/bin/backup.sh",no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAA... user@deb1
A command= prefix in authorized_keys forces that key to run only the named command, ignoring whatever the connecting client actually asked for, and the trailing options strip out forwarding capabilities the key doesn't need. Standard practice for a key handed to a backup script or CI system instead of a person.
Turn off password authentication server-side
# /etc/ssh/sshd_config
PasswordAuthentication no
Forces every login through a key, closing off online password-guessing entirely. Requires root on the server and a restart of sshd (systemctl reload ssh) to take effect; confirm at least one working key-based login first, or a mistake here locks out password access with no way back in except console access.