When you need a remote shell, the usual pain is reconstruction: user, hostname, non-default port, which key — then a password prompt anyway. Land on the wrong hop and you are debugging the bastion instead of the app. SSH already solves that. Put the hop in a Host alias once, then type ssh app. Add a key so you stop typing the password, scp for whole-file copies, and -L / -R when a port on one side should appear on the other.

You do not need every OpenSSH flag on day one. Start with a lab key and a tiny config, then layer copy and tunnels as the job gets real. The sections below walk through the commands you will reach for most often — with enough context that each one feels intentional, not magical.

Warm-up: a lab key and a Host alias

Before touching ~/.ssh, give yourself a playground that cannot overwrite a real identity. Generate an Ed25519 key in a demo directory, then write a config that names the hop. The private key stays on disk in that folder — print the public key only.

Create the lab once, then reuse the files for every example that does not need a live server:

rm -rf ssh-demo
mkdir -p ssh-demo
ssh-keygen -t ed25519 -f ssh-demo/id_ed25519 -N '' -C 'geekmonks-lab'

Typical output (the fingerprint will differ):

Generating public/private ed25519 key pair.
Your identification has been saved in ssh-demo/id_ed25519
Your public key has been saved in ssh-demo/id_ed25519.pub
The key fingerprint is:
SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx geekmonks-lab

Show only the public half. That line is what you install on a server; it is not a secret:

cat ssh-demo/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... geekmonks-lab

Now write a Host block. Host is the short name you type. HostName, User, IdentityFile, and Port are the details you refuse to retype:

cat > ssh-demo/config <<'EOF'
Host app
  HostName app.example.net
  User deploy
  IdentityFile ~/.ssh/id_ed25519
  Port 22
  IdentitiesOnly yes
EOF

Prove the alias resolves without opening a network connection. -G prints the effective client config:

ssh -G -F ssh-demo/config app | grep -E '^(hostname|user|port|identityfile) '
user deploy
hostname app.example.net
port 22
identityfile /home/you/.ssh/id_ed25519

When you are ready to use this hop for real, copy the key pair into ~/.ssh, keep the private file mode 600, and append the Host block to ~/.ssh/config. After that, ssh app is enough — no -i, no -p, no user@host.

Note: Never paste a private key into chat, tickets, or a gist. An empty passphrase is fine for this lab. For a real identity, prefer ssh-agent over putting a passphrase-less key in ~/.ssh. IdentitiesOnly yes stops OpenSSH from offering every other key on the agent and hitting “too many authentication failures.”

First connect, then a key login

The first time you reach a host, OpenSSH does not yet know its host key. It prints a fingerprint and asks whether to continue. That prompt is a check, not a formality:

The authenticity of host 'app.example.net (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:abcdEFGHijklMNOPqrstUVWXyz0123456789ABCD.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Note: Compare that fingerprint with something you trust — a cloud console, a teammate, or known_hosts from another machine you already verified. Typing yes blindly is how a first-connect intercept works. After you accept, the key is stored in ~/.ssh/known_hosts; a later mismatch is a warning to stop, not a prompt to overwrite in a hurry.

With password login still enabled on the server, install the public key so the next session can skip the password. Point ssh-copy-id at the .pub file:

ssh-copy-id -i ssh-demo/id_ed25519.pub deploy@app.example.net

Typical success:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'deploy@app.example.net'"
and check to make sure that only the key(s) you wanted were added.

Once the Host block lives in ~/.ssh/config and the private key is the one IdentityFile names, the everyday shell is the alias — not a 40-flag one-liner:

ssh app

The equivalent without config is the line you should not have to remember:

ssh -i ~/.ssh/id_ed25519 -p 22 deploy@app.example.net

If the app host is only reachable through a jump box, keep it in config as well. ProxyJump is one extra keyword on app; you still type ssh app:

Host bastion
  HostName bastion.example.net
  User jump

Host app
  HostName 10.0.2.15
  User deploy
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump bastion

The one-liner form is ssh -J jump@bastion.example.net deploy@10.0.2.15. Prefer the config when you will type it more than once.

Copy files with scp

scp uses the same Host alias, the same key, and the same port. Local path first to push; remote path first to pull. The remote side is alias:path.

Push a single file into the remote home directory:

echo 'deploy notes' > notes.txt
scp notes.txt app:~/notes.txt

Typical progress line (sizes vary):

notes.txt                                     100%   13     1.2KB/s   00:00

Pull a remote file back:

scp app:/var/log/app.log ./app.log

Copy a directory tree with -r:

scp -r dist/ app:/var/www/app/

Without a Host alias you would also fight the port-flag split: ssh uses -p, scp uses -P. Config makes that mismatch disappear.

Note: scp copies whole files. For an incremental mirror or a remote backup over SSH, use Linux rsync — one tool, delta transfers, dry-runs. Do not reach for scp -r when you meant “sync what changed.”

Local port forward (-L)

A service on the remote often listens only on localhost — a database UI, a metrics page, an admin port you must not expose on the public interface. Local forwarding makes that remote port appear on your laptop.

The shape is -L local_port:target_host:target_port. target_host is resolved on the SSH server, not on your laptop. 127.0.0.1:3000 therefore means “the process listening on 3000 on the remote box”:

ssh -L 8080:127.0.0.1:3000 app

Leave that session open and browse http://127.0.0.1:8080 locally. Traffic to 8080 on your machine is sent through the SSH connection and comes out on the remote as a connection to 127.0.0.1:3000.

You can also aim at a host only the remote can see — for example a database on the app network:

ssh -L 5432:db.internal:5432 app

Then point your local client at 127.0.0.1:5432.

When you want the tunnel without a remote shell, add -N (no command) and -f (background after authentication):

ssh -N -f -L 8080:127.0.0.1:3000 app

Confirm the local listen with a socket listing if you need to; Linux ss and netstat covers that view in depth:

ss -ltn | grep 8080
LISTEN  0      128        127.0.0.1:8080       0.0.0.0:*

Note: Foreground ssh -L dies when you close the terminal. Background -f keeps the tunnel until you stop that ssh process. If 8080 is already taken locally, pick another local port — the remote target does not have to match.

Remote port forward (-R)

Remote forwarding is the opposite direction: a port on the server is forwarded to a host:port as seen from your laptop. Use it when something on the remote (or a teammate logged in there) should reach a service that only exists on your machine.

Expose local port 3000 as 127.0.0.1:9000 on app:

ssh -N -R 9000:127.0.0.1:3000 app

On the remote, a client that connects to 127.0.0.1:9000 is tunneled back to port 3000 on your laptop. By default the remote bind is localhost only. Opening it on all interfaces requires GatewayPorts on the server — treat that as an explicit server decision, not a client flag to sprinkle on every hop.

Note: -L brings a remote (or remote-visible) port to you. -R takes a local port to the server. If you remember only the direction of the first letter’s “near” side — local listen vs remote listen — the rest of the host:port triple stays readable.

Quick reference card

Keep this nearby until the aliases become muscle memory:

GoalCommand
Generate Ed25519 keyssh-keygen -t ed25519 -f key -C 'comment'
Install public keyssh-copy-id -i key.pub user@host
Dump effective configssh -G app
Shell via Host aliasssh app
Jump via bastionssh -J bastion app (or ProxyJump in config)
Push filescp local.txt app:~/local.txt
Pull filescp app:/path/file ./file
Recursive copyscp -r dir/ app:/dest/
Local forwardssh -L 8080:127.0.0.1:3000 app
Remote forwardssh -R 9000:127.0.0.1:3000 app
Tunnel only, backgroundssh -N -f -L 8080:127.0.0.1:3000 app

Practice drills

Use the ssh-demo files (recreate them from the warm-up if needed) and try these without peeking. Live hosts can stay as app.example.net templates — the point is to choose the alias, the path, and the tunnel direction with intent.

  1. Generate an Ed25519 key at ssh-demo/id_ed25519 with comment geekmonks-lab and an empty passphrase.
  2. Write a Host app block for deploy@app.example.net on port 2222, with IdentitiesOnly yes.
  3. Using that alias, write the scp command that copies notes.txt into the remote home directory.
  4. Open a background local forward: laptop 8080 to remote 127.0.0.1:3000, no remote shell.
  5. Open a remote forward: remote 9000 to this laptop’s 127.0.0.1:3000, no remote shell.

When you are ready to compare, here are solid answers — not the only ones, but clear and portable:

ssh-keygen -t ed25519 -f ssh-demo/id_ed25519 -N '' -C 'geekmonks-lab'
# Host app / HostName app.example.net / User deploy / Port 2222 / IdentitiesOnly yes
scp notes.txt app:~/notes.txt
ssh -N -f -L 8080:127.0.0.1:3000 app
ssh -N -R 9000:127.0.0.1:3000 app

If you can work through those five comfortably, you already cover most real SSH client work: a named hop, a key instead of a password, whole-file copy, and tunnels in both directions. Start with Host + ssh-copy-id, use scp for one-shot files, and add -L or -R only when a port must move — -N -f when you do not need a shell on the far side.