ssh_filter_btrbk: refactoring/hardening:

- switched to bash interpreter
- enable fine-grained (--source, --target, ...) capabilities by command-line options
- added "--restrict_path" command-line option
- added sudo flag
- added man-page
- print SSH_ORIGINAL_COMMAND in error message
pull/48/head
Axel Burri 2015-09-02 19:20:58 +02:00
parent 28abe96747
commit f01304df35
2 changed files with 214 additions and 27 deletions

97
doc/ssh_filter_btrbk.1 Normal file
View File

@ -0,0 +1,97 @@
.TH "ssh_filter_btrbk" "1" "2015-09-03" "btrbk v0.20.0" ""
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
.ad l
.SH NAME
ssh_filter_btrbk.sh \- ssh command filter script for btrbk
.SH SYNOPSIS
.nf
\fBssh_filter_btrbk.sh\fR [\-s|\-\-source] [\-t|\-\-target] [\-d|\-\-delete] [\-i|\-\-info]
[\-p|\-\-restrict\-path <path>] [\-l|\-\-log] [\-\-sudo]
.fi
.SH DESCRIPTION
\fBssh_filter_btrbk.sh\fR restricts SSH commands to \fIbtrfs\fR
commands used by \fIbtrbk\fR. It examines the SSH_ORIGINAL_COMMAND
environment variable (set by sshd) and executes it only if it matches
commands used by \fIbtrbk\fR. The accepted commands are specified by
the "\-\-source", "\-\-target", "\-\-delete" and "\-\-info" options.
.PP
Note that the following btrfs commands are always allowed: "btrfs
subvolume show", "btrfs subvolume list".
.PP
Example line in /root/.ssh/authorized_keys on a backup target host:
.PP
.RS 4
.nf
command="ssh_filter_btrbk.sh \-\-target \-\-delete \-\-restrict\-path /mnt/btr_backup" ssh\-rsa AAAAB3NzaC1...hwumXFRQBL btrbk@mydomain.com
.fi
.RE
.SH OPTIONS
.PP
\-s, \-\-source
.RS 4
Allow commands for backup source: "btrfs subvolume snapshot", "btrfs
send". Equivalent to "\-\-snapshot \-\-send".
.RE
.PP
\-t, \-\-target
.RS 4
Allow commands for backup target: "btrfs receive". Equivalent to
"\-\-receive".
.RE
.PP
\-d, \-\-delete
.RS 4
Allow commands for subvolume deletion: "btrfs subvolume delete". This
is used for backup source if \fIsnapshot_preserve_daily\fR is not set
to \[lq]all\[rq], and for backup targets if
\fItarget_preserve_daily\fR is not set to \[lq]all\[rq].
.RE
.PP
\-i, \-\-info
.RS 4
Allow informative commands: "btrfs subvolume find-new", "btrfs
filesystem usage". This is used by btrbk \fIinfo\fR and \fIdiff\fR
commands.
.RE
.PP
\-\-snapshot
.RS 4
Allow btrfs snapshot command: "btrfs subvolume snapshot".
.RE
.PP
\-\-send
.RS 4
Allow btrfs send command: "btrfs send".
.RE
.PP
\-\-receive
.RS 4
Allow btrfs receive command: "btrfs receive".
.RE
.PP
\-p, \-\-restrict\-path <path>
.RS 4
Restrict btrfs commands to <path>.
.RE
.PP
\-l, \-\-log
.RS 4
Log ACCEPT and REJECT messages to the system log.
.RE
.PP
\-\-sudo
.RS 4
Call SSH_ORIGINAL_COMMAND using sudo.
.RE
.SH AVAILABILITY
Please refer to the btrbk project page
\fBhttp://www.digint.ch/btrbk/\fR for further
details.
.SH SEE ALSO
.BR btrbk (1),
.BR btrbk.conf (5),
.BR btrfs (1)
.SH AUTHOR
Axel Burri <axel@tty0.ch>

View File

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
set -e
set -u
@ -6,47 +6,137 @@ set -u
export PATH=/sbin:/bin:/usr/sbin:/usr/bin
enable_log=
if [ "$#" -ge 1 ] && [ "$1" = "-l" ]; then
enable_log=1
fi
use_sudo=
restrict_path_list=
allow_list=
log_cmd()
{
if [ -n "$enable_log" ]; then
logger -p $1 -t ssh_filter_btrbk.sh "$2 (Name: ${LOGNAME:-<unknown>}; Remote: ${SSH_CLIENT:-<unknown>}): $SSH_ORIGINAL_COMMAND"
if [[ -n "$enable_log" ]]; then
logger -p $1 -t ssh_filter_btrbk.sh "$2 (Name: ${LOGNAME:-<unknown>}; Remote: ${SSH_CLIENT:-<unknown>})${3:+: $3}: $SSH_ORIGINAL_COMMAND"
fi
}
allow_cmd()
{
allow_list="${allow_list}|$1"
}
reject_and_die()
{
log_cmd "auth.err" "btrbk REJECT"
/bin/echo "ERROR: ssh command rejected" 1>&2
local reason=$1
log_cmd "auth.err" "btrbk REJECT" "$reason"
echo "ERROR: ssh_filter_btrbk.sh: ssh command rejected: $reason: $SSH_ORIGINAL_COMMAND" 1>&2
exit 1
}
run_cmd()
{
log_cmd "auth.info" "btrbk ACCEPT"
$SSH_ORIGINAL_COMMAND
$use_sudo $SSH_ORIGINAL_COMMAND
}
reject_filtered_cmd()
{
# note that the backslash is NOT a metacharacter in a POSIX bracket expression!
option_match='-[a-zA-Z-]+' # matches short as well as long options
file_match='[0-9a-zA-Z_@+./-]+' # matches file path (equal to $file_match in btrbk)
if [[ -n "$restrict_path_list" ]]; then
# match any of restrict_path_list with or without trailing slash,
# or any file/directory (matching file_match) below restrict_path
path_match="(${restrict_path_list})(/|/${file_match})?"
else
# match any absolute file/directory (matching file_match)
path_match="/${file_match}"
fi
# allow multiple paths (e.g. "btrfs subvolume snapshot <src> <dst>")
btrfs_cmd_match="^(${allow_list})( ${option_match})*( $path_match)+$"
if [[ ! $SSH_ORIGINAL_COMMAND =~ $btrfs_cmd_match ]] ; then
reject_and_die "disallowed command${restrict_path_list:+ (restrict-path: \"${restrict_path_list//|/\", \"}\")}"
fi
}
allow_cmd "btrfs subvolume show"; # subvolume queries are always allowed
allow_cmd "btrfs subvolume list"; # subvolume queries are always allowed
while [[ "$#" -ge 1 ]]; do
key="$1"
case $key in
-l|--log)
enable_log=1
;;
--sudo)
use_sudo="sudo"
;;
-p|--restrict-path)
restrict_path_list="${restrict_path_list}|${2%/}" # add to list while removing trailing slash
shift # past argument
;;
-s|--source)
allow_cmd "btrfs subvolume snapshot"
allow_cmd "btrfs send"
;;
-t|--target)
allow_cmd "btrfs receive"
;;
-d|--delete)
allow_cmd "btrfs subvolume delete"
;;
-i|--info)
allow_cmd "btrfs subvolume find-new"
allow_cmd "btrfs filesystem usage"
;;
--snapshot)
allow_cmd "btrfs subvolume snapshot"
;;
--send)
allow_cmd "btrfs send"
;;
--receive)
allow_cmd "btrfs receive"
;;
*)
echo "ERROR: ssh_filter_btrbk.sh: failed to parse command line option: $key" 1>&2
exit 1
;;
esac
shift
done
# remove leading "|" on alternation lists
allow_list=${allow_list#\|}
restrict_path_list=${restrict_path_list#\|}
case "$SSH_ORIGINAL_COMMAND" in
*\$*) reject_and_die ;;
*\&*) reject_and_die ;;
*\(*) reject_and_die ;;
*\{*) reject_and_die ;;
*\;*) reject_and_die ;;
*\<*) reject_and_die ;;
*\>*) reject_and_die ;;
*\`*) reject_and_die ;;
*\|*) reject_and_die ;;
btrfs\ subvolume\ show\ *) run_cmd ;; # mandatory
btrfs\ subvolume\ list\ *) run_cmd ;; # mandatory
btrfs\ subvolume\ snapshot\ *) run_cmd ;; # mandatory if this host is backup source
btrfs\ send\ *) run_cmd ;; # mandatory if this host is backup source
btrfs\ receive\ *) run_cmd ;; # mandatory if this host is backup target
btrfs\ subvolume\ delete\ *) run_cmd ;; # mandatory if scheduling is active
btrfs\ subvolume\ find-new\ *) run_cmd ;; # needed for "btrbk diff"
btrfs\ filesystem\ usage\ *) run_cmd ;; # needed for "btrbk info"
*) reject_and_die ;;
*\$*) reject_and_die "unsafe character" ;;
*\&*) reject_and_die "unsafe character" ;;
*\(*) reject_and_die "unsafe character" ;;
*\{*) reject_and_die "unsafe character" ;;
*\;*) reject_and_die "unsafe character" ;;
*\<*) reject_and_die "unsafe character" ;;
*\>*) reject_and_die "unsafe character" ;;
*\`*) reject_and_die "unsafe character" ;;
*\|*) reject_and_die "unsafe character" ;;
*\.\./*) reject_and_die "directory traversal" ;;
*)
reject_filtered_cmd
run_cmd
;;
esac