SSH Configuration, Jump Hosts, and Port Forwarding
Turn repeated SSH command-line options into precise client profiles, traverse bastion hosts without exposing agents, and use local, remote, and dynamic forwarding with explicit security boundaries.
Learning objectives
By the end of this lesson
- Understand client configuration files, matching, precedence, and first-obtained values.
- Create clear host aliases with destination-specific users, ports, identities, and trust policy.
-
Use
ProxyJumpto reach private hosts through a bastion. - Differentiate local, remote, and dynamic forwarding and identify which interface receives the listener.
- Validate effective configuration and forwarding plans before opening a connection.
1. Client configuration turns intent into repeatable policy
Long command lines are difficult to review and easy to mistype.
OpenSSH client configuration maps aliases and host patterns to
options such as the real hostname, user, port, identity, proxy path,
host-key database, timeouts, and forwarding behavior. User
configuration normally lives in ~/.ssh/config;
system-wide defaults live in /etc/ssh/ssh_config and
included files.
For most keywords, the first value obtained wins. That means specific host blocks should normally appear before broad wildcard defaults. Command-line options have higher precedence than user configuration, which has higher precedence than system configuration.
flowchart TD A["ssh app-prod"] --> B["Command-line options"] B --> C["User ~/.ssh/config"] C --> D["System ssh_config and includes"] D --> E["First obtained value for most options"] E --> F["Connect to bastion if ProxyJump applies"] F --> G["End-to-end SSH connection to destination"] G --> H["Session or forwarding channels"]
2. Build explicit host profiles
# ~/.ssh/config
Host bastion-prod
HostName bastion.example.net
User ops
Port 22
IdentityFile ~/.ssh/id_ed25519_bastion
IdentitiesOnly yes
Host app-prod
HostName 10.20.30.40
User deploy
IdentityFile ~/.ssh/id_ed25519_app_prod
IdentitiesOnly yes
ProxyJump bastion-prod
ServerAliveInterval 30
ServerAliveCountMax 3
# Broad defaults belong after more specific blocks.
Host *
HashKnownHosts yes
ConnectTimeout 10
UpdateHostKeys yes
ForwardAgent no
chmod 700 "$HOME/.ssh"
chmod 600 "$HOME/.ssh/config"
# Print the effective configuration for review
ssh -G app-prod | grep -E '^(hostname|user|port|identityfile|proxyjump|forwardagent|serveralive) '
# Parse and explain verbose connection setup without prompting for a password
ssh -vvv -o BatchMode=yes -o ConnectTimeout=5 app-prod true 2> ssh-debug.log || true
sed -n '1,180p' ssh-debug.log
IdentitiesOnly yes prevents the client from offering
every key available through files and agents. This reduces
authentication noise and avoids hitting server attempt limits before
the intended identity is tried.
3. ProxyJump traverses a bastion without creating a
shell there
A jump host is a network transit point. The client authenticates to the bastion, asks it to connect to the destination, and carries an end-to-end SSH connection through that channel. The destination still performs its own host verification and user authentication.
# One-time command-line form
ssh -J ops@bastion.example.net deploy@10.20.30.40
# Multiple hops are comma-separated
ssh -J ops@edge.example.net,ops@bastion.internal deploy@app.internal
# Inspect the generated proxy command
ssh -G app-prod | grep -E '^(proxyjump|proxycommand) '
# Copy through the same profile
scp ./artifact.tar.gz app-prod:/srv/releases/
sftp app-prod
ProxyJump does not require exposing your local agent
to the bastion. Use separate identities for the bastion and
destination, and keep ForwardAgent no unless a
reviewed workflow specifically requires it.
4. Forwarding binds a listener and carries another connection
-LSSH client sideReach a service visible from the server side
-RSSH server sideExpose a client-side service to the server side
-DSSH client sideCreate a SOCKS proxy for client applications
# Local forwarding: localhost:15432 -> database.internal:5432 via app-prod
ssh -N \
-o ExitOnForwardFailure=yes \
-L 127.0.0.1:15432:database.internal:5432 \
app-prod
# Dynamic SOCKS proxy bound only to loopback
ssh -N \
-o ExitOnForwardFailure=yes \
-D 127.0.0.1:1080 \
app-prod
# Remote forwarding: server loopback:18080 -> client loopback:8080
ssh -N \
-o ExitOnForwardFailure=yes \
-R 127.0.0.1:18080:127.0.0.1:8080 \
app-prod
# Verify local listeners in another terminal
ss -lntp | grep -E ':(15432|1080)\b' || true
The bind address controls exposure. Binding a local forward to
127.0.0.1 limits access to the client host. Binding to
0.0.0.0 can expose the forwarded service to an entire
network. Remote forwarding exposure is also affected by server-side
GatewayPorts and forwarding policy.
5. Connection sharing and background tunnels need lifecycle controls
Multiplexing can reuse one authenticated connection for later
sessions, reducing latency and authentication prompts. Long-running
tunnels must have clear ownership, socket paths, failure detection,
restart policy, and teardown procedures. An unexplained background
ssh -fN process is operational debt.
Host app-prod
ControlMaster auto
ControlPersist 10m
ControlPath ~/.ssh/control-%C
ExitOnForwardFailure yes
# Ask whether a master connection exists
ssh -O check app-prod 2>&1 || true
# Start a master intentionally
ssh -MNf app-prod
ssh -O check app-prod
# Open another session through it
ssh app-prod 'hostname; id'
# Stop the master cleanly
ssh -O exit app-prod
# Inspect control sockets
find "$HOME/.ssh" -maxdepth 1 -type s -name 'control-*' -ls 2>/dev/null || true
A tunnel can make an internal service appear on another host or network. Treat forwards as temporary network changes: document source, destination, bind address, owner, purpose, lifetime, and monitoring.
6. Hands-on lab: compile and audit an isolated SSH configuration
This lab creates a separate configuration file and asks OpenSSH to
resolve it. It makes no network connection and does not alter
~/.ssh/config.
lab="$HOME/devops-academy/linux/chapter13/lesson03"
rm -rf "$lab"
install -d -m 700 "$lab"
cd "$lab"
cat > ssh_config_lab <<'EOF'
Host lab-bastion
HostName bastion.example.net
User ops
IdentityFile ~/.ssh/id_ed25519_lab_bastion
IdentitiesOnly yes
Host lab-app
HostName 10.20.30.40
User deploy
IdentityFile ~/.ssh/id_ed25519_lab_app
IdentitiesOnly yes
ProxyJump lab-bastion
LocalForward 127.0.0.1:15432 database.internal:5432
ExitOnForwardFailure yes
Host *
ConnectTimeout 8
ForwardAgent no
HashKnownHosts yes
EOF
chmod 600 ssh_config_lab
ssh -F ssh_config_lab -G lab-app > effective.txt
grep -E '^(hostname|user|identityfile|proxyjump|localforward|exitonforwardfailure|forwardagent|connecttimeout) ' \
effective.txt > audit.txt
# Assertions: fail the lab if risky or unexpected values appear
grep -qx 'hostname 10.20.30.40' audit.txt
grep -qx 'user deploy' audit.txt
grep -qx 'proxyjump lab-bastion' audit.txt
grep -qx 'forwardagent no' audit.txt
grep -q 'localforward \[127.0.0.1\]:15432 \[database.internal\]:5432' audit.txt
sha256sum ssh_config_lab effective.txt audit.txt > evidence.sha256
Verification checklist
7. Common configuration and forwarding mistakes
“The last matching Host block wins.”
For most keywords, the first obtained value wins. Put specific rules before broad defaults.
“ProxyJump requires ForwardAgent.”
It does not. The client creates transport through the bastion while keeping destination authentication local.
“A local forward is automatically local-only.”
The bind address decides exposure. An all-interface bind may expose the tunnel to other hosts.
“A successful SSH login proves the forward works.”
Use ExitOnForwardFailure and verify the listener
and destination application separately.
8. Knowledge check
Question 1. Why are specific Host blocks usually placed before
Host *?
Question 2. What security advantage does ProxyJump have over agent forwarding?
Question 3. What does
-L 127.0.0.1:15432:db:5432 create?
9. Summary
SSH client configuration is executable network policy. Clear
aliases, destination-specific identities, first-value precedence,
and ssh -G make behavior reviewable.
ProxyJump provides controlled transit without agent
exposure. Local, remote, and dynamic forwards create real listeners
and must be bounded by bind addresses, server policy, failure
detection, ownership, and lifecycle controls.
10. Further reading
-
OpenSSH
ssh_config(5)manual. -
ssh(1)options for jump hosts, forwarding, multiplexing, and control commands. - Your organization’s bastion, network segmentation, and privileged-access standards.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.