Commit Diff


commit - cd1db57e49723e38c55e6b86cc8123fecd389ee7
commit + b148e63d1f09f60fc25d5a6e2c76e126c9827c2d
blob - 5195f23ace2cd3b0f5e41c202214651aece284a8
blob + 23faa509f27428d3149445b6a2102fb502e52ceb
--- gitwrapper/gitwrapper.c
+++ gitwrapper/gitwrapper.c
@@ -27,6 +27,8 @@
 #include <err.h>
 #include <errno.h>
 #include <event.h>
+#include <fcntl.h>
+#include <imsg.h>
 #include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -104,6 +106,7 @@ main(int argc, char *argv[])
 {
 	const struct got_error *error;
 	const char *confpath = NULL;
+	int conf_fd = -1;
 	char *command = NULL, *repo_name = NULL; /* for matching gotd.conf */
 	char *myserver = NULL;
 	const char *repo_path = NULL; /* as passed on the command line */
@@ -133,12 +136,13 @@ main(int argc, char *argv[])
 	 * checks whether repository paths exist on disk.
 	 * Parsing errors and warnings will be logged to stderr.
 	 * Upon failure we will run Git's native tooling so do not
-	 * bother checking for errors here.
+	 * bother checking for errors here, not even from open(2).
 	 */
 	confpath = getenv("GOTD_CONF_PATH");
 	if (confpath == NULL)
 		confpath = GOTD_CONF_PATH;
-	gotd_parse_config(confpath, GOTD_PROC_GITWRAPPER, NULL, &gotd);
+	conf_fd = open(confpath, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+	gotd_parse_config(confpath, conf_fd, GOTD_PROC_GITWRAPPER, NULL, &gotd);
 
 	error = apply_unveil(myserver);
 	if (error)
@@ -209,6 +213,8 @@ done:
 	free(repo_name);
 	free(myserver);
 	free(gitcommand);
+	if (conf_fd != -1 && close(conf_fd) && error == NULL)
+		error = got_error_from_errno("close");
 	if (error) {
 		fprintf(stderr, "%s: %s\n", getprogname(), error->msg);
 		return 1;
blob - 07c32a0a3163a6d2e93ca633c2b16134f6043ad9
blob + 8a340c0208ad6cab755ff5fdae8113732a85ab6b
--- gotctl/gotctl.8
+++ gotctl/gotctl.8
@@ -64,6 +64,35 @@ Stop a running
 .Xr gotd 8
 instance.
 This operation requires root privileges.
+.It Cm reload Oo Fl c Ar config-file Oc Oo Fl n Oc Oo Fl s Ar secrets Oc
+Reload a running
+.Xr gotd 8
+instance.
+.Xr gotd 8
+will relaunch with an updated configuration read from the provided
+configuration files.
+The previous instance of
+.Xr gotd 8
+will continue to serve existing client connections and then exit.
+.Pp
+This operation requires root privileges.
+.Pp
+The options for
+.Cm gotctl reload
+are as follows:
+.Bl -tag -width Ds
+.It Fl f Ar config-file
+Set the path to the configuration file.
+If not specified, the file
+.Pa /etc/gotd.conf
+will be used.
+.It Fl n
+Only check the configuration files for validity.
+.It Fl s Ar secrets
+Set the path to the secrets file.
+If not specified, the file
+.Pa /etc/gotd-secrets.conf
+will be used if it exists.
 .El
 .Sh SEE ALSO
 .Xr got 1 ,
blob - de46b28e1688e0f673242c0fdd78785c32388f95
blob + 4d80fc06bb5915fe9ecf740992bb312660cc36ba
--- gotctl/gotctl.c
+++ gotctl/gotctl.c
@@ -19,15 +19,19 @@
 #include <sys/queue.h>
 #include <sys/socket.h>
 #include <sys/un.h>
+#include <sys/stat.h>
 
 #include <err.h>
+#include <errno.h>
 #include <event.h>
+#include <fcntl.h>
 #include <imsg.h>
 #include <limits.h>
 #include <locale.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <syslog.h>
 #include <getopt.h>
 #include <unistd.h>
 
@@ -39,6 +43,8 @@
 #include "got_lib_gitproto.h"
 
 #include "gotd.h"
+#include "secrets.h"
+#include "log.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -57,13 +63,16 @@ __dead static void	usage(int, int);
 
 __dead static void	usage_info(void);
 __dead static void	usage_stop(void);
+__dead static void	usage_reload(void);
 
 static const struct got_error*		cmd_info(int, char *[], int);
 static const struct got_error*		cmd_stop(int, char *[], int);
+static const struct got_error*		cmd_reload(int, char *[], int);
 
 static const struct gotctl_cmd gotctl_commands[] = {
 	{ "info",	cmd_info,	usage_info },
 	{ "stop",	cmd_stop,	usage_stop },
+	{ "reload",	cmd_reload,	usage_reload },
 };
 
 __dead static void
@@ -164,6 +173,12 @@ cmd_info(int argc, char *argv[], int gotd_sock)
 	struct imsgbuf ibuf;
 	struct imsg imsg;
 
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1)
+		return got_error_from_errno("pledge");
+#endif
 	if (imsgbuf_init(&ibuf, gotd_sock) == -1)
 		return got_error_from_errno("imsgbuf_init");
 
@@ -220,15 +235,262 @@ cmd_stop(int argc, char *argv[], int gotd_sock)
 	struct imsgbuf ibuf;
 	struct imsg imsg;
 
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1)
+		return got_error_from_errno("pledge");
+#endif
 	if (imsgbuf_init(&ibuf, gotd_sock) == -1)
 		return got_error_from_errno("imsgbuf_init");
 
 	if (imsg_compose(&ibuf, GOTD_IMSG_STOP, 0, 0, -1, NULL, 0) == -1) {
 		imsgbuf_clear(&ibuf);
 		return got_error_from_errno("imsg_compose STOP");
+	}
+
+	err = gotd_imsg_flush(&ibuf);
+	while (err == NULL) {
+		err = gotd_imsg_poll_recv(&imsg, &ibuf, 0);
+		if (err) {
+			if (err->code == GOT_ERR_EOF)
+				err = NULL;
+			break;
+		}
+
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_ERROR:
+			err = gotd_imsg_recv_error(NULL, &imsg);
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	imsgbuf_clear(&ibuf);
+	return err;
+}
+
+__dead static void
+usage_reload(void)
+{
+	fprintf(stderr, "usage: %s reload [-c config-file] [-s secrets]\n",
+	    getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+check_file_secrecy(int fd, const char *fname)
+{
+	struct stat st;
+
+	if (fstat(fd, &st))
+		return got_error_from_errno2("stat", fname);
+
+	if (st.st_uid != 0) {
+		return got_error_fmt(GOT_ERR_UID,
+		    "secrets file %s must be owned by root", fname);
+	}
+
+	if (st.st_gid != 0) {
+		return got_error_fmt(GOT_ERR_GID,
+		    "secrets file %s must be owned by group wheel/root",
+		    fname);
+	}
+
+	if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) {
+		return got_error_fmt(GOT_ERR_GID,
+		    "secrets file %s must not be group writable or world "
+		    "readable/writable", fname);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+cmd_reload(int argc, char *argv[], int gotd_sock)
+{
+	const struct got_error *err = NULL;
+	struct imsgbuf ibuf;
+	struct gotd gotd;
+	struct gotd_secrets *secrets = NULL;
+	struct imsg imsg;
+	char *confpath = NULL, *secretspath = NULL;
+	int ch, conf_fd = -1, secrets_fd = -1;
+	int no_action = 0;
+
+	log_init(1, LOG_DAEMON); /* log to stderr . */
+
+#ifndef PROFILE
+	if (pledge("stdio rpath sendfd unveil", NULL) == -1)
+		return got_error_from_errno("pledge");
+#endif
+	while ((ch = getopt(argc, argv, "c:ns:")) != -1) {
+		switch (ch) {
+		case 'c':
+			if (unveil(optarg, "r") != 0)
+				return got_error_from_errno("unveil");
+			confpath = realpath(optarg, NULL);
+			if (confpath == NULL) {
+				return got_error_from_errno2("realpath",
+				    optarg);
+			}
+			break;
+		case 'n':
+			no_action = 1;
+			break;
+		case 's':
+			if (unveil(optarg, "r") != 0)
+				return got_error_from_errno("unveil");
+			secretspath = realpath(optarg, NULL);
+			if (secretspath == NULL) {
+				return got_error_from_errno2("realpath",
+				    optarg);
+			}
+			break;
+		default:
+			usage_reload();
+			/* NOTREACHED */
+		}
+	}
+
+	if (confpath == NULL) {
+		confpath = strdup(GOTD_CONF_PATH);
+		if (confpath == NULL)
+			return got_error_from_errno("strdup");
+	}
+
+	if (unveil(confpath, "r") != 0)
+		return got_error_from_errno("unveil");
+
+	if (unveil(secretspath ? secretspath : GOTD_SECRETS_PATH, "r") != 0)
+		return got_error_from_errno("unveil");
+
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+
+	secrets_fd = open(secretspath ? secretspath : GOTD_SECRETS_PATH,
+	    O_RDONLY | O_NOFOLLOW);
+	if (secrets_fd == -1) {
+		if (secretspath != NULL || errno != ENOENT) {
+			return got_error_from_errno2("open",
+			    secretspath ? secretspath : GOTD_SECRETS_PATH);
+		}
+	} else if (secretspath == NULL) {
+		secretspath = strdup(GOTD_SECRETS_PATH);
+		if (secretspath == NULL)
+			return got_error_from_errno("strdup");
 	}
 
+	conf_fd = open(confpath, O_RDONLY | O_NOFOLLOW);
+	if (conf_fd == -1)
+		return got_error_from_errno2("open", confpath);
+
+	if (secrets_fd != -1) {
+		int fd;
+		FILE *fp;
+
+		err = check_file_secrecy(secrets_fd, secretspath);
+		if (err)
+			goto done;
+
+		fd = dup(secrets_fd);
+		if (fd == -1) {
+			err = got_error_from_errno("dup");
+			goto done;
+		}
+
+		fp = fdopen(fd, "r");
+		if (fp == NULL) {
+			err = got_error_from_errno2("fdopen", secretspath);
+			close(fd);
+			goto done;
+		}
+		err = gotd_secrets_parse(secretspath, fp, &secrets);
+		fclose(fp);
+		if (err) {
+			err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "failed to parse secrets file %s: %s",
+			    secretspath, err->msg);
+			goto done;
+		}
+	}
+
+	if (gotd_parse_config(confpath, conf_fd, GOTD_PROC_GOTCTL,
+	    secrets, &gotd) != 0) {
+		/* Errors were already printed. Silence this one. */
+		err = got_error_msg(GOT_ERR_PARSE_CONFIG, "");
+		goto done;
+	}
+
+	if (no_action) {
+		fprintf(stderr, "configuration OK\n");
+		goto done;
+	}
+
+#ifndef PROFILE
+	if (pledge("stdio sendfd", NULL) == -1) {
+		err = got_error_from_errno("pledge");
+		goto done;
+	}
+#endif
+	if (secrets_fd != -1 && lseek(secrets_fd, 0L, SEEK_SET) == -1) {
+		err = got_error_from_errno2("lseek", secretspath);
+		goto done;
+	}
+	if (lseek(conf_fd, 0L, SEEK_SET) == -1) {
+		err = got_error_from_errno2("lseek", confpath);
+		goto done;
+	}
+
+	if (imsgbuf_init(&ibuf, gotd_sock) == -1) {
+		err = got_error_from_errno("imsgbuf_init");
+		goto done;
+	}
+	imsgbuf_allow_fdpass(&ibuf);
+
+	if (secrets_fd != -1) {
+		if (imsg_compose(&ibuf, GOTD_IMSG_RELOAD_SECRETS, 0, 0,
+		    secrets_fd, secretspath ? secretspath : GOTD_SECRETS_PATH,
+		    secretspath ?
+		    strlen(secretspath) : strlen(GOTD_SECRETS_PATH)) == -1) {
+			err = got_error_from_errno("imsg_compose "
+			    "RELOAD_SECRETS");
+			imsgbuf_clear(&ibuf);
+			goto done;
+		}
+		secrets_fd = -1;
+	} else {
+		if (imsg_compose(&ibuf, GOTD_IMSG_RELOAD_SECRETS, 0, 0, -1,
+		    NULL, 0) == -1) {
+			err = got_error_from_errno("imsg_compose "
+			    "RELOAD_SECRETS");
+			imsgbuf_clear(&ibuf);
+			goto done;
+		}
+	}
+
+	if (imsg_compose(&ibuf, GOTD_IMSG_RELOAD, 0, 0, conf_fd,
+	    confpath, strlen(confpath)) == -1) {
+		err = got_error_from_errno("imsg_compose RELOAD");
+		imsgbuf_clear(&ibuf);
+		goto done;
+
+	}
+	conf_fd = -1;
+
 	err = gotd_imsg_flush(&ibuf);
+	if (err)
+		goto done;
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1) {
+		err = got_error_from_errno("pledge");
+		goto done;
+	}
+#endif
 	while (err == NULL) {
 		err = gotd_imsg_poll_recv(&imsg, &ibuf, 0);
 		if (err) {
@@ -250,6 +512,13 @@ cmd_stop(int argc, char *argv[], int gotd_sock)
 	}
 
 	imsgbuf_clear(&ibuf);
+done:
+	free(confpath);
+	free(secretspath);
+	if (conf_fd != -1 && close(conf_fd) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (secrets_fd != -1 && close(secrets_fd) == -1 && err == NULL)
+		err = got_error_from_errno("close");
 	return err;
 }
 
@@ -278,37 +547,15 @@ usage(int hflag, int status)
 	exit(status);
 }
 
-static const struct got_error *
-apply_unveil(const char *unix_socket_path)
-{
-#ifdef PROFILE
-	if (unveil("gmon.out", "rwc") != 0)
-		return got_error_from_errno2("unveil", "gmon.out");
-#endif
-	if (unveil(unix_socket_path, "w") != 0)
-		return got_error_from_errno2("unveil", unix_socket_path);
-
-	if (unveil(NULL, NULL) != 0)
-		return got_error_from_errno("unveil");
-
-	return NULL;
-}
-
 static int
 connect_gotd(const char *socket_path)
 {
-	const struct got_error *error = NULL;
 	int gotd_sock = -1;
 	struct sockaddr_un sun;
 
-	error = apply_unveil(socket_path);
-	if (error)
-		errx(1, "%s", error->msg);
+	if (unveil(socket_path, "w") != 0)
+		err(1, "unveil %s", socket_path);
 
-#ifndef PROFILE
-	if (pledge("stdio unix", NULL) == -1)
-		err(1, "pledge");
-#endif
 	if ((gotd_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
 		err(1, "socket");
 
@@ -321,10 +568,9 @@ connect_gotd(const char *socket_path)
 		err(1, "connect: %s", socket_path);
 
 #ifndef PROFILE
-	if (pledge("stdio", NULL) == -1)
+	if (pledge("stdio rpath sendfd unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
-
 	return gotd_sock;
 }
 
@@ -344,10 +590,9 @@ main(int argc, char *argv[])
 	setlocale(LC_CTYPE, "");
 
 #ifndef PROFILE
-	if (pledge("stdio unix unveil", NULL) == -1)
+	if (pledge("stdio rpath unix sendfd unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
-
 	while ((ch = getopt_long(argc, argv, "+hf:V", longopts, NULL)) != -1) {
 		switch (ch) {
 		case 'h':
@@ -388,13 +633,16 @@ main(int argc, char *argv[])
 
 		if (hflag)
 			cmd->cmd_usage();
-
+#ifdef PROFILE
+		if (unveil("gmon.out", "rwc") != 0)
+			err(1, "unveil", "gmon.out");
+#endif
 		gotd_sock = connect_gotd(socket_path);
 		if (gotd_sock == -1)
 			return 1;
 		error = cmd->cmd_main(argc, argv, gotd_sock);
 		close(gotd_sock);
-		if (error) {
+		if (error && error->msg[0] != '\0') {
 			fprintf(stderr, "%s: %s\n", getprogname(), error->msg);
 			return 1;
 		}
blob - e202efef1d899a13eed237db063dc45818bd74bc
blob + 77da458596ecb00f013cf151b97639966bcbe0da
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -118,6 +118,12 @@ volatile int client_cnt;
 static struct timeval auth_timeout = { 5, 0 };
 static struct gotd gotd;
 static int gotd_socket = -1;
+static int gotd_reload_conf_fd = -1;
+static int gotd_reload_secrets_fd = -1;
+static int have_reload_secrets;
+static char *gotd_reload_secrets_path;
+static int listener_halted;
+static uint32_t reload_client_id;
 
 void gotd_sighdlr(int sig, short event, void *arg);
 static void gotd_shutdown(void);
@@ -129,7 +135,9 @@ static const struct got_error *start_auth_child(struct
     struct gotd_repo *, char *, const char *, int, int);
 static void kill_proc(struct gotd_child_proc *, int);
 static void disconnect(struct gotd_client *);
-static void drop_privs(struct passwd *);
+static pid_t start_child(enum gotd_procid, const char *, char *,
+    const char *, const char *, int, int, int);
+static void kill_proc_timeout(int, short, void *);
 
 __dead static void
 usage(void)
@@ -427,6 +435,9 @@ disconnect(struct gotd_client *client)
 	free(client->username);
 	free(client);
 	client_cnt--;
+
+	if (listener_halted && client_cnt == 0)
+		event_loopexit(NULL);
 }
 
 static void
@@ -564,9 +575,229 @@ stop_gotd(struct gotd_client *client)
 	gotd_shutdown();
 	/* NOTREACHED */
 	return NULL;
+}
+
+static const struct got_error *
+send_reload_config(struct gotd_imsgev *iev)
+{
+	const struct got_error *err = NULL;
+	int fd;
+
+	fd = dup(gotd_socket);
+	if (fd == -1) {
+		err = got_error_from_errno("dup");
+		goto done;
+	}
+
+	if (imsg_compose(&iev->ibuf, GOTD_IMSG_LISTEN_SOCKET,
+	    GOTD_PROC_GOTD, gotd.pid, fd, NULL, 0) == -1) {
+		close(fd);
+		err = got_error_from_errno("imsg compose LISTEN_SOCKET");
+		goto done;
+	}
+
+	if (imsg_compose(&iev->ibuf, GOTD_IMSG_RELOAD_SECRETS,
+	    GOTD_PROC_GOTD, gotd.pid, gotd_reload_secrets_fd, NULL, 0) == -1) {
+		err = got_error_from_errno("imsg compose RELOAD_SECRETS");
+		goto done;
+	}
+
+	if (imsg_compose(&iev->ibuf, GOTD_IMSG_GOTD_CONF,
+	    GOTD_PROC_GOTD, gotd.pid, gotd_reload_conf_fd, NULL, 0) == -1) {
+		err = got_error_from_errno("imsg compose GOTD_CONF");
+		goto done;
+	}
+
+	err = gotd_imsg_flush(&iev->ibuf);
+	if (err)
+		return err;
+done:
+	if (gotd_reload_conf_fd != -1) {
+		close(gotd_reload_conf_fd);
+		gotd_reload_conf_fd = -1;
+	}
+
+	if (gotd_reload_conf_fd != -1) {
+		close(gotd_reload_secrets_fd);
+		gotd_reload_secrets_fd = -1;
+	}
+	have_reload_secrets = 0;
+
+	return err;
+}
+
+static const struct got_error *
+halt_listener(struct gotd_imsgev *iev)
+{
+	if (gotd_imsg_compose_event(&gotd.listen_proc->iev,
+	    GOTD_IMSG_HALT, GOTD_PROC_GOTD, -1,
+	    &reload_client_id, sizeof(reload_client_id)) == -1)
+		return got_error_from_errno("imsg compose HALT");
+
+	listener_halted = 1;
+	return NULL;
+}
+
+static void
+gotd_dispatch_reload(int fd, short event, void *arg)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct gotd_child_proc *proc = gotd.reload_proc;
+	ssize_t n;
+	int shut = 0;
+	struct imsg imsg;
+
+	if (proc->iev.ibuf.fd != fd)
+		fatalx("%s: unexpected fd %d", __func__, fd);
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1)
+			fatal("imsgbuf_read error");
+		if (n == 0) {
+			/* Connection closed. */
+			shut = 1;
+			goto done;
+		}
+	}
+
+	if (event & EV_WRITE) {
+		err = gotd_imsg_flush(ibuf);
+		if (err)
+			fatalx("%s", err->msg);
+	}
+
+	for (;;) {
+		const struct got_error *err = NULL;
+
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("%s: imsg_get error", __func__);
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_ERROR:
+			err = gotd_imsg_recv_error(NULL, &imsg);
+			break;
+		case GOTD_IMSG_RELOAD_READY:
+			err = send_reload_config(iev);
+			if (err)
+				break;
+			err = halt_listener(iev);
+			if (err)
+				break;
+			shut = 1;
+			break;
+		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+
+		if (err) {
+			log_warnx("reloading failed: %s", err->msg);
+			kill_proc(gotd.reload_proc, 0);
+			gotd.reload_proc = NULL;
+			imsg_free(&imsg);
+			return;
+		}
+
+		imsg_free(&imsg);
+	}
+done:
+	if (!shut) {
+		gotd_imsg_event_add(iev);
+	} else {
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		if (listener_halted && client_cnt == 0)
+			event_loopexit(NULL);
+	}
 }
 
 static const struct got_error *
+reload_gotd(struct gotd_client *client, struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	size_t datalen;
+	char *confpath = NULL;
+	struct gotd_child_proc *proc = NULL;
+
+	if (client->euid != 0)
+		return got_error_set_errno(EPERM, "reload");
+
+	if (gotd.reload_proc != NULL || gotd_reload_conf_fd != -1)
+		return got_error_set_errno(EALREADY, "reload");
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen == 0)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	confpath = strndup(imsg->data, datalen);
+	if (confpath == NULL)
+		return got_error_from_errno("strndup");
+	
+	gotd_reload_conf_fd = imsg_get_fd(imsg);
+	if (gotd_reload_conf_fd == -1) {
+		err = got_error(GOT_ERR_PRIVSEP_NO_FD);
+		goto done;
+	}
+
+	/* TODO: parse provided config for verification */
+
+	proc = calloc(1, sizeof(*proc));
+	if (proc == NULL) {
+		err = got_error_from_errno("calloc");
+		goto done;
+	}
+
+	proc->type = GOTD_PROC_RELOAD;
+
+	if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+	    PF_UNSPEC, proc->pipe) == -1) {
+		err = got_error_from_errno("socketpair");
+		free(proc);
+		proc = NULL;
+		goto done;
+	}
+
+	if (imsgbuf_init(&proc->iev.ibuf, proc->pipe[0]) == -1) {
+		err = got_error_from_errno("imsgbuf_init");
+		goto done;
+	}
+
+	proc->pid = start_child(GOTD_PROC_RELOAD, NULL, gotd.argv0,
+	    confpath, gotd_reload_secrets_path, proc->pipe[1],
+	    gotd.daemonize, gotd.verbosity);
+	imsgbuf_allow_fdpass(&proc->iev.ibuf);
+	proc->iev.handler = gotd_dispatch_reload;
+	proc->iev.events = EV_READ;
+	proc->iev.handler_arg = NULL;
+	event_set(&proc->iev.ev, proc->iev.ibuf.fd, EV_READ,
+	    gotd_dispatch_reload, &proc->iev);
+	gotd_imsg_event_add(&proc->iev);
+	evtimer_set(&proc->tmo, kill_proc_timeout, proc);
+
+	TAILQ_INSERT_HEAD(&procs, proc, entry);
+
+	gotd.reload_proc = proc;
+
+	log_info("gotd is reloading with PID %d", proc->pid);
+
+	reload_client_id = client->id;
+done:
+	if (err) {
+		if (proc) {
+			close(proc->pipe[0]);
+			close(proc->pipe[1]);
+			free(proc);
+		}
+	}
+	free(confpath);
+	return err;
+}
+
+static const struct got_error *
 start_client_authentication(struct gotd_client *client, struct imsg *imsg)
 {
 	const struct got_error *err;
@@ -629,6 +860,38 @@ start_client_authentication(struct gotd_client *client
 	return NULL;
 }
 
+static const struct got_error *
+recv_reload_secrets(struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	size_t datalen;
+
+	gotd_reload_secrets_fd = imsg_get_fd(imsg);
+	if (gotd_reload_secrets_fd == -1)
+		return NULL; /* no secrets being used */
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen == 0) {
+		err = got_error(GOT_ERR_PRIVSEP_LEN);
+		goto done;
+	}
+
+	gotd_reload_secrets_path = strndup(imsg->data, datalen);
+	if (gotd_reload_secrets_path == NULL)
+		err = got_error_from_errno("strndup");
+done:
+	if (err) {
+		if (gotd_reload_secrets_fd != -1) {
+			close(gotd_reload_secrets_fd);
+			gotd_reload_secrets_fd = -1;
+		}
+		free(gotd_reload_secrets_path);
+		gotd_reload_secrets_path = NULL;
+	}
+
+	return err;
+}
+
 static void
 gotd_request(int fd, short events, void *arg)
 {
@@ -638,6 +901,7 @@ gotd_request(int fd, short events, void *arg)
 	const struct got_error *err = NULL;
 	struct imsg imsg;
 	ssize_t n;
+	int do_disconnect = 0;
 
 	if (events & EV_WRITE) {
 		err = gotd_imsg_flush(ibuf);
@@ -677,7 +941,7 @@ gotd_request(int fd, short events, void *arg)
 		}
 	}
 
-	while (err == NULL) {
+	while (err == NULL && !do_disconnect) {
 		n = imsg_get(ibuf, &imsg);
 		if (n == -1) {
 			err = got_error_from_errno("imsg_get");
@@ -694,7 +958,23 @@ gotd_request(int fd, short events, void *arg)
 			break;
 		case GOTD_IMSG_STOP:
 			err = stop_gotd(client);
+			break;
+		case GOTD_IMSG_RELOAD_SECRETS:
+			if (have_reload_secrets) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = recv_reload_secrets(&imsg);
+			if (err)
+				break;
+			have_reload_secrets = 1;
 			break;
+		case GOTD_IMSG_RELOAD:
+			err = reload_gotd(client, &imsg);
+			if (err)
+				break;
+			do_disconnect = 1;
+			break;
 		case GOTD_IMSG_LIST_REFS:
 			err = start_client_authentication(client, &imsg);
 			break;
@@ -709,6 +989,8 @@ gotd_request(int fd, short events, void *arg)
 
 	if (err) {
 		disconnect_on_error(client, err);
+	} else if (do_disconnect) {
+		disconnect(client);
 	} else {
 		gotd_imsg_event_add(&client->iev);
 	}
@@ -821,6 +1103,8 @@ static const char *gotd_proc_names[GOTD_PROC_MAX] = {
 	"gitwrapper",
 	"notify",
 	"gotsys",
+	"reload",
+	"gotctl",
 };
 
 static void
@@ -939,10 +1223,10 @@ gotd_sighdlr(int sig, short event, void *arg)
 
 	switch (sig) {
 	case SIGHUP:
-		log_info("%s: ignoring SIGHUP", __func__);
+		log_info("ignoring SIGHUP; run 'gotctl reload' instead");
 		break;
 	case SIGUSR1:
-		log_info("%s: ignoring SIGUSR1", __func__);
+		log_info("ignoring SIGUSR1");
 		break;
 	case SIGTERM:
 	case SIGINT:
@@ -1189,13 +1473,20 @@ setup_listener(struct gotd_imsgev *iev)
 {
 	struct gotd_imsg_listen_socket isocket;
 	size_t i;
+	int fd;
 
 	memset(&isocket, 0, sizeof(isocket));
 	isocket.nconnection_limits = gotd.nconnection_limits;
 
+	fd = dup(gotd_socket);
+	if (fd == -1)
+		return got_error_from_errno("dup");
+
 	if (gotd_imsg_compose_event(iev, GOTD_IMSG_LISTEN_SOCKET,
-	    GOTD_PROC_GOTD, gotd_socket, &isocket, sizeof(isocket)) == -1)
+	    GOTD_PROC_GOTD, fd, &isocket, sizeof(isocket)) == -1) {
+		close(fd);
 		return got_error_from_errno("imsg compose LISTEN_SOCKET");
+	}
 
 	for (i = 0; i < gotd.nconnection_limits; i++) {
 		if (gotd_imsg_compose_event(iev, GOTD_IMSG_CONNECTION_LIMIT,
@@ -2521,9 +2812,10 @@ done:
 
 static pid_t
 start_child(enum gotd_procid proc_id, const char *repo_path,
-    char *argv0, const char *confpath, int fd, int daemonize, int verbosity)
+    char *argv0, const char *confpath, const char *secretspath,
+    int fd, int daemonize, int verbosity)
 {
-	const char	*argv[11];
+	const char	*argv[13];
 	int		 argc = 0;
 	pid_t		 pid;
 
@@ -2565,6 +2857,9 @@ start_child(enum gotd_procid proc_id, const char *repo
 		break;
 	case GOTD_PROC_NOTIFY:
 		argv[argc++] = "-TN";
+		break;
+	case GOTD_PROC_RELOAD:
+		argv[argc++] = "-TG";
 		break;
 	default:
 		fatalx("invalid process id %d", proc_id);
@@ -2572,6 +2867,11 @@ start_child(enum gotd_procid proc_id, const char *repo
 
 	argv[argc++] = "-f";
 	argv[argc++] = confpath;
+
+	if (secretspath) {
+		argv[argc++] = "-s";
+		argv[argc++] = secretspath;
+	}
 
 	if (repo_path) {
 		argv[argc++] = "-P";
@@ -2614,7 +2914,7 @@ start_listener(char *argv0, const char *confpath, int 
 	    PF_UNSPEC, proc->pipe) == -1)
 		fatal("socketpair");
 
-	proc->pid = start_child(proc->type, NULL, argv0, confpath,
+	proc->pid = start_child(proc->type, NULL, argv0, confpath, NULL,
 	    proc->pipe[1], daemonize, verbosity);
 	if (imsgbuf_init(&proc->iev.ibuf, proc->pipe[0]) == -1)
 		fatal("imsgbuf_init");
@@ -2650,7 +2950,7 @@ start_notifier(char *argv0, const char *confpath, int 
 	    PF_UNSPEC, proc->pipe) == -1)
 		fatal("socketpair");
 
-	proc->pid = start_child(proc->type, NULL, argv0, confpath,
+	proc->pid = start_child(proc->type, NULL, argv0, confpath, NULL,
 	    proc->pipe[1], daemonize, verbosity);
 	if (imsgbuf_init(&proc->iev.ibuf, proc->pipe[0]) == -1)
 		fatal("imsgbuf_init");
@@ -2698,7 +2998,7 @@ start_session_child(struct gotd_client *client, struct
 	if (socketpair(AF_UNIX, sock_flags, PF_UNSPEC, proc->pipe) == -1)
 		fatal("socketpair");
 	proc->pid = start_child(proc->type, proc->repo_path, argv0,
-	    confpath, proc->pipe[1], daemonize, verbosity);
+	    confpath, NULL, proc->pipe[1], daemonize, verbosity);
 	if (imsgbuf_init(&proc->iev.ibuf, proc->pipe[0]) == -1)
 		fatal("imsgbuf_init");
 	imsgbuf_allow_fdpass(&proc->iev.ibuf);
@@ -2754,7 +3054,7 @@ start_repo_child(struct gotd_client *client, enum gotd
 	    PF_UNSPEC, proc->pipe) == -1)
 		fatal("socketpair");
 	proc->pid = start_child(proc->type, proc->repo_path, argv0,
-	    confpath, proc->pipe[1], daemonize, verbosity);
+	    confpath, NULL, proc->pipe[1], daemonize, verbosity);
 	if (imsgbuf_init(&proc->iev.ibuf, proc->pipe[0]) == -1)
 		fatal("imsgbuf_init");
 	imsgbuf_allow_fdpass(&proc->iev.ibuf);
@@ -2801,7 +3101,7 @@ start_auth_child(struct gotd_client *client, int requi
 	    PF_UNSPEC, proc->pipe) == -1)
 		fatal("socketpair");
 	proc->pid = start_child(proc->type, proc->repo_path, argv0,
-	    confpath, proc->pipe[1], daemonize, verbosity);
+	    confpath, NULL, proc->pipe[1], daemonize, verbosity);
 	if (imsgbuf_init(&proc->iev.ibuf, proc->pipe[0]) == -1)
 		fatal("imsgbuf_init");
 	imsgbuf_allow_fdpass(&proc->iev.ibuf);
@@ -2974,6 +3274,9 @@ main(int argc, char **argv)
 			switch (*optarg) {
 			case 'A':
 				proc_id = GOTD_PROC_AUTH;
+				break;
+			case 'G':
+				proc_id = GOTD_PROC_RELOAD;
 				break;
 			case 'L':
 				proc_id = GOTD_PROC_LISTEN;
@@ -3019,6 +3322,7 @@ main(int argc, char **argv)
 
 	if (proc_id == GOTD_PROC_GOTD) {
 		const char *p = secretspath ? secretspath : GOTD_SECRETS_PATH;
+		int conf_fd = -1;
 
 		fp = fopen(p, "r");
 		if (fp == NULL && (secretspath != NULL || errno != ENOENT))
@@ -3033,8 +3337,15 @@ main(int argc, char **argv)
 				    p, error->msg);
 		}
 
-		if (gotd_parse_config(confpath, proc_id, secrets, &gotd) != 0)
+		conf_fd = open(confpath, O_RDONLY | O_NOFOLLOW);
+		if (conf_fd == -1)
+			fatal("open %s", confpath);
+
+		if (gotd_parse_config(confpath, conf_fd, proc_id, secrets,
+		    &gotd) != 0)
 			return 1;
+		close(conf_fd);
+		conf_fd = -1;
 
 		pw = getpwnam(gotd.user_name);
 		if (pw == NULL) {
@@ -3083,13 +3394,139 @@ main(int argc, char **argv)
 		if (gotd_socket == -1) {
 			fatal("cannot listen on unix socket %s",
 			    gotd.unix_socket_path);
+		}
+
+		if (gethostname(hostname, sizeof(hostname)) == -1)
+			fatal("gethostname");
+		if (asprintf(&gotd.default_sender, "%s@%s", pw->pw_name,
+		    hostname) == -1)
+			fatal("asprintf");
+	} else if (proc_id == GOTD_PROC_RELOAD) {
+		struct imsgbuf ibuf;
+		struct imsg imsg;
+
+		if (imsgbuf_init(&ibuf, GOTD_FILENO_MSG_PIPE) == -1)
+			fatal("imsgbuf_init");
+		imsgbuf_allow_fdpass(&ibuf);
+	
+		if (imsg_compose(&ibuf, GOTD_IMSG_RELOAD_READY,
+		    GOTD_PROC_RELOAD, getpid(), -1, NULL, 0) == -1)
+			fatal("imsg compose RELOAD_READY");
+		error = gotd_imsg_flush(&ibuf);
+		if (error) {
+			gotd_imsg_send_error(&ibuf, proc_id, 0, error);
+			fatal("imsg flush: %s", error->msg);
 		}
+		while (error == NULL &&
+		    (gotd_socket == -1  || gotd_reload_conf_fd == -1)) {
+			error = gotd_imsg_poll_recv(&imsg, &ibuf, 0);
+			if (error)
+				break;
+			switch (imsg.hdr.type) {
+			case GOTD_IMSG_ERROR:
+				error = gotd_imsg_recv_error(NULL, &imsg);
+				break;
+			case GOTD_IMSG_LISTEN_SOCKET:
+				if (gotd_socket != -1) {
+					error = got_error(GOT_ERR_PRIVSEP_MSG);
+					break;
+				}
+				gotd_socket = imsg_get_fd(&imsg);
+				if (gotd_socket != -1)
+					break;
+				error = got_error(GOT_ERR_PRIVSEP_NO_FD);
+				break;
+			case GOTD_IMSG_RELOAD_SECRETS:
+				if (have_reload_secrets) {
+					error = got_error(GOT_ERR_PRIVSEP_MSG);
+					break;
+				}
+				error = recv_reload_secrets(&imsg);
+				if (error)
+					break;
+				have_reload_secrets = 1;
+				break;
+			case GOTD_IMSG_GOTD_CONF:
+				if (!have_reload_secrets ||
+				    gotd_reload_conf_fd != -1) {
+					error = got_error(GOT_ERR_PRIVSEP_MSG);
+					break;
+				}
+				gotd_reload_conf_fd = imsg_get_fd(&imsg);
+				if (gotd_reload_conf_fd != -1)
+					break;
+				error = got_error(GOT_ERR_PRIVSEP_NO_FD);
+				break;
+			}
+		}
 
+		if (error) {
+			gotd_imsg_send_error(&ibuf, proc_id, 0, error);
+			fatal("reloading: %s", error->msg);
+		}
+
+		if (gotd_reload_secrets_fd != -1) {
+			char *p = gotd_reload_secrets_path;
+
+			check_file_secrecy(gotd_reload_secrets_fd, p);
+			fp = fdopen(gotd_reload_secrets_fd, "r");
+			if (fp == NULL) {
+				error = got_error_from_errno2("fdopen",
+				    gotd_reload_secrets_path);
+				gotd_imsg_send_error(&ibuf, proc_id, 0, error);
+				fatal("reloading: %s", error->msg);
+			}
+
+			error = gotd_secrets_parse(p, fp, &secrets);
+			fclose(fp);
+			if (error) {
+				gotd_imsg_send_error(&ibuf, proc_id, 0, error);
+				fatalx("failed to parse secrets file %s: %s",
+				    p, error->msg);
+			}
+		}
+
+		if (gotd_parse_config(confpath, gotd_reload_conf_fd,
+		    GOTD_PROC_GOTD, secrets, &gotd) != 0)
+			return 1;
+		close(gotd_reload_conf_fd);
+		gotd_reload_conf_fd = -1;
+
+		pw = getpwnam(gotd.user_name);
+		if (pw == NULL) {
+			uid = strtonum(gotd.user_name, 0, UID_MAX - 1, &errstr);
+			if (errstr == NULL)
+				pw = getpwuid(uid);
+		}
+		if (pw == NULL) {
+			error = got_error_fmt(GOT_ERR_UID,
+			    "user %s not found", gotd.user_name);
+			gotd_imsg_send_error(&ibuf, proc_id, 0, error);
+			fatalx("%s", error->msg);
+		}
+
+		if (pw->pw_uid == 0) {
+			error = got_error_fmt(GOT_ERR_UID,
+			    "cannot run %s as the superuser", getprogname());
+			gotd_imsg_send_error(&ibuf, proc_id, 0, error);
+			fatalx("%s", error->msg);
+		}
+
 		if (gethostname(hostname, sizeof(hostname)) == -1)
 			fatal("gethostname");
 		if (asprintf(&gotd.default_sender, "%s@%s", pw->pw_name,
 		    hostname) == -1)
 			fatal("asprintf");
+
+		imsgbuf_clear(&ibuf);
+		close(GOTD_FILENO_MSG_PIPE);
+
+		/* Clear state such that reloading again will work. */
+		close(gotd_reload_conf_fd);
+		gotd_reload_conf_fd = -1;
+		close(gotd_reload_secrets_fd);
+		gotd_reload_secrets_fd = -1;
+		have_reload_secrets = 0;
 	}
 
 	gotd.argv0 = argv0;
@@ -3110,6 +3547,11 @@ main(int argc, char **argv)
 		if (daemonize && daemon(1, 0) == -1)
 			fatal("daemon");
 		gotd.pid = getpid();
+	} else if (proc_id == GOTD_PROC_RELOAD) {
+		snprintf(title, sizeof(title), "%s",
+		    gotd_proc_names[GOTD_PROC_GOTD]);
+		arc4random_buf(&clients_hash_key, sizeof(clients_hash_key));
+		gotd.pid = getpid();
 	} else if (proc_id == GOTD_PROC_LISTEN) {
 		snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]);
 	} else if (proc_id == GOTD_PROC_AUTH) {
@@ -3146,6 +3588,9 @@ main(int argc, char **argv)
 			fatal("setgid %d failed", pw->pw_gid);
 		if (setuid(pw->pw_uid) == -1)
 			fatal("setuid %d failed", pw->pw_uid);
+		/* FALLTHROUGH */
+	case GOTD_PROC_RELOAD:
+		proc_id = GOTD_PROC_GOTD;
 		if (verbosity) {
 			log_info("socket: %s", gotd.unix_socket_path);
 			log_info("user: %s", pw->pw_name);
blob - f04ba7601ab2f6ff8ec1e20c104c69749cacf93a
blob + 57c83b55a87d19e7fd97463282757061ded30a78
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -68,6 +68,8 @@ enum gotd_procid {
 	GOTD_PROC_GITWRAPPER,
 	GOTD_PROC_NOTIFY,
 	GOTD_PROC_GOTSYS,
+	GOTD_PROC_RELOAD,
+	GOTD_PROC_GOTCTL,
 	GOTD_PROC_MAX,
 };
 
@@ -175,6 +177,7 @@ struct gotd {
 	int nrepos;
 	struct gotd_child_proc *listen_proc;
 	struct gotd_child_proc *notify_proc;
+	struct gotd_child_proc *reload_proc;
 	int notifications_enabled;
 	struct timeval request_timeout;
 	struct timeval auth_timeout;
@@ -198,6 +201,7 @@ enum gotd_imsg_type {
 	GOTD_IMSG_INFO_REPO,
 	GOTD_IMSG_INFO_CLIENT,
 	GOTD_IMSG_STOP,
+	GOTD_IMSG_RELOAD,
 
 	/* Request a list of references. */
 	GOTD_IMSG_LIST_REFS,
@@ -259,6 +263,12 @@ enum gotd_imsg_type {
 	GOTD_IMSG_REPO_CHILD_READY,
 	GOTD_IMSG_CONNECT_REPO_CHILD,
 
+	/* Reloading. */
+	GOTD_IMSG_RELOAD_READY,
+	GOTD_IMSG_RELOAD_SECRETS,
+	GOTD_IMSG_GOTD_CONF,
+	GOTD_IMSG_HALT,
+
 	/* Auth child process. */
 	GOTD_IMSG_AUTH_READY,
 	GOTD_IMSG_AUTH_ACCESS_RULE,
@@ -639,9 +649,8 @@ struct gotd_imsg_notify {
 	/* Followed by username_len data bytes. */
 };
 
-int enter_chroot(const char *);
-int gotd_parse_config(const char *, enum gotd_procid, struct gotd_secrets *,
-    struct gotd *);
+int gotd_parse_config(const char *, int, enum gotd_procid,
+    struct gotd_secrets *, struct gotd *);
 struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd_repolist *);
 struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd *);
 struct gotd_uid_connection_limit *gotd_find_uid_connection_limit(
blob - 24af2498fc5909a23d8edfaec7cbf75e44e24bf9
blob + ec806d3f673b48425a89894e6428720af4b2699a
--- gotd/listen.c
+++ gotd/listen.c
@@ -58,6 +58,7 @@ static struct gotd_listen_clients gotd_listen_clients[
 static SIPHASH_KEY clients_hash_key;
 static volatile int listen_client_cnt;
 static int inflight;
+static int listener_halted;
 
 struct gotd_uid_connection_counter {
 	STAILQ_ENTRY(gotd_uid_connection_counter) entry;
@@ -215,6 +216,9 @@ disconnect(struct gotd_listen_client *client)
 	listen_client_cnt--;
 	if (close(client_fd) == -1)
 		return got_error_from_errno("close");
+
+	if (listener_halted && listen_client_cnt == 0)
+		listen_shutdown();
 
 	return NULL;
 }
@@ -543,6 +547,26 @@ listen_dispatch(int fd, short event, void *arg)
 			if (err)
 				log_warnx("disconnect: %s", err->msg);
 			break;
+		case GOTD_IMSG_HALT: {
+			uint32_t reload_client_id;
+			struct gotd_listen_client *client;
+
+			event_del(&gotd_listen.iev.ev);
+			evtimer_del(&gotd_listen.pause_ev);
+			listener_halted = 1;
+			log_debug("gotd reloading; stopped listening for "
+			    "connections");
+
+			if (imsg_get_data(&imsg, &reload_client_id,
+			    sizeof(reload_client_id)) == -1) {
+				err = got_error_from_errno("imsg-get_data");
+				break;
+			}
+			client = find_client(reload_client_id);
+			if (client)
+				disconnect(client);
+			break;
+		}
 		default:
 			log_debug("unexpected imsg %d", imsg.hdr.type);
 			break;
@@ -556,7 +580,8 @@ listen_dispatch(int fd, short event, void *arg)
 	} else {
 		/* This pipe is dead. Remove its event handler */
 		event_del(&iev->ev);
-		event_loopexit(NULL);
+		if (!listener_halted)
+			event_loopexit(NULL);
 	}
 }
 
blob - df4c6ae0933e6455023be61dfd1d6524ea69cbb3
blob + eeac71346eaccf9b930bcfdb65a805e2f5d19570
--- gotd/parse.y
+++ gotd/parse.y
@@ -62,7 +62,7 @@ static struct file {
 	int			 lineno;
 	int			 errors;
 } *file;
-struct file	*newfile(const char *, int, int);
+struct file	*newfile(const char *, int, int, int);
 static void	 closefile(struct file *);
 int		 check_file_secrecy(int, const char *);
 int		 yyparse(void);
@@ -1084,9 +1084,10 @@ check_file_secrecy(int fd, const char *fname)
 }
 
 struct file *
-newfile(const char *name, int secret, int required)
+newfile(const char *name, int conf_fd, int secret, int required)
 {
 	struct file *nfile;
+	int fd = -1;
 
 	nfile = calloc(1, sizeof(struct file));
 	if (nfile == NULL) {
@@ -1099,7 +1100,12 @@ newfile(const char *name, int secret, int required)
 		free(nfile);
 		return (NULL);
 	}
-	nfile->stream = fopen(nfile->name, "r");
+
+	fd = dup(conf_fd);
+	if (fd == -1)
+		return NULL;
+
+	nfile->stream = fdopen(fd, "r");
 	if (nfile->stream == NULL) {
 		if (required)
 			log_warn("open %s", nfile->name);
@@ -1126,7 +1132,7 @@ closefile(struct file *xfile)
 }
 
 int
-gotd_parse_config(const char *filename, enum gotd_procid proc_id,
+gotd_parse_config(const char *filename, int conf_fd, enum gotd_procid proc_id,
     struct gotd_secrets *secrets, struct gotd *env)
 {
 	struct sym *sym, *next;
@@ -1155,7 +1161,10 @@ gotd_parse_config(const char *filename, enum gotd_proc
 	gotd->request_timeout.tv_sec = GOTD_DEFAULT_REQUEST_TIMEOUT;
 	gotd->request_timeout.tv_usec = 0;
 
-	file = newfile(filename, 0, require_config_file);
+	if (conf_fd == -1 && !require_config_file)
+		return 0;
+
+	file = newfile(filename, conf_fd, 0, require_config_file);
 	if (file == NULL)
 		return require_config_file ? -1 : 0;