Another cool ssh thing.— dade (@0xdade) March 20, 2021
authorized_keys is a file format. That file format supports the command directive. That command directive is a fun place to hide, but it's going to be super obvious if anyone looks at it.pic.twitter.com/fOSEtbXaxi
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.
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.
no-* suite of
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 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
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 firstname.lastname@example.org 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.
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
.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
Restricting keys to specific origins
Perhaps my second favorite of all the
authorized_keys directives is the
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’
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
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.
Finally! My favorite of all the
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.
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
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.