mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-05-18 20:06:07 -04:00
rrsync improvements
- Convert rrsync to python. - Enhance security of arg & option checking. - Reject `-L` (`--copy-links`) by default. - Add `-munge` and `-no-del` options. - Tweak the logfile line format. - Created an rrsync man page. - Use `configure --with-rrsync` if you want `make install` to install rrsync and its man page. - Give lsh more rrsync testing support.
This commit is contained in:
10
Makefile.in
10
Makefile.in
@@ -6,6 +6,7 @@ exec_prefix=@exec_prefix@
|
||||
bindir=@bindir@
|
||||
libdir=@libdir@/rsync
|
||||
mandir=@mandir@
|
||||
with_rrsync=@with_rrsync@
|
||||
|
||||
LIBS=@LIBS@
|
||||
CC=@CC@
|
||||
@@ -80,6 +81,10 @@ install: all
|
||||
if test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi
|
||||
if test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi
|
||||
if test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi
|
||||
if test "$(with_rrsync)" = yes; then \
|
||||
$(INSTALLCMD) -m 755 $(srcdir)/support/rrsync $(DESTDIR)$(bindir); \
|
||||
if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \
|
||||
fi
|
||||
|
||||
install-ssl-daemon: stunnel-rsyncd.conf
|
||||
-$(MKDIR_P) $(DESTDIR)/etc/stunnel
|
||||
@@ -247,7 +252,7 @@ proto.h-tstamp: $(srcdir)/*.c $(srcdir)/lib/compat.c daemon-parm.h
|
||||
$(AWK) -f $(srcdir)/mkproto.awk $(srcdir)/*.c $(srcdir)/lib/compat.c daemon-parm.h
|
||||
|
||||
.PHONY: man
|
||||
man: rsync.1 rsync-ssl.1 rsyncd.conf.5
|
||||
man: rsync.1 rsync-ssl.1 rsyncd.conf.5 rrsync.1
|
||||
|
||||
rsync.1: rsync.1.md md2man version.h Makefile
|
||||
@$(srcdir)/maybe-make-man $(srcdir) rsync.1.md
|
||||
@@ -258,6 +263,9 @@ rsync-ssl.1: rsync-ssl.1.md md2man version.h Makefile
|
||||
rsyncd.conf.5: rsyncd.conf.5.md md2man version.h Makefile
|
||||
@$(srcdir)/maybe-make-man $(srcdir) rsyncd.conf.5.md
|
||||
|
||||
rrsync.1: support/rrsync.1.md md2man Makefile
|
||||
@$(srcdir)/maybe-make-man $(srcdir) support/rrsync.1.md
|
||||
|
||||
.PHONY: clean
|
||||
clean: cleantests
|
||||
rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) \
|
||||
|
||||
18
NEWS.md
18
NEWS.md
@@ -97,9 +97,15 @@
|
||||
|
||||
- More ASM optimizations from Shark64.
|
||||
|
||||
- Make rrsync pass --munge-links to rsync by default to make the restricted
|
||||
dir extra safe (with an option to turn it off if you trust your users).
|
||||
Also updated the known options list.
|
||||
- Transformed rrsync into a python script with improvements: security has been
|
||||
beefed up; the known rsync options were updated to include recent additions;
|
||||
rrsync rejects `-L` (`--copy-links`) by default to make it harder to exploit
|
||||
any out-of-subdir symlinks; a new rrsync option of `-munge` tells rrsync to
|
||||
always enable the `--munge-links` rsync option on the server side; a new
|
||||
rrsync option of `-no-del` disables all `--remove*` and `--delete*` rsync
|
||||
options on the server side; the log format has been tweaked slightly to add
|
||||
seconds to the timestamp and output the command executed as a tuple; an
|
||||
rrsync.1 manpage is now created.
|
||||
|
||||
- Work around a glibc bug where lchmod() breaks in a chroot w/o /proc mounted.
|
||||
|
||||
@@ -107,6 +113,12 @@
|
||||
|
||||
### PACKAGING RELATED:
|
||||
|
||||
- Give configure the --with-rrsync option if you want `make install` to
|
||||
install the (now python3) rrsync script and its (new) man page.
|
||||
|
||||
- If the rrsync script is installed, make its package depend on python3 and
|
||||
(suggested but not required) the python3 braceexpand lib.
|
||||
|
||||
- When creating a package from a non-release version (w/o a git checkout), the
|
||||
packager can elect to create git-version.h and define RSYNC_GITVER to the
|
||||
string they want `--version` to output. (The file is still auto-generated
|
||||
|
||||
@@ -136,6 +136,13 @@ if test x"$GCC" = x"yes"; then
|
||||
CFLAGS="$CFLAGS -Wall -W"
|
||||
fi
|
||||
|
||||
AC_ARG_WITH(rrsync,
|
||||
AS_HELP_STRING([--with-rrsync],[also install the rrsync script and its man page]))
|
||||
if test x"$with_rrsync" != x"yes"; then
|
||||
with_rrsync=no
|
||||
fi
|
||||
AC_SUBST(with_rrsync)
|
||||
|
||||
AC_ARG_WITH(included-popt,
|
||||
AS_HELP_STRING([--with-included-popt],[use bundled popt library, not from system]))
|
||||
|
||||
|
||||
@@ -37,4 +37,4 @@ if [ ! -f "$flagfile" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
"$srcdir/md2man" "$srcdir/$inname"
|
||||
"$srcdir/md2man" -s "$srcdir" "$srcdir/$inname"
|
||||
|
||||
13
md2man
13
md2man
@@ -85,7 +85,9 @@ def main():
|
||||
die('Failed to parse NAME.NUM.md out of input file:', args.mdfile)
|
||||
fi = argparse.Namespace(**fi.groupdict())
|
||||
|
||||
if not fi.srcdir:
|
||||
if args.srcdir:
|
||||
fi.srcdir = args.srcdir + '/'
|
||||
elif not fi.srcdir:
|
||||
fi.srcdir = './'
|
||||
|
||||
fi.title = fi.prog + '(' + fi.sect + ') man page'
|
||||
@@ -105,7 +107,7 @@ def main():
|
||||
for fn in (fi.srcdir + 'version.h', 'Makefile'):
|
||||
try:
|
||||
st = os.lstat(fn)
|
||||
except:
|
||||
except OSError:
|
||||
die('Failed to find', fi.srcdir + fn)
|
||||
if not fi.mtime:
|
||||
fi.mtime = st.st_mtime
|
||||
@@ -129,6 +131,10 @@ def main():
|
||||
if var == 'srcdir':
|
||||
break
|
||||
|
||||
fi.prog_ver = 'rsync ' + env_subs['VERSION']
|
||||
if fi.prog != 'rsync':
|
||||
fi.prog_ver = fi.prog + ' from ' + fi.prog_ver
|
||||
|
||||
with open(fi.fn, 'r', encoding='utf-8') as fh:
|
||||
txt = fh.read()
|
||||
|
||||
@@ -140,7 +146,7 @@ def main():
|
||||
txt = None
|
||||
|
||||
fi.date = time.strftime('%d %b %Y', time.localtime(fi.mtime))
|
||||
fi.man_headings = (fi.prog, fi.sect, fi.date, fi.prog + ' ' + env_subs['VERSION'], env_subs['prefix'])
|
||||
fi.man_headings = (fi.prog, fi.sect, fi.date, fi.prog_ver, env_subs['prefix'])
|
||||
|
||||
HtmlToManPage(fi)
|
||||
|
||||
@@ -374,6 +380,7 @@ def die(*msg):
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Transform a NAME.NUM.md markdown file into a NAME.NUM.html web page & a NAME.NUM man page.', add_help=False)
|
||||
parser.add_argument('--srcdir', '-s', help='Specify the source dir if the input file is not in it.')
|
||||
parser.add_argument('--test', action='store_true', help='Test if we can parse the input w/o updating any files.')
|
||||
parser.add_argument('--debug', '-D', action='count', default=0, help='Output copious info on the html parsing. Repeat for even more.')
|
||||
parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
|
||||
|
||||
@@ -7,7 +7,7 @@ import re, argparse
|
||||
|
||||
short_no_arg = { }
|
||||
short_with_num = { '@': 1 };
|
||||
long_opt = { # These include some extra long-args that BackupPC uses:
|
||||
long_opts = { # These include some extra long-args that BackupPC uses:
|
||||
'block-size': 1,
|
||||
'daemon': -1,
|
||||
'debug': 1,
|
||||
@@ -25,6 +25,7 @@ long_opt = { # These include some extra long-args that BackupPC uses:
|
||||
'owner': 0,
|
||||
'perms': 0,
|
||||
'recursive': 0,
|
||||
'stderr': 1,
|
||||
'times': 0,
|
||||
'write-devices': -1,
|
||||
}
|
||||
@@ -49,8 +50,8 @@ def main():
|
||||
m = re.search(r'args\[ac\+\+\] = "--([^"=]+)"', line)
|
||||
if m:
|
||||
last_long_opt = m.group(1)
|
||||
if last_long_opt not in long_opt:
|
||||
long_opt[last_long_opt] = 0
|
||||
if last_long_opt not in long_opts:
|
||||
long_opts[last_long_opt] = 0
|
||||
else:
|
||||
last_long_opt = None
|
||||
continue
|
||||
@@ -58,13 +59,13 @@ def main():
|
||||
if last_long_opt:
|
||||
m = re.search(r'args\[ac\+\+\] = ([^["\s]+);', line)
|
||||
if m:
|
||||
long_opt[last_long_opt] = 2
|
||||
long_opts[last_long_opt] = 2
|
||||
last_long_opt = None
|
||||
continue
|
||||
|
||||
m = re.search(r'return "--([^"]+-dest)";', line)
|
||||
if m:
|
||||
long_opt[m.group(1)] = 2
|
||||
long_opts[m.group(1)] = 2
|
||||
last_long_opt = None
|
||||
continue
|
||||
|
||||
@@ -74,19 +75,18 @@ def main():
|
||||
if not m:
|
||||
m = re.search(r'fmt = .*: "--([^"=]+)=', line)
|
||||
if m:
|
||||
long_opt[m.group(1)] = 1
|
||||
long_opts[m.group(1)] = 1
|
||||
last_long_opt = None
|
||||
|
||||
long_opt['files-from'] = 3
|
||||
long_opts['files-from'] = 3
|
||||
|
||||
txt = """
|
||||
# These options are the only options that rsync might send to the server,
|
||||
# and only in the option format that the stock rsync produces.
|
||||
txt = """\
|
||||
### START of options data produced by the cull_options script. ###
|
||||
|
||||
# To disable a short-named option, add its letter to this string:
|
||||
"""
|
||||
|
||||
txt += str_assign('short_disabled', 's') + "\n"
|
||||
txt += str_assign('short_disabled', 'Ls') + "\n"
|
||||
txt += str_assign('short_no_arg', ''.join(sorted(short_no_arg)), 'DO NOT REMOVE ANY')
|
||||
txt += str_assign('short_with_num', ''.join(sorted(short_with_num)), 'DO NOT REMOVE ANY')
|
||||
|
||||
@@ -99,24 +99,24 @@ def main():
|
||||
print(txt, end='')
|
||||
|
||||
if args.python:
|
||||
print("long_opt = {")
|
||||
print("long_opts = {")
|
||||
sep = ':'
|
||||
else:
|
||||
print("our %long_opt = (")
|
||||
sep = ' =>'
|
||||
|
||||
for opt in sorted(long_opt):
|
||||
for opt in sorted(long_opts):
|
||||
if opt.startswith(('min-', 'max-')):
|
||||
val = 1
|
||||
else:
|
||||
val = long_opt[opt]
|
||||
val = long_opts[opt]
|
||||
print(' ', repr(opt) + sep, str(val) + ',')
|
||||
|
||||
if args.python:
|
||||
print("}")
|
||||
else:
|
||||
print(");")
|
||||
print('')
|
||||
print("\n### END of options data produced by the cull_options script. ###")
|
||||
|
||||
|
||||
def str_assign(name, val, comment=None):
|
||||
@@ -129,10 +129,12 @@ def str_assign(name, val, comment=None):
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Output culled rsync options for rrsync.", add_help=False)
|
||||
out_group = parser.add_mutually_exclusive_group()
|
||||
out_group.add_argument('--perl', action='store_true', help="Output perl code (the default).")
|
||||
out_group.add_argument('--python', action='store_true', help="Output python code.")
|
||||
out_group.add_argument('--perl', action='store_true', help="Output perl code.")
|
||||
out_group.add_argument('--python', action='store_true', help="Output python code (the default).")
|
||||
parser.add_argument('--help', '-h', action='help', help="Output this help message and exit.")
|
||||
args = parser.parse_args()
|
||||
if not args.perl:
|
||||
args.python = True
|
||||
main()
|
||||
|
||||
# vim: sw=4 et
|
||||
|
||||
12
support/lsh
12
support/lsh
@@ -18,6 +18,8 @@ GetOptions(
|
||||
'rrsync=s' => \( my $rrsync_dir ),
|
||||
'ro' => \( my $rrsync_ro = '' ),
|
||||
'wo' => \( my $rrsync_wo = '' ),
|
||||
'munge' => \( my $rrsync_munge = '' ),
|
||||
'no-del' => \( my $rrsync_no_del = '' ),
|
||||
) or &usage;
|
||||
&usage unless @ARGV > 1;
|
||||
|
||||
@@ -71,16 +73,12 @@ unless ($no_chdir) {
|
||||
}
|
||||
|
||||
if ($rrsync_dir) {
|
||||
my $cmd = '';
|
||||
foreach (@ARGV) {
|
||||
(my $arg = $_) =~ s/(['";|()\[\]{}\$!*?<> \t&~\\])/\\$1/g;
|
||||
$cmd .= ' ' . $arg;
|
||||
}
|
||||
$cmd =~ s/^\s+//;
|
||||
$ENV{SSH_ORIGINAL_COMMAND} = $cmd;
|
||||
$ENV{SSH_ORIGINAL_COMMAND} = join(' ', @ARGV);
|
||||
push @cmd, 'rrsync';
|
||||
push @cmd, '-ro' if $rrsync_ro;
|
||||
push @cmd, '-wo' if $rrsync_wo;
|
||||
push @cmd, '-munge' if $rrsync_munge;
|
||||
push @cmd, '-no-del' if $rrsync_no_del;
|
||||
push @cmd, $rrsync_dir;
|
||||
} else {
|
||||
push @cmd, '/bin/sh', '-c', "@ARGV";
|
||||
|
||||
573
support/rrsync
573
support/rrsync
@@ -1,282 +1,353 @@
|
||||
#!/usr/bin/env perl
|
||||
# Name: /usr/local/bin/rrsync (should also have a symlink in /usr/bin)
|
||||
# Purpose: Restricts rsync to subdirectory declared in .ssh/authorized_keys
|
||||
# Author: Joe Smith <js-cgi@inwap.com> 30-Sep-2004
|
||||
# Modified by: Wayne Davison <wayne@opencoder.net>
|
||||
use strict;
|
||||
#!/usr/bin/env python3
|
||||
|
||||
use Socket;
|
||||
use Cwd 'abs_path';
|
||||
use File::Glob ':glob';
|
||||
# Restricts rsync to subdirectory declared in .ssh/authorized_keys. See
|
||||
# the rrsync man page for details of how to make use of this script.
|
||||
|
||||
# You may configure these values to your liking. See also the section
|
||||
# of options if you want to disable any options that rsync accepts.
|
||||
use constant RSYNC => '/usr/bin/rsync';
|
||||
use constant LOGFILE => 'rrsync.log';
|
||||
# NOTE: install python3 braceexpand to support brace expansion in the args!
|
||||
|
||||
my $Usage = <<EOM;
|
||||
Use 'command="$0 [-ro|-wo|-no-munge] SUBDIR"'
|
||||
in front of lines in $ENV{HOME}/.ssh/authorized_keys
|
||||
EOM
|
||||
# Originally a perl script by: Joe Smith <js-cgi@inwap.com> 30-Sep-2004
|
||||
# Python version by: Wayne Davison <wayne@opencoder.net>
|
||||
|
||||
# Handle the -ro, -wo, & -no-munge options.
|
||||
our $only = '';
|
||||
our $force_munge = 1;
|
||||
while (@ARGV) {
|
||||
if ($ARGV[0] =~ /^-([rw])o$/) {
|
||||
my $r_or_w = $1;
|
||||
if ($only && $only ne $r_or_w) {
|
||||
die "$0: the -ro and -wo options conflict.\n";
|
||||
}
|
||||
$only = $r_or_w;
|
||||
} elsif ($ARGV[0] eq '-no-munge') {
|
||||
$force_munge = 0;
|
||||
} else {
|
||||
last;
|
||||
}
|
||||
shift;
|
||||
}
|
||||
# You may configure these 2 values to your liking. See also the section of
|
||||
# short & long options if you want to disable any options that rsync accepts.
|
||||
RSYNC = '/usr/bin/rsync'
|
||||
LOGFILE = 'rrsync.log' # NOTE: the file must exist for a line to be appended!
|
||||
|
||||
our $subdir = shift;
|
||||
die "$0: No subdirectory specified\n$Usage" unless defined $subdir;
|
||||
$subdir = abs_path($subdir);
|
||||
die "$0: Restricted directory does not exist!\n" if $subdir ne '/' && !-d $subdir;
|
||||
# The following options are mainly the options that a client rsync can send
|
||||
# to the server, and usually just in the one option format that the stock
|
||||
# rsync produces. However, there are some additional convenience options
|
||||
# added as well, and thus a few options are present in both the short and
|
||||
# long lists (such as --group, --owner, and --perms).
|
||||
|
||||
# The client uses "rsync -av -e ssh src/ server:dir/", and sshd on the server
|
||||
# executes this program when .ssh/authorized_keys has 'command="..."'.
|
||||
# For example:
|
||||
# command="rrsync logs/client" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzGhEeNlPr...
|
||||
# command="rrsync -ro results" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmkHG1WCjC...
|
||||
#
|
||||
# Format of the environment variables set by sshd:
|
||||
# SSH_ORIGINAL_COMMAND=rsync --server -vlogDtpr --partial . ARG # push
|
||||
# SSH_ORIGINAL_COMMAND=rsync --server --sender -vlogDtpr --partial . ARGS # pull
|
||||
# SSH_CONNECTION=client_addr client_port server_port
|
||||
|
||||
my $command = $ENV{SSH_ORIGINAL_COMMAND};
|
||||
die "$0: Not invoked via sshd\n$Usage" unless defined $command;
|
||||
die "$0: SSH_ORIGINAL_COMMAND='$command' is not rsync\n" unless $command =~ s/^rsync\s+//;
|
||||
die "$0: --server option is not first\n" unless $command =~ /^--server\s/;
|
||||
our $am_sender = $command =~ /^--server\s+--sender\s/; # Restrictive on purpose!
|
||||
die "$0 sending to read-only server not allowed\n" if $only eq 'r' && !$am_sender;
|
||||
die "$0 reading from write-only server not allowed\n" if $only eq 'w' && $am_sender;
|
||||
# NOTE when disabling: check for both a short & long version of the option!
|
||||
|
||||
### START of options data produced by the cull_options script. ###
|
||||
|
||||
# These options are the only options that rsync might send to the server,
|
||||
# and only in the option format that the stock rsync produces.
|
||||
|
||||
# To disable a short-named option, add its letter to this string:
|
||||
our $short_disabled = 's';
|
||||
short_disabled = 'Ls'
|
||||
|
||||
our $short_no_arg = 'ACDEHIJKLNORSUWXbcdgklmnopqrstuvxyz'; # DO NOT REMOVE ANY
|
||||
our $short_with_num = '@B'; # DO NOT REMOVE ANY
|
||||
short_no_arg = 'ACDEHIJKLNORSUWXbcdgklmnopqrstuvxyz' # DO NOT REMOVE ANY
|
||||
short_with_num = '@B' # DO NOT REMOVE ANY
|
||||
|
||||
# To disable a long-named option, change its value to a -1. The values mean:
|
||||
# 0 = the option has no arg; 1 = the arg doesn't need any checking; 2 = only
|
||||
# check the arg when receiving; and 3 = always check the arg.
|
||||
our %long_opt = (
|
||||
'append' => 0,
|
||||
'backup-dir' => 2,
|
||||
'block-size' => 1,
|
||||
'bwlimit' => 1,
|
||||
'checksum-choice' => 1,
|
||||
'checksum-seed' => 1,
|
||||
'compare-dest' => 2,
|
||||
'compress-choice' => 1,
|
||||
'compress-level' => 1,
|
||||
'copy-dest' => 2,
|
||||
'copy-unsafe-links' => 0,
|
||||
'daemon' => -1,
|
||||
'debug' => 1,
|
||||
'delay-updates' => 0,
|
||||
'delete' => 0,
|
||||
'delete-after' => 0,
|
||||
'delete-before' => 0,
|
||||
'delete-delay' => 0,
|
||||
'delete-during' => 0,
|
||||
'delete-excluded' => 0,
|
||||
'delete-missing-args' => 0,
|
||||
'existing' => 0,
|
||||
'fake-super' => 0,
|
||||
'files-from' => 3,
|
||||
'force' => 0,
|
||||
'from0' => 0,
|
||||
'fsync' => 2,
|
||||
'fuzzy' => 0,
|
||||
'group' => 0,
|
||||
'groupmap' => 1,
|
||||
'hard-links' => 0,
|
||||
'iconv' => 1,
|
||||
'ignore-errors' => 0,
|
||||
'ignore-existing' => 0,
|
||||
'ignore-missing-args' => 0,
|
||||
'ignore-times' => 0,
|
||||
'info' => 1,
|
||||
'inplace' => 0,
|
||||
'link-dest' => 2,
|
||||
'links' => 0,
|
||||
'list-only' => 0,
|
||||
'log-file' => 3,
|
||||
'log-format' => 1,
|
||||
'max-alloc' => 1,
|
||||
'max-delete' => 1,
|
||||
'max-size' => 1,
|
||||
'min-size' => 1,
|
||||
'mkpath' => 0,
|
||||
'modify-window' => 1,
|
||||
'msgs2stderr' => 0,
|
||||
'munge-links' => 0,
|
||||
'new-compress' => 0,
|
||||
'no-W' => 0,
|
||||
'no-implied-dirs' => 0,
|
||||
'no-msgs2stderr' => 0,
|
||||
'no-munge-links' => -1,
|
||||
'no-r' => 0,
|
||||
'no-relative' => 0,
|
||||
'no-specials' => 0,
|
||||
'numeric-ids' => 0,
|
||||
'old-compress' => 0,
|
||||
'one-file-system' => 0,
|
||||
'only-write-batch' => 1,
|
||||
'open-noatime' => 0,
|
||||
'owner' => 0,
|
||||
'partial' => 0,
|
||||
'partial-dir' => 2,
|
||||
'perms' => 0,
|
||||
'preallocate' => 0,
|
||||
'recursive' => 0,
|
||||
'remove-sent-files' => 0,
|
||||
'remove-source-files' => 0,
|
||||
'safe-links' => 0,
|
||||
'sender' => 0,
|
||||
'server' => 0,
|
||||
'size-only' => 0,
|
||||
'skip-compress' => 1,
|
||||
'specials' => 0,
|
||||
'stats' => 0,
|
||||
'suffix' => 1,
|
||||
'super' => 0,
|
||||
'temp-dir' => 2,
|
||||
'timeout' => 1,
|
||||
'times' => 0,
|
||||
'use-qsort' => 0,
|
||||
'usermap' => 1,
|
||||
'write-devices' => -1,
|
||||
);
|
||||
long_opts = {
|
||||
'append': 0,
|
||||
'backup-dir': 2,
|
||||
'block-size': 1,
|
||||
'bwlimit': 1,
|
||||
'checksum-choice': 1,
|
||||
'checksum-seed': 1,
|
||||
'compare-dest': 2,
|
||||
'compress-choice': 1,
|
||||
'compress-level': 1,
|
||||
'copy-dest': 2,
|
||||
'copy-unsafe-links': 0,
|
||||
'daemon': -1,
|
||||
'debug': 1,
|
||||
'delay-updates': 0,
|
||||
'delete': 0,
|
||||
'delete-after': 0,
|
||||
'delete-before': 0,
|
||||
'delete-delay': 0,
|
||||
'delete-during': 0,
|
||||
'delete-excluded': 0,
|
||||
'delete-missing-args': 0,
|
||||
'existing': 0,
|
||||
'fake-super': 0,
|
||||
'files-from': 3,
|
||||
'force': 0,
|
||||
'from0': 0,
|
||||
'fsync': 2,
|
||||
'fuzzy': 0,
|
||||
'group': 0,
|
||||
'groupmap': 1,
|
||||
'hard-links': 0,
|
||||
'iconv': 1,
|
||||
'ignore-errors': 0,
|
||||
'ignore-existing': 0,
|
||||
'ignore-missing-args': 0,
|
||||
'ignore-times': 0,
|
||||
'info': 1,
|
||||
'inplace': 0,
|
||||
'link-dest': 2,
|
||||
'links': 0,
|
||||
'list-only': 0,
|
||||
'log-file': 3,
|
||||
'log-format': 1,
|
||||
'max-alloc': 1,
|
||||
'max-delete': 1,
|
||||
'max-size': 1,
|
||||
'min-size': 1,
|
||||
'mkpath': 0,
|
||||
'modify-window': 1,
|
||||
'msgs2stderr': 0,
|
||||
'munge-links': 0,
|
||||
'new-compress': 0,
|
||||
'no-W': 0,
|
||||
'no-implied-dirs': 0,
|
||||
'no-msgs2stderr': 0,
|
||||
'no-munge-links': -1,
|
||||
'no-r': 0,
|
||||
'no-relative': 0,
|
||||
'no-specials': 0,
|
||||
'numeric-ids': 0,
|
||||
'old-compress': 0,
|
||||
'one-file-system': 0,
|
||||
'only-write-batch': 1,
|
||||
'open-noatime': 0,
|
||||
'owner': 0,
|
||||
'partial': 0,
|
||||
'partial-dir': 2,
|
||||
'perms': 0,
|
||||
'preallocate': 0,
|
||||
'recursive': 0,
|
||||
'remove-sent-files': 0,
|
||||
'remove-source-files': 0,
|
||||
'safe-links': 0,
|
||||
'sender': 0,
|
||||
'server': 0,
|
||||
'size-only': 0,
|
||||
'skip-compress': 1,
|
||||
'specials': 0,
|
||||
'stats': 0,
|
||||
'stderr': 1,
|
||||
'suffix': 1,
|
||||
'super': 0,
|
||||
'temp-dir': 2,
|
||||
'timeout': 1,
|
||||
'times': 0,
|
||||
'use-qsort': 0,
|
||||
'usermap': 1,
|
||||
'write-devices': -1,
|
||||
}
|
||||
|
||||
### END of options data produced by the cull_options script. ###
|
||||
|
||||
if ($only eq 'r') {
|
||||
foreach my $opt (keys %long_opt) {
|
||||
if ($opt =~ /^(remove-|log-file)/) {
|
||||
$long_opt{$opt} = -1;
|
||||
}
|
||||
}
|
||||
} elsif ($only eq 'w') {
|
||||
$long_opt{'sender'} = -1;
|
||||
}
|
||||
import os, sys, re, argparse, glob, socket, time
|
||||
from argparse import RawTextHelpFormatter
|
||||
|
||||
if ($short_disabled ne '') {
|
||||
$short_no_arg =~ s/[$short_disabled]//go;
|
||||
$short_with_num =~ s/[$short_disabled]//go;
|
||||
}
|
||||
$short_no_arg = "[$short_no_arg]" if length($short_no_arg) > 1;
|
||||
$short_with_num = "[$short_with_num]" if length($short_with_num) > 1;
|
||||
try:
|
||||
from braceexpand import braceexpand
|
||||
except:
|
||||
braceexpand = lambda x: [ DE_BACKSLASH_RE.sub(r'\1', x) ]
|
||||
|
||||
my $write_log = -f LOGFILE && open(LOG, '>>', LOGFILE);
|
||||
HAS_DOT_DOT_RE = re.compile(r'(^|/)\.\.(/|$)')
|
||||
LONG_OPT_RE = re.compile(r'^--([^=]+)(?:=(.*))?$')
|
||||
DE_BACKSLASH_RE = re.compile(r'\\(.)')
|
||||
|
||||
chdir($subdir) or die "$0: Unable to chdir to restricted dir: $!\n";
|
||||
def main():
|
||||
if not os.path.isdir(args.dir):
|
||||
die("Restricted directory does not exist!")
|
||||
|
||||
my(@opts, @args);
|
||||
my $in_options = 1;
|
||||
my $last_opt = '';
|
||||
my $check_type;
|
||||
while ($command =~ /((?:[^\s\\]+|\\.[^\s\\]*)+)/g) {
|
||||
$_ = $1;
|
||||
if ($check_type) {
|
||||
push(@opts, check_arg($last_opt, $_, $check_type));
|
||||
$check_type = 0;
|
||||
} elsif ($in_options) {
|
||||
if ($_ eq '.') {
|
||||
$in_options = 0;
|
||||
} else {
|
||||
die "$0: invalid option: '-'\n" if $_ eq '-';
|
||||
push(@opts, $_);
|
||||
next if /^-$short_no_arg*(e\d*\.\w*)?$/o || /^-$short_with_num\d+$/o;
|
||||
# The format of the environment variables set by sshd:
|
||||
# SSH_ORIGINAL_COMMAND:
|
||||
# rsync --server -vlogDtpre.iLsfxCIvu --etc . ARG # push
|
||||
# rsync --server --sender -vlogDtpre.iLsfxCIvu --etc . ARGS # pull
|
||||
# SSH_CONNECTION (client_ip client_port server_ip server_port):
|
||||
# 192.168.1.100 64106 192.168.1.2 22
|
||||
|
||||
my($opt,$arg) = /^--([^=]+)(?:=(.*))?$/;
|
||||
my $disabled;
|
||||
if (defined $opt) {
|
||||
my $ct = $long_opt{$opt};
|
||||
last unless defined $ct;
|
||||
next if $ct == 0;
|
||||
if ($ct > 0) {
|
||||
if (!defined $arg) {
|
||||
$check_type = $ct;
|
||||
$last_opt = $opt;
|
||||
next;
|
||||
}
|
||||
$arg = check_arg($opt, $arg, $ct);
|
||||
$opts[-1] =~ s/=.*/=$arg/;
|
||||
next;
|
||||
}
|
||||
$disabled = 1;
|
||||
$opt = "--$opt";
|
||||
} elsif ($short_disabled ne '') {
|
||||
$disabled = /^-$short_no_arg*([$short_disabled])/o;
|
||||
$opt = "-$1";
|
||||
}
|
||||
command = os.environ.get('SSH_ORIGINAL_COMMAND', None)
|
||||
if not command:
|
||||
die("Not invoked via sshd")
|
||||
command = command.split(' ', 2)
|
||||
if command[0:1] != ['rsync']:
|
||||
die("SSH_ORIGINAL_COMMAND does not run rsync")
|
||||
if command[1:2] != ['--server']:
|
||||
die("--server option is not the first arg")
|
||||
command = '' if len(command) < 3 else command[2]
|
||||
|
||||
last unless $disabled; # Generate generic failure
|
||||
die "$0: option $opt has been disabled on this server.\n";
|
||||
}
|
||||
} else {
|
||||
if ($subdir ne '/') {
|
||||
# Validate args to ensure they don't try to leave our restricted dir.
|
||||
s{//+}{/}g;
|
||||
s{^/}{};
|
||||
s{^$}{.};
|
||||
}
|
||||
push(@args, bsd_glob($_, GLOB_LIMIT|GLOB_NOCHECK|GLOB_BRACE|GLOB_QUOTE));
|
||||
}
|
||||
}
|
||||
die "$0: invalid rsync-command syntax or options\n" if $in_options;
|
||||
global am_sender
|
||||
am_sender = command.startswith("--sender ") # Restrictive on purpose!
|
||||
if args.ro and not am_sender:
|
||||
die("sending to read-only server is not allowed")
|
||||
if args.wo and am_sender:
|
||||
die("reading from write-only server is not allowed")
|
||||
|
||||
if ($subdir ne '/') {
|
||||
die "$0: do not use .. in any path!\n" if grep m{(^|/)\.\.(/|$)}, @args;
|
||||
}
|
||||
if args.wo or not am_sender:
|
||||
long_opts['sender'] = -1
|
||||
if args.no_del:
|
||||
for opt in long_opts:
|
||||
if opt.startswith(('remove', 'delete')):
|
||||
long_opts[opt] = -1
|
||||
if args.ro:
|
||||
long_opts['log-file'] = -1
|
||||
|
||||
if ($force_munge) {
|
||||
push(@opts, '--munge-links');
|
||||
}
|
||||
short_no_arg_re = short_no_arg
|
||||
short_with_num_re = short_with_num
|
||||
if short_disabled:
|
||||
for ltr in short_disabled:
|
||||
short_no_arg_re = short_no_arg_re.replace(ltr, '')
|
||||
short_with_num_re = short_with_num_re.replace(ltr, '')
|
||||
short_disabled_re = re.compile(r'^-[%s]*([%s])' % (short_no_arg_re, short_disabled))
|
||||
short_no_arg_re = re.compile(r'^-(?=.)[%s]*(e\d*\.\w*)?$' % short_no_arg_re)
|
||||
short_with_num_re = re.compile(r'^-[%s]\d+$' % short_with_num_re)
|
||||
|
||||
@args = ( '.' ) if !@args;
|
||||
log_fh = open(LOGFILE, 'a') if os.path.isfile(LOGFILE) else None
|
||||
|
||||
if ($write_log) {
|
||||
my ($mm,$hh) = (localtime)[1,2];
|
||||
my $host = $ENV{SSH_CONNECTION} || 'unknown';
|
||||
$host =~ s/ .*//; # Keep only the client's IP addr
|
||||
$host =~ s/^::ffff://;
|
||||
$host = gethostbyaddr(inet_aton($host),AF_INET) || $host;
|
||||
printf LOG "%02d:%02d %-13s [%s]\n", $hh, $mm, $host, "@opts @args";
|
||||
close LOG;
|
||||
}
|
||||
try:
|
||||
os.chdir(args.dir)
|
||||
except OSError as e:
|
||||
die('unable to chdir to restricted dir:', str(e))
|
||||
|
||||
# Note: This assumes that the rsync protocol will not be maliciously hijacked.
|
||||
exec(RSYNC, @opts, '--', '.', @args) or die "exec(rsync @opts -- . @args) failed: $? $!";
|
||||
rsync_opts = [ '--server' ]
|
||||
rsync_args = [ ]
|
||||
saw_the_dot_arg = False
|
||||
last_opt = check_type = None
|
||||
|
||||
sub check_arg
|
||||
{
|
||||
my($opt, $arg, $type) = @_;
|
||||
$arg =~ s/\\(.)/$1/g;
|
||||
if ($subdir ne '/' && ($type == 3 || ($type == 2 && !$am_sender))) {
|
||||
$arg =~ s{//}{/}g;
|
||||
die "Do not use .. in --$opt; anchor the path at the root of your restricted dir.\n"
|
||||
if $arg =~ m{(^|/)\.\.(/|$)};
|
||||
$arg =~ s{^/}{$subdir/};
|
||||
}
|
||||
$arg;
|
||||
}
|
||||
for arg in re.findall(r'(?:[^\s\\]+|\\.[^\s\\]*)+', command):
|
||||
if check_type:
|
||||
rsync_opts.append(validated_arg(last_opt, arg, check_type))
|
||||
check_type = None
|
||||
elif saw_the_dot_arg:
|
||||
# NOTE: an arg that starts with a '-' is safe due to our use of "--" in the cmd tuple.
|
||||
try:
|
||||
b_e = braceexpand(arg) # Also removes backslashes
|
||||
except: # Handle errors such as unbalanced braces by just de-backslashing the arg:
|
||||
b_e = [ DE_BACKSLASH_RE.sub(r'\1', arg) ]
|
||||
for xarg in b_e:
|
||||
rsync_args += validated_arg('arg', xarg, wild=True)
|
||||
else: # parsing the option args
|
||||
if arg == '.':
|
||||
saw_the_dot_arg = True
|
||||
continue
|
||||
rsync_opts.append(arg)
|
||||
if short_no_arg_re.match(arg) or short_with_num_re.match(arg):
|
||||
continue
|
||||
disabled = False
|
||||
m = LONG_OPT_RE.match(arg)
|
||||
if m:
|
||||
opt = m.group(1)
|
||||
opt_arg = m.group(2)
|
||||
ct = long_opts.get(opt, None)
|
||||
if ct is None:
|
||||
break # Generate generic failure due to unfinished arg parsing
|
||||
if ct == 0:
|
||||
continue
|
||||
opt = '--' + opt
|
||||
if ct > 0:
|
||||
if opt_arg is not None:
|
||||
rsync_opts[-1] = opt + '=' + validated_arg(opt, opt_arg, ct)
|
||||
else:
|
||||
check_type = ct
|
||||
last_opt = opt
|
||||
continue
|
||||
disabled = True
|
||||
elif short_disabled:
|
||||
m = short_disabled_re.match(arg)
|
||||
if m:
|
||||
disabled = True
|
||||
opt = '-' + m.group(1)
|
||||
|
||||
# vim: sw=2
|
||||
if disabled:
|
||||
die("option", opt, "has been disabled on this server.")
|
||||
break # Generate a generic failure
|
||||
|
||||
if not saw_the_dot_arg:
|
||||
die("invalid rsync-command syntax or options")
|
||||
|
||||
if args.munge:
|
||||
rsync_opts.append('--munge-links')
|
||||
|
||||
if not rsync_args:
|
||||
rsync_args = [ '.' ]
|
||||
|
||||
cmd = (RSYNC, *rsync_opts, '--', '.', *rsync_args)
|
||||
|
||||
if log_fh:
|
||||
now = time.localtime()
|
||||
host = os.environ.get('SSH_CONNECTION', 'unknown').split()[0] # Drop everything after the IP addr
|
||||
if host.startswith('::ffff:'):
|
||||
host = host[7:]
|
||||
try:
|
||||
host = socket.gethostbyaddr(socket.inet_aton(host))
|
||||
except:
|
||||
pass
|
||||
log_fh.write("%02d:%02d:%02d %-16s %s\n" % (now.tm_hour, now.tm_min, now.tm_sec, host, str(cmd)))
|
||||
log_fh.close()
|
||||
|
||||
# NOTE: This assumes that the rsync protocol will not be maliciously hijacked.
|
||||
os.execlp(RSYNC, *cmd)
|
||||
die("execlp(", RSYNC, *cmd, ') failed')
|
||||
|
||||
|
||||
def validated_arg(opt, arg, typ=3, wild=False):
|
||||
if opt != 'arg': # arg values already have their backslashes removed.
|
||||
arg = DE_BACKSLASH_RE.sub(r'\1', arg)
|
||||
|
||||
orig_arg = arg
|
||||
if arg.startswith('./'):
|
||||
arg = arg[1:]
|
||||
arg = arg.replace('//', '/')
|
||||
if args.dir != '/':
|
||||
if HAS_DOT_DOT_RE.search(arg):
|
||||
die("do not use .. in", opt, "(anchor the path at the root of your restricted dir)")
|
||||
if arg.startswith('/'):
|
||||
arg = args.dir + arg
|
||||
|
||||
if wild:
|
||||
got = glob.glob(arg)
|
||||
if not got:
|
||||
got = [ arg ]
|
||||
else:
|
||||
got = [ arg ]
|
||||
|
||||
ret = [ ]
|
||||
for arg in got:
|
||||
if args.dir != '/' and arg != '.' and (typ == 3 or (typ == 2 and not am_sender)):
|
||||
arg_has_trailing_slash = arg.endswith('/')
|
||||
if arg_has_trailing_slash:
|
||||
arg = arg[:-1]
|
||||
else:
|
||||
arg_has_trailing_slash_dot = arg.endswith('/.')
|
||||
if arg_has_trailing_slash_dot:
|
||||
arg = arg[:-2]
|
||||
real_arg = os.path.realpath(arg)
|
||||
if arg != real_arg and not real_arg.startswith(args.dir_slash):
|
||||
die('unsafe arg:', orig_arg, [arg, real_arg])
|
||||
if arg_has_trailing_slash:
|
||||
arg += '/'
|
||||
elif arg_has_trailing_slash_dot:
|
||||
arg += '/.'
|
||||
if opt == 'arg' and arg.startswith(args.dir_slash):
|
||||
arg = arg[args.dir_slash_len:]
|
||||
if arg == '':
|
||||
arg = '.'
|
||||
ret.append(arg)
|
||||
|
||||
return ret if wild else ret[0]
|
||||
|
||||
|
||||
def die(*msg):
|
||||
print(sys.argv[0], 'error:', *msg, file=sys.stderr)
|
||||
if sys.stdin.isatty():
|
||||
arg_parser.print_help(sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# This class displays the --help to the user on argparse error IFF they're running it interactively.
|
||||
class OurArgParser(argparse.ArgumentParser):
|
||||
def error(self, msg):
|
||||
die(msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
our_desc = """Use "man rrsync" to learn how to restrict ssh users to using a restricted rsync command."""
|
||||
arg_parser = OurArgParser(description=our_desc, add_help=False)
|
||||
only_group = arg_parser.add_mutually_exclusive_group()
|
||||
only_group.add_argument('-ro', action='store_true', help="Allow only reading from the DIR. Implies -no-del.")
|
||||
only_group.add_argument('-wo', action='store_true', help="Allow only writing to the DIR.")
|
||||
arg_parser.add_argument('-no-del', action='store_true', help="Disable rsync's --delete* and --remove* options.")
|
||||
arg_parser.add_argument('-munge', action='store_true', help="Enable rsync's --munge-links on the server side.")
|
||||
arg_parser.add_argument('-help', '-h', action='help', help="Output this help message and exit.")
|
||||
arg_parser.add_argument('dir', metavar='DIR', help="The restricted directory to use.")
|
||||
args = arg_parser.parse_args()
|
||||
args.dir = os.path.realpath(args.dir)
|
||||
args.dir_slash = args.dir + '/'
|
||||
args.dir_slash_len = len(args.dir)
|
||||
if args.ro:
|
||||
args.no_del = True
|
||||
main()
|
||||
|
||||
# vim: sw=4 et
|
||||
|
||||
89
support/rrsync.1.md
Normal file
89
support/rrsync.1.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# NAME
|
||||
|
||||
rrsync - a script to setup restricted rsync users via ssh logins
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
```
|
||||
rrsync [-ro|-rw] [-munge] [-no-del] DIR
|
||||
```
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
A user's ssh login can be restricted to only allow the running of an rsync
|
||||
transfer in one of two easy ways: forcing the running of the rrsync script
|
||||
or forcing the running of an rsync daemon-over-ssh command.
|
||||
|
||||
To use the rrsync script, add a prefix like one of the following (followed by a
|
||||
space) in front of each ssh-key line in the user's `~/.ssh/authorized_keys`
|
||||
file that should be restricted:
|
||||
|
||||
> ```
|
||||
> command="rrsync DIR"
|
||||
> command="rrsync -ro DIR"
|
||||
> command="rrsync -munge -no-del DIR"
|
||||
> ```
|
||||
|
||||
Then, ensure that the rrsync script has your desired option restrictions. You
|
||||
may want to copy the script to a local bin dir with a unique name if you want
|
||||
to have multiple configurations. One or more rrsync options can be specified
|
||||
prior to the `DIR` if you want to further restrict the transfer.
|
||||
|
||||
To use an rsync daemon setup, add one of the following prefixes (followed by a
|
||||
space) in front of each ssh-key line in the user's `~/.ssh/authorized_keys`
|
||||
file that should be restricted:
|
||||
|
||||
> ```
|
||||
> command="rsync --server --daemon ."
|
||||
> command="rsync --server --daemon --config=/PATH/TO/rsyncd.conf ."
|
||||
> ```
|
||||
|
||||
Then, ensure that the rsyncd.conf file is created with one or more module names
|
||||
with the appropriate path and option restrictions. If the `--config` option is
|
||||
omitted, it defaults to `~/rsyncd.conf`. See the `rsyncd.conf` man page for
|
||||
details of how to configure an rsync daemon.
|
||||
|
||||
The remainder of this man page is dedicated to using the rrsync script.
|
||||
|
||||
# OPTION SUMMARY
|
||||
|
||||
```
|
||||
-ro Allow only reading from the DIR. Implies -no-del.
|
||||
-wo Allow only writing to the DIR.
|
||||
-no-del Disable rsync's --delete* and --remove* options.
|
||||
-munge Enable rsync's --munge-links on the server side.
|
||||
-help, -h Output this help message and exit.
|
||||
```
|
||||
|
||||
A single non-option argument specifies the restricted DIR to use. It can be
|
||||
relative to the user's home directory or an absolute path.
|
||||
|
||||
# SECURITY RESTRICTIONS
|
||||
|
||||
The rrsync script validates the path arguments it is sent to try to restrict
|
||||
them to staying within the specified DIR.
|
||||
|
||||
The rrsync script rejects rsync's `--copy-links`` option (by default) so that a
|
||||
copy cannot dereference a symlink within the DIR to get to a file outside the
|
||||
DIR.
|
||||
|
||||
The rrsync script rejects rsync's `--protect-args` (`-s`) option because it
|
||||
would allow options to be sent to the server-side that the script could not
|
||||
check. If you want to support `--protect-args`, use a daemon-over-ssh setup.
|
||||
|
||||
The rrsync script accepts just a subset of rsync's options that the real rsync
|
||||
uses when running the server command. A few extra convenience options are also
|
||||
included to help it to interact with BackupPC and accept some convenient user
|
||||
overrides.
|
||||
|
||||
The script (or a copy of it) can be manually edited if you want it to customize
|
||||
the option handling.
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
The `.ssh/authorized_keys` file might have lines in it like this:
|
||||
|
||||
> ```
|
||||
> command="rrsync client/logs" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzG...
|
||||
> command="rrsync -ro results" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmk...
|
||||
> ```
|
||||
Reference in New Issue
Block a user