From 45fb79dd5d7871bc7a80821ad198bc70d43fdf2e Mon Sep 17 00:00:00 2001 From: oetiker Date: Sun, 16 Aug 2009 22:22:26 +0000 Subject: [PATCH] From: Florian Forster MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This patch introduces per-socket, per-command permissions to the RRD caching daemon, RRDCacheD. This allows administrators to limit access to the daemon in a fine-grained manner. The patch removes the command line option `-L' and introduces `-P' instead. The option must appear *before* the `-l' option and the given permissions are set for all following sockets or another `-P' option. For example: rrdcached -P FLUSH,PENDING -l 10.2.3.4 -P UPDATE -l unix:/some/path Allow `FLUSH' and `PENDING' commands to clients connected via IPv4 networking and `UPDATE' to clients connected via a UNIX domain socket only. There are a couple of exceptions: - The commands `HELP' and `QUIT' are always allowed. - If the command `BATCH' is allowed, the command `.' is automatically allowed, too. By default, i. e. if no `-P' option is specified, all commands will be allowed. As a shortcut to reset the behavior to the default behavior, you can use the slightly hackish `-P ""' syntax. Signed-off-by: Florian Forster git-svn-id: svn://svn.oetiker.ch/rrdtool/trunk/program@1892 a5681a0c-68f1-0310-ab6d-d61299d08faa --- doc/rrdcached.pod | 110 ++++++++++++++++++++------- src/rrd_daemon.c | 224 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 233 insertions(+), 101 deletions(-) diff --git a/doc/rrdcached.pod b/doc/rrdcached.pod index 5113c2d..460899c 100644 --- a/doc/rrdcached.pod +++ b/doc/rrdcached.pod @@ -7,7 +7,8 @@ rrdcached - Data caching daemon for rrdtool =head1 SYNOPSIS B -[B<-l/-L>EI
] +[B<-P>EI] +[B<-l>EI
] [B<-w>EI] [B<-z>EI] [B<-f>EI] @@ -48,7 +49,9 @@ For network sockets, a port may be specified by using the form CI
B<]:>I>. If the address is an IPv4 address or a fully qualified domain name (i.Ee. the address contains at least one dot (C<.>)), the square brackets can be omitted, resulting in the (simpler) -CB<:>I> pattern.. The default port is B<42217/udp>. +CB<:>I> pattern. The default port is B<42217/udp>. If you +specify a network socket, it is mandatory to read the +L section. The following formats are accepted. Please note that the address of the UNIX domain socket B start with a slash in the second case! @@ -62,10 +65,40 @@ domain socket B start with a slash in the second case! If the B<-l> option is not specified the default address, C, will be used. -=item B<-L> I
+=item B<-P> I[,I[,...]] -Same as B<-l>, except creates a low-privilege socket. See B for more information. +Specifies the commands accepted via a network socket. This allows +administrators of I to control the actions accepted from various +sources. + +The arguments given to the B<-P> option is a comma separated list of commands. +For example, to allow the C and C commands one could specify: + + rrdcached -P FLUSH,PENDING $MORE_ARGUMENTS + +The B<-P> option effects the I socket addresses (the following B<-l> +options). In the following example, only the IPv4 network socket (address +C<10.0.0.1>) will be restricted to the C and C commands: + + rrdcached -l unix:/some/path -P FLUSH,PENDING -l 10.0.0.1 + +A complete list of available commands can be found in the section +L below. There are two minor special exceptions: + +=over 4 + +=item * + +The C and C commands are always allowed. + +=item * + +If the C command is accepted, the B<.>Ecommand will automatically +be accepted, too. + +=back + +Please also read L below. =item B<-w> I @@ -305,45 +338,68 @@ ASCII art rocks. =head1 SECURITY CONSIDERATIONS -The client/server protocol does not have any authentication or -authorization mechanism. Therefore, take care to restrict which users can -connect to the daemon. +=head2 Authentication -Control sockets are divided into high-privilege (B<-l>) and low-privilege -(B<-L>) sockets. High-privilege sockets accept all commands, whereas -low-privilege sockets accept only B, B, and B. +There is no authentication. -For a multi-user environment where only certain users require read/write -access, the recommended configuration uses two sockets as follows: +The client/server protocol does not yet have any authentication mechanism. It +is likely that authentication and encryption will be added in a future version, +but for the time being it is the administrator's responsibility to secure the +traffic from/to the daemon! -=over +It is highly recommended to install a packet filter or similar mechanism to +prevent unauthorized connections. Unless you have a dedicated VLAN or VPN for +this, using network sockets is probably a bad idea! -=item B<-l> I +=head2 Authorization -Create a high-privilege unix-domain socket. This should be protected with -the same Unix permissions that are used to protect the RRD files. Updates -should be directed to this socket. +There is minimal per-socket authorization. -=item B<-L> I<127.0.0.1> +Authorization is currently done on a per-socket basis. That means each socket +has a list of commands it will accept and it will accept. It will accept only +those commands explicitly listed but it will (currently) accept these commands +from anyone reaching the socket. -Create a low-privilege TCP socket listening on localhost. All users on -the local system may use this to trigger FLUSH of individual files. Users -with read-only access should be directed to this socket. +If the networking sockets are to be used, it is necessary to restrict the +accepted commands to those needed by external clients. If, for example, +external clients want to draw graphs of the cached data, they should only be +allowed to use the C command. -=back +=head2 Encryption + +There is no encryption. + +Again, this may be added in the future, but for the time being it is your job +to keep your private data private. Install a VPN or an encrypted tunnel if you +statistics are confidential! -If you (want to) use the network capability, i.Ee. let the daemon bind to -an IPv4 or IPv6 socket, it is B job to install a packet filter or similar -mechanism to prevent unauthorized connections. Unless you have a dedicated VLAN -or VPN for this, using the network option is probably a bad idea! +=head2 Sanity checking + +There is no sanity checking. The daemon will blindly write to any file it gets told, so you really should create a separate user just for this daemon. Also it does not do any sanity checks, so if it gets told to write values for a time far in the future, your files will be messed up good! +=head2 Conclusion + +=over 4 + +=item * + +Security is the job of the administrator. + +=item * + +We recommend to allow write access via UNIX domain sockets only. + +=item * + You have been warned. +=back + =head1 PROTOCOL The daemon communicates with clients using a line based ASCII protocol which is diff --git a/src/rrd_daemon.c b/src/rrd_daemon.c index 199ebdb..b4bc1ee 100644 --- a/src/rrd_daemon.c +++ b/src/rrd_daemon.c @@ -1,7 +1,7 @@ /** * RRDTool - src/rrd_daemon.c - * Copyright (C) 2008 Florian octo Forster - * Copyright (C) 2008 Kevin Brintnall + * Copyright (C) 2008,2009 Florian octo Forster + * Copyright (C) 2008,2009 Kevin Brintnall * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -116,12 +116,6 @@ /* * Types */ -typedef enum -{ - PRIV_LOW, - PRIV_HIGH -} socket_privilege; - typedef enum { RESP_ERR = -1, RESP_OK = 0 } response_code; struct listen_socket_s @@ -129,7 +123,6 @@ struct listen_socket_s int fd; char addr[PATH_MAX + 1]; int family; - socket_privilege privilege; /* state for BATCH processing */ time_t batch_start; @@ -142,23 +135,25 @@ struct listen_socket_s char *wbuf; ssize_t wbuf_len; + + uint32_t permissions; }; typedef struct listen_socket_s listen_socket_t; -struct command; +struct command_s; +typedef struct command_s command_t; /* note: guard against "unused" warnings in the handlers */ #define DISPATCH_PROTO listen_socket_t *sock __attribute__((unused)),\ time_t now __attribute__((unused)),\ char *buffer __attribute__((unused)),\ size_t buffer_size __attribute__((unused)) -#define HANDLER_PROTO struct command *cmd __attribute__((unused)),\ +#define HANDLER_PROTO command_t *cmd __attribute__((unused)),\ DISPATCH_PROTO -struct command { +struct command_s { char *cmd; int (*handler)(HANDLER_PROTO); - socket_privilege min_priv; char context; /* where we expect to see it */ #define CMD_CONTEXT_CLIENT (1<<0) @@ -479,7 +474,7 @@ static char *next_cmd (listen_socket_t *sock, ssize_t *len) /* {{{ */ /* NOTREACHED */ assert(1==0); -} +} /* }}} char *next_cmd */ /* add the characters directly to the write buffer */ static int add_to_wbuf(listen_socket_t *sock, char *str, size_t len) /* {{{ */ @@ -1068,20 +1063,6 @@ static void get_abs_path(char **filename, char *tmp) *filename = tmp; } /* }}} static int get_abs_path */ -/* returns 1 if we have the required privilege level, - * otherwise issue an error to the user on sock */ -static int has_privilege (listen_socket_t *sock, /* {{{ */ - socket_privilege priv) -{ - if (sock == NULL) /* journal replay */ - return 1; - - if (sock->privilege >= priv) - return 1; - - return send_response(sock, RESP_ERR, "%s\n", rrd_strerror(EACCES)); -} /* }}} static int has_privilege */ - static int flush_file (const char *filename) /* {{{ */ { cache_item_t *ci; @@ -1110,7 +1091,7 @@ static int flush_file (const char *filename) /* {{{ */ return (0); } /* }}} int flush_file */ -static int syntax_error(listen_socket_t *sock, struct command *cmd) /* {{{ */ +static int syntax_error(listen_socket_t *sock, command_t *cmd) /* {{{ */ { char *err = "Syntax error.\n"; @@ -1513,11 +1494,10 @@ static int handle_request_quit (HANDLER_PROTO) /* {{{ */ return -1; } /* }}} static int handle_request_quit */ -struct command COMMANDS[] = { +static command_t list_of_commands[] = { /* {{{ */ { "UPDATE", handle_request_update, - PRIV_HIGH, CMD_CONTEXT_ANY, "UPDATE [ ...]\n" , @@ -1532,7 +1512,6 @@ struct command COMMANDS[] = { { "WROTE", handle_request_wrote, - PRIV_HIGH, CMD_CONTEXT_JOURNAL, NULL, NULL @@ -1540,7 +1519,6 @@ struct command COMMANDS[] = { { "FLUSH", handle_request_flush, - PRIV_LOW, CMD_CONTEXT_CLIENT | CMD_CONTEXT_BATCH, "FLUSH \n" , @@ -1550,7 +1528,6 @@ struct command COMMANDS[] = { { "FLUSHALL", handle_request_flushall, - PRIV_HIGH, CMD_CONTEXT_CLIENT, "FLUSHALL\n" , @@ -1559,7 +1536,6 @@ struct command COMMANDS[] = { { "PENDING", handle_request_pending, - PRIV_HIGH, CMD_CONTEXT_CLIENT, "PENDING \n" , @@ -1569,7 +1545,6 @@ struct command COMMANDS[] = { { "FORGET", handle_request_forget, - PRIV_HIGH, CMD_CONTEXT_ANY, "FORGET \n" , @@ -1579,7 +1554,6 @@ struct command COMMANDS[] = { { "QUEUE", handle_request_queue, - PRIV_LOW, CMD_CONTEXT_CLIENT, "QUEUE\n" , @@ -1592,7 +1566,6 @@ struct command COMMANDS[] = { { "STATS", handle_request_stats, - PRIV_LOW, CMD_CONTEXT_CLIENT, "STATS\n" , @@ -1602,7 +1575,6 @@ struct command COMMANDS[] = { { "HELP", handle_request_help, - PRIV_LOW, CMD_CONTEXT_CLIENT, "HELP []\n", NULL, /* special! */ @@ -1610,7 +1582,6 @@ struct command COMMANDS[] = { { "BATCH", batch_start, - PRIV_LOW, CMD_CONTEXT_CLIENT, "BATCH\n" , @@ -1634,7 +1605,6 @@ struct command COMMANDS[] = { { ".", /* BATCH terminator */ batch_done, - PRIV_LOW, CMD_CONTEXT_BATCH, NULL, NULL @@ -1642,34 +1612,79 @@ struct command COMMANDS[] = { { "QUIT", handle_request_quit, - PRIV_LOW, CMD_CONTEXT_CLIENT | CMD_CONTEXT_BATCH, "QUIT\n" , "Disconnect from rrdcached.\n" - }, - {NULL,NULL,0,0,NULL,NULL} /* LAST ENTRY */ -}; + } +}; /* }}} command_t list_of_commands[] */ +static size_t list_of_commands_len = sizeof (list_of_commands) + / sizeof (list_of_commands[0]); -static struct command *find_command(char *cmd) +static command_t *find_command(char *cmd) { - struct command *c = COMMANDS; - - while (c->cmd != NULL) - { - if (strcasecmp(cmd, c->cmd) == 0) - break; - c++; - } + size_t i; - if (c->cmd == NULL) - return NULL; - else - return c; + for (i = 0; i < list_of_commands_len; i++) + if (strcasecmp(cmd, list_of_commands[i].cmd) == 0) + return (&list_of_commands[i]); + return NULL; } +/* We currently use the index in the `list_of_commands' array as a bit position + * in `listen_socket_t.permissions'. This member schould NEVER be accessed from + * outside these functions so that switching to a more elegant storage method + * is easily possible. */ +static ssize_t find_command_index (const char *cmd) /* {{{ */ +{ + size_t i; + + for (i = 0; i < list_of_commands_len; i++) + if (strcasecmp(cmd, list_of_commands[i].cmd) == 0) + return ((ssize_t) i); + return (-1); +} /* }}} ssize_t find_command_index */ + +static int socket_permission_check (listen_socket_t *sock, /* {{{ */ + const char *cmd) +{ + ssize_t i; + + if (cmd == NULL) + return (-1); + + if ((strcasecmp ("QUIT", cmd) == 0) + || (strcasecmp ("HELP", cmd) == 0)) + return (1); + else if (strcmp (".", cmd) == 0) + cmd = "BATCH"; + + i = find_command_index (cmd); + if (i < 0) + return (-1); + assert (i < 32); + + if ((sock->permissions & (1 << i)) != 0) + return (1); + return (0); +} /* }}} int socket_permission_check */ + +static int socket_permission_add (listen_socket_t *sock, /* {{{ */ + const char *cmd) +{ + ssize_t i; + + i = find_command_index (cmd); + if (i < 0) + return (-1); + assert (i < 32); + + sock->permissions |= (1 << i); + return (0); +} /* }}} int socket_permission_add */ + /* check whether commands are received in the expected context */ -static int command_check_context(listen_socket_t *sock, struct command *cmd) +static int command_check_context(listen_socket_t *sock, command_t *cmd) { if (sock == NULL) return (cmd->context & CMD_CONTEXT_JOURNAL); @@ -1687,7 +1702,7 @@ static int handle_request_help (HANDLER_PROTO) /* {{{ */ int status; char *cmd_str; char *resp_txt; - struct command *help = NULL; + command_t *help = NULL; status = buffer_get_field (&buffer, &buffer_size, &cmd_str); if (status == 0) @@ -1708,14 +1723,15 @@ static int handle_request_help (HANDLER_PROTO) /* {{{ */ } else { - help = COMMANDS; + size_t i; + resp_txt = "Command overview\n"; - while (help->cmd) + for (i = 0; i < list_of_commands_len; i++) { - if (help->syntax) - add_response_info(sock, "%s", help->syntax); - help++; + if (list_of_commands[i].syntax == NULL) + continue; + add_response_info (sock, "%s", list_of_commands[i].syntax); } } @@ -1727,7 +1743,7 @@ static int handle_request (DISPATCH_PROTO) /* {{{ */ { char *buffer_ptr = buffer; char *cmd_str = NULL; - struct command *cmd = NULL; + command_t *cmd = NULL; int status; assert (buffer[buffer_size - 1] == '\0'); @@ -1746,9 +1762,8 @@ static int handle_request (DISPATCH_PROTO) /* {{{ */ if (!cmd) return send_response(sock, RESP_ERR, "Unknown command: %s\n", cmd_str); - status = has_privilege(sock, cmd->min_priv); - if (status <= 0) - return status; + if (!socket_permission_check (sock, cmd->cmd)) + return send_response(sock, RESP_ERR, "Permission denied.\n"); if (!command_check_context(sock, cmd)) return send_response(sock, RESP_ERR, "Can't use '%s' here.\n", cmd_str); @@ -2681,7 +2696,10 @@ static int read_options (int argc, char **argv) /* {{{ */ int option; int status = 0; - while ((option = getopt(argc, argv, "gl:L:f:w:z:t:Bb:p:Fj:h?")) != -1) + char **permissions = NULL; + size_t permissions_len = 0; + + while ((option = getopt(argc, argv, "gl:P:f:w:z:t:Bb:p:Fj:h?")) != -1) { switch (option) { @@ -2689,7 +2707,6 @@ static int read_options (int argc, char **argv) /* {{{ */ stay_foreground=1; break; - case 'L': case 'l': { listen_socket_t *new; @@ -2703,7 +2720,40 @@ static int read_options (int argc, char **argv) /* {{{ */ memset(new, 0, sizeof(listen_socket_t)); strncpy(new->addr, optarg, sizeof(new->addr)-1); - new->privilege = (option == 'l') ? PRIV_HIGH : PRIV_LOW; + + /* Add permissions to the socket {{{ */ + if (permissions_len != 0) + { + size_t i; + for (i = 0; i < permissions_len; i++) + { + status = socket_permission_add (new, permissions[i]); + if (status != 0) + { + fprintf (stderr, "read_options: Adding permission \"%s\" to " + "socket failed. Most likely, this permission doesn't " + "exist. Check your command line.\n", permissions[i]); + status = 4; + } + } + } + else /* if (permissions_len == 0) */ + { + /* Add permission for ALL commands to the socket. */ + size_t i; + for (i = 0; i < list_of_commands_len; i++) + { + status = socket_permission_add (new, list_of_commands[i].cmd); + if (status != 0) + { + fprintf (stderr, "read_options: Adding permission \"%s\" to " + "socket failed. This should never happen, ever! Sorry.\n", + permissions[i]); + status = 4; + } + } + } + /* }}} Done adding permissions. */ if (!rrd_add_ptr((void ***)&config_listen_address_list, &config_listen_address_list_len, new)) @@ -2714,6 +2764,28 @@ static int read_options (int argc, char **argv) /* {{{ */ } break; + case 'P': + { + char *optcopy; + char *saveptr; + char *dummy; + char *ptr; + + rrd_free_ptrs ((void *) &permissions, &permissions_len); + + optcopy = strdup (optarg); + dummy = optcopy; + saveptr = NULL; + while ((ptr = strtok_r (dummy, ", ", &saveptr)) != NULL) + { + dummy = NULL; + rrd_add_strdup ((void *) &permissions, &permissions_len, ptr); + } + + free (optcopy); + } + break; + case 'f': { int temp; @@ -2871,13 +2943,15 @@ static int read_options (int argc, char **argv) /* {{{ */ case 'h': case '?': - printf ("RRDCacheD %s Copyright (C) 2008 Florian octo Forster\n" + printf ("RRDCacheD %s\n" + "Copyright (C) 2008,2009 Florian octo Forster and Kevin Brintnall\n" "\n" "Usage: rrdcached [options]\n" "\n" "Valid options are:\n" " -l
Socket address to listen to.\n" - " -L
Socket address to listen to ('FLUSH' only).\n" + " -P Sets the permissions to assign to all following " + "sockets\n" " -w Interval in which to write data.\n" " -z Delay writes up to seconds to spread load\n" " -t Number of write threads.\n" @@ -2913,6 +2987,8 @@ static int read_options (int argc, char **argv) /* {{{ */ if (journal_dir == NULL) config_flush_at_shutdown = 1; + rrd_free_ptrs ((void *) &permissions, &permissions_len); + return (status); } /* }}} int read_options */ -- 2.11.0