Restrict commands by SSH authorized_keys command option
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:
- 2018-03-06 Simple and Secure External Backup: Restricting SSH access to rsync only from a specific directory
- 2021-08-03 Secure External Backup with ZFS Native Encryption: Restricting SSH access to a list of specific command patterns
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 frombackup:/backups/
to/mnt/backups
but only needs to provide the relative path tobackup
as rsyncing is enforced over rrsync to/backups/
directory on the remote side. If we want to pull all data frombackup:/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 acommand="..."
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.
#!/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!