"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Omar Polo <op@omarpolo.com>
Subject:
initial gotd-secrets.conf implementation
To:
gameoftrees@openbsd.org
Date:
Sat, 07 Sep 2024 17:01:42 +0200

Download raw body.

Thread
This is a first implementation of the mechanism discusse in the 'RFC:
secrets for gotd' thread.  The overall idea is to move the
authentication data (username and password) for HTTP notifications in a
separate file, and the HMAC secret too.

This different file, gotd-secrets.conf, is meant to be root-owned,
unlike gotd.conf which has to be world-readable for gitwrapper to work.

What's still missing:

 - checking the permissions on the file
 - improving the documentation

We're also currently using the username as "key" in gotd.conf, which is
fine but slightly annoying since we might want to have multiple
notifications that are using the same username but different password.
This is something that I've overlooked in the RFC thread and only
realized while implementing it.  Still, this is a starting point and we
can improve in tree I believe.

ok?

diff /home/op/w/got
commit - 477e0bbd518b16f53d572bd7e09c0392655c73e8
path + /home/op/w/got
blob - f1443d0b0dc69d389f59208ecb4ce2354a246f6d
file + gitwrapper/Makefile
--- gitwrapper/Makefile
+++ gitwrapper/Makefile
@@ -10,7 +10,7 @@ BINDIR ?=	${PREFIX}/bin
 PROG=		gitwrapper
 
 SRCS=		gitwrapper.c parse.y log.c dial.c path.c error.c \
-		reference_parse.c hash.c object_qid.c
+		reference_parse.c hash.c object_qid.c secrets.c
 
 CLEANFILES = parse.h
 
blob - 0c22d1dced690c764d8a882746e76ec45a9c7226
file + gitwrapper/gitwrapper.c
--- gitwrapper/gitwrapper.c
+++ gitwrapper/gitwrapper.c
@@ -132,7 +132,7 @@ main(int argc, char *argv[])
 	confpath = getenv("GOTD_CONF_PATH");
 	if (confpath == NULL)
 		confpath = GOTD_CONF_PATH;
-	parse_config(confpath, PROC_GITWRAPPER, &gotd);
+	parse_config(confpath, PROC_GITWRAPPER, NULL, &gotd);
 
 	error = apply_unveil(myserver);
 	if (error)
blob - 40093462c835f76308db6ab0dc60b0735412ab92
file + gotd/Makefile
--- gotd/Makefile
+++ gotd/Makefile
@@ -10,7 +10,8 @@ BINDIR ?=	${PREFIX}/sbin
 
 PROG=		gotd
 SRCS=		gotd.c auth.c repo_read.c repo_write.c log.c privsep_stub.c \
-		listen.c imsg.c parse.y pack_create.c ratelimit.c deltify.c \
+		listen.c imsg.c parse.y secrets.c pack_create.c ratelimit.c \
+		deltify.c \
 		bloom.c buf.c date.c deflate.c delta.c delta_cache.c error.c \
 		gitconfig.c gotconfig.c inflate.c lockfile.c murmurhash2.c \
 		object.c object_cache.c object_create.c object_idset.c \
@@ -25,7 +26,7 @@ SRCS=		gotd.c auth.c repo_read.c repo_write.c log.c pr
 
 CLEANFILES = parse.h
 
-MAN =		${PROG}.conf.5 ${PROG}.8
+MAN =		${PROG}.conf.5 ${PROG}-secrets.conf.5 ${PROG}.8
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}
 YFLAGS =
blob - /dev/null
file + gotd/gotd-secrets.conf.5 (mode 644)
--- /dev/null
+++ gotd/gotd-secrets.conf.5
@@ -0,0 +1,103 @@
+.\"
+.\" Copyright (c) 2024 Omar Polo <op@openbsd.org>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate$
+.Dt GOTD-SECRETS.CONF 5
+.Os
+.Sh NAME
+.Nm gotd-secrets.conf
+.Nd gotd secrets file
+.Sh DESCRIPTION
+.Nm
+holds the authentication data and HMAC secrets for
+.Xr gotd 8
+notifications.
+.Pp
+The file format is line-based, with one entry per line.
+Comments can be put at the start of the line using a hash mark
+.Pq Sq # ,
+and extend to the end of it.
+Blank lines are also ignored.
+.Pp
+The entries have the following syntax:
+.Pp
+.Dl type key value
+.Pp
+with spaces or tabs to separate the fields.
+No quoting is supported, so a space or a tab can't appear as part of
+any field.
+.Pp
+The type is one of:
+.Bl -tag -width Ds
+.It Ic auth
+The entry is for HTTP Basic Authentication.
+.Ar key
+is the username and
+.ar value
+the password.
+The username is also used to identify this secret.
+.It Ic hmac
+The entry is for signing the notification HTTP payload with HMAC.
+The
+.Ar key
+is a label to identify this secret and
+.Ar value
+is the HMAC secret.
+.Pp
+Suitable secrets can be generated with
+.Xr openssl 1
+as follows:
+.Pp
+.Dl $ openssl rand -base64 32
+.El
+.Pp
+The key must be unique between entries with the same type.
+.Sh FILES
+.Bl -tag -width Ds -compact
+.It Pa /etc/gotd-secrets.conf
+Location of the
+.Nm
+configuration file.
+.El
+.Sh EXAMPLES
+This example configuration defines two secrets, the first for
+HTTP authentication and the second for HMAC signign.
+.Bd -literal -offset indent
+# /etc/gotd-secrets.conf
+auth flan super-strong-password!
+hmac hacker q0tcl8QhjYs7U75MW/2rwB30CpdbAhONkfLGxFHm/+8=
+.Ed
+.Pp
+These values can be referenced in
+.Xr gotd.conf 5
+as:
+.Bd -literal -offset indent
+# /etc/gotd.conf
+repository "openbsd/ports" {
+	path "/var/git/ports.git"
+	permit rw :porters
+	permit ro anonymous
+
+	notify {
+		url https://flan.com/notify/ auth flan
+		url https://hacker.com/notify/ hmac hacker
+	}
+}
+.El
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr gotsh 1 ,
+.Xr gotd.conf 5 ,
+.Xr gotd 8
blob - 38567d159047ce5179b9982fcefdf2bff0d21e9b
file + gotd/gotd.8
--- gotd/gotd.8
+++ gotd/gotd.8
@@ -23,6 +23,7 @@
 .Nm
 .Op Fl dnv
 .Op Fl f Ar config-file
+.Op Fl s Ar secrets
 .Sh DESCRIPTION
 .Nm
 is a Git repository server which listens on a
@@ -63,6 +64,11 @@ will be used.
 .It Fl n
 Configtest mode.
 Only check the configuration file 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.
 .It Fl v
 Verbose mode.
 Verbosity increases if this option is used multiple times.
@@ -109,6 +115,7 @@ The flan_hacker user can now populate the empty reposi
 .Xr gotsh 1 ,
 .Xr git-repository 5 ,
 .Xr gotd.conf 5
+.Xr gotd-secrets.conf 5
 .Sh AUTHORS
 .An Stefan Sperling Aq Mt stsp@openbsd.org
 .Sh CAVEATS
blob - 5830c85ab86441288df899ce965b061f45d21f62
file + gotd/gotd.c
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -67,6 +67,7 @@
 #include "repo_read.h"
 #include "repo_write.h"
 #include "notify.h"
+#include "secrets.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -127,7 +128,8 @@ static void disconnect(struct gotd_client *);
 __dead static void
 usage(void)
 {
-	fprintf(stderr, "usage: %s [-dnv] [-f config-file]\n", getprogname());
+	fprintf(stderr, "usage: %s [-dnv] [-f config-file] [-s secrets]\n",
+	    getprogname());
 	exit(1);
 }
 
@@ -2002,8 +2004,10 @@ int
 main(int argc, char **argv)
 {
 	const struct got_error *error = NULL;
+	struct gotd_secrets *secrets = NULL;
 	int ch, fd = -1, daemonize = 1, verbosity = 0, noaction = 0;
 	const char *confpath = GOTD_CONF_PATH;
+	const char *secretspath = NULL;
 	char *argv0 = argv[0];
 	char title[2048];
 	struct passwd *pw = NULL;
@@ -2015,6 +2019,7 @@ main(int argc, char **argv)
 	struct gotd_repo *repo = NULL;
 	char *default_sender = NULL;
 	char hostname[HOST_NAME_MAX + 1];
+	FILE *fp;
 	FILE *diff_f1 = NULL, *diff_f2 = NULL;
 	int diff_fd1 = -1, diff_fd2 = -1;
 	const char *errstr;
@@ -2023,7 +2028,7 @@ main(int argc, char **argv)
 
 	log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
 
-	while ((ch = getopt(argc, argv, "df:nP:T:v")) != -1) {
+	while ((ch = getopt(argc, argv, "df:nP:s:T:v")) != -1) {
 		switch (ch) {
 		case 'd':
 			daemonize = 0;
@@ -2039,6 +2044,9 @@ main(int argc, char **argv)
 			if (repo_path == NULL)
 				fatal("realpath '%s'", optarg);
 			break;
+		case 's':
+			secretspath = optarg;
+			break;
 		case 'T':
 			switch (*optarg) {
 			case 'A':
@@ -2084,7 +2092,23 @@ main(int argc, char **argv)
 	if (geteuid() && (proc_id == PROC_GOTD || proc_id == PROC_LISTEN))
 		fatalx("need root privileges");
 
-	if (parse_config(confpath, proc_id, &gotd) != 0)
+	if (proc_id == PROC_GOTD) {
+		const char *p = secretspath ? secretspath : GOTD_SECRETS_PATH;
+
+		fp = fopen(p, "r");
+		if (fp == NULL && (secretspath != NULL || errno != ENOENT))
+			fatal("can't open secret file %s", p);
+
+		if (fp != NULL) {
+			error = gotd_secrets_parse(p, fp, &secrets);
+			fclose(fp);
+			if (error)
+				fatalx("failed to parse secrets file %s: %s",
+				    p, error->msg);
+		}
+	}
+
+	if (parse_config(confpath, proc_id, secrets, &gotd) != 0)
 		return 1;
 
 	pw = getpwnam(gotd.user_name);
@@ -2346,9 +2370,57 @@ main(int argc, char **argv)
 	signal_add(&evsigchld, NULL);
 
 	gotd_imsg_event_add(&gotd.listen_proc->iev);
-	if (gotd.notify_proc)
+	if (gotd.notify_proc) {
+		struct imsgbuf *imsgbuf = &gotd.notify_proc->iev.ibuf;
+		struct gotd_secret *s;
+		size_t i, n = 0;
+
 		gotd_imsg_event_add(&gotd.notify_proc->iev);
 
+		if (gotd.secrets)
+			n = gotd.secrets->len;
+
+		if (imsg_compose(imsgbuf, GOTD_IMSG_SECRETS, 0, 0, -1,
+		    &n, sizeof(n)) == -1)
+			fatal("imsg_compose GOTD_IMSG_SECRETS");
+		if (imsg_flush(imsgbuf))
+			fatal("imsg_flush");
+
+		for (i = 0; i < n; ++i) {
+			struct iovec iov[5];
+			int keylen, vallen;
+
+			s = &gotd.secrets->secrets[i];
+
+			keylen = strlen(s->key) + 1;
+			vallen = strlen(s->val) + 1;
+
+			iov[0].iov_base = &s->type;
+			iov[0].iov_len = sizeof(s->type);
+
+			iov[1].iov_base = &keylen;
+			iov[1].iov_len = sizeof(keylen);
+
+			iov[2].iov_base = &vallen;
+			iov[2].iov_len = sizeof(vallen);
+
+			iov[3].iov_base = s->key;
+			iov[3].iov_len = keylen;
+
+			iov[4].iov_base = s->val;
+			iov[4].iov_len = vallen;
+
+			if (imsg_composev(imsgbuf, GOTD_IMSG_SECRET,
+			    0, 0, -1, iov, 5) == -1)
+				fatal("imsg_composev GOTD_IMSG_SECRET");
+			if (imsg_flush(imsgbuf))
+				fatal("imsg_flush");
+		}
+
+		gotd_secrets_free(gotd.secrets);
+		gotd.secrets = NULL;
+	}
+
 	event_dispatch();
 
 	free(repo_path);
blob - 966c6008573b577e99cc85b6d7eada163453396c
file + gotd/gotd.conf.5
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
@@ -333,7 +333,7 @@ The
 and
 .Ic port
 directives can be used to specify a different SMTP server address and port.
-.It Ic url Ar URL Oo Ic user Ar user Ic password Ar password Oo Ic insecure Oc Oc Oo Ic hmac Ar secret Oc 
+.It Ic url Ar URL Oo Ic auth Ar auth Oo Ic insecure Oc Oc Oo Ic hmac Ar label Oc 
 Send notifications via HTTP.
 This directive may be specified multiple times to build a list of
 HTTP servers to send notifications to.
@@ -348,18 +348,8 @@ If HTTPS is used, sending of notifications will only s
 no TLS errors occur.
 .Pp
 The optional
-.Ic user
-and
-.Ic password
-directives enable HTTP Basic authentication.
-If used, both a
-.Ar user
-and a
-.Ar password
-must be specified.
-The
-.Ar password
-must not be an empty string.
+.Ic auth
+directive enables HTTP Basic authentication.
 Unless the
 .Ic insecure
 option is specified the notification target
@@ -370,16 +360,20 @@ URL to avoid leaking of authentication credentials.
 .Pp
 If a
 .Ic hmac
-.Ar secret
+.Ar label
 is provided, the request body will be signed using HMAC, allowing the
 receiver to verify the notification message's authenticity and integrity.
 The signature uses HMAC-SHA256 and will be sent in the HTTP header
 .Dq X-Gotd-Signature .
-Suitable secrets can be generated with
-.Xr openssl 1
-as follows:
 .Pp
-.Dl $ openssl rand -base64 32
+If provided,
+the authentication data
+.Ar auth
+and the HMAC secret
+.Ar label
+are resolved using the
+.Xr gotd-secrets.conf 5
+file.
 .Pp
 The request body contains a JSON object with a
 .Dq notifications
@@ -563,4 +557,5 @@ connection {
 .Sh SEE ALSO
 .Xr got 1 ,
 .Xr gotsh 1 ,
+.Xr gotd-secrets.conf 5 ,
 .Xr gotd 8
blob - 4c8e79a9beb60c52b4a922d424c3d7db0000dfb5
file + gotd/gotd.h
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -19,6 +19,7 @@
 #define GOTD_UNIX_SOCKET_BACKLOG 10
 #define GOTD_USER	"_gotd"
 #define GOTD_CONF_PATH	"/etc/gotd.conf"
+#define GOTD_SECRETS_PATH "/etc/gotd-secrets.conf"
 #define GOTD_EMPTY_PATH	"/var/empty"
 
 #ifndef GOT_LIBEXECDIR
@@ -110,9 +111,8 @@ struct gotd_notification_target {
 			char *hostname;
 			char *port;
 			char *path;
-			char *user;
-			char *password;
-			char *hmac_secret;
+			char *auth;
+			char *hmac;
 		} http;
 	} conf;
 };
@@ -153,6 +153,7 @@ struct gotd_uid_connection_limit {
 
 struct gotd_child_proc;
 
+struct gotd_secrets;
 struct gotd {
 	pid_t pid;
 	char unix_socket_path[PATH_MAX];
@@ -166,6 +167,7 @@ struct gotd {
 	struct timeval auth_timeout;
 	struct gotd_uid_connection_limit *connection_limits;
 	size_t nconnection_limits;
+	struct gotd_secrets *secrets;
 
 	char *argv0;
 	const char *confpath;
@@ -240,7 +242,11 @@ enum gotd_imsg_type {
 	GOTD_IMSG_CONNECT_NOTIFIER,
 	GOTD_IMSG_CONNECT_SESSION,
 	GOTD_IMSG_NOTIFY,
-	GOTD_IMSG_NOTIFICATION_SENT
+	GOTD_IMSG_NOTIFICATION_SENT,
+
+	/* Secrets. */
+	GOTD_IMSG_SECRETS,	/* number of secrets */
+	GOTD_IMSG_SECRET,
 };
 
 /* Structure for GOTD_IMSG_ERROR. */
@@ -482,7 +488,8 @@ struct gotd_imsg_notify {
 	/* Followed by username_len data bytes. */
 };
 
-int parse_config(const char *, enum gotd_procid, struct gotd *);
+int parse_config(const char *, 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 - 01439b9e598a7bf45e46c1328da61d697115f13a
file + gotd/notify.c
--- gotd/notify.c
+++ gotd/notify.c
@@ -40,11 +40,14 @@
 #include "gotd.h"
 #include "log.h"
 #include "notify.h"
+#include "secrets.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
 #endif
 
+static struct gotd_secrets	secrets;
+
 static struct gotd_notify {
 	pid_t pid;
 	const char *title;
@@ -273,6 +276,7 @@ static void
 notify_http(struct gotd_notification_target *target, const char *repo,
     const char *username, int fd)
 {
+	const char *http_user = NULL, *http_pass = NULL, *hmac = NULL;
 	const char *argv[12];
 	int argc = 0;
 
@@ -293,9 +297,18 @@ notify_http(struct gotd_notification_target *target, c
 
 	argv[argc] = NULL;
 
+	if (target->conf.http.auth) {
+		http_user = target->conf.http.auth;
+		http_pass = gotd_secrets_get(&secrets, GOTD_SECRET_AUTH,
+		    http_user);
+	}
+	if (target->conf.http.hmac) {
+		hmac = gotd_secrets_get(&secrets, GOTD_SECRET_HMAC,
+		    target->conf.http.hmac);
+	}
+
 	run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
-	    target->conf.http.user, target->conf.http.password,
-	    target->conf.http.hmac_secret);
+	    http_user, http_pass, hmac);
 }
 
 static const struct got_error *
@@ -462,6 +475,10 @@ notify_dispatch(int fd, short event, void *arg)
 	ssize_t n;
 	int shut = 0;
 	struct imsg imsg;
+	struct ibuf ibuf;
+	struct gotd_secret *s;
+	int keylen, vallen;
+	char *key, *val;
 
 	if (event & EV_READ) {
 		if ((n = imsg_read(imsgbuf)) == -1 && errno != EAGAIN)
@@ -496,6 +513,39 @@ notify_dispatch(int fd, short event, void *arg)
 		case GOTD_IMSG_CONNECT_SESSION:
 			err = recv_session(&imsg);
 			break;
+		case GOTD_IMSG_SECRETS:
+			if (secrets.cap != 0)
+				fatal("unexpected GOTD_IMSG_SECRETS");
+			if (imsg_get_data(&imsg, &secrets.cap,
+			    sizeof(secrets.cap)) == -1)
+				fatalx("corrupted GOTD_IMSG_SECRETS");
+			if (secrets.cap == 0)
+				break;
+			secrets.secrets = calloc(secrets.cap,
+			    sizeof(*secrets.secrets));
+			if (secrets.secrets == NULL)
+				fatal("calloc");
+			break;
+		case GOTD_IMSG_SECRET:
+			if (secrets.len == secrets.cap)
+				fatalx("unexpected GOTD_SECRET_AUTH");
+			s = &secrets.secrets[secrets.len++];
+			if (imsg_get_ibuf(&imsg, &ibuf) == -1)
+				fatal("imsg_get_ibuf");
+			if (ibuf_get(&ibuf, &s->type, sizeof(s->type)) == -1 ||
+			    ibuf_get(&ibuf, &keylen, sizeof(keylen)) == -1 ||
+			    ibuf_get(&ibuf, &vallen, sizeof(vallen)) == -1 ||
+			    keylen <= 0 || vallen <= 0 ||
+			    ibuf_size(&ibuf) != (keylen + vallen) ||
+			    (key = ibuf_data(&ibuf)) == NULL ||
+			    (val = ibuf_seek(&ibuf, keylen, vallen)) == NULL ||
+			    key[keylen - 1] != '\0' || val[vallen - 1] != '\0')
+				fatalx("corrupted GOTD_IMSG_SECRET");
+			s->key = strdup(key);
+			s->val = strdup(val);
+			if (s->key == NULL || s->val == NULL)
+				fatal("strdup");
+			break;
 		default:
 			log_debug("unexpected imsg %d", imsg.hdr.type);
 			break;
blob - da033ac49c81846361825b0bae646e833d0f11bb
file + gotd/parse.y
--- gotd/parse.y
+++ gotd/parse.y
@@ -52,6 +52,7 @@
 #include "gotd.h"
 #include "auth.h"
 #include "listen.h"
+#include "secrets.h"
 
 TAILQ_HEAD(files, file)		 files = TAILQ_HEAD_INITIALIZER(files);
 static struct file {
@@ -111,7 +112,7 @@ static int			 conf_notify_ref_namespace(struct gotd_re
 static int			 conf_notify_email(struct gotd_repo *,
 				    char *, char *, char *, char *, char *);
 static int			 conf_notify_http(struct gotd_repo *,
-				    char *, char *, char *, int, char *);
+				    char *, char *, char *, int);
 static enum gotd_procid		 gotd_proc_id;
 
 typedef struct {
@@ -128,7 +129,7 @@ typedef struct {
 %token	PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY
 %token	RO RW CONNECTION LIMIT REQUEST TIMEOUT
 %token	PROTECT NAMESPACE BRANCH TAG REFERENCE RELAY PORT
-%token	NOTIFY EMAIL FROM REPLY TO URL PASSWORD INSECURE HMAC
+%token	NOTIFY EMAIL FROM REPLY TO URL INSECURE HMAC AUTH
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -611,51 +612,47 @@ notifyflags	: BRANCH STRING {
 			    gotd_proc_id == PROC_SESSION_WRITE ||
 			    gotd_proc_id == PROC_NOTIFY) {
 				if (conf_notify_http(new_repo, $2, NULL,
-				    NULL, 0, NULL)) {
+				    NULL, 0)) {
 					free($2);
 					YYERROR;
 				}
 			}
 			free($2);
 		}
-		| URL STRING USER STRING PASSWORD STRING {
+		| URL STRING AUTH STRING {
 			if (gotd_proc_id == PROC_GOTD ||
 			    gotd_proc_id == PROC_SESSION_WRITE ||
 			    gotd_proc_id == PROC_NOTIFY) {
-				if (conf_notify_http(new_repo, $2, $4, $6, 0,
-				    NULL)) {
+				if (conf_notify_http(new_repo, $2, $4, NULL,
+				    0)) {
 					free($2);
 					free($4);
-					free($6);
 					YYERROR;
 				}
 			}
 			free($2);
 			free($4);
-			free($6);
 		}
-		| URL STRING USER STRING PASSWORD STRING INSECURE {
+		| URL STRING AUTH STRING INSECURE {
 			if (gotd_proc_id == PROC_GOTD ||
 			    gotd_proc_id == PROC_SESSION_WRITE ||
 			    gotd_proc_id == PROC_NOTIFY) {
-				if (conf_notify_http(new_repo, $2, $4, $6, 1,
-				    NULL)) {
+				if (conf_notify_http(new_repo, $2, $4, NULL,
+				    1)) {
 					free($2);
 					free($4);
-					free($6);
 					YYERROR;
 				}
 			}
 			free($2);
 			free($4);
-			free($6);
 		}
 		| URL STRING HMAC STRING {
 			if (gotd_proc_id == PROC_GOTD ||
 			    gotd_proc_id == PROC_SESSION_WRITE ||
 			    gotd_proc_id == PROC_NOTIFY) {
-				if (conf_notify_http(new_repo, $2, NULL,
-				    NULL, 0, $4)) {
+				if (conf_notify_http(new_repo, $2, NULL, $4,
+				    0)) {
 					free($2);
 					free($4);
 					YYERROR;
@@ -664,41 +661,37 @@ notifyflags	: BRANCH STRING {
 			free($2);
 			free($4);
 		}
-		| URL STRING USER STRING PASSWORD STRING HMAC STRING {
+		| URL STRING AUTH STRING HMAC STRING {
 			if (gotd_proc_id == PROC_GOTD ||
 			    gotd_proc_id == PROC_SESSION_WRITE ||
 			    gotd_proc_id == PROC_NOTIFY) {
-				if (conf_notify_http(new_repo, $2, $4, $6, 0,
-				    $8)) {
+				if (conf_notify_http(new_repo, $2, $4, $6,
+				    0)) {
 					free($2);
 					free($4);
 					free($6);
-					free($8);
 					YYERROR;
 				}
 			}
 			free($2);
 			free($4);
 			free($6);
-			free($8);
 		}
-		| URL STRING USER STRING PASSWORD STRING INSECURE HMAC STRING {
+		| URL STRING AUTH STRING INSECURE HMAC STRING {
 			if (gotd_proc_id == PROC_GOTD ||
 			    gotd_proc_id == PROC_SESSION_WRITE ||
 			    gotd_proc_id == PROC_NOTIFY) {
-				if (conf_notify_http(new_repo, $2, $4, $6, 1,
-				    $9)) {
+				if (conf_notify_http(new_repo, $2, $4, $7,
+				    1)) {
 					free($2);
 					free($4);
-					free($6);
-					free($9);
+					free($7);
 					YYERROR;
 				}
 			}
 			free($2);
 			free($4);
-			free($6);
-			free($9);
+			free($7);
 		}
 		;
 
@@ -839,6 +832,7 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{ "auth",			AUTH },
 		{ "branch",			BRANCH },
 		{ "connection",			CONNECTION },
 		{ "deny",			DENY },
@@ -851,7 +845,6 @@ lookup(char *s)
 		{ "namespace",			NAMESPACE },
 		{ "notify",			NOTIFY },
 		{ "on",				ON },
-		{ "password",			PASSWORD },
 		{ "path",			PATH },
 		{ "permit",			PERMIT },
 		{ "port",			PORT },
@@ -1192,7 +1185,7 @@ closefile(struct file *xfile)
 
 int
 parse_config(const char *filename, enum gotd_procid proc_id,
-    struct gotd *env)
+    struct gotd_secrets *secrets, struct gotd *env)
 {
 	struct sym *sym, *next;
 	struct gotd_repo *repo;
@@ -1202,6 +1195,7 @@ parse_config(const char *filename, enum gotd_procid pr
 
 	gotd = env;
 	gotd_proc_id = proc_id;
+	gotd->secrets = secrets;
 	TAILQ_INIT(&gotd->repos);
 
 	/* Apply default values. */
@@ -1614,8 +1608,8 @@ conf_notify_email(struct gotd_repo *repo, char *sender
 }
 
 static int
-conf_notify_http(struct gotd_repo *repo, char *url, char *user, char *password,
-    int insecure, char *hmac_secret)
+conf_notify_http(struct gotd_repo *repo, char *url, char *auth, char *hmac,
+    int insecure)
 {
 	const struct got_error *error;
 	struct gotd_notification_target *target;
@@ -1650,15 +1644,23 @@ conf_notify_http(struct gotd_repo *repo, char *url, ch
 		}
 	}
 
-	if ((user != NULL && password == NULL) ||
-	    (user == NULL && password != NULL)) {
-		yyerror("missing username or password");
+	if (auth != NULL && gotd_proc_id == PROC_GOTD &&
+	    (gotd->secrets == NULL || gotd_secrets_get(gotd->secrets,
+	    GOTD_SECRET_AUTH, auth) == NULL)) {
+		yyerror("no auth secret `%s' defined", auth);
 		ret = -1;
 		goto done;
 	}
 
-	if (!insecure && strcmp(proto, "http") == 0 &&
-	    (user != NULL || password != NULL)) {
+	if (hmac != NULL && gotd_proc_id == PROC_GOTD &&
+	    (gotd->secrets == NULL && gotd_secrets_get(gotd->secrets,
+	    GOTD_SECRET_HMAC, hmac) == NULL)) {
+		yyerror("no hmac secret `%s' defined", hmac);
+		ret = -1;
+		goto done;
+	}
+
+	if (!insecure && strcmp(proto, "http") == 0 && auth) {
 		yyerror("%s: HTTP notifications with basic authentication "
 		    "over plaintext HTTP will leak credentials; add the "
 		    "'insecure' config keyword if this is intentional", url);
@@ -1690,17 +1692,14 @@ conf_notify_http(struct gotd_repo *repo, char *url, ch
 	target->conf.http.path = path;
 	hostname = port = path = NULL;
 
-	if (user) {
-		target->conf.http.user = strdup(user);
-		if (target->conf.http.user == NULL)
+	if (auth) {
+		target->conf.http.auth = strdup(auth);
+		if (target->conf.http.auth == NULL)
 			fatal("strdup");
-		target->conf.http.password = strdup(password);
-		if (target->conf.http.password == NULL)
-			fatal("strdup");
 	}
-	if (hmac_secret) {
-		target->conf.http.hmac_secret = strdup(hmac_secret);
-		if (target->conf.http.hmac_secret == NULL)
+	if (hmac) {
+		target->conf.http.hmac = strdup(hmac);
+		if (target->conf.http.hmac == NULL)
 			fatal("strdup");
 	}
 
blob - /dev/null
file + gotd/secrets.c (mode 644)
--- /dev/null
+++ gotd/secrets.c
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2024 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "got_error.h"
+
+#include "log.h"
+#include "secrets.h"
+
+static const struct got_error *
+push(struct gotd_secrets *s, const char *path, int lineno,
+    const char *type, const char *key, const char *val)
+{
+	size_t			 newcap, i;
+	void			*t;
+
+	if (s->len == s->cap) {
+		newcap = s->cap + 16;
+		t = reallocarray(s->secrets, newcap, sizeof(*s->secrets));
+		if (t == NULL)
+			return got_error_from_errno("reallocarray");
+		s->secrets = t;
+		s->cap = newcap;
+	}
+
+	i = s->len;
+	if (!strcmp(type, "auth"))
+		s->secrets[i].type = GOTD_SECRET_AUTH;
+	else if (!strcmp(type, "hmac"))
+		s->secrets[i].type = GOTD_SECRET_HMAC;
+	else {
+		log_warnx("%s:%d invalid type %s", path, lineno, type);
+		return got_error(GOT_ERR_PARSE_CONFIG);
+	}
+
+	if (gotd_secrets_get(s, s->secrets[i].type, key) != NULL) {
+		log_warnx("%s:%d duplicate %s entry %s", path, lineno,
+		    type, key);
+		return got_error(GOT_ERR_PARSE_CONFIG);
+	}
+
+	s->secrets[i].key = strdup(key);
+	if (s->secrets[i].key == NULL)
+		return got_error_from_errno("strdup");
+	s->secrets[i].val = strdup(val);
+	if (s->secrets[i].val == NULL)
+		return got_error_from_errno("strdup");
+
+	s->len++;
+	return NULL;
+}
+
+const struct got_error *
+gotd_secrets_parse(const char *path, FILE *fp, struct gotd_secrets **s)
+{
+	const struct got_error	*err = NULL;
+	int			 lineno = 0;
+	char			*line = NULL;
+	size_t			 linesize = 0;
+	ssize_t			 linelen;
+	char			*type, *key, *val, *t;
+	struct gotd_secrets	*secrets;
+
+	*s = NULL;
+
+	secrets = calloc(1, sizeof(*secrets));
+	if (secrets == NULL)
+		return got_error_from_errno("calloc");
+
+	while ((linelen = getline(&line, &linesize, fp)) != -1) {
+		lineno++;
+		if (line[linelen - 1] == '\n')
+			line[--linelen] = '\0';
+
+		if (*line == '\0' || *line == '#')
+			continue;
+
+		type = line;
+
+		key = type + strcspn(type, " \t");
+		*key++ = '\0';
+		key += strspn(key, " \t");
+
+		val = key + strcspn(key, " \t");
+		*val++ = '\0';
+		val += strspn(val, " \t");
+
+		t = val + strcspn(val, " \t");
+		if (*t != '\0') {
+			log_warnx("%s:%d malformed entry\n", path, lineno);
+			err = got_error(GOT_ERR_PARSE_CONFIG);
+			break;
+		}
+
+		err = push(secrets, path, lineno, type, key, val);
+		if (err)
+			break;
+	}
+	free(line);
+	if (ferror(fp) && err == NULL)
+		err = got_error_from_errno("getline");
+
+	if (err) {
+		gotd_secrets_free(secrets);
+		secrets = NULL;
+	}
+
+	*s = secrets;
+	return err;
+}
+
+const char *
+gotd_secrets_get(struct gotd_secrets *s, enum gotd_secret_type type,
+    const char *key)
+{
+	size_t		 i;
+
+	for (i = 0; i < s->len; ++i) {
+		if (s->secrets[i].type != type)
+			continue;
+		if (strcmp(s->secrets[i].key, key) != 0)
+			continue;
+		return s->secrets[i].val;
+	}
+
+	return NULL;
+}
+
+void
+gotd_secrets_free(struct gotd_secrets *s)
+{
+	size_t		 i;
+
+	for (i = 0; i < s->len; ++i) {
+		free(s->secrets[i].key);
+		free(s->secrets[i].val);
+	}
+
+	free(s);
+}
blob - /dev/null
file + gotd/secrets.h (mode 644)
--- /dev/null
+++ gotd/secrets.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+enum gotd_secret_type {
+	GOTD_SECRET_AUTH,
+	GOTD_SECRET_HMAC,
+};
+
+struct gotd_secret {
+	enum gotd_secret_type	 type;
+	char			*key;	/* label or username		*/
+	char			*val;	/* hmac secret or password	*/
+};
+
+struct gotd_secrets {
+	struct gotd_secret	*secrets;
+	size_t			 len;
+	size_t			 cap;
+};
+
+const struct got_error *gotd_secrets_parse(const char *, FILE *,
+    struct gotd_secrets **);
+const char *gotd_secrets_get(struct gotd_secrets *, enum gotd_secret_type,
+    const char *);
+void gotd_secrets_free(struct gotd_secrets *);
blob - a460654e422802961de72d85be843313f25dc003
file + regress/gotd/.gitignore
--- regress/gotd/.gitignore
+++ regress/gotd/.gitignore
@@ -1 +1,2 @@
 gotd.conf
+gotd-secrets.conf
blob - 5bcb3d151e4aa620cbbc366ecb2783c7b851d6fa
file + regress/gotd/Makefile
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -187,44 +187,48 @@ start_gotd_email_notification: ensure_root
 	@$(GOTD_TRAP); sleep .5
 
 start_gotd_http_notification: ensure_root
+	@echo 'auth flan password' > $(PWD)/gotd-secrets.conf
 	@echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
 	@echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
 	@echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
 	@echo '    path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
 	@echo '    permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
 	@echo '    notify {' >> $(PWD)/gotd.conf
-	@echo '         url "http://localhost:${GOTD_TEST_HTTP_PORT}/" user flan password "password" insecure' >> $(PWD)/gotd.conf
+	@echo '         url "http://localhost:${GOTD_TEST_HTTP_PORT}/" auth flan insecure' >> $(PWD)/gotd.conf
 	@echo "    }" >> $(PWD)/gotd.conf
 	@echo "}" >> $(PWD)/gotd.conf
-	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); $(GOTD_START_CMD) -s $(PWD)/gotd-secrets.conf
 	@$(GOTD_TRAP); sleep .5
 
 start_gotd_email_and_http_notification: ensure_root
+	@echo 'auth flan password' > $(PWD)/gotd-secrets.conf
 	@echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
 	@echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
 	@echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
 	@echo '    path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
 	@echo '    permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
 	@echo '    notify {' >> $(PWD)/gotd.conf
-	@echo '         url "http://localhost:${GOTD_TEST_HTTP_PORT}/" user flan password "password" insecure' >> $(PWD)/gotd.conf
+	@echo '         url "http://localhost:${GOTD_TEST_HTTP_PORT}/" auth flan insecure' >> $(PWD)/gotd.conf
 	@echo -n '      email to ${GOTD_DEVUSER}' >> $(PWD)/gotd.conf
 	@echo ' relay 127.0.0.1 port ${GOTD_TEST_SMTP_PORT}' >> $(PWD)/gotd.conf
 	@echo "    }" >> $(PWD)/gotd.conf
 	@echo "}" >> $(PWD)/gotd.conf
-	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); $(GOTD_START_CMD) -s $(PWD)/gotd-secrets.conf
 	@$(GOTD_TRAP); sleep .5
 
 start_gotd_http_notification_hmac: ensure_root
+	@echo 'auth flan password' > $(PWD)/gotd-secrets.conf
+	@echo 'hmac flan ${GOTD_TEST_HMAC_SECRET}' >> $(PWD)/gotd-secrets.conf
 	@echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
 	@echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
 	@echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
 	@echo '    path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
 	@echo '    permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
 	@echo '    notify {' >> $(PWD)/gotd.conf
-	@echo '         url "http://localhost:${GOTD_TEST_HTTP_PORT}/" user flan password "password" insecure hmac "${GOTD_TEST_HMAC_SECRET}"' >> $(PWD)/gotd.conf
+	@echo '         url "http://localhost:${GOTD_TEST_HTTP_PORT}/" auth flan insecure hmac flan' >> $(PWD)/gotd.conf
 	@echo "    }" >> $(PWD)/gotd.conf
 	@echo "}" >> $(PWD)/gotd.conf
-	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); $(GOTD_START_CMD) -s $(PWD)/gotd-secrets.conf
 	@$(GOTD_TRAP); sleep .5
 
 prepare_test_repo: ensure_root