diff --git a/README.md b/README.md index 3ad4a73..f0bfd6d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ are made. -L text|file Set or load server location for caps.txt -A admin Set admin email for caps.txt + -U paths Specify a colon-separated list of extra unveil(2) paths + (OpenBSD only). + -nv Disable virtual hosting -nl Disable parent directory links -nh Disable menu header (title) @@ -108,6 +111,32 @@ The `-nx` option prevents execution of any script or external file, and the `-nu` option suppresses scanning for and serving of `~user` directories (which are normally at `~/public_html/` for each user). +### OpenBSD-specific Security + +If you are running Gophernicus on OpenBSD, you may (depending on what features +you want to use) be able to take advantage of unveil(2) and pledge(2). + +If you run without executable map support (i.e. you run with `-nx`) then +unveil(2) will be enabled and the server root will automatically be unveiled. +If run with personal gopherspaces enabled (i.e. you run without `-nu`), then +the password database (`/etc/pwd.db`) will automatically be unveiled, but you +will have to manually unveil the filesystem path(s) from which to serve +personal gopherspaces (see `-U`). + +Running with `-nm -nu -nx` results in the strictest set of pledge(2) promises. +If you have executable maps enabled (i.e. you run without `-nx`), then the +promises are relaxed to allow `exec`. If you have personal gopherspaces enabled +(i.e. you run without `-nu`), then the promises are relaxed to allow `getpw`. +If you have shared memory enabled (i.e. you run without `-nm`), then pledge(2) +support cannot be used at all. + +In short, you probably want to run Gophernicus with `-nm -nu -nx` and then +remove the flags that would otherwise disable the features you want. + +To see what is going on with regards to pledge(2) and unveil(2), run +Gophernicus with `-d` (to turn on debug logging) and look in your system logs. + + ## Gophermaps By default all gopher menus are automatically generated from the diff --git a/gophernicus.c b/gophernicus.c index d4e905b..460704d 100644 --- a/gophernicus.c +++ b/gophernicus.c @@ -448,6 +448,11 @@ void init_state(state *st) strclear(st->server_platform); strclear(st->server_admin); +#ifdef __OpenBSD__ + st->extra_unveil_paths = NULL; +#endif + + /* Session */ st->session_timeout = DEFAULT_SESSION_TIMEOUT; st->session_max_kbytes = DEFAULT_SESSION_MAX_KBYTES; @@ -498,13 +503,17 @@ int main(int argc, char *argv[]) #ifdef HAVE_SHMEM struct shmid_ds shm_ds; shm_state *shm; - int shmid; + int shmid = -1; #endif #ifdef ENABLE_HAPROXY1 char remote[BUFSIZE]; char local[BUFSIZE]; int dummy; #endif +#ifdef __OpenBSD__ + char pledges[256]; + char *extra_unveil; +#endif /* Get the name of this binary */ if ((c = strrchr(argv[0], '/'))) sstrlcpy(self, c + 1); @@ -523,6 +532,87 @@ int main(int argc, char *argv[]) /* Open syslog() */ if (st.opt_syslog) openlog(self, LOG_PID, LOG_DAEMON); +#ifdef __OpenBSD__ + /* unveil(2) support. + * + * We only enable unveil(2) if the user isn't expecting to shell-out to + * arbitrary commands. + */ + if (st.opt_exec) { + if (st.extra_unveil_paths != NULL) { + die(&st, NULL, "-U and executable maps cannot co-exist"); + } + if (st.debug) + syslog(LOG_INFO, "executable gophermaps are enabled, no unveil(2)"); + } else { + if (unveil(st.server_root, "r") == -1) + die(&st, NULL, "unveil"); + + /* + * If we want personal gopherspaces, then we have to unveil(2) the user + * database. This isn't actually needed if pledge(2) is enabled, as the + * 'getpw' promise will ensure access to this file, but it doesn't hurt + * to unveil it anyway. + */ + if (st.opt_personal_spaces) { + if (st.debug) + syslog(LOG_INFO, "unveiling /etc/pwd.db"); + if (unveil("/etc/pwd.db", "r") == -1) + die(&st, NULL, "unveil"); + } + + /* Any extra unveil paths that the user has specified */ + char *p = st.extra_unveil_paths; + while (p != NULL) { + extra_unveil = strsep(&p, ":"); + if (*extra_unveil == '\0') + continue; /* empty path */ + + if (st.debug) + syslog(LOG_INFO, "unveiling extra path: %s\n", extra_unveil); + if (unveil(extra_unveil, "r") == -1) + die(&st, NULL, "unveil"); + } + + if (unveil(NULL, NULL) == -1) + die(&st, NULL, "unveil"); + } + + /* pledge(2) support */ + if (st.opt_shm) { + /* pledge(2) never allows shared memory */ + if (st.debug) + syslog(LOG_INFO, "shared-memory enabled, can't pledge(2)"); + } else { + strlcpy(pledges, + "stdio rpath inet sendfd recvfd proc", + sizeof(pledges)); + + /* Executable maps shell-out using popen(3) */ + if (st.opt_exec) { + strlcat(pledges, " exec", sizeof(pledges)); + if (st.debug) { + syslog(LOG_INFO, + "executable gophermaps enabled, " + "adding 'exec' to pledge(2)"); + } + } + + /* Personal spaces require getpwnam(3) and getpwent(3) */ + if (st.opt_personal_spaces) { + strlcat(pledges, " getpw", sizeof(pledges)); + if (st.debug) { + syslog(LOG_INFO, + "personal gopherspaces enabled, " + "adding 'getpw' to pledge(2)"); + } + } + + if (pledge(pledges, NULL) == -1) + die(&st, NULL, "pledge"); + } +#endif + /* Check if TCP wrappers have something to say about this connection */ #ifdef HAVE_LIBWRAP if (sstrncmp(st.req_remote_addr, UNKNOWN_ADDR) != MATCH && @@ -544,30 +634,31 @@ int main(int argc, char *argv[]) /* Try to get shared memory */ #ifdef HAVE_SHMEM - if ((shmid = shmget(SHM_KEY, sizeof(shm_state), IPC_CREAT | SHM_MODE)) == ERROR) { + if (st.opt_shm) { + if ((shmid = shmget(SHM_KEY, sizeof(shm_state), IPC_CREAT | SHM_MODE)) == ERROR) { - /* Getting memory failed -> delete the old allocation */ - shmctl(shmid, IPC_RMID, &shm_ds); + /* Getting memory failed -> delete the old allocation */ + shmctl(shmid, IPC_RMID, &shm_ds); + shm = NULL; + } + else { + /* Map shared memory */ + if ((shm = (shm_state *) shmat(shmid, (void *) 0, 0)) == (void *) ERROR) + shm = NULL; + + /* Initialize mapped shared memory */ + if (shm && shm->start_time == 0) { + shm->start_time = time(NULL); + + /* Keep server platform & description in shm */ + platform(&st); + sstrlcpy(shm->server_platform, st.server_platform); + sstrlcpy(shm->server_description, st.server_description); + } + } + } else { shm = NULL; } - else { - /* Map shared memory */ - if ((shm = (shm_state *) shmat(shmid, (void *) 0, 0)) == (void *) ERROR) - shm = NULL; - - /* Initialize mapped shared memory */ - if (shm && shm->start_time == 0) { - shm->start_time = time(NULL); - - /* Keep server platform & description in shm */ - platform(&st); - sstrlcpy(shm->server_platform, st.server_platform); - sstrlcpy(shm->server_description, st.server_description); - } - } - - /* For debugging shared memory issues */ - if (!st.opt_shm) shm = NULL; /* Get server platform and description */ if (shm) { diff --git a/gophernicus.h b/gophernicus.h index aa08f98..f721383 100644 --- a/gophernicus.h +++ b/gophernicus.h @@ -342,6 +342,10 @@ typedef struct { srewrite rewrite[MAX_REWRITE]; int rewrite_count; +#ifdef __OpenBSD__ + char *extra_unveil_paths; +#endif + /* Session */ int session_timeout; int session_max_kbytes; diff --git a/options.c b/options.c index cd099e1..c107de7 100644 --- a/options.c +++ b/options.c @@ -101,7 +101,11 @@ void parse_args(state *st, int argc, char *argv[]) int opt; /* Parse args */ - while ((opt = getopt(argc, argv, "h:p:T:r:t:g:a:c:u:m:l:w:o:s:i:k:f:e:R:D:L:A:P:n:dbv?-")) != ERROR) { + while ((opt = getopt(argc, argv, +#ifdef __OpenBSD__ + "U:" /* extra unveil(2) paths are OpenBSD only */ +#endif + "h:p:T:r:t:g:a:c:u:m:l:w:o:s:i:k:f:e:R:D:L:A:P:n:dbv?-")) != ERROR) { switch(opt) { case 'h': sstrlcpy(st->server_host, optarg); break; case 'p': st->server_port = atoi(optarg); break; @@ -133,7 +137,9 @@ void parse_args(state *st, int argc, char *argv[]) case 'D': sstrlcpy(st->server_description, optarg); break; case 'L': sstrlcpy(st->server_location, optarg); break; case 'A': sstrlcpy(st->server_admin, optarg); break; - +#ifdef __OpenBSD__ + case 'U': st->extra_unveil_paths = optarg; break; +#endif case 'n': if (*optarg == 'v') { st->opt_vhost = FALSE; break; } if (*optarg == 'l') { st->opt_parent = FALSE; break; }