Advanced SSH usage

The SSH protocol, version 2, is one of the foundations of modern and secure computer networks. It is cryptographically sound, fast, incredibly versatile, and virtually ubiquitous. Not even the largest cloud providers try to replace it with some alternative, proprietary solution of their own, which should only amplify its staying power. Even better still, the most popular and comprehensive implementation you will find is proper Free Software, brought to you by the great OpenSSH project. For the tiniest of embedded platforms, there are decently complete alternatives available, and interoperability between them is generally very robust.

In this article, I would like to showcase some potentially lesser known features and abilities the modern SSH protocol and its OpenSSH implementation provide. It is not meant to be and exhaustive treatise of (Open)SSH as a whole, but highlight some of the powerful and versatile facilities that I find useful regularly (or found so at least once, and remember well enough to mention them here), and that may have remained hidden to some users in the depths of the (comprehensive, but also rather long) documentation of the project.

What users consider “advanced” will certainly vary between individuals, but I hope that most readers will find one or even a few new things to take away from here, making this text worth their while.

OpenSSH client features (ssh, scp, sftp)

First, let’s take a look at OpenSSH client features in particular - you will be able to use many/most of these whether or not the server side of your connection was consciously prepared for it.

Storing configuration in ~/.ssh/config

OpenSSH has a rather powerful client configuration mechanism that I find criminally underused by many people who use ssh to shuffle around bytes each day. It’s a text file with the per-user default location of .ssh/config in each users’ home directory. Its absence does not bother ssh when invoked, but if you use it right, it can make your life much easier.

A pattern I’ve often seen while glancing over the shoulders of other users it people setting up their shell’s alias feature to bend ssh to their will. In most cases, you can replace any alias that specifies OpenSSH options with a few lines in the configuration file. This has the immediate benefit that other OpenSSH-provided tools will also pick up the subset of options that make sense for them. As an example, assume you have a host named crunch.example.com, with sshd listening on port 2222 (instead of the default 22), and you will need a particular SSH identity located at ~/.id_c to connect to.

You could solve that by having a shell alias of the form crunch='ssh -p2222 -i~/.id_c crunch.example.org' in one of your shell configuration/initialization files, and use that whenever you need to log into crunch via ssh. That sure is handy, unless you need to transfer some files over, and would need to invoke scp or sftp instead of ssh, which your alias inevitably expands to.

To solve the same kind of problem (that is, applying a number of nonstandard options when connecting to crunch) by means of the configuration file, you could set it up like so:

Host crunch
  Hostname crunch.example.com
  Port 2222
  IdentityFile /home/user/.id_c

With this stanza placed in your ~/.ssh/config, you can just issue a command that reads ssh crunch, and will find the two required options applied to the session automagically, and with the proper hostname resolved via DNS. scp and sftp will pick them up, too - and there’s even some third-party tools that will either use these utilities under the hood, or interpret the SSH configuration file on their own, to provide the same set of features and conventions without added effort.

As we can see, the configuration file can be used to alias a particularly long hostname to a custom handle of your choosing (by setting the Hostname property in a given Host block - the mapping is arbitrary; the Host value does not need to be a substring of the Hostname value, or anything like that), and to set most of the other handy SSH session options and settings in one central place.

Sysops can use the system-wide /etc/ssh/ssh_config sister file to set a host’s default options they would prefer their users to have set, which can (and will) be overridden by contradicting settings in the user-specific file, which will again be overridden by whatever users specify on the command line of a particular ssh invocation. It’s a very flexible and versatile mechanism to get the most out of (Open)SSH, if you lay out your configuration in a clever way. You really should use it!

Escape sequences

Interactive OpenSSH usage by means of ssh plays a few clever tricks on your terminal to make the presence of SSH as a middle-man between the local and the remote side imperceptible. For instance, hitting [Ctrl] + [z] on your keyboard will not put the local ssh instance connecting you to the remote host into the background, but instead background whatever process is running in the foreground on the remote side of the SSH session. Same story for [Ctrl] + [c], which will transparently pass through ssh, and deliver a SIGINT signal to the process on the remote side instead.

This has both benefits and drawbacks: You can use most remotely executed programs and tools pretty much exactly the way you would if they were running locally. But if something goes wrong with a running ssh instance itself, you could get into hot waters, unless you are aware of this little and important feature.

OpenSSH defines a special escape sequence that will make a running ssh process sensitive to an in-band communication protocol that allows the user to interact with it, albeit in a limited manner. By default, it is defined to become active when the user keys in a combination of a linebreak (i.e., by hitting [Enter]), followed by the tilde key ([~]).

The next keypress right after this magic sequence is crucial - it can, for instance, terminate the very session you are using right now. To learn about your options, hit the [?] key, which will produce a complete list. Note that you will have to re-initiate the escape sequence before you will be able to invoke any of the actions printed this way (and keep in mind that they will, as the online help dutifully reminds you, only work immediately after a newline!).

To get a feel for escape sequences and how to use them for if (or rather, when) you are going to need them, it’s best to play around with them in a session you initiate for that express purpose.

RemoteForward and LocalForward

SSH can be used to tunnel network traffic between hosts of your local and an SSH server’s remote network. This tunnel will exhibit all the properties your SSH session itself has: Traffic between your local host and the remote SSH server’s host is encrypted, and happens inside the same TCP connection that also handles your interactive session (if there is any - as you can also use SSHv2 for forwarding-only sessions just fine).

The two greatest and most useful properties of these two features are their ease of setup, and the fact that SSH is so broadly available. As long as you have SSH connectivity and an interactive shell on the remote side, chances are you will be able to find a way to let resources on both networks talk to each other - but it will take some experience and creative setup of forwarding cascades in scenarios where complex packet filtering rules or a locked-down SSH server configuration (in particular, with OpenSSHd, a lack of the GatewayPorts feature) tries to stop you.

To show you a simple example of how to get started, let’s assume the local host you are logged in to is on a local network of its own, with a server it can reach under mynas.lan, which is running sshd bound to its TCP port 22. Leaning on the prior example, we want to enable other users on the host crunch to connect to that SSH server process on mynas.lan (for accessing resources on that particular host, for example). To do that, we would issue ssh -R1337:mynas.lan:22 crunch, which will, after authentication, pop us into a login shell on host crunch.

But the SSH session that brought us there has also established a listening socket on TCP port 1337 on crunch (by default, only on addresses configured on its loopback interface), and will pull all traffic arriving there into the tunnel ssh has established due to our -R switch’s arguments. All this traffic will travel through the tunnel to the other side of the SSH session, and there be relayed to mynas.lan:22 via TCP, on the local network where your ssh command originated. Note that for this to work, crunch does not need to know about mynas.lan - that particular hostname will only be used by the local part of the SSH session that had it in its -R switch.

The -L switch pretty much works in the exact reverse manner - it can tunnel a network resource on the remote side of the session (i.e., something on the network that crunch can connect to, and perceives as local to its own network) into the local network, and expose it to local users that way. So ssh -L9876:coolstuff.local:23 crunch will make the local host’s TCP port 9876 relay traffic over the encrypted SSH tunnel to crunch, and there pass it on to a host identified in (crunch’s local DNS) as coolstuff.local, targeting its TCP port 23. From the host that initiated the ssh process that was tasked with doing that, you could then talk some telnet to coolstuff.local by issuing socat - tcp4-connect:localhost:9876 as a consequence.

To learn more about the intricacies of both -L and -R, it’s best to carefully read the relevant docs in ssh_config(5) - look for LocalForward and RemoteForward. (These are the keywords you would use to permanently establish these kinds of forwards in ~/.ssh/config.) You will find that you are not required to always bind the listening socket to the loopback interface (if the remote’s sshd configuration allows for it).

Once you got this down, you’ll rarely find yourself hampered by excessively restrictive firewalling. Remember, though, that with this power comes considerable responsibility.

Recent-ish releases of OpenSSH have learned to relay traffic for and over UNIX domain sockets (UDS), as well, by the way.

DynamicForward

Both LocalForward and RemoteForward are very capable and useful features, but they can only ever lead one kind of endpoint (like a TCP listening socket) to any single other (like another TCP listening socket on the remote side of the SSH session). For some use cases, it would certainly be nice to gain some flexibility there, and DynamicForward delivers that: If you connect to our old friend crunch via ssh -D1234 crunch, this will make the local ssh process establish a listening socket on TCP port 1234 (bound to addresses in the loopback interface). TCP traffic that arrives there, and conforms to the SOCKS5 proxying protocol, will get relayed through the encrypted pipe to the remote side of the SSH session running on crunch, and boldly go wherever the SOCKS proxy magic told it to go. So if you have an application on your local machine that can be configured to use a SOCKS proxy, point that to localhost:1234, and it will have its traffic “teleport” through the SSH DynamicForward tunnel onto crunch.

To use this capability in an example with curl, pop a second shell on your local host (with the SSH session that specified -D1234 open still), and begin a command line starting with curl --preproxy socks5://localhost:1234 ..., affixing an URL of your choosing - this will make curl proxy its TCP connection through the SOCKS5 proxy listener that ssh requesting DynamicForward had created earlier, and cause all curl traffic arriving at the eventual destination look and feel as if it had originated from crunch. Consequently, that traffic is subject to whatever crunch’s network conditions/firewall rules/etc. dictate, which will probably result in a different set of possibilities and limitations than what applies to your local host.

Many popular software products like Firefox can be configured to use SOCKS proxies quite easily. With this trick, any remote SSH server in a suitable country might be enough to circumvent basic (yet nevertheless nasty) geoblocking shenanigans.

Please do not mistake configuring a SOCKS proxy in your browser (or setting up and using some VPN service, for that matter) for a surefire measure to conceal your online identity and real-world location - if you feel a need for that, this is the topic of other, more in-depth guides you will need to find and follow.

BatchMode

SSH has established itself as a very popular Layer 7 “transport protocol” of sorts - many programs like rsync will assume they can use it to connect to a remote host to perform work and shuttle data back and forth without having to care too much about confidentiality and strong authentication themselves.

Many sysops regularly use ssh in scripts and programs to non-interactively perform work on remote hosts, and in this case, it is usually not a good idea to risk having an ssh process in the background wait for a long time for a user-supplied password that just won’t ever be keyed in (because there is no human in the loop who supervises the SSH connection attempt). For that purpose, OpenSSH developers have devised the BatchMode setting. It will break off an ssh connection attempt (that is, all the SSH state machinery that must be successful before the session has been authenticated, authorized, and had a chance to actually begin/become interactive) the moment it requires user interaction.

To activate it when connection to crunch, you will have to use the long-form syntax for OpenSSH switches, since it lacks a short switch of its own, like so: ssh -oBatchMode=yes crunch.

It is ideal for use when using ssh for automation purposes, and will also take care to set up a few other parameters that benefit long-running sessions without much interactivity - check the docs for exhaustive info. You will mostly find this setting enabled in specific Host stanzas in an SSH configuration file, where it makes the most sense in my experience.

ssh -N

This is kind of a weird one, as the -N switch does not have a corresponding long-form syntax. It’s a switch that will cause your ssh session to not execute any remote command, and not even try to spawn a shell there. With -N in effect, ssh will mostly be useful to perform some of the other duties it can, some of which I’ve presented above. This has the added benefit that something (like an auto-logout timer) ending the remote shell session hosting the SSH connection’s remote side won’t interfere with carefully set up forwardings and the like.

A conceivable use case could look like this: ssh -N -D10101 crunch - which would instruct ssh to only create the magical SOCKS5 proxy presented earlier, but not actually establish an interactive session/shell on crunch. Note that this will disable the escape sequence feature of OpenSSH introduced earlier, and also make the ssh process subject to normal job control of your shell.

This feature can come in very handy to set up long-lasting tunnels over SSH that take up minimal resources on the remote side.

ServerAliveInterval

This sometimes handy feature could very well be confusing to some, as it might seem redundant with a pretty fundamental feature of contemporary TCP itself: TCP Keepalive. It is similar in spirit, too, but provides important benefits under some particularly adverse conditions. On today’s networks and the Internet, “middleboxes” often mess with traffic flows in rather unwelcome ways - large ISPs that rely on Carrier-grade NAT to bring millions of mobile devices online will, at times, break long-established TCP connections to conserve resources on their CGNAT-gateways. To that end, they even sabotage/ignore TCP Keepalive, and tear down perfectly valid TCP streams that have not been utilized for user traffic for an arbitrary amount of time.

With ServerAliveInterval, you can make the SSH client and server agree on a heartbeat-like periodic exchange within the SSH protocol itself - obfuscated and authenticated by encryption, and without any middlebox in its path able to discern the nature of this communication. This will generally significantly reduce the likelihood of your SSH connection getting the axe by one of these mean-spirited and misbehaving Internet obstructions.

The BatchMode switch we looked at before will also set up a ServerAliveInterval, by the way. If you choose to do so manually, please also check out the docs on ServerAliveCountMax, and consider the implications for flaky or slow connections that will eventually “self-heal” due to SSH using TCP as its underlying transport.

ForwardAgent

If you’ve worked with SSH identities/SSH public key authentication before for a bit, you will quickly appreciate the benefits of having an ssh-agent(1) at your service to store and maintain often-used identities in a semi-persistent session. For me, having an agent running with my primary SSH id loaded is actually non-optional for daily use.

Under the hood, the SSH agent uses a clever IPC mechanism - UNIX domain sockets - to implement its functionality. Attentive readers of the sections prior will suspect that this could also qualify agent data/connections to be forwarded to remote hosts, and indeed, the boolean option ForwardAgent does exactly that - it will make a local ssh-agent instance available for further use on a remote host that allows it. That way, you can use chained invocations across different hosts to re-use your local agent (and its stored identities) to authenticate at remote hosts in a transitive manner.

Suppose we have an agent running and loaded with identities locally, and would like to have that available when using the ssh command on crunch further down the road. In that case, ssh -A crunch will take care of that for us (-A is the short-form switch to enable agent forwarding). To check if that worked out, run ssh-add -L on the resulting login shell on the remote host - it should list the IDs loaded in your local ssh-agent instance.

NB: Be aware that malicious (super)users on the host you’re forwarding your agent to could abuse your local ssh-agent instance to authenticate in your name at other hosts. Depending on your particular network and circumstances, this could very well be a deal breaker, and make the (very handy) feature unusable to you. YMMV.

ControlMaster

OK, this is where stuff gets a little funky… ControlMaster is not for the faint of heart, but such a useful feature for common use cases that I cannot leave it unmentioned.

SSH suffers from relatively poor performance when you use it to establish many short-lived sessions, and do little work in each of them. Popular configuration tools such as ansible are guilty of this pattern of behavior, and every little bit of optimization work to make establishing an SSH session faster (like switching from RSA to ecdsa or ed25519 keys will, for instance) pays off relatively quickly when using it. Getting ControlMaster mode set up properly will, however, vastly outperform any other tuning measure you could come up with - but has its pretty unique share of drawbacks, too.

Let’s look at this in detail, so you can make up your mind if this trade-off is worth it to you.

A persistent SSH connection socket per remote host

Enabling ControlMaster will switch ssh into a fundamentally different mode of operation: Instead of each new ssh process establishing a TCP connection to its remote host by itself, the first process to do so that has a suitable ControlPath configured will take care of this, and establish a (UDS) listening socket on its own at the pathname specified by ControlPath. Subsequent connections to the same remote host that share a ControlPath with that instance will connect to that local listening socket first, and create another, parallel SSH session over the TCP/SSH connection that had already been established by the first ssh process to successfully connect there. This will bypass the TCP 3-way handshake for a new OSI Layer 4 connection, and also skip SSH authentication on OSI Layer 7 - shaving off the two most taxing and latency-inducing steps a new SSH connection has to go through under normal conditions.

Of course, having other processes piggyback onto the initial processes’ session incurs a number of restrictions, and potential gotchas. First and foremost, If the process that actually holds the established session open finds an untimely end, the party is over for all those that came after it to re-use the existing session. Given the fact that there could be a lot of additional sessions active in parallel over one connection, terminating the one “wrong” PID can have pretty devastating consequences for applications or tasks executing on that remote host. One may use ControlPersist to deal with this particular wart, but having a modern systemd-based Linux host without systemd-logind session lingering configured might still turn your initial ssh process into a ticking time bomb that could take others with it once it goes off. So beware.

Secondly, if that first session is interactive, other processes attaching to the ControlMaster socket will prevent a normal termination of the initial process/session once the interactive shell part ends - the ssh process will seem to “hang” while it waits for other users of its facilities to terminate, much like when an interactive sessions with forwardings configured (and in active use) terminates its login shell. Again, this is alleviated by ControlPersist, but conditions (see above) apply.

Thirdly, anyone who can access the ControlMaster socket established at the ControlPath in the file system will be able to attach to the shared session, too. It’s a bit like the “forwarded agent onto a rogue host”-scenarion, but potentially worse, since an attacker would not even need to have information about which remotes the agent’s identities are valid for.

Test-driving ControlMaster

To play around with the feature, have two shell sessions open simultaneously. Set up a SSH configuration stanza like the following example, adapted to your environment:

Host cmaster-test
  Hostname replace-with-actual-remote.example.com
  BatchMode yes
  ControlMaster auto
  ControlPath ~/.ssh/cmaster-test-do-not-use-this-in-production
  # ControlPersist yes # also try to toggle this!

Now, connect to the example host using ssh cmaster-test in both shells. Notice how the second session will initiate noticeably faster (and probably skip displaying motd). Try terminating the first SSH session by logging out, and see what happens to the second one. Maybe create a third one, and connect again. Retry everything with ControlPersist again. Kill some ssh processes out under each other, fool around a bit, go to town!

Summing up, having ControlMaster (and ControlPath) set up as enabled by default is probably a bad idea. But given its unique performance benefits, it is a real treat to have for hosts configured in your ansible inventories. If you play your cards right in both ansible’s configuration and your user’s ~/.ssh/config file, you can get the best for both worlds, re-using persistent ControlMaster sockets in scenarios where that makes sense, and leaving it alone for use cases where you deem it more of a risk than a benefit. It can pay off hugely to try it out and get it right, so I encourage you to see and decide for yourself. If you like the experience, please also review the ssh_config(5) docs around ControlPath, and make sure these sockets end up in a private, secure directory that only authorized accounts may gain access to.

ProxyJump and ProxyCommand

Often times, corporate security policies (or just someone wanting to be extra careful) enforce the use of “bastion” hosts - a protocol break where on has to connect to using SSH, and then use that bastion host as the origin of the connection to the actual destination, where the user intends to end up at. If implemented properly, this can actually improve your org’s security, while maybe also feeding the fires of the ever-burning compliance furnaces. On the flip side, it makes connecting to the intended remote a bit of a hassle, and may break scp, sftp, and all their useful friends.

ProxyJump can help you deal with that sort of setup, and jump over an (in theory) arbitrary number of chained SSH hops in between your local host, and the intended remote. This option will cause a “pre-flight” SSH connection to the ProxyJump host, and then use SSH protocol features of that host’s server to establish a forwarding to the actual destination’s SSH server listener. Of course, if the bastion host’s SSH server configuration prevents forwardings from being established, this falls flat on the face.

In previous releases of OpenSSH, ProxyJump was not yet available, but could be replicated by clever use of the ProxyCommand option, and some netcat/socat magic. Today, you should prefer ProxyJump over it if getting to your destination is only a matter of a single SSH hop - but ProxyCommand could in theory still be worth knowing about if you need to perform weird VPN-like setup incantations or the like for your remote to become reachable.

If I had known of that feature back in 2007 at one of my first jobs, I’d have saved myself a lot of hassle implementing an ssh connection wrapper framework implemented as a bash script so horrible, it made you want to gouge your eyes out.

OpenSSH server features (sshd)

Of course, OpenSSH does not only make the industry standard ssh client available, but also what’s probably the most widely deployed SSH server implementation. I will describe some (more or less) advanced configuration options and patterns I’ve come to use over the years, in the hope you will find them useful. These are mostly targeted at hosts running Debian GNU/Linux, and whenever I reference 3rd party software by package name, you should be able to find those in the package archive of the stable Debian release (11/bullseye at the time of writing).

LogLevel

This one’s hardly a secret or obscure setting, but I find the default configuration for sshd often lacking and frustrating when debugging authentication problems. I prefer having it set to VERBOSE (which does not slot right into any of the priorities defined by syslog(2), but that doesn’t make it any less useful!), as that level of detail usually contains enough of the reasons why sshd would deny a seemingly OK login attempt.

StrictModes

I mention this because it’s a gotcha of sorts that you can usually sort out quickly with the correct LogLevel, but that can be a stinker if you don’t have that luxury. Also, the docs in sshd_config(5) do a suboptimal job at explaining what this option actually does - to hopefully help make it more clear: Make sure that users’ ~/.ssh/-directory (or whatever path you configured for your sshd instance to regard as this default’s equivalent), all files in it (authorized_keys, first and foremost), along with the account’s home directory itself have “sensible” permissions. Especially important are the lack of “other” (aka world-)access, and keeping the possibility of the group owner(s) to write/modify their contents in check.

Match

Match blocks are a powerful primitive to conditionally adapt OpenSSH sshd’s configuration and behavior. With it, you can partition its configuration into sections that will only apply to a session if all the criteria the Match directive requires are satisfied. Commonly used criteria are matches on the User or Group of a session being established, or also its Address property, for evaluating the remote TCP peer’s IP address.

Naturally, you cannot override any and all properties that the daemon’s global configuration file imposes - you cannot, for example, make sshd bind to a different ListenAddress via Match whenever someone’s connecting from crunch, or anything like that. Please refer to the documentation for an exhaustive list of options available.

I most frequently happen to use stuff like ForceCommand and GatewayPorts to either severely limit what a user can do over SSH, or to expose more “dangerous” features of the protocol to users who have earned the requisite level of trust.

SetEnv

You can use this as a no-frills way to inject environment variables (and values) into SSH sessions that will get set by the sshd process, not having to bother with PAM session config or, worse, crude user initialisation scripts. Makes a powerful tool in combination with Match blocks that only affect certain users or groups.

AcceptEnv

This allowlists environment variables (L values only) that the client sessions passes in, for them to take effect/get set in the resulting session under sshd. It’s advisable to tightly control this list in case there’s another shellshock-like vulnerability hiding somewhere deep within your login stack, and also to keep in mind when users report trouble to have their interactive shell environments “just work” when ssh’ing into a machine, due to TERM, LC_*, LANG, et al. not being able to pass through to the other side.

AllowUsers, AllowGroups

There’s not much to tell about these, and their cousins DenyUsers and DenyGroups, except that they are decent alternatives to messing around with PAM (or any authorization providers/services that might end up feeding PAM policies) to allow- and denylist accounts and groups for logging in via SSH (which is often the one vector you actually care about). Don’t forget you have these in your arsenal when you are dutifully trying to Keep It Simple, Stupid!

UsePAM

Pluggable Authentication Modules, or PAM, are a SunOS/Solaris and Linux thing people mostly either absolutely love or ferociously hate (or maybe simply don’t know about). PAM stacks allow for flexible, fine-grained control over authentication and user session instantiation on a per-service basis. To no-one’s surprise, this power and flexibility comes with a lot of complexity to boot.

Treating PAM adequately would require an article at least as long as this one, and then probably some, so I am not going to try to do that while I am busy with sshd. What I want to make more widely known, though, is one particularity about OpenSSH’s sshd PAM support: On most distros, you will find a pretty generic PAM stack for sshd located at /etc/pam.d/sshd, which implies that sshd uses a PAM service name of “sshd” when creating its PAM context (to initialize the PAM stack configuration).

A while ago, I wanted to have more than one sshd-specific PAM stack available for two instances of sshd on the same host, and became stuck, because there’s no sshd_config(5)-documented setting for overriding that particular PAM service name. Digging into the OpenSSH source quickly reveals how you do that: You need to change the executable’s name that is passed into the exec(2) syscall family to invoke the sshd process, so that it ends in a pathname component that matches the intended PAM service name (and therefore PAM stack configuration file name). Now what exactly does that mean?

Say I would want to have an sshd instance be subject to a PAM configuration located at /etc/pam.d/sshd-mfa. First, I would create a symlink to the /usr/bin/sshd executable at a location like /usr/local/sbin/sshd-mfa, enable UsePAM in the (probably separate) sshd_config file prepared for the new instance, and write a new startup script or systemd service unit that would end up invoking /usr/local/sbin/sshd-mfa instead of /usr/bin/sshd when starting up the instance. The resulting process would see and evaluate its argument vector and changed executable name (sshd-mfa - or any other name you chose in the symlink pointing to sshd), and upon noticing it was configured to use PAM, read the stack configuration from /etc/pam.d/sshd-mfa instead.

Somewhat obscure, but actually quite easy to handle. Using this trick, you can experiment with sshd PAM configuration changes with a limited blast radius.

MFA via AuthenticationMethods

For a long time, most SSH users used passwords to authenticate to their servers. More sophisticated (or more lazy) users resorted to public key authentication instead, where automatically solving a cryptographic riddle using a secret only the authenticating could know helps ensuring authentication is solid. In recent years, snakeoil peddlers and serious security actors alike have begun to enforce Multi-Factor Authentication (MFA) for accessing critical services. Since OpenSSH often is the line of defense that really matters, it has kept pace, and also supports its ways of implementing MFA.

OpenSSH’s sshd allows for a very liberal combination of independent factors to ascertain an identity is who they claim to be. The configuration knob in its config to look at in detail is AuthenticationMethods, and it will accept a space-separated list of items that will have to succeed in order to grant access. These items are comma-separated authentication methods that are logically ORed to determine an individual item’s success. If this confused to, please take a look at the docs - they explain it very well.

A fun fact about this is that you can use the same item more than once, and so have a user present not one, but two (different) authorized public keys for that account in sequence. So setting up AuthenticationMethods publickey publickey will do exactly that. Or, you can make OpenSSH expect the client to present an authorized public key SSH identity after having passed the password check, via AuthenticationMethods password publickey. Order matters here, so this will have the authentication flow ask for the password first, and expect the client to be able to satisfy its prompt for an authorized key second. Of course, you are free to add more factors after these to your own delight.

My personal variant of SSH MFA however is to use a combination of AuthenticationsMethods of publickey and keyboard-interactive:pam, with a PAM stack specifically set up to use the oath toolkit (Debian-packaged as libpam-oath), to require both a valid SSH identity, and a (TOTP- or HOTP-based) one-time password.

AuthorizedKeysFile, AuthorizedKeysCommand

Most SSH users are used to committing their public keys into ~/.ssh/authorized_keys in a newline-separated, string-based format, and use the associated private key to be able to log in henceforth. It’s probably not surprising that the location of that file/database of allowlisted keys is not set in stone, and that you can put it in a place where users themselves may not even write to it. That’s one way to prevent users from “smuggling” new identities into their arsenal of valid login credentials. Check the docs on the option, as it allows for token expansion in its value, to make the file’s path depend on values like the account’s username or UID.

What may amount to more of a surprise to some is that you can also choose to specify a program or script to generate/compute/fetch/younameit a list of valid, authorized public keys whenever an account tries to log in. That’s pretty awesome to fetch public keys from a central identity management solution like an LDAP directory, or maybe a configuration database or other kind of internal distribution list. Advanced authentication solutions use this feature to hook into OpenSSH that way. If it works for them, it can also work for you. If you choose to, make sure to 1.) write your script very defensively, 2.) also think about the requirements for the AuthorizedKeysCommandUser option, and 3.) write your script very defensively. I would also make sure to write the script defensively, as you do not want Zero Cool being able to log in, all because you forgot to write your script defensively. Godspeed!

IPQoS

Bonus round! Just in case you already knew all of the above, this is me faintly hoping you haven’t seen that one before!

But let’s deal with the bad news first: This option is probably not going to have an effect in your environment. That’s because most ISPs’ networking equipment and routing infrastructure will not give a rat’s ass about the well-meant DiffServ (DSCP) settings sshd will imbue its egress packets with as a consequence of this option. (It actually does apply one of two classes by default, too, but exercising manual control over IPQoS would improve granularity.) But in theory, this would be a nice feature to have for, say, SSH-tunneled backup traffic that really should not clog your - or anyone’s - pipes. Or you could ensure that your MUD session’s IP datagrams always take the priority they rightfully deserve over your friend Bob’s.

To learn more, check the Wikipedia Article on DSCP and the sshd_config(5) man-page. Consider use in Match blocks for conditional configuration on a, for instance, per-group basis. With some local (netfilter) policy in addition to this setting, this could provide the basis for locally rate-limiting egress SSH in a fine-grained manner.

In closing

Thanks for taking the time to read this article - I hope you had at least some of the fun that I had writing it. If you find the information contained useful, please spread the word about it. For any corrections or additions, you’re very welcome to shoot me a quick message.

Happy hacking, and may your ControlMaster-fueled sessions never break!

Copyright ©2022 Johannes Truschnigg

This work is licensed under a Creative Commons Attribution-NoDerivatives 4.0 International License.