diff options
author | Csaba Henk <csaba@gluster.com> | 2011-09-11 19:45:57 +0200 |
---|---|---|
committer | Vijay Bellur <vijay@gluster.com> | 2011-09-22 05:23:32 -0700 |
commit | 2ab00369e7ef99d287dad5301d2f334dcfd67a70 (patch) | |
tree | c4e7c2ac9f3285f6d2617133b131d924044dc786 | |
parent | 1098aaa51d2e3dca9e6c48ee1e9cb43bc87936f4 (diff) |
gsyncd: implement restricted mode and utility dispatch
With this change, the suggested way of setting up a geo-sync
slave is to use an ssh key with gsyncd as a forced command
(see sshd(8)), or set gsyncd as shell. This prevents the master
in executing arbitrary commands on slave (a major security hole).
Detailed list the changes:
- All gsyncd invocations that are not done by glusterd are
considered unsafe and then we operate in so-called "restricted mode"
(see below)
- if we are invoked on purpose (ie. it's not the case that sshd forced
us to run as frontend of a remote-invoked command), we execute gsyncd.py
- if invoked by sshd as frontend command, we check the remote command
line and call the required utility if it's among the allowed ones
(rsyncd and gsyncd)
- with rsync, we check if invocation is server mode and some other
sanity measures
- with gsyncd, in restricted mode we enforce the usage of the glusterd
provided config file, and in python, we enforce operation in
server mode and some other sanity checks
Impact on using geo-rep the old way: remote file slave now also
requires a running glusterd (to pick up config from).
Missing: we not implemented check of the rsync target path.
The issue of master being able to modify arbitrary locations
is planned to be mitigated by using geo-rep with an unprivileged
user.
Change-Id: I9b5825bfe282a9ca777429aadd554d78708f1638
BUG: 2825
Reviewed-on: http://review.gluster.com/460
Tested-by: Gluster Build System <jenkins@build.gluster.com>
Reviewed-by: Vijay Bellur <vijay@gluster.com>
-rw-r--r-- | xlators/features/marker/utils/src/gsyncd.c | 205 | ||||
-rw-r--r-- | xlators/features/marker/utils/syncdaemon/gsyncd.py | 15 |
2 files changed, 182 insertions, 38 deletions
diff --git a/xlators/features/marker/utils/src/gsyncd.c b/xlators/features/marker/utils/src/gsyncd.c index ec1d3a65a..cebca1aea 100644 --- a/xlators/features/marker/utils/src/gsyncd.c +++ b/xlators/features/marker/utils/src/gsyncd.c @@ -32,52 +32,67 @@ #include "run.h" #define _GLUSTERD_CALLED_ "_GLUSTERD_CALLED_" +#define _GSYNCD_DISPATCHED_ "_GSYNCD_DISPATCHED_" #define GSYNCD_CONF "geo-replication/gsyncd.conf" +#define RSYNC "rsync" + +int restricted = 0; static int -config_wanted (int argc, char **argv) +duplexpand (void **buf, size_t tsiz, size_t *len) { - char *evas = NULL; - char *oa = NULL; - int i = 0; - int one_more_arg = 0; + size_t osiz = tsiz * *len; - evas = getenv (_GLUSTERD_CALLED_); - if (evas && strcmp (evas, "1") == 0) { - /* OK, we know glusterd called us, no need to look for further config - * ... altough this conclusion should not inherit to our children - */ - unsetenv (_GLUSTERD_CALLED_); - return 0; - } + *buf = realloc (*buf, osiz << 1); + if (!buf) + return -1; - for (i = 1; i < argc; i++) { - /* -c found, see if it has an argument */ - if (one_more_arg) { - if (argv[i][0] != '-') - return 0; - one_more_arg = 0; - } + memset ((char *)*buf + osiz, 0, osiz); + *len <<= 1; - if ((strcmp (argv[i], "-c") && strcmp (argv[i], "--config-file")) == 0) { - one_more_arg = 1; - continue; - } + return 0; +} + +static int +str2argv (char *str, char ***argv) +{ + char *p = NULL; + int argc = 0; + size_t argv_len = 32; + int ret = 0; + + assert (str); + str = strdup (str); + if (!str) + return -1; - oa = strtail (argv[i], "-c"); - if (oa && !*oa) - oa = NULL; - if (!oa) - oa = strtail (argv[i], "--config-file="); - if (oa) - return 0; + *argv = calloc (argv_len, sizeof (**argv)); + if (!*argv) + goto error; + + while ((p = strtok (str, " "))) { + str = NULL; + + argc++; + if (argc == argv_len) { + ret = duplexpand ((void *)argv, + sizeof (**argv), + &argv_len); + if (ret == -1) + goto error; + } + (*argv)[argc - 1] = p; } - return 1; + return argc; + + error: + fprintf (stderr, "out of memory\n"); + return -1; } -int -main(int argc, char **argv) +static int +invoke_gsyncd (int argc, char **argv) { char config_file[PATH_MAX] = {0,}; size_t gluster_workdir_len = 0; @@ -86,7 +101,8 @@ main(int argc, char **argv) int j = 0; char *nargv[argc + 4]; - if (config_wanted (argc, argv)) { + if (restricted) { + /* in restricted mode we forcibly use the system-wide config */ runinit (&runner); runner_add_args (&runner, SBIN_DIR"/gluster", "--log-file=/dev/stderr", "system::", "getwd", @@ -106,18 +122,21 @@ main(int argc, char **argv) config_file[gluster_workdir_len] = '/'; strcat (config_file, GSYNCD_CONF); } else - config_file[0] = '\0'; + goto error; + + if (setenv ("_GSYNCD_RESTRICTED_", "1", 1) == -1) + goto error; } j = 0; nargv[j++] = PYTHON; nargv[j++] = GSYNCD_PREFIX"/python/syncdaemon/gsyncd.py"; + for (i = 1; i < argc; i++) + nargv[j++] = argv[i]; if (config_file[0]) { nargv[j++] = "-c"; nargv[j++] = config_file; } - for (i = 1; i < argc; i++) - nargv[j++] = argv[i]; nargv[j++] = NULL; execvp (PYTHON, nargv); @@ -129,3 +148,113 @@ main(int argc, char **argv) fprintf (stderr, "gsyncd initializaion failed\n"); return 1; } + +static int +invoke_rsync (int argc, char **argv) +{ + int i = 0; + + assert (argv[argc] == NULL); + + if (argc < 2 || strcmp (argv[1], "--server") != 0) + goto error; + + for (i = 2; i < argc && argv[i][0] == '-'; i++); + + if (!(i == argc || + (i == argc - 2 && strcmp (argv[i], ".") == 0 && argv[i + 1][0] == '/'))) + goto error; + + /* XXX a proper check would involve the following: + * - require rsync to not protect args (ie. pass target in command line) + * - find out proper synchronization target by: + * - looking up sshd process we origin from + * - within its children, find the gsyncd process + * that maintains the aux mount + * - find out mount directory by checking the working directory + * of the gsyncd process + * - demand that rsync target equals to sync target + * + * As of now, what we implement is dispatching rsync invocation to + * our system rsync, that handles the cardinal issue of controlling + * remote-requested command invocations. + */ + + argv[0] = RSYNC; + + execvp (RSYNC, argv); + + fprintf (stderr, "exec of "RSYNC" failed\n"); + return 127; + + error: + fprintf (stderr, "disallowed "RSYNC" invocation\n"); + return 1; +} + +struct invocable { + char *name; + int (*invoker) (int argc, char **argv); +}; + +struct invocable invocables[] = { + { "rsync", invoke_rsync }, + { "gsyncd", invoke_gsyncd }, + { NULL, NULL} +}; + +int +main (int argc, char **argv) +{ + char *evas = NULL; + struct invocable *i = NULL; + char *b = NULL; + char *sargv = NULL; + + evas = getenv (_GLUSTERD_CALLED_); + if (evas && strcmp (evas, "1") == 0) + /* OK, we know glusterd called us, no need to look for further config + * ... altough this conclusion should not inherit to our children + */ + unsetenv (_GLUSTERD_CALLED_); + else { + /* we regard all gsyncd invocations unsafe + * that do not come from glusterd and + * therefore restrict it + */ + restricted = 1; + + if (!getenv (_GSYNCD_DISPATCHED_)) { + evas = getenv ("SSH_ORIGINAL_COMMAND"); + if (evas) + sargv = evas; + else { + evas = getenv ("SHELL"); + if (evas && strcmp (basename (evas), "gsyncd") == 0 && + argc == 3 && strcmp (argv[1], "-c") == 0) + sargv = argv[2]; + } + } + + } + + if (!(sargv && restricted)) + return invoke_gsyncd (argc, argv); + + argc = str2argv (sargv, &argv); + if (argc == -1 || setenv (_GSYNCD_DISPATCHED_, "1", 1) == -1) { + fprintf (stderr, "internal error\n"); + return 1; + } + + b = basename (argv[0]); + for (i = invocables; i->name; i++) { + if (strcmp (b, i->name) == 0) + return i->invoker (argc, argv); + } + + fprintf (stderr, "invoking %s in restricted SSH session is not allowed\n", + b); + + return 1; +} diff --git a/xlators/features/marker/utils/syncdaemon/gsyncd.py b/xlators/features/marker/utils/syncdaemon/gsyncd.py index 9cae4d407..6747acbce 100644 --- a/xlators/features/marker/utils/syncdaemon/gsyncd.py +++ b/xlators/features/marker/utils/syncdaemon/gsyncd.py @@ -190,6 +190,8 @@ def main_i(): op.add_option('--canonicalize-escape-url', dest='url_print', action='callback', callback=store_local_curry('canon_esc')) tunables = [ norm(o.get_opt_string()[2:]) for o in op.option_list if o.callback in (store_abs, 'store_true', None) and o.get_opt_string() not in ('--version', '--help') ] + remote_tunables = [ 'listen', 'go_daemon', 'timeout', 'session_owner', 'config_file' ] + rq_remote_tunables = { 'listen': True } # precedence for sources of values: 1) commandline, 2) cfg file, 3) defaults # -- for this to work out we need to tell apart defaults from explicitly set @@ -206,6 +208,19 @@ def main_i(): sys.stderr.write(op.get_usage() + "\n") sys.exit(1) + if os.getenv('_GSYNCD_RESTRICTED_'): + allopts = {} + allopts.update(opts.__dict__) + allopts.update(rconf) + bannedtuns = set(allopts.keys()) - set(remote_tunables) + if bannedtuns: + raise GsyncdError('following tunables cannot be set with restricted SSH invocaton: ' + \ + ', '.join(bannedtuns)) + for k, v in rq_remote_tunables.items(): + if not k in allopts or allopts[k] != v: + raise GsyncdError('tunable %s is not set to value %s required for restricted SSH invocaton' % \ + (k, v)) + if getattr(confdata, 'rx', None): # peers are regexen, don't try to parse them canon_peers = args |