Add a --copy-as=USER[:GROUP] option
authorWayne Davison <wayned@samba.org>
Sun, 29 Mar 2020 20:01:13 +0000 (13:01 -0700)
committerWayne Davison <wayned@samba.org>
Sun, 29 Mar 2020 20:18:20 +0000 (13:18 -0700)
This can be used by a root-run rsync to try to make reading or writing
files safer in a situation where you can't run the whole rsync command
as a non-root user.

main.c
options.c
rsync.yo

diff --git a/main.c b/main.c
index 6a6ac55967ef9bba8ad4ece6b17acfb1394142b6..f79054948ea521cfb5afed65d6638519553555dd 100644 (file)
--- a/main.c
+++ b/main.c
@@ -89,6 +89,7 @@ extern char *shell_cmd;
 extern char *batch_name;
 extern char *password_file;
 extern char *backup_dir;
+extern char *copy_as;
 extern char curr_dir[MAXPATHLEN];
 extern char backup_dir_buf[MAXPATHLEN];
 extern char *basis_dir[MAX_BASIS_DIRS+1];
@@ -231,6 +232,74 @@ void read_del_stats(int f)
        stats.deleted_files += stats.deleted_specials = read_varint(f);
 }
 
+static void become_copy_as_user()
+{
+       char *gname;
+       uid_t uid;
+       gid_t gid;
+
+       if (!copy_as)
+               return;
+
+       if (DEBUG_GTE(CMD, 2))
+               rprintf(FINFO, "[%s] copy_as=%s\n", who_am_i(), copy_as);
+
+       if ((gname = strchr(copy_as, ':')) != NULL)
+               *gname++ = '\0';
+
+       if (!user_to_uid(copy_as, &uid, True)) {
+               rprintf(FERROR, "Invalid copy-as user: %s\n", copy_as);
+               exit_cleanup(RERR_SYNTAX);
+       }
+
+       if (gname) {
+               if (!group_to_gid(gname, &gid, True)) {
+                       rprintf(FERROR, "Invalid copy-as group: %s\n", gname);
+                       exit_cleanup(RERR_SYNTAX);
+               }
+       } else {
+               struct passwd *pw;
+               if ((pw = getpwuid(uid)) == NULL) {
+                       rsyserr(FERROR, errno, "getpwuid failed");
+                       exit_cleanup(RERR_SYNTAX);
+               }
+               gid = pw->pw_gid;
+       }
+
+       if (setgid(gid) < 0) {
+               rsyserr(FERROR, errno, "setgid failed");
+               exit_cleanup(RERR_SYNTAX);
+       }
+#ifdef HAVE_SETGROUPS
+       if (setgroups(1, &gid)) {
+               rsyserr(FERROR, errno, "setgroups failed");
+               exit_cleanup(RERR_SYNTAX);
+       }
+#endif
+#ifdef HAVE_INITGROUPS
+       if (!gname && initgroups(copy_as, gid) < 0) {
+               rsyserr(FERROR, errno, "initgroups failed");
+               exit_cleanup(RERR_SYNTAX);
+       }
+#endif
+
+       if (setuid(uid) < 0
+#ifdef HAVE_SETEUID
+        || seteuid(uid) < 0
+#endif
+       ) {
+               rsyserr(FERROR, errno, "setuid failed");
+               exit_cleanup(RERR_SYNTAX);
+       }
+
+       our_uid = MY_UID();
+       our_gid = MY_GID();
+       am_root = (our_uid == 0);
+
+       if (gname)
+               gname[-1] = ':';
+}
+
 /* This function gets called from all 3 processes.  We want the client side
  * to actually output the text, but the sender is the only process that has
  * all the stats we need.  So, if we're a client sender, we do the report.
@@ -824,6 +893,8 @@ static void do_server_sender(int f_in, int f_out, int argc, char *argv[])
                exit_cleanup(RERR_SYNTAX);
        }
 
+       become_copy_as_user();
+
        dir = argv[0];
        if (!relative_paths) {
                if (!change_dir(dir, CD_NORMAL)) {
@@ -1027,6 +1098,8 @@ static void do_server_recv(int f_in, int f_out, int argc, char *argv[])
                return;
        }
 
+       become_copy_as_user();
+
        if (argc > 0) {
                char *dir = argv[0];
                argc--;
@@ -1186,6 +1259,9 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[])
 
                if (write_batch && !am_server)
                        start_write_batch(f_out);
+
+               become_copy_as_user();
+
                flist = send_file_list(f_out, argc, argv);
                if (DEBUG_GTE(FLIST, 3))
                        rprintf(FINFO,"file list sent\n");
@@ -1219,6 +1295,8 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[])
                        io_start_buffering_out(f_out);
        }
 
+       become_copy_as_user();
+
        send_filter_list(read_batch ? -1 : f_out);
 
        if (filesfrom_fd >= 0) {
index e5b0cb68280ed5e1d5cfea93e314c1c67c74ebe9..a9b0718478e419da845e4f7ff4b2e5d05deae39f 100644 (file)
--- a/options.c
+++ b/options.c
@@ -126,6 +126,7 @@ int inplace = 0;
 int delay_updates = 0;
 long block_size = 0; /* "long" because popt can't set an int32. */
 char *skip_compress = NULL;
+char *copy_as = NULL;
 item_list dparam_list = EMPTY_ITEM_LIST;
 
 /** Network address family. **/
@@ -777,6 +778,7 @@ void usage(enum logcode F)
   rprintf(F,"     --files-from=FILE       read list of source-file names from FILE\n");
   rprintf(F," -0, --from0                 all *-from/filter files are delimited by 0s\n");
   rprintf(F," -s, --protect-args          no space-splitting; only wildcard special-chars\n");
+  rprintf(F,"     --copy-as=USER[:GROUP]  specify user & optional group for the copy\n");
   rprintf(F,"     --address=ADDRESS       bind address for outgoing socket to daemon\n");
   rprintf(F,"     --port=PORT             specify double-colon alternate port number\n");
   rprintf(F,"     --sockopts=OPTIONS      specify custom TCP options\n");
@@ -1030,6 +1032,7 @@ static struct poptOption long_options[] = {
   {"no-8-bit-output",  0,  POPT_ARG_VAL,    &allow_8bit_chars, 0, 0, 0 },
   {"no-8",             0,  POPT_ARG_VAL,    &allow_8bit_chars, 0, 0, 0 },
   {"qsort",            0,  POPT_ARG_NONE,   &use_qsort, 0, 0, 0 },
+  {"copy-as",          0,  POPT_ARG_STRING, &copy_as, 0, 0, 0 },
   {"address",          0,  POPT_ARG_STRING, &bind_address, 0, 0, 0 },
   {"port",             0,  POPT_ARG_INT,    &rsync_port, 0, 0, 0 },
   {"sockopts",         0,  POPT_ARG_STRING, &sockopts, 0, 0, 0 },
index 207d487eb11fbca9a26a4afa4058794519f17360..1950349c128c99e18d3290d3f0facae4581baef9 100644 (file)
--- a/rsync.yo
+++ b/rsync.yo
@@ -440,6 +440,7 @@ to the detailed description below for a complete description.  verb(
      --files-from=FILE       read list of source-file names from FILE
  -0, --from0                 all *from/filter files are delimited by 0s
  -s, --protect-args          no space-splitting; wildcard chars only
+     --copy-as=USER[:GROUP]  specify user & optional group for the copy
      --address=ADDRESS       bind address for outgoing socket to daemon
      --port=PORT             specify double-colon alternate port number
      --sockopts=OPTIONS      specify custom TCP options
@@ -624,8 +625,8 @@ resolution (allowing times to differ from the original by up to 1 second).
 If you want all your transfers to default to comparing nanoseconds, you can
 create a ~/.popt file and put these lines in it:
 
-quote(tt(   rsync alias -a -a@-1))
-quote(tt(   rsync alias -t -t@-1))
+verb(    rsync alias -a -a@-1)
+verb(    rsync alias -t -t@-1)
 
 With that as the default, you'd need to specify bf(--modify-window=0) (aka
 bf(-@0)) to override it and ignore nanoseconds, e.g. if you're copying between
@@ -713,12 +714,12 @@ just the last parts of the filenames. This is particularly useful when
 you want to send several different directories at the same time. For
 example, if you used this command:
 
-quote(tt(   rsync -av /foo/bar/baz.c remote:/tmp/))
+verb(    rsync -av /foo/bar/baz.c remote:/tmp/)
 
 ... this would create a file named baz.c in /tmp/ on the remote
 machine. If instead you used
 
-quote(tt(   rsync -avR /foo/bar/baz.c remote:/tmp/))
+verb(    rsync -avR /foo/bar/baz.c remote:/tmp/)
 
 then a file named /tmp/foo/bar/baz.c would be created on the remote
 machine, preserving its full path.  These extra path elements are called
@@ -739,24 +740,22 @@ implied directories for each path you specify.  With a modern rsync on the
 sending side (beginning with 2.6.7), you can insert a dot and a slash into
 the source path, like this:
 
-quote(tt(   rsync -avR /foo/./bar/baz.c remote:/tmp/))
+verb(    rsync -avR /foo/./bar/baz.c remote:/tmp/)
 
 That would create /tmp/bar/baz.c on the remote machine.  (Note that the
 dot must be followed by a slash, so "/foo/." would not be abbreviated.)
 For older rsync versions, you would need to use a chdir to limit the
 source path.  For example, when pushing files:
 
-quote(tt(   (cd /foo; rsync -avR bar/baz.c remote:/tmp/) ))
+verb(    (cd /foo; rsync -avR bar/baz.c remote:/tmp/) )
 
 (Note that the parens put the two commands into a sub-shell, so that the
 "cd" command doesn't remain in effect for future commands.)
 If you're pulling files from an older rsync, use this idiom (but only
 for a non-daemon transfer):
 
-quote(
-tt(   rsync -avR --rsync-path="cd /foo; rsync" \ )nl()
-tt(       remote:bar/baz.c /tmp/)
-)
+verb(  rsync -avR --rsync-path="cd /foo; rsync" \ )
+verb(       remote:bar/baz.c /tmp/)
 
 dit(bf(--no-implied-dirs)) This option affects the default behavior of the
 bf(--relative) option.  When it is specified, the attributes of the implied
@@ -1089,11 +1088,11 @@ behavior easier to type, you could define a popt alias for it, such as
 putting this line in the file ~/.popt (the following defines the bf(-Z) option,
 and includes --no-g to use the default group of the destination dir):
 
-quote(tt(   rsync alias -Z --no-p --no-g --chmod=ugo=rwX))
+verb(    rsync alias -Z --no-p --no-g --chmod=ugo=rwX)
 
 You could then use this new option in a command such as this one:
 
-quote(tt(   rsync -avZ src/ dest/))
+verb(    rsync -avZ src/ dest/)
 
 (Caveat: make sure that bf(-a) does not follow bf(-Z), or it will re-enable
 the two "--no-*" options mentioned above.)
@@ -1277,7 +1276,7 @@ The bf(--fake-super) option only affects the side where the option is used.
 To affect the remote side of a remote-shell connection, use the
 bf(--remote-option) (bf(-M)) option:
 
-quote(tt(  rsync -av -M--fake-super /src/ host:/dest/))
+verb(    rsync -av -M--fake-super /src/ host:/dest/)
 
 For a local copy, this option affects both the source and the destination.
 If you wish a local copy to enable this option just for the destination
@@ -1592,10 +1591,8 @@ inside a single-quoted string gives you a single-quote; likewise for
 double-quotes (though you need to pay attention to which quotes your
 shell is parsing and which quotes rsync is parsing).  Some examples:
 
-quote(
-tt(    -e 'ssh -p 2234')nl()
-tt(    -e 'ssh -o "ProxyCommand nohup ssh firewall nc -w1 %h %p"')nl()
-)
+verb(    -e 'ssh -p 2234')
+verb(    -e 'ssh -o "ProxyCommand nohup ssh firewall nc -w1 %h %p"')
 
 (Note that ssh users can alternately customize site-specific connect
 options in their .ssh/config file.)
@@ -1616,20 +1613,20 @@ communicate.
 One tricky example is to set a different default directory on the remote
 machine for use with the bf(--relative) option.  For instance:
 
-quote(tt(    rsync -avR --rsync-path="cd /a/b && rsync" host:c/d /e/))
+verb(    rsync -avR --rsync-path="cd /a/b && rsync" host:c/d /e/)
 
 dit(bf(-M, --remote-option=OPTION)) This option is used for more advanced
 situations where you want certain effects to be limited to one side of the
 transfer only.  For instance, if you want to pass bf(--log-file=FILE) and
 bf(--fake-super) to the remote system, specify it like this:
 
-quote(tt(    rsync -av -M --log-file=foo -M--fake-super src/ dest/))
+verb(    rsync -av -M --log-file=foo -M--fake-super src/ dest/)
 
 If you want to have an option affect only the local side of a transfer when
 it normally affects both sides, send its negation to the remote side.  Like
 this:
 
-quote(tt(    rsync -av -x -M--no-x src/ dest/))
+verb(    rsync -av -x -M--no-x src/ dest/)
 
 Be cautious using this, as it is possible to toggle an option that will cause
 rsync to have a different idea about what data to expect next over the socket,
@@ -1696,14 +1693,14 @@ See the FILTER RULES section for detailed information on this option.
 dit(bf(-F)) The bf(-F) option is a shorthand for adding two bf(--filter) rules to
 your command.  The first time it is used is a shorthand for this rule:
 
-quote(tt(   --filter='dir-merge /.rsync-filter'))
+verb(    --filter='dir-merge /.rsync-filter')
 
 This tells rsync to look for per-directory .rsync-filter files that have
 been sprinkled through the hierarchy and use their rules to filter the
 files in the transfer.  If bf(-F) is repeated, it is a shorthand for this
 rule:
 
-quote(tt(   --filter='exclude .rsync-filter'))
+verb(    --filter='exclude .rsync-filter')
 
 This filters out the .rsync-filter files themselves from the transfer.
 
@@ -1757,7 +1754,7 @@ source dir -- any leading slashes are removed and no ".." references are
 allowed to go higher than the source dir.  For example, take this
 command:
 
-quote(tt(   rsync -a --files-from=/tmp/foo /usr remote:/backup))
+verb(  rsync -a --files-from=/tmp/foo /usr remote:/backup)
 
 If /tmp/foo contains the string "bin" (or even "/bin"), the /usr/bin
 directory will be created as /backup/bin on the remote host.  If it
@@ -1778,7 +1775,7 @@ instead of the local host if you specify a "host:" in front of the file
 specify just a prefix of ":" to mean "use the remote end of the
 transfer".  For example:
 
-quote(tt(   rsync -a --files-from=:/path/file-list src:/ /tmp/copy))
+verb(  rsync -a --files-from=:/path/file-list src:/ /tmp/copy)
 
 This would copy all the files specified in the /path/file-list file that
 was located on the remote "src" host.
@@ -1829,6 +1826,36 @@ default (with is overridden by both the environment and the command-line).
 This option will eventually become a new default setting at some
 as-yet-undetermined point in the future.
 
+dit(bf(--copy-as=USER[:GROUP])) This option instructs rsync to use the USER and
+(if specified after a colon) the GROUP for the copy operations. This only works
+if the user that is running rsync has the ability to change users. If the group
+is not specified then the user's default groups are used.
+
+The option only affects one side of the transfer unless the transfer is local,
+in which case it affects both sides. Use the bf(--remote-option) to affect the
+remote side, such as bf(-M--copy-as=joe). For a local transfer, see the "lsh"
+support file provides a local-shell helper script that can be used to allow a
+"localhost:" host-spec to be specified without needing to setup any remote
+shells (allowing you to specify remote options that affect the side of the
+transfer that is using the host-spec, and local options for the other side).
+
+This option can help to reduce the risk of an rsync being run as root into or
+out of a directory that might have live changes happening to it and you want to
+make sure that root-level read or write actions of system files are not
+possible. While you could alternatively run all of rsync as the specified user,
+sometimes you need the root-level host-access credentials to be used, so this
+allows rsync to drop root for the copying part of the operation after the
+remote-shell or daemon connection is established.
+
+For example, the following rsync writes the local files as user "joe":
+
+verb(   sudo rsync -aiv --copy-as=joe host1:backups/joe/ /home/joe/)
+
+This makes all files owned by user "joe", limits the groups to those that are
+available to that user, and makes it impossible for the joe user to do a timed
+exploit of the path to induce a change to a file that the joe use has no
+permissions to change.
+
 dit(bf(-T, --temp-dir=DIR)) This option instructs rsync to use DIR as a
 scratch directory when creating temporary copies of the files transferred
 on the receiving side.  The default behavior is to create each temporary
@@ -1924,7 +1951,7 @@ The files must be identical in all preserved attributes (e.g. permissions,
 possibly ownership) in order for the files to be linked together.
 An example:
 
-quote(tt(  rsync -av --link-dest=$PWD/prior_dir host:src_dir/ new_dir/))
+verb(  rsync -av --link-dest=$PWD/prior_dir host:src_dir/ new_dir/)
 
 If file's aren't linking, double-check their attributes.  Also check if some
 attributes are getting forced outside of rsync's control, such a mount option