A common trap when writing or reviewing an SSH setup guide is trusting the guide's own narrative: key generation, chmod, ssh-copy-id, ~/.ssh/config, server hardening — the whole checklist reads fine on paper. What often gets skipped is the boring command that should be part of any such draft from the start:
ssh -G dev-web
Run it against the guide's example config, and a Host * diagram that looked harmless can turn out to be backwards.
TL;DR: if a guide claims what a Host block does, make ssh -G prove it before publishing.
Quick answer: where should Host * go in ssh_config?
If Host * sets values that a specific host also sets, put the specific Host block first and put Host * later.
OpenSSH does not treat Host * like a CSS reset that can be overridden later. For many client options, the first obtained value wins. That means a broad Host * block can quietly prevent later per-host values like User or Port from taking effect.
Check the effective SSH config before you trust the prose:
ssh -G your-host-alias | awk '/^(hostname|user|port|identityfile|identitiesonly) /{print}'
If the output does not match the host you thought you configured, the article, runbook, or config snippet is not ready.
The ssh_config footgun: Host star is not CSS
The bad advice looked harmless:
Put the shared rules in
Host *, then put exceptions below.
That sounds right if your mental model is CSS: broad rule first, specific rule later, later wins.
OpenSSH client config does not work like that for many options. For each parameter, the first obtained value is used. That means a broad Host * block can quietly eat settings that a later, more specific block was supposed to override.
Here is the tiny repro.
Host *
Port 2222
User common
Host dev-web
Port 2200
User developer
Now ask OpenSSH what it will actually use:
ssh -F common-first.conf -G dev-web |
awk '/^(user|port) /{print}'
The output:
user common
port 2222
Nope.
The specific host did not win. The broad block got there first.
Now flip the order:
Host dev-web
Port 2200
User developer
Host *
Port 2222
User common
Same check:
ssh -F specific-first.conf -G dev-web |
awk '/^(user|port) /{print}'
Output:
user developer
port 2200
That matches the intended behavior.
This is why "put common stuff at the top" does not hold up as blanket SSH advice. A safer rule is:
Put broad defaults at the end when they can collide with per-host settings.
Host * is still useful for defaults like ServerAliveInterval. The fix is simply to avoid placing override-sensitive defaults above host-specific blocks and hoping ordering will not matter.
Smoke-test SSH examples before publishing them
When a guide's examples need to be copy-paste-safe, building a temp fixture instead of touching a real ~/.ssh directory keeps the verification safe to repeat.
ssh-keygen -t ed25519 \
-f "$TMP/home/.ssh/projectA_ed25519" \
-N '' \
-C 'you@example.com on testhost'
chmod 700 "$TMP/home/.ssh"
chmod 600 "$TMP/home/.ssh/config"
chmod 600 "$TMP/home/.ssh/projectA_ed25519"
chmod 644 "$TMP/home/.ssh/projectA_ed25519.pub"
stat -f '%Lp %N' \
"$TMP/home/.ssh" \
"$TMP/home/.ssh/config" \
"$TMP/home/.ssh/projectA_ed25519" \
"$TMP/home/.ssh/projectA_ed25519.pub"
What a clean run looks like:
ssh-keygen EXIT=0
700 /tmp/.../home/.ssh
600 /tmp/.../home/.ssh/config
600 /tmp/.../home/.ssh/projectA_ed25519
644 /tmp/.../home/.ssh/projectA_ed25519.pub
That is not exciting content. It is the kind of boring output that prevents a reader from losing an afternoon to UNPROTECTED PRIVATE KEY FILE.
The result is documentation with less "trust me bro" and more receipts.
Use ssh -G to see the effective SSH config
For client-side SSH config, the receipt is usually ssh -G.
Here is a safe example config:
Host project-a
HostName 127.0.0.1
User deploy
Port 65000
IdentityFile /tmp/.../home/.ssh/projectA_ed25519
IdentitiesOnly yes
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Then:
ssh -F "$TMP/home/.ssh/config" -G project-a |
awk '/^(hostname|user|port|identityfile|identitiesonly|serveraliveinterval|serveralivecountmax) /{print}'
Output:
user deploy
hostname 127.0.0.1
port 65000
identitiesonly yes
serveralivecountmax 3
serveraliveinterval 60
identityfile /tmp/.../home/.ssh/projectA_ed25519
This is the step that closes the gap between what a guide claims and what OpenSSH actually resolves.
If a guide says "this alias connects as deploy on port 65000 with this key," ssh -G can prove that claim without hitting the network.
Server-side examples need a smoke test too
Server-side sections often include examples like this:
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
Port 65000
A production server is not required to check whether the example is internally valid.
A temp host key and a temp sshd_config are enough:
sshd -t -f "$TMP/sshd_config"
sshd -T -f "$TMP/sshd_config" |
awk '/^(passwordauthentication|permitrootlogin|port|pubkeyauthentication) /{print}'
Output:
sshd -t EXIT=0
port 65000
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
Does that prove a real server rollout is safe? No.
It proves the snippet being copied is not nonsense. That is a lower bar, but it is a bar worth clearing.
The delegation checks
Some commands need a remote server to fully succeed:
ssh-copy-id -i ~/.ssh/projectA_ed25519.pub -p 2222 deploy@server.example.com
scp project-a:/path/file .
rsync -avz project-a:/path/ ./path/
git clone project-a:org/repo.git
Spinning up a full SSH server is not necessary for this check. Pointing the alias at 127.0.0.1:65000, where nothing is listening, is enough to check whether each tool delegates to SSH the way a guide implies.
For scp, the expected failure looks like this:
Executing: program /usr/bin/ssh host project-a, user (unspecified), command sftp
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 65000.
ssh: connect to host 127.0.0.1 port 65000: Connection refused
For Git:
Reading configuration data /tmp/.../home/.ssh/config
Applying options for project-a
Connecting to 127.0.0.1 [127.0.0.1] port 65000.
fatal: Could not read from remote repository.
The connection fails as expected — there is no server listening.
The important part is that the tools use the SSH alias and reach the intended host and port. That is the evidence a guide's claims need to hold up.
An OpenSSH checklist for articles and runbooks
SSH articles are often written as plain setup notes.
Treating them like tiny integration tests instead catches this class of bug before publication.
If the doc contains an SSH config, the doc should also contain the command that proves how OpenSSH resolves it.
A practical shortlist:
# client config resolution
ssh -F ./example.conf -G host-alias
# server config syntax and effective values
sshd -t -f ./sshd_config
sshd -T -f ./sshd_config
# local file modes on macOS/BSD stat
stat -f '%Lp %N' ~/.ssh ~/.ssh/config ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
# local file modes on GNU/Linux
stat -c '%a %n' ~/.ssh ~/.ssh/config ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
That is the whole trick.
Not a framework. Not a yak shave. Just make OpenSSH tell you what it thinks before you tell humans what to think.
Try it on your own config
Pick one host alias from your current ~/.ssh/config and run this:
ssh -G your-host-alias |
awk '/^(hostname|user|port|identityfile|identitiesonly) /{print}'
Then ask one question:
Is this the connection you thought you had configured?
If the answer is no, congrats. You found the bug before production did.
FAQ
Does Host * always need to be at the bottom?
No. Host * is safe when it sets defaults that do not collide with more specific host blocks. Put it later when it can collide with values such as User, Port, HostName, IdentityFile, or other per-host settings.
How can I check what SSH config is actually used?
Use ssh -G with the host alias:
ssh -G your-host-alias
For a shorter review, filter the output:
ssh -G your-host-alias |
awk '/^(hostname|user|port|identityfile|identitiesonly) /{print}'
That prints the effective SSH config after OpenSSH has applied matching Host blocks.
Is ssh -G safe to run?
Yes. ssh -G prints the resolved client configuration and exits. It does not open an SSH session to the server.
Should I also test sshd_config?
Yes, if the article or runbook includes server-side SSH settings. Use sshd -t -f ./sshd_config for syntax and sshd -T -f ./sshd_config to inspect effective server settings. Depending on your environment, sshd -T may require privileges or a complete server config fixture.
What is the practical rule for SSH setup guides?
Do not stop at "this config looks right." Add the command that proves what OpenSSH will actually use. For client config, that command is usually ssh -G.
Sho Naka, writing as nomurasan, publishes implementation notes on teaching, automation, and operational tooling.
AI helped with English editing and structure. The technical verification, corrections, and final responsibility for the content of this article rest with the author.
Top comments (0)