DEV Community

Sho Naka
Sho Naka

Posted on • Edited on

Don't Trust Your SSH Config Until ssh -G Proves It: The Host * Ordering Trap

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now ask OpenSSH what it will actually use:

ssh -F common-first.conf -G dev-web |
  awk '/^(user|port) /{print}'
Enter fullscreen mode Exit fullscreen mode

The output:

user common
port 2222
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Same check:

ssh -F specific-first.conf -G dev-web |
  awk '/^(user|port) /{print}'
Enter fullscreen mode Exit fullscreen mode

Output:

user developer
port 2200
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then:

ssh -F "$TMP/home/.ssh/config" -G project-a |
  awk '/^(hostname|user|port|identityfile|identitiesonly|serveraliveinterval|serveralivecountmax) /{print}'
Enter fullscreen mode Exit fullscreen mode

Output:

user deploy
hostname 127.0.0.1
port 65000
identitiesonly yes
serveralivecountmax 3
serveraliveinterval 60
identityfile /tmp/.../home/.ssh/projectA_ed25519
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

Output:

sshd -t EXIT=0
port 65000
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For a shorter review, filter the output:

ssh -G your-host-alias |
  awk '/^(hostname|user|port|identityfile|identitiesonly) /{print}'
Enter fullscreen mode Exit fullscreen mode

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)