I built a small container to run coding agents on my ZimaBoard, reachable over SSH, key-only, no root login. It is the same box from the Tailscale login that could never finish. The first login was refused:

Permission denied (publickey)

Narrowing it down

An ssh -v trace showed my machine offering the right key and the server rejecting it, so the fault was inside the container. The sshd config was fine: sshd -T confirmed PasswordAuthentication no, PermitRootLogin no and AllowUsers dev, all as intended.

docker logs named it exactly:

User dev not allowed because account is locked

How the account got locked

I had made the dev user by renaming the base image’s node user, so that files on the bind-mounted home directory keep a sane owner. The node user ships with no password, and “no password” in /etc/shadow is written as !.

The two characters people use for that field do not mean the same thing:

  • ! means locked. sshd running without PAM refuses the account entirely, before it looks at any key.
  • * means no password can ever match, but the account is not locked, so key login works.

Both make password login impossible, which is why they look interchangeable and are not. With ! in the password field, sshd without PAM rejects your public key too.

The fix

One line in the Dockerfile, with the comment it now carries:

# The password field is then set to "*". The node user ships with "!", which
# sshd (running without PAM) reads as a locked account and refuses outright,
# public key or not. "*" means no password can ever match, without meaning
# locked, so key login works and password login stays impossible.
RUN usermod -l dev -d /home/dev -m node \
    && groupmod -n dev node \
    && usermod -p '*' dev

After a rebuild the key was accepted. The host keys live on a volume, so the rebuild didn’t change the host identity either: no new host key prompt.

This one is specific to sshd without PAM, which is the usual setup in a slim container and not the usual setup on a full distro, where PAM makes its own decision about locked accounts. If you renamed or created a user in a Dockerfile and your key is refused, read the container’s log before you touch authorized_keys. The log line says “locked” in plain words, and the client side never will.