Chapter 13Lesson 03~65 minutes

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.

ssh_configProxyJumpTunneling lab

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 ProxyJump to 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.

SSH client configuration and jump-host path
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
Prefer jump transport to agent forwarding

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

ModeListener locationTypical purpose
Local -LSSH client sideReach a service visible from the server side
Remote -RSSH server sideExpose a client-side service to the server side
Dynamic -DSSH client sideCreate a SOCKS proxy for client applications
Stream local/remoteClient or server sideForward Unix-domain sockets where supported
# 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
Forwarding bypasses ordinary network paths

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

Next lesson

Secure File Transfer with scp, sftp, and rsync

Use SSH transport for direct copies, scripted file operations, and verified synchronization.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.