Hi tech@,
I'm using acme-client(1) to handle my certificates on a bunch of mailservers
(smtps, imaps, pops) and a dedicated syslogd(8) server with tls. My daily cron
on these machines contain a line like this:
rcctl -f start httpd >/dev/null && (acme-client foo.netsend.nl; rcctl stop
httpd >/dev/null)
I think it would be nicer if acme-client is able to start and stop httpd(8)
itself with the config mentioned in acme-client(5) so users on non-webservers
don't have to be bothered with setting up a web server themselves.
The attached patch makes this possible by adding a "-l" switch to acme-client
and simplifies the aforementioned cron entry to the following:
acme-client -l foo.netsend.nl
On first use, the code creates a new acme-client specific httpd.conf(5) in
/etc/httpd.acme.conf but I'm not entirely happy with that. I think it would be
nicer if /etc/ could be left untouched. Other options could be to extend httpd
so that it can read configuration files from stdin, so there is no fiddling
with any config files at all. Or to create the config file in a temporary
location each time acme-client starts.
Another possible improvement might be to automatically and temporarily open up
port 80 in pf, again I'm not sure how desirable that would be.
Feedback and directions are very welcome. :)
Kind regards,
Tim
Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/Makefile,v
retrieving revision 1.8
diff -u -p -r1.8 Makefile
--- Makefile 3 Jul 2017 22:21:47 -0000 1.8
+++ Makefile 1 Oct 2017 18:23:28 -0000
@@ -2,7 +2,7 @@
PROG= acme-client
SRCS= acctproc.c base64.c certproc.c chngproc.c dbg.c dnsproc.c
SRCS+= fileproc.c http.c jsmn.c json.c keyproc.c main.c netproc.c
-SRCS+= parse.y revokeproc.c rsa.c util.c
+SRCS+= parse.y revokeproc.c rsa.c servproc.c util.c
MAN= acme-client.1 acme-client.conf.5
Index: acme-client.1
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/acme-client.1,v
retrieving revision 1.22
diff -u -p -r1.22 acme-client.1
--- acme-client.1 22 Mar 2017 11:14:14 -0000 1.22
+++ acme-client.1 1 Oct 2017 18:23:28 -0000
@@ -41,6 +41,8 @@ Create a new RSA domain key if one does
Force updating the certificate signature even if it's too soon.
.It Fl f Ar configfile
Specify an alternative configuration file.
+.It Fl l
+Listen: let acme-client start a web server internally.
.It Fl n
No operation: check and print configuration.
.It Fl r
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/extern.h,v
retrieving revision 1.8
diff -u -p -r1.8 extern.h
--- extern.h 21 Jan 2017 08:54:26 -0000 1.8
+++ extern.h 1 Oct 2017 18:23:28 -0000
@@ -37,6 +37,14 @@ enum acctop {
};
/*
+ * Requests to servproc.
+ */
+enum servop {
+ SERV_STOP = 0,
+ SERV__MAX
+};
+
+/*
* Requests to and from chngproc.
*/
enum chngop {
@@ -105,6 +113,7 @@ enum comp {
COMP_FILE, /* handles writing certs */
COMP_DNS, /* handles DNS lookups */
COMP_REVOKE, /* checks X509 expiration */
+ COMP_SERV, /* httpd */
COMP__MAX
};
@@ -119,6 +128,7 @@ enum comm {
COMM_PAY,
COMM_NONCE,
COMM_TOK,
+ COMM_SERV_OP,
COMM_CHNG_OP,
COMM_CHNG_ACK,
COMM_ACCT,
@@ -175,6 +185,7 @@ __BEGIN_DECLS
int acctproc(int, const char *, int);
int certproc(int, int);
int chngproc(int, const char *);
+int servproc(int);
int dnsproc(int);
int revokeproc(int, const char *, const char *,
int, int, const char *const *, size_t);
@@ -182,7 +193,7 @@ int fileproc(int, const char *, const
const char *);
int keyproc(int, const char *,
const char **, size_t, int);
-int netproc(int, int, int, int, int, int, int, int,
+int netproc(int, int, int, int, int, int, int, int, int,
struct authority_c *, const char *const *,
size_t, const char *);
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/main.c,v
retrieving revision 1.35
diff -u -p -r1.35 main.c
--- main.c 27 May 2017 08:31:08 -0000 1.35
+++ main.c 1 Oct 2017 18:23:28 -0000
@@ -42,7 +42,7 @@ main(int argc, char *argv[])
char *chngdir = NULL, *auth = NULL, *agreement = NULL;
char *conffile = CONF_FILE;
int key_fds[2], acct_fds[2], chng_fds[2], cert_fds[2];
- int file_fds[2], dns_fds[2], rvk_fds[2];
+ int file_fds[2], dns_fds[2], rvk_fds[2], serv_fds[2];
int force = 0;
int c, rc, revocate = 0;
int popts = 0;
@@ -56,7 +56,7 @@ main(int argc, char *argv[])
struct domain_c *domain = NULL;
struct altname_c *ac;
- while ((c = getopt(argc, argv, "FADrvnf:")) != -1)
+ while ((c = getopt(argc, argv, "FADlrvnf:")) != -1)
switch (c) {
case 'f':
if ((conffile = strdup(optarg)) == NULL)
@@ -71,6 +71,9 @@ main(int argc, char *argv[])
case 'D':
popts |= ACME_OPT_NEWDKEY;
break;
+ case 'l':
+ popts |= ACME_OPT_LISTEN;
+ break;
case 'r':
revocate = 1;
break;
@@ -239,6 +242,8 @@ main(int argc, char *argv[])
err(EXIT_FAILURE, "socketpair");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, rvk_fds) == -1)
err(EXIT_FAILURE, "socketpair");
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, serv_fds) == -1)
+ err(EXIT_FAILURE, "socketpair");
/* Start with the network-touching process. */
@@ -255,9 +260,14 @@ main(int argc, char *argv[])
close(file_fds[1]);
close(dns_fds[0]);
close(rvk_fds[0]);
+ close(serv_fds[0]);
+ if ((popts & ACME_OPT_LISTEN) == 0) {
+ close(serv_fds[1]);
+ serv_fds[1] = -1;
+ }
c = netproc(key_fds[1], acct_fds[1],
chng_fds[1], cert_fds[1],
- dns_fds[1], rvk_fds[1],
+ dns_fds[1], rvk_fds[1], serv_fds[1],
(popts & ACME_OPT_NEWACCT), revocate, authority,
(const char *const *)alts, altsz,
agreement);
@@ -271,6 +281,7 @@ main(int argc, char *argv[])
close(cert_fds[1]);
close(dns_fds[1]);
close(rvk_fds[1]);
+ close(serv_fds[1]);
/* Now the key-touching component. */
@@ -286,6 +297,7 @@ main(int argc, char *argv[])
close(chng_fds[0]);
close(file_fds[0]);
close(file_fds[1]);
+ close(serv_fds[0]);
c = keyproc(key_fds[0], domain->key,
(const char **)alts, altsz, (popts & ACME_OPT_NEWDKEY));
free(alts);
@@ -308,6 +320,7 @@ main(int argc, char *argv[])
close(chng_fds[0]);
close(file_fds[0]);
close(file_fds[1]);
+ close(serv_fds[0]);
c = acctproc(acct_fds[0], acctkey, (popts & ACME_OPT_NEWACCT));
exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
}
@@ -327,6 +340,7 @@ main(int argc, char *argv[])
close(rvk_fds[0]);
close(file_fds[0]);
close(file_fds[1]);
+ close(serv_fds[0]);
c = chngproc(chng_fds[0], chngdir);
exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
}
@@ -344,6 +358,7 @@ main(int argc, char *argv[])
close(dns_fds[0]);
close(rvk_fds[0]);
close(file_fds[1]);
+ close(serv_fds[0]);
c = certproc(cert_fds[0], file_fds[0]);
exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
}
@@ -361,6 +376,7 @@ main(int argc, char *argv[])
free(alts);
close(dns_fds[0]);
close(rvk_fds[0]);
+ close(serv_fds[0]);
c = fileproc(file_fds[1], certdir, certfile, chainfile,
fullchainfile);
/*
@@ -381,6 +397,7 @@ main(int argc, char *argv[])
proccomp = COMP_DNS;
free(alts);
close(rvk_fds[0]);
+ close(serv_fds[0]);
c = dnsproc(dns_fds[0]);
exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
}
@@ -394,6 +411,7 @@ main(int argc, char *argv[])
if (pids[COMP_REVOKE] == 0) {
proccomp = COMP_REVOKE;
+ close(serv_fds[0]);
c = revokeproc(rvk_fds[0], certdir,
certfile != NULL ? certfile : fullchainfile,
force, revocate,
@@ -404,6 +422,27 @@ main(int argc, char *argv[])
close(rvk_fds[0]);
+ /* Optional server component. */
+
+ pids[COMP_SERV] = -1;
+
+ if (popts & ACME_OPT_LISTEN) {
+
+ /* Start a web server ourselves. */
+
+ if ((pids[COMP_SERV] = fork()) == -1)
+ err(EXIT_FAILURE, "fork");
+
+ if (pids[COMP_SERV] == 0) {
+ proccomp = COMP_SERV;
+ free(alts);
+ c = servproc(serv_fds[0]);
+ exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
+ }
+ }
+
+ close(serv_fds[0]);
+
/* Jail: sandbox, file-system, user. */
if (pledge("stdio", NULL) == -1) {
@@ -425,10 +464,15 @@ main(int argc, char *argv[])
checkexit(pids[COMP_DNS], COMP_DNS) +
checkexit(pids[COMP_REVOKE], COMP_REVOKE);
+ if (pids[COMP_SERV] != -1)
+ rc += checkexit(pids[COMP_SERV], COMP_SERV);
+ else
+ rc++;
+
free(alts);
return rc != COMP__MAX ? EXIT_FAILURE : (c == 2 ? EXIT_SUCCESS : 2);
usage:
fprintf(stderr,
- "usage: acme-client [-ADFnrv] [-f configfile] domain\n");
+ "usage: acme-client [-ADFlnrv] [-f configfile] domain\n");
return EXIT_FAILURE;
}
Index: netproc.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/netproc.c,v
retrieving revision 1.13
diff -u -p -r1.13 netproc.c
--- netproc.c 24 Jan 2017 13:32:55 -0000 1.13
+++ netproc.c 1 Oct 2017 18:23:28 -0000
@@ -565,7 +565,7 @@ dofullchain(struct conn *c, const char *
* account key information.
*/
int
-netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
+netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, int sfd,
int newacct, int revocate, struct authority_c *authority,
const char *const *alts,size_t altsz, const char *agreement)
{
@@ -737,13 +737,19 @@ netproc(int kfd, int afd, int Cfd, int c
}
/*
- * Write our acknowledgement that the challenges are over.
- * The challenge process will remove all of the files.
+ * Write our acknowledgement that the challenges are over to both the
+ * challenge process and the server process, if started.
+ * The challenge process will remove all of the files and the server
process
+ * will stop the http server.
*/
if (writeop(Cfd, COMM_CHNG_OP, CHNG_STOP) <= 0)
goto out;
+ if (sfd != -1)
+ if (writeop(sfd, COMM_SERV_OP, SERV_STOP) <= 0)
+ goto out;
+
/* Wait to receive the certificate itself. */
if ((cert = readstr(kfd, COMM_CERT)) == NULL)
@@ -782,6 +788,8 @@ out:
close(Cfd);
close(dfd);
close(rfd);
+ if (sfd != -1)
+ close(sfd);
free(cert);
free(url);
free(thumb);
Index: parse.h
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/parse.h,v
retrieving revision 1.7
diff -u -p -r1.7 parse.h
--- parse.h 21 Jan 2017 12:59:06 -0000 1.7
+++ parse.h 1 Oct 2017 18:23:28 -0000
@@ -62,6 +62,7 @@ struct keyfile {
#define ACME_OPT_NEWACCT 0x00000002
#define ACME_OPT_NEWDKEY 0x00000004
#define ACME_OPT_CHECK 0x00000008
+#define ACME_OPT_LISTEN 0x00000016
struct acme_conf {
int opts;
Index: servproc.c
===================================================================
RCS file: servproc.c
diff -N servproc.c
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ servproc.c 1 Oct 2017 18:23:28 -0000
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2017 Tim Kuijsten <[email protected]>
+ *
+ * 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 AUTHORS DISCLAIM ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS 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 <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "extern.h"
+
+#define HTTPDCONF "/etc/httpd.acme.conf"
+#define HTTPDBIN "/usr/sbin/httpd"
+
+int
+servproc(int netsock)
+{
+ int rc = 0, s, fd = -1;
+ pid_t pid;
+ char cfg[] = "server \"acmeclient\" {\n"
+ " listen on egress port 80\n"
+ " location \"/.well-known/acme-challenge/*\" {\n"
+ " root \"/acme\"\n"
+ " root strip 2\n"
+ " }\n"
+ "}\n";
+
+ if (pledge("stdio wpath cpath proc exec", NULL) == -1)
+ err(EXIT_FAILURE, "%s: pledge", __func__);
+
+ /*
+ * Create a minimal acme config for httpd if one does not yet exist.
+ */
+
+ if ((fd = open(HTTPDCONF, O_WRONLY | O_CREAT | O_EXCL, 0644)) == -1) {
+ if (errno != EEXIST)
+ err(EXIT_FAILURE, "open");
+ } else {
+ s = write(fd, cfg, strlen(cfg));
+ if (s != (ssize_t)strlen(cfg))
+ err(EXIT_FAILURE, "%s: write", __func__);
+ if (close(fd) == -1)
+ err(EXIT_FAILURE, "%s: close", __func__);
+ }
+
+ /* Fork+exec httpd. */
+
+ if ((pid = fork()) == -1)
+ err(EXIT_FAILURE, "%s: fork", __func__);
+
+ if (pid == 0) {
+ if (verbose < 2)
+ close(STDOUT_FILENO);
+ if (verbose < 1)
+ close(STDERR_FILENO);
+
+ if (execle(HTTPDBIN, "httpd", "-df", HTTPDCONF, NULL, NULL) ==
-1)
+ err(EXIT_FAILURE, "%s: execle", __func__);
+ }
+
+ if (pledge("stdio proc", NULL) == -1)
+ err(EXIT_FAILURE, "%s: pledge", __func__);
+
+ /*
+ * Wait until we get a request to stop.
+ */
+
+ if (readop(netsock, COMM_SERV_OP) != SERV_STOP) {
+ warnx("unknown operation from netproc");
+ goto out;
+ }
+
+ if (kill(pid, SIGINT) == -1) {
+ warnx("%s: kill", __func__);
+ goto out;
+ }
+
+ rc = checkexit(pid, COMP_SERV);
+out:
+ if (rc == 0) {
+ warnx("%s: killing httpd, SIGKILL", __func__);
+ if (kill(pid, SIGKILL) == -1)
+ warnx("%s: kill", __func__);
+ rc = checkexit(pid, COMP_SERV);
+ }
+
+ close(netsock);
+ return rc;
+}
Index: util.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/util.c,v
retrieving revision 1.9
diff -u -p -r1.9 util.c
--- util.c 24 Jan 2017 13:32:55 -0000 1.9
+++ util.c 1 Oct 2017 18:23:28 -0000
@@ -42,6 +42,7 @@ static const char *const comps[COMP__MAX
"fileproc", /* COMP_FILE */
"dnsproc", /* COMP_DNS */
"revokeproc", /* COMP_REVOKE */
+ "servproc", /* COMP_SERV */
};
static const char *const comms[COMM__MAX] = {
@@ -51,6 +52,7 @@ static const char *const comms[COMM__MAX
"payload", /* COMM_PAY */
"nonce", /* COMM_NONCE */
"token", /* COMM_TOK */
+ "serv-op", /* COMM_SERV_OP */
"challenge-op", /* COMM_CHNG_OP */
"challenge-ack", /* COMM_CHNG_ACK */
"account", /* COMM_ACCT */