The second in my series of posts about SSH things that people don’t always think about, today we’ll take a look at the authorized_keys file format.

That’s right, it’s a whole file format. Not just a place you drop your keys and forget about them, you can place all sorts of restrictions or behaviors on individual keys. Whether you want a key to only be valid for running a specific command, or you want to ensure a key is only used when coming from specific sources, authorized_keys has you covered. For the authoritative source of information about this file format, man sshd on a linux terminal near you, or visit the authoritative source online. Avoid the linux.die.net man pages, which are out of date.

We’re only going to look at a few options that are particularly interesting to me. I encourage you to dig into the man pages yourself to find additional interesting things that might be more relevant to you.

Restricting Defaults

Default settings for authorized_keys options tend to be pretty open. You can establish LocalForward and RemoteForwards, you can forward your SSH Agent, you can setup X11 forwarding, allocate a PTY, and execute ~/.ssh/rc (which is a discussion for another blog post). This is all pretty great when you want to supply a typical linux user experience for a user, or when you setup your own administrative keys on a machine. But what if you want to allow users to SSH in but not allow them to establish forwards, or prevent them from opening a PTY.

Enter the no-* suite of authorized_keys options.

no-agent-forwarding

While I’m generally a proponent of just broadly disabling SSH Agent Forwarding, perhaps that isn’t feasible in your environment for one reason or another. But maybe you want to ensure that users aren’t loading their yubikeys into their SSH agents and then forwarding that, effectively eliminating the “2FA” component of using a yubikey in the first place. You can use the no-agent-forwarding option before the relevant SSH key(s) to not allow agent forwarding when that key is used for authentication. That way maybe there is a key type that you want to allow agent forwarding on, like agent forwarding with a deploy key used to pull an application from some repository, but your typical user keys aren’t able to use agent forwarding.

This restriction can be a little bit confusing when a user has multiple keys in their authorized_keys file, or multiple keys in their SSH agent. Let’s say we have two authorized keys in our authorized_keys file, and those same two keys are loaded in our SSH agent. One of those keys has the no-agent-forwarding restriction but the other does not. The key that was first loaded into the agent will be used, REGARDLESS of the key that is first in the authorized_keys. So if the unrestricted key was added to the agent first, ssh -A would go ahead and forward that socket for you, regardless of the key ordering in authorized_keys or the restriction on the other key.

It’s important to be aware of this behavior, because once the $SSH_AUTH_SOCK has been established, the remote session has access to ALL keys in the forwarded agent, regardless of whether or not the key is listed as no-agent-forwarding in your authorized_keys file. So in our earlier example of a yubikey loaded into the agent, if the user simply authenticates with a different key that isn’t restricted, they’d still be able to access their yubikey key on the forwarded agent.

no-port-forwarding

no-port-forwarding is interesting to me because it can be useful for restricting the ability to pivot through your server and into potentially restricted networks. The man pages say specifically that TCP forwarding is forbidden, but a little testing by yours truly suggests that the same applies for unix-domain socket forwarding via ssh -R.

Another interesting note is that this doesn’t prevent agent forwarding, even though agent forwarding is effectively just a forwarded unix-domain socket. I’ve tried to create an arbitrary socket of my own and then call SSH_AUTH_SOCK=/home/dade/testsock ssh -A dade@old.0xda.de in order to test whether this could be used to still establish arbitrary unix-domain socket forwarding to the remote server, but it seems like ssh will just hang indefinitely if SSH_AUTH_SOCK isn’t a valid SSH agent socket.

no-user-rc

I have a recent fascination with ~/.ssh/rc – it’s a file that I didn’t know could exist, many sysadmins probably aren’t paying attention to, and since it’s not in the homedir like .bashrc or .bash_profile, it’s much less likely to get noticed by a typical user – as users we tend to spend a lot more time in ~ than we do in ~/.ssh. I plan to write a whole separate post about this file, but for now all you really need to know is that it can be home to commands that run when an ssh session is established, prior to the user’s commands or shell. Well, this directive tells sshd to not execute the contents of ~/.ssh/rc when this particular key is used to authenticate. Of course from a security perspective this is pretty irrelevant because anyone who can modify your ~/.ssh/rc can probably modify your authorized_keys file to just remove this directive.

Inverting the Defaults

Instead of manually specifying a bunch of restrictions for a key, newer versions of sshd support the restrict option. This option automatically defaults to all of the restrictive settings and then you can explicitly enable functionality you care about. For instance, the opposite of no-agent-forwarding would be restrict agent-forwarding - This restricts the key from port forwarding, having a pty, X11 forwarding, etc, but it explicitly allows it to use agent forwarding.

This table maps the no-* directives to the inverted directives used with restrict. They are pretty obvious, though - just remove the no- prefix!

no-* inverted
no-agent-forwarding agent-forwarding
no-port-forwarding port-forwarding
no-pty pty
no-user-rc user-rc
no-X11-forwarding X11-forwarding

Restricting keys to specific origins

Perhaps my second favorite of all the authorized_keys directives is the from= directive. from supports a list of patterns to restrict a key’s origin - if the key tries to authenticate from anywhere but the specified origin(s), it will fail. You can use wildcard matching to match DNS names like from="*.rooted-servers.net" to allow the key to be used from any host with a DNS name ending in .rooted-servers.net. You can also use CIDR notation to restrict the key to only be used from specific IP ranges - e.g. from="10.0.0.0/8" to restrict the key to only be allowed to authenticate to the server if it’s coming from the RFC1918 Class A network that the server is on. Alternatively you could use it like from="bastion.rooted-servers.net to ensure that the key is only able to authenticate if it first comes through your bastion server.

NOTE: When using DNS names here, it’s the PTR record that gets checked against. If you don’t have a PTR record for the IP address that matches the DNS name(s) you specify, your authentication will be rejected. You can check the PTR record of an IP address with dig -x <ip>.

NOTE 2: I’d recommend restricting ssh origin via iptables before setting up any per-key authentication restrictions, if applicable. iptables rules that accept traffic to sshd from only specific origins are much less likely to go wrong than relying entirely on authorized_keys restrictions, especially if you’re not tightly controlling the contents of all users’ authorized_keys files.

Expiring keys

Expiring keys is a directive I didn’t know existed before sitting down to write this post. The name of the directive is expiry-time and it takes a value in the format of YYYYMMDD or YYYYMMDDHHMM[SS]. Once the system time has passed the specified time specification, the ssh key will no longer be valid for authentication. It’s also worth noting that if you just set the date without setting the time, the key will expire as soon as the system time becomes that date. E.g. expiry-time="20210320" means that the last time the key will be valid for authentication is 2021-03-19T23:59:59 system time. As soon as the clock rolls to 2021-03-20T00:00:00, the key will be expired. This may be important to consider when the system time is UTC and the keys expire many hours before or after the user’s local time hits that date.

Executing Commands

Finally! My favorite of all the authorized_keys directives. command=.

This is exactly what it sounds like - a command that executes when a particular key is used to authenticate. The caveat for the command directive is that it executes instead of whatever the user tried to execute. If you have the ability to set command= on your own, you could use this to perform a specific action when that key is used to authenticate and then still drop you into your shell or run whatever command you intended to run. It’s pretty gross, but it works:

command="echo hello authorized_keys && if [ -n \"$SSH_ORIGINAL_COMMAND\" ]; then eval $SSH_ORIGINAL_COMMAND; else $SHELL; fi"

This command simply echos hello authorized_keys and then executes the user’s original ssh command, which helpfully gets shoved into $SSH_ORIGINAL_COMMAND for us, or executes the user’s $SHELL if no remote command was supplied. A more creative user might replace that echo hello authorized_keys with something a little more malicious, if they so choose. But there’s a big down side to trying to do anything malicious this way: it stays in your ps output for the entire duration of the session.

ps output

That said, I’m sure there are many perfectly legitimate reasons to use this. You can get authorized_keys from different users and set restrict,command="redeploy" in order to automatically trigger a redeployment when that user tries to ssh in with their key. Or maybe restrict,command="tail -f /var/log/nginx/access.log" to give some users the ability to see the nginx access logs in real time without needing to give them a full shell. I’m sure legitimate system administrators can come up with tons of cool uses for this.

If you’re pretending to be a bad guy, like I do from 9am to 5pm every week day and from 9pm to 5am every weekend, then instead of executing commands in ~/.ssh/authorized_keys, you should be checking out ~/.ssh/rc.

Adieu

Check out the various ssh man pages for more fun. Or just wait for me to write about more ssh fun.

Also, check out the OpenSSH logo. I wish I had a briefcase that said Top Secret on it.

openssh logo