Posts in category “Networking”

SSH Alias Added, Still Denied? Check the Guest's authorized_keys

A Host block only tells SSH where to connect and which key to offer. A new VM can be reachable and still return Permission denied (publickey) until the guest account trusts that public key.

First confirm the client selected the intended host, user, and identity:

ssh -G vm-alias | grep -E '^(hostname|user|identityfile) '

Then, from the VM console or hypervisor, find the account's real home instead of assuming it is /home/$USER; some Lima guests use a suffixed path:

home=$(getent passwd "$(id -un)" | cut -d: -f6)
install -d -m 700 "$home/.ssh"
cat /path/to/client_key.pub >> "$home/.ssh/authorized_keys"
chmod 600 "$home/.ssh/authorized_keys"

Only the client's .pub key belongs on the guest; keep the private key on the client. Test the complete path with:

ssh -o BatchMode=yes vm-alias 'hostname; id -un'

ssh -G validates client configuration, not server-side authorization.

Lost the SSH public key? Derive it from the private key

An SSH .pub file is only the public half of a key pair. If it is missing, or if it is an old file that no longer matches the private key, regenerate it from the private key:

cp ~/.ssh/id_rsa.pub ~/.ssh/id_rsa.pub.backup 2>/dev/null || true
ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub
chmod 644 ~/.ssh/id_rsa.pub

ssh-keygen -y derives and prints the public key without exposing the private key. This also fixes errors such as:

identity_sign: private key ... contents do not match public

Verify the resulting public-key fingerprint with:

ssh-keygen -lf ~/.ssh/id_rsa.pub

Routing Claude Code through a gateway that's behind HTTP Basic Auth

Say you expose an Anthropic-compatible gateway on the public internet and put nginx Basic Auth in front of it. Point Claude Code at it and every request 401s — or worse, the gateway rejects it with a cryptic error. The reason is a header collision, and it's easy to untangle once you see it.

Basic Auth lives in the Authorization header (Authorization: Basic <base64>). Claude Code's own auth also wants a header — but which one depends on how you authenticate:

  • ANTHROPIC_API_KEY → sent as X-Api-Key
  • ANTHROPIC_AUTH_TOKEN (and subscription/OAuth login) → sent as Authorization: Bearer <token>

So the trick is to stay in API-key mode. Then Authorization is free for Basic Auth and your key rides in the separate X-Api-Key header — no collision:

export ANTHROPIC_BASE_URL="https://gateway.example.com"
export ANTHROPIC_CUSTOM_HEADERS="Authorization: Basic $(echo -n 'user:pass' | base64)"
export ANTHROPIC_API_KEY="<your-key>"
unset ANTHROPIC_AUTH_TOKEN   # Bearer would fight Basic for the Authorization header

On the nginx side, strip the client's Authorization after Basic Auth passes, so the Basic credential never leaks upstream to the gateway:

location / {
    auth_basic           "gateway";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://backend:9949;
    proxy_set_header Authorization "";   # consumed by auth_basic; don't forward
}

Two gotchas worth knowing. If you're logged into Claude Code with a subscription, it sends Authorization: Bearer <oauth> and ignores ANTHROPIC_API_KEY — so it collides with Basic and never sends X-Api-Key. Log out (or use a config dir with no login) to force API-key mode. And if your gateway reads a pool/backend selector from X-Api-Key (some proxies do), the value must be the selector name, not a real sk-ant-... key — a real key or a dummy matches no pool and fails closed.

The failure mode that eats the most time isn't any of the above, though: a typo in the variable name. ANTROPIC_API_KEY (missing the H) sets nothing, Claude Code silently falls back to whatever else it can find, and you'll chase the server for an hour before spotting the letter. Echo env | grep -i anthropic before blaming the proxy.

SSH Agent Forwarding: Stop Copying Your Private Key to Every Jump Host

You SSH through a bastion box. So you copied your private key onto it. Now that key lives on one more machine — one more place it can be stolen from, one more copy to rotate when something goes wrong.

SSH agent forwarding removes the need entirely.

What it actually does

This is the part most explanations hand-wave past. Your private key never needed to be on the bastion — that's not how SSH auth works.

  • The target machine needs your public key (in ~/.ssh/authorized_keys). It always did.
  • The client (you) holds the private key and signs a challenge the target sends.

Without forwarding, when you SSH from the bastion to a third machine, the bastion becomes the client — so it needs the private key to sign. That's the only reason you ever copied it there.

With forwarding, the bastion doesn't sign anything itself. It forwards the challenge back to your agent on your laptop, your agent signs it, and the signature travels back. The bastion never touches the private key.

No forwarding:   laptop (key) → bastion (key) → target (pubkey)
With forwarding: laptop (key+agent) → bastion (no key) → target (pubkey)

Setup

  1. Load your key into the agent on your laptop (the agent is usually already running):
ssh-add ~/.ssh/id_rsa
  1. Enable forwarding for the bastion in ~/.ssh/config:
Host bastion
    HostName bastion.example.com
    User myuser
    ForwardAgent yes
  1. Verify it works:
ssh bastion
ssh-add -l        # lists your keys → forwarding is live
ssh internal-vm   # connects, bastion never had your key

The catch

Only forward through machines you trust. Anyone with root on the bastion can request signatures from your agent while your session is open — effectively borrowing your identity. Never use -A on a shared or untrusted host.

Do you still need it with a passphrase-less key?

If your key has no passphrase and you've already copied it everywhere, forwarding isn't strictly required — direct auth from the bastion works fine. But keep ForwardAgent yes in the config anyway. It costs nothing, and the day you switch to a passphrase-protected key, you'll only ssh-add once on your laptop instead of typing the passphrase on every connection.

clean up docker networks/interfaces

docker network ls
then 
docker network rm networkname