RE:NODE

AuthorizedKeysFile in sshd_config: tokens, paths and testing

By RE:NODE Team
15 min read


AuthorizedKeysFile tells sshd where to look for the public keys a user is allowed to log in with. If you never set it, OpenSSH checks .ssh/authorized_keys and .ssh/authorized_keys2 in the user's home directory, though many distributions ship a config that narrows it to the first one. You change it for one of three reasons: to move keys out of users' hands into a root-owned directory such as /etc/ssh/authorized_keys/%u, to make keys readable before a home directory is mounted, or to give one group of accounts a different source of keys through a Match block.

The setting is short and the failure mode is not. Point it somewhere sshd considers unsafe, or forget that it replaces the default rather than adding to it, and every key login on the machine stops working at once - including yours. This post covers what the directive accepts, how to build a central key directory that passes StrictModes, how it interacts with sshd_config.d and Match, and how to prove the change works before you close the session you are relying on. Key generation, agents and the general hardening list are in SSH keys and sshd_config hardening; this is the one directive, in depth.

What AuthorizedKeysFile does and its default#

During public key authentication the client offers a public key, and sshd searches for that key in the files named by AuthorizedKeysFile, in order. The first file containing a matching line wins, and the options at the start of that line (restrict, from=, command= and the rest) apply to the session. If no listed file contains the key, the key is refused and the client moves on to its next key or method.

The compiled-in default in current OpenSSH is:

code
AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2

What you actually get depends on the shipped config file:

DistributionShipped lineEffective value
Debian, Ubuntucommented outboth files
RHEL, Rocky, Alma, Fedora.ssh/authorized_keysone file
Upstream OpenSSH sample.ssh/authorized_keysone file

authorized_keys2 is a leftover from the SSH protocol 1 to 2 transition and has been deprecated for over two decades. Nobody should be putting keys in it, which is precisely why it is worth excluding: a file nobody looks at is a good place for an attacker who briefly had a shell to leave a key behind. Setting the value explicitly to .ssh/authorized_keys is a small, safe improvement on Debian and Ubuntu.

Two other values are legal. none disables file-based keys entirely, which only makes sense when keys come from AuthorizedKeysCommand (covered further down) or authentication is by certificate. And because the directive can appear inside Match, the value can differ per user, group or source address.

Changes to the contents of an authorized keys file take effect on the next login with no reload, because sshd reads the file at authentication time. Changes to the AuthorizedKeysFile directive itself need sshd to reload its configuration.

Paths and the tokens %h, %u, %U and %%#

After token expansion, each value is taken as either an absolute path or a path relative to the user's home directory. That is why the bare .ssh/authorized_keys works: there is an implied home directory in front of it.

TokenExpands toExample result
%hhome directory/home/alice
%uuser namealice
%Unumeric user ID1001
%%a literal %%

%U is accepted by current OpenSSH releases; older ones only know %h, %u and %%, so check man sshd_config on the machine before relying on it. The home directory comes from the password database (/etc/passwd, or LDAP through NSS), not from $HOME, so a user cannot redirect it with an environment variable.

Some equivalent and non-equivalent forms, because the differences catch people out:

code
# These two are the same fileAuthorizedKeysFile .ssh/authorized_keysAuthorizedKeysFile %h/.ssh/authorized_keys# Absolute, one file per user, outside the home directoryAuthorizedKeysFile /etc/ssh/authorized_keys/%u# Not what it looks like: relative to the home directory, so /home/alice/etc/keysAuthorizedKeysFile etc/keys

The tilde is not a token in this directive. Write %h when you mean the home directory. And %u is used verbatim, so a user name containing characters your filesystem dislikes (rare, but LDAP and Active Directory accounts sometimes carry a domain suffix) becomes part of the file name. Look at the expanded path in the debug output, described below, rather than assuming.

Listing more than one file#

Several paths go on one line, separated by spaces. They are searched in order and the first match wins.

/etc/ssh/sshd_config.d/20-keys.conf
AuthorizedKeysFile /etc/ssh/authorized_keys/%u .ssh/authorized_keys

This is the shape of a migration. Keys managed centrally - by you, by configuration management, by a provisioning script - live under /etc/ssh, and each user's own file still works while you move people across. A missing file is not an error: sshd notes it at debug level and tries the next one. So you can roll the setting out to a fleet before every user has a central file.

Order matters in one situation. If the same key appears in both files with different options, the first file's line decides. A key restricted with from= in the central file is not loosened by an unrestricted copy in the user's own file, because the search stops at the first match - but the reverse is also true, so put the file you control first.

The important consequence of the directive is that it replaces the default. Setting AuthorizedKeysFile /etc/ssh/authorized_keys/%u on its own means every ~/.ssh/authorized_keys on the machine is ignored from the next reload. If your own key is only in your home directory, you have just locked yourself out. Listing both paths during the change is the cheapest insurance there is.

A central, root-owned key directory#

The main reason to change AuthorizedKeysFile is control. With the default, any user can append keys to their own file, which means anyone who once had a shell as that user can leave a permanent way back in, and every audit has to read files scattered across home directories. A root-owned directory turns "who can log in as whom" into something that lives in one place and changes only through root or your configuration management.

It also fixes two practical problems: home directories on network storage that is not mounted until after login, and encrypted home directories that are unreadable until the user has authenticated with a password. A key file outside /home is readable at authentication time regardless.

The layout:

bash
$ sudo mkdir -p /etc/ssh/authorized_keys$ sudo chown root:root /etc/ssh/authorized_keys$ sudo chmod 755 /etc/ssh/authorized_keys$ sudo cp /home/alice/.ssh/authorized_keys /etc/ssh/authorized_keys/alice$ sudo chown root:root /etc/ssh/authorized_keys/alice$ sudo chmod 644 /etc/ssh/authorized_keys/alice

The 644 is the part people get wrong, and it is counter-intuitive. sshd reads authorized keys files with the target user's privileges, not as root. A file that is root:root 600 is unreadable to alice and therefore unreadable to sshd while it checks her key, so the login fails. Public keys are public; the file being world-readable discloses nothing an attacker can use. What must not happen is the user, or anyone else, being able to write to it.

There are two acceptable ownership models:

  • root:root, mode 644. The user can read their keys but not change them. This is the point of a central directory.
  • Owned by the user, mode 600 or 644, in a root-owned directory. The user can edit their own file but not create or delete files for others. A middle ground, and it gives back most of what you were trying to take away.

A side effect to warn colleagues about: ssh-copy-id writes to ~/.ssh/authorized_keys on the far side and knows nothing about your setting. It reports success, and if the home file is no longer in the list, the key it installed is never consulted.

StrictModes and the permission checks#

StrictModes defaults to yes, and it should stay that way. With it on, before trusting an authorized keys file sshd checks the file and every directory on the path to it. The rules, in short:

ObjectMust be owned byMust not be
The keys fileroot or the usergroup- or world-writable
Each directory above itroot or the usergroup- or world-writable
The user's home directoryroot or the usergroup- or world-writable

If anything fails, the key is refused and the client sees nothing but "Permission denied (publickey)". The server log, at the default level, carries the real reason:

code
Authentication refused: bad ownership or modes for directory /home/alice

That message is the single most common cause of "my key stopped working", and it is usually caused by something unrelated to SSH: a deploy script that ran chmod -R 775 on a home directory, a group-writable /home/alice created by a control panel, or a central directory created by a non-root account. The fix is to correct the permissions, never to set StrictModes no. Turning the check off means a user whose home directory is writable by a group can have keys planted by every member of that group.

For a central directory, the path checked is /etc, /etc/ssh, /etc/ssh/authorized_keys and the file. On a normal system the first two are root:root 755 already. Problems appear when the directory lives somewhere less tidy - under /srv on a volume mounted with odd permissions, or on a share where ownership is mapped. Game server file permissions and ownership explains how to read ls -l output if the modes are not second nature.

On RHEL-family systems with SELinux enforcing, there is a second gate. Keys in a home directory need the ssh_home_t label, which restorecon -Rv ~/.ssh fixes after a copy or restore. Files under /etc/ssh normally inherit a label sshd can read. If you choose some other location, the symptom is the same denied login with clean file modes; check ausearch -m avc -ts recent and label the directory with semanage fcontext rather than switching SELinux off.

sshd_config.d, Include and first-value-wins#

Since OpenSSH 8.2, sshd_config can pull in other files with Include. Debian 12, Ubuntu 22.04 and later, and recent Fedora and RHEL releases ship a main file whose early lines include /etc/ssh/sshd_config.d/*.conf. Included files are read in lexical order, as if pasted in at the Include line.

Combine that with the rule that matters most in sshd_config: for most keywords, the first value obtained is the one used. A setting in /etc/ssh/sshd_config.d/10-local.conf beats the same setting further down in the main file, and 10-local.conf beats 50-cloud-init.conf because it sorts first. Editing the main file and seeing no change is almost always this.

The practical layout:

/etc/ssh/sshd_config.d/20-authorized-keys.conf
# Central keys first, the user's own file as a fallback during migrationAuthorizedKeysFile /etc/ssh/authorized_keys/%u .ssh/authorized_keys

Keep each concern in its own small, numbered file. Before writing one, list what is already in the directory: cloud images often drop a file there that sets authentication options, and you want to know about it before you test. Then rely on sshd -T, below, to tell you what won rather than reasoning about the order by hand.

Match blocks: different keys for different accounts#

AuthorizedKeysFile is one of the keywords allowed inside a Match block, so the key source can differ by user, group, source address or local port. A Match block runs until the next Match or the end of the file it is in, so blocks belong at the end of a file, after every global setting. Match all ends a block explicitly if you need to return to global context.

A common pattern: ordinary staff keep their own authorized_keys, while service accounts and SFTP-only customers get keys that only root can change.

/etc/ssh/sshd_config.d/30-match.conf
Match Group sftp-only    AuthorizedKeysFile /etc/ssh/authorized_keys/%u    ChrootDirectory /srv/sftp/%u    ForceCommand internal-sftp    AllowTcpForwarding noMatch User deploy,backup    AuthorizedKeysFile /etc/ssh/authorized_keys/%u

The SFTP case shows why this pairing is useful. Authentication happens before the chroot is applied, so the keys file sits completely outside the directory the user can see. They cannot read, replace or append to their own keys from inside their SFTP session, which a key in a home directory under the chroot would allow.

Remember that, inside a Match, the first-value rule still applies to the block that matched. If two Match blocks both match a user and both set AuthorizedKeysFile, the earlier one wins. Test the specific user, not just the file.

Testing with sshd -t and sshd -T#

Two flags, two jobs. Both need root, because sshd wants to read its host keys even for a test, and both live at /usr/sbin/sshd, which is not always in a normal user's PATH.

sshd -t is the syntax check. It parses every file, including every Include, and exits silently with status 0 if the configuration is valid. A misspelt keyword, a keyword not allowed inside Match, or an unreadable host key produces an error and a non-zero exit. Run it before every reload, every time.

sshd -T prints the effective configuration: every keyword, lower-cased, after defaults, includes and first-value-wins have been applied. It is the only reliable answer to "what is sshd actually going to do". Add -C to evaluate Match blocks for a specific connection:

bash
$ sudo sshd -t && echo config okconfig ok$ sudo sshd -T | grep -i authorizedkeysfileauthorizedkeysfile /etc/ssh/authorized_keys/%u .ssh/authorized_keys$ sudo sshd -T -C user=deploy,host=laptop,addr=203.0.113.50 | grep -i authorizedkeysauthorizedkeysfile /etc/ssh/authorized_keys/%u

Older OpenSSH versions insist on all three of user, host and addr in -C; newer ones accept a subset. Pass all three and it works everywhere. Note that sshd -T shows the tokens unexpanded; the expansion happens per login.

To watch a real login against a new configuration without touching the running daemon, start a second sshd in debug mode on a spare port. It handles one connection, prints every decision to your terminal, and exits:

bash
$ sudo /usr/sbin/sshd -d -p 2222$ ssh -p 2222 -i ~/.ssh/id_ed25519 alice@203.0.113.10   # from your own machine

The debug output names each authorized keys file it tries, says when one is missing, reports the exact permissions failure, and on success identifies the file and line that matched. Open port 2222 in the firewall for the test and close it afterwards; the ufw guide has the one-line rule. A configuration given with -f can be tested the same way before it is copied into place.

Reloading safely and recovering#

The order that never locks anybody out:

  1. Keep an existing root shell open on the server. Do not close it until step 5 has succeeded.
  2. Create the central file, copy keys into it, and set ownership and modes as above.
  3. Write the directive into its own file in sshd_config.d, listing the old location as well.
  4. Run sudo sshd -t, then sudo sshd -T with -C for at least one affected user.
  5. Reload, then open a new connection from a second terminal as each kind of affected account.
bash
$ sudo sshd -t && sudo systemctl reload ssh     # Debian, Ubuntu$ sudo sshd -t && sudo systemctl reload sshd    # RHEL, Rocky, Alma, Fedora

A reload sends SIGHUP; the listening daemon re-reads its configuration and existing sessions carry on undisturbed. Even a restart does not drop sessions already established on modern systemd units, but reload is the gentler habit. The && matters: if the syntax check fails, nothing is reloaded.

When it does go wrong, the open session is the fix: revert the file, sshd -t, reload. Without one, you need your provider's console or rescue mode, which is why the first hour on a new VDS recommends finding it before you need it. Once you are back in, the reasons are almost always in journalctl -u ssh (or -u sshd): a permissions refusal, a file at the wrong path, or the old location silently dropped from the list.

Beyond files: AuthorizedKeysCommand#

When the list of keys lives in a directory service, a database or a fleet tool, writing files on every machine is the wrong shape. AuthorizedKeysCommand runs a program that prints authorized keys on standard output, in the same format as the file, and AuthorizedKeysCommandUser names the unprivileged account it runs as. The command and the files are both consulted, so you can keep a local break-glass key in a file while day-to-day keys come from the command. The program and its directory must be root-owned and not writable by anyone else, the same principle as the central directory. This is how SSSD serves keys from LDAP or FreeIPA.

For more than a handful of people and machines, SSH certificates (TrustedUserCAKeys) remove the per-server key list entirely, as the hardening post describes. AuthorizedKeysFile remains the right tool for a small fleet, a single VDS, or the break-glass key that has to work when everything clever is down. And keep LogLevel VERBOSE on so that every login records the fingerprint of the key used - logs worth keeping covers holding on to those lines.

On RE:NODE, this is a VDS concern only. Game, app and web servers have no SSH daemon for you to configure: file access is over SFTP with credentials issued per server, and the console is in the panel, as SFTP and the file manager describes. A VDS gives you root and the whole of sshd_config, including every setting above and the responsibility for testing it.

FAQ#

Do I need to restart sshd after adding a key to authorized_keys?

No. The file is read every time someone authenticates, so a new line works on the next connection. A reload is only needed when you change AuthorizedKeysFile itself or any other directive in the configuration.

Why does my central key file work for root but not for other users?

Almost always because the file is 600 and owned by root. sshd opens it with the logging-in user's privileges, so that user must be able to read it. Make it 644 and keep it root-owned; the user can read their public keys but cannot change them.

Can a user still add their own keys after I move to /etc/ssh/authorized_keys/%u?

Only if .ssh/authorized_keys is still in the list. Once the central path is the only entry, a user can write whatever they like in their home directory and sshd will never read it. That is the intended outcome, so tell people before you do it.

Should I just set StrictModes no when a key is refused?

No. The refusal is pointing at a real problem: a home directory, .ssh directory or keys file that someone other than the user can write to. Fix the ownership or mode the log names, and the key works with the check still protecting you.

Does AuthorizedKeysFile affect SFTP logins?

Yes. SFTP runs over SSH and authenticates the same way, so the same files are searched. With a Match block and ChrootDirectory, keys outside the chroot are the normal arrangement, because authentication happens before the user is confined.

Is it worth removing authorized_keys2 from the default?

It is a one-line change with no downside on any modern system. Nothing legitimate should be using the file, and a key hidden there is easy to miss in an audit. Set AuthorizedKeysFile .ssh/authorized_keys explicitly and confirm with sshd -T.