Files
howtos/topics/ssh.md
user 7e8661f68b docs: add non-interactive ssh-add patterns to ssh howto
Cover passphrase injection via script(1) with env vars
and the SSH_ASKPASS alternative for headless automation.
2026-02-22 02:13:32 +01:00

9.1 KiB

SSH

Secure shell — encrypted remote access, tunnels, and file transfer.

Basics

# Connect
ssh user@host
ssh user@host -p 2222                  # non-standard port
ssh host                               # uses current username

# Run command remotely (no interactive shell)
ssh user@host 'uptime'
ssh user@host 'cat /etc/os-release'
ssh user@host 'sudo systemctl restart nginx'

# Run local script on remote host
ssh user@host 'bash -s' < local-script.sh

# Force pseudo-terminal (needed for interactive commands via ssh)
ssh -t user@host 'sudo vim /etc/hosts'

Keys

# Generate key pair
ssh-keygen -t ed25519 -C "user@host"
ssh-keygen -t ed25519 -f ~/.ssh/id_project -C "project key"

# Copy public key to remote host
ssh-copy-id user@host
ssh-copy-id -i ~/.ssh/id_project.pub -p 2222 user@host

# Manual copy (when ssh-copy-id unavailable)
cat ~/.ssh/id_ed25519.pub | ssh user@host 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'

# Permissions (must be strict or SSH refuses)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519           # private key
chmod 644 ~/.ssh/id_ed25519.pub       # public key
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/config

# Show key fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub
ssh-keygen -lvf ~/.ssh/id_ed25519.pub  # visual ASCII art

SSH Agent

# Start agent
eval "$(ssh-agent -s)"

# Add key
ssh-add ~/.ssh/id_ed25519
ssh-add -t 3600 ~/.ssh/id_ed25519     # expire after 1 hour

# List loaded keys
ssh-add -l

# Remove all keys
ssh-add -D

# Forward agent to remote host (-A flag)
ssh -A user@bastion                    # remote can use your local keys

Non-Interactive ssh-add (passphrase from env)

ssh-add insists on a terminal for passphrase input. Use script to fake a TTY and feed the passphrase from an environment variable:

# SSH_KEY_PASS must be set (e.g. sourced from a secrets file)
{ sleep 0.1; echo "$SSH_KEY_PASS"; } | script -q /dev/null -c "ssh-add $HOME/.ssh/id_ed25519"

Useful in automation (CI, cron, Ansible) where no interactive terminal exists.

# Full pattern: source secrets, start agent, add key
source ~/.bashrc.secrets               # exports SSH_KEY_PASS
eval "$(ssh-agent -s)"
{ sleep 0.1; echo "$SSH_KEY_PASS"; } | script -q /dev/null -c "ssh-add $HOME/.ssh/id_ed25519"
ssh-add -l                             # verify key loaded

Alternative with SSH_ASKPASS (avoids script):

export SSH_ASKPASS_REQUIRE=force
export SSH_ASKPASS="$(mktemp)" && printf '#!/bin/sh\necho "$SSH_KEY_PASS"' > "$SSH_ASKPASS" && chmod +x "$SSH_ASKPASS"
ssh-add ~/.ssh/id_ed25519
rm -f "$SSH_ASKPASS"

Config File (~/.ssh/config)

# Global defaults
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes

# Simple host alias
Host web1
    HostName 192.168.1.10
    User deploy
    Port 22
    IdentityFile ~/.ssh/id_ed25519

# Jump host (bastion)
Host internal-db
    HostName 10.0.0.50
    User admin
    ProxyJump bastion

Host bastion
    HostName bastion.example.com
    User jump
    IdentityFile ~/.ssh/id_bastion
    ForwardAgent yes

# Wildcard match
Host *.staging.example.com
    User deploy
    IdentityFile ~/.ssh/id_staging
    StrictHostKeyChecking no

# Multiple jump hosts
Host deep-internal
    HostName 10.10.0.5
    User admin
    ProxyJump bastion,middle-host

# Keep connection alive for reuse
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

After config, connect with just:

ssh web1                               # expands to full connection
ssh internal-db                        # routes through bastion

Config Directives Reference

Directive Purpose
HostName Real hostname/IP
User Login username
Port SSH port (default: 22)
IdentityFile Path to private key
IdentitiesOnly Only use specified keys, not agent
ProxyJump Jump through bastion host(s)
ProxyCommand Custom proxy command
ForwardAgent Forward agent to remote (yes/no)
ServerAliveInterval Keepalive interval in seconds
ServerAliveCountMax Keepalive failures before disconnect
ControlMaster Enable connection multiplexing
ControlPath Socket path for multiplexed connections
ControlPersist Keep master connection alive (seconds)
StrictHostKeyChecking ask, yes, no, accept-new
UserKnownHostsFile Path to known_hosts file
LocalForward Persistent local tunnel (same as -L)
RemoteForward Persistent remote tunnel (same as -R)
DynamicForward Persistent SOCKS proxy (same as -D)

Tunnels / Port Forwarding

Local Forward (-L)

Access a remote service through a local port.

# Local:8080 -> remote's localhost:80
ssh -L 8080:localhost:80 user@host

# Local:5432 -> db server via jump host
ssh -L 5432:db.internal:5432 user@bastion

# Bind to all interfaces (not just localhost)
ssh -L 0.0.0.0:8080:localhost:80 user@host

# Background tunnel (no shell)
ssh -fNL 8080:localhost:80 user@host

Remote Forward (-R)

Expose a local service to the remote host.

# Remote:9090 -> local:3000
ssh -R 9090:localhost:3000 user@host

# Background tunnel
ssh -fNR 9090:localhost:3000 user@host

Dynamic Forward (-D) — SOCKS Proxy

# SOCKS5 proxy on local:1080
ssh -D 1080 user@host

# Use with curl
curl --socks5-hostname localhost:1080 https://example.com

# Background SOCKS proxy
ssh -fND 1080 user@host

Tunnel Flags

Flag Purpose
-f Fork to background after authentication
-N No remote command (tunnel only)
-T Disable pseudo-terminal allocation
-g Allow remote hosts to use local forwards

File Transfer

SCP

# Local -> remote
scp file.txt user@host:/tmp/
scp -P 2222 file.txt user@host:/tmp/    # non-standard port
scp -r ./dir user@host:/opt/            # recursive

# Remote -> local
scp user@host:/var/log/app.log ./
scp -r user@host:/opt/data ./local-data/

# Between two remotes
scp user@host1:/tmp/file.txt user@host2:/tmp/

Rsync over SSH

# Sync directory to remote
rsync -avz ./src/ user@host:/opt/app/src/

# Sync from remote
rsync -avz user@host:/var/log/ ./logs/

# Custom SSH port
rsync -avz -e 'ssh -p 2222' ./src/ user@host:/opt/app/

# Dry run
rsync -avzn ./src/ user@host:/opt/app/src/

# Delete remote files not in local
rsync -avz --delete ./src/ user@host:/opt/app/src/

# Exclude patterns
rsync -avz --exclude='.git' --exclude='*.pyc' ./src/ user@host:/opt/app/

SFTP

sftp user@host
# Interactive commands: ls, cd, get, put, mkdir, rm

# Non-interactive
sftp user@host <<< 'get /var/log/app.log'

Known Hosts

# Remove stale entry (after server rebuild)
ssh-keygen -R hostname
ssh-keygen -R 192.168.1.10
ssh-keygen -R "[hostname]:2222"        # non-standard port

# Show stored fingerprint
ssh-keygen -F hostname

# Accept new keys automatically, reject changed keys
# In ~/.ssh/config:
#   StrictHostKeyChecking accept-new

Escape Sequences

During an SSH session, press Enter then:

Sequence Action
~. Disconnect (kill hung session)
~^Z Suspend SSH (background)
~# List forwarded connections
~& Background SSH (when waiting to close)
~? Show escape help
~~ Send literal ~

Hardening (Server-Side)

# /etc/ssh/sshd_config — key settings
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
X11Forwarding no
AllowUsers deploy admin
# Restart after changes
sudo systemctl restart sshd

Gotchas

  • Permissions too open on ~/.ssh/ or keys = SSH silently refuses them
  • ForwardAgent yes on untrusted hosts = your keys are exposed to that host's root
  • ~. escape only works at start of line — press Enter first
  • -L/-R tunnels bind to localhost by default — use 0.0.0.0: to expose wider
  • ControlPersist keeps connections open after exit — ssh -O exit host to close
  • ProxyJump requires OpenSSH 7.3+ — older versions need ProxyCommand ssh -W %h:%p bastion
  • scp is deprecated in favor of sftp or rsync in newer OpenSSH — still works but may warn
  • Agent forwarding through multiple hops: each hop must have ForwardAgent yes

See Also

  • ansible — uses SSH as transport
  • SSH.com Academy
  • man ssh_config — full config reference