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:
| Goal | Command |
|---|---|
| Generate Ed25519 key | ssh-keygen -t ed25519 -f key -C 'comment' |
| Install public key | ssh-copy-id -i key.pub user@host |
| Dump effective config | ssh -G app |
| Shell via Host alias | ssh app |
| Jump via bastion | ssh -J bastion app (or ProxyJump in config) |
| Push file | scp local.txt app:~/local.txt |
| Pull file | scp app:/path/file ./file |
| Recursive copy | scp -r dir/ app:/dest/ |
| Local forward | ssh -L 8080:127.0.0.1:3000 app |
| Remote forward | ssh -R 9000:127.0.0.1:3000 app |
| Tunnel only, background | ssh -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.
- Generate an Ed25519 key at
ssh-demo/id_ed25519with commentgeekmonks-laband an empty passphrase. - Write a
Host appblock fordeploy@app.example.neton port2222, withIdentitiesOnly yes. - Using that alias, write the
scpcommand that copiesnotes.txtinto the remote home directory. - Open a background local forward: laptop
8080to remote127.0.0.1:3000, no remote shell. - Open a remote forward: remote
9000to this laptop’s127.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.