Restrict commands by SSH authorized_keys command option

August 3rd, 2021 by Philip Iezzi 5 min read
cover image

SSH authorized_keys allows you to define a command which is executed upon authentication with a specific key by prefixing it with the command="cmd" option.

authorized_keys man page explains this wonderful feature as follows:

command="command"

Specifies that the command is executed whenever this key is used for authentication. The command supplied by the user (if any) is ignored. (...) This option might be useful to restrict certain public keys to perform just a specific operation. An example might be a key that permits remote backups but nothing else. (...) The command originally supplied by the client is available in the SSH_ORIGINAL_COMMAND environment variable.

The syntax goes like this, put command="cmd" and all other options before a pubkey in authorized_keys:

command="cmd",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAAB3N...

A detailed description of these options can be found here:

You might have missed the two use cases from my other blog posts, as they are quite long and complex:

So here's the two use cases in short:

Use Case 1: Limit rsync to a directory (rrsync)

Let's suppose we need full root permissions to pull data from a backup server, but its access should be limited to rsync from a specific directory and not executing any other commands on the remote backup server. The host that pulls data is going to be called extbackup.

There is a simple rsync wrapper for this which comes shipped with Debian's rsync package but first needs to be unpacked/installed:

backup$ zcat /usr/share/doc/rsync/scripts/rrsync.gz > /usr/local/bin/rrsync
backup$ chmod 0755 /usr/local/bin/rrsync
backup$ ln -s /usr/local/bin/rrsync /usr/bin/rrsync

We then add this to /etc/sudoers.d/extbackup on backup:

%backuppers ALL= NOPASSWD:SETENV: /usr/bin/rrsync

Let's now create a specific user extbackup and add him to group backuppers:

backup$ adduser extbackup
backup$ adduser extbackup backuppers

Add the public key of extbackup (found in extbackup:~/.ssh/id_rsa.pub) to backup:/home/extbackup/.ssh/authorized_keys, prefixed by the following options:

command="sudo -E /usr/bin/rrsync -ro /backups/",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAABxyz...

This limits extbackup to only pull data from backup:/backups/ directory over rsync. Test-run:

extbackup$ rsync -aH --numeric-ids --delete --delete-before extbackup@backup: /mnt/backups

Explanation: extbackup's root user rsyncs data from backup:/backups/ to /mnt/backups but only needs to provide the relative path to backup as rsyncing is enforced over rrsync to /backups/ directory on the remote side. If we want to pull all data from backup:/backups/, simply provide an empty relative path after the colon.

Isn't that cool? We can pull data with root rights from our backup server but are limited to one specific directory and are not able to execute any other commands except rsync!

Use Case 2: Restrict to specific command patterns

As pointed out here, OpenSSH's authorized_keys command option enforces a single command (by overriding the original command). So the question was:

The authorized_keys has a command="..." option that restricts a key to a single command. Is there a way to restrict a key to multiple commands? E.g. by having a regex there, or by editing some other configuration file?

This can be simply accomplished by a wrapper script, as OpenSSH makes the original command supplied by the client available in the SSH_ORIGINAL_COMMAND environment variable.

In Secure External Backup with ZFS Native Encryption our requirement was to limit root access over SSH to only specific command patterns, so that the client (in this case extbackup which pulls data from backup server) was only able to execute commands which were needed for PVE-zsync backup/replication.

To accomplish this specific restrictions, we need to use a script in command section before the extbackup's pubkey in .ssh/authorized_keys:

command="/root/.ssh/allowed-commands.sh",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAAB3N... root@extbackup

allowed-commands.sh then does pattern matching to verify the commands - using $SSH_ORIGINAL_COMMAND to get the original command:

For numeric ranges in Bash regexes I have used Regex Numeric Range Generator.

allowed-commands.sh
#!/bin/bash

## CONFIGURATION ##############################################
veid_pattern='(19[4-9]|2[0-4][0-9]|25[0-4])'
backup_srcdir='/backup'
pool_name=dpool
###############################################################

# Regex patterns
# WARNING: '\w' is not supported in Bash, so we need to use '[:alnum:]' instead
ds_pattern="${pool_name}/zfsdisks/subvol-${veid_pattern}-disk-1"
ts_pattern='20[2-9][0-9]-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])_(0[0-9]|1[0-9]|2[0-3])(:(0[0-9]|[1-5][0-9])){2}'
snap_prefix_pattern='rep_extbackup_([1-9]a_)?'
snap_pattern="@${snap_prefix_pattern}${ts_pattern}"
ds_snap_pattern="${ds_pattern}${snap_pattern}"

patt1='zfs list -r( -)?t snapshot -Ho name( -S creation)? '"$ds_pattern($snap_pattern)?"
patt2='zfs (snapshot|destroy) '"$ds_snap_pattern"
patt3='zfs send '"(-i $ds_snap_pattern )?-- $ds_snap_pattern"
patt4="zfs rename $ds_snap_pattern $ds_snap_pattern" # only used in extbackup-migrate-zfs-snaps.sh

cmd="$SSH_ORIGINAL_COMMAND"

if [[ "$cmd" == 'list-datasets' ]]; then
    # special command to get a server list of all datasets and mountpoints
    zfs list -H -o name,mountpoint,usedds | grep $backup_srcdir
    exit
elif [[ $cmd =~ ^$patt1$ || $cmd =~ ^$patt2$ || $cmd =~ ^$patt3$ || $cmd =~ ^$patt4$ ]]; then
    $SSH_ORIGINAL_COMMAND
    exit
else
    logger "$(basename $0) violation: $cmd"
    echo "Access denied"
    exit 1
fi

We can then test the magic list-datasets command to get a server list of all datasets and mountpoints:

extbackup$ ssh backup list-datasets

And pve-zsync is able to execute remote commands like such:

$ zfs list -r -t snapshot -Ho name -S creation dpool/zfsdisks/subvol-181-disk-1
$ zfs snapshot dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
$ zfs send -- dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
$ zfs destroy dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:13:13

Whenever we try to fire another command, we get an Access denied and the command is logged to syslog.

Sidenote: I trust extbackup as a client, especially since its SSH private key is encrypted and is only getting decrypted to volatile memory. But I still wanted to restrict it to a small subset of commands, and also to a subset of ZFS datasets it can pull data from, for improved security.

OpenSSH's authorized_keys command option gives us full flexibility with such a wrapper script!