From c0fcf9b9bf0a5162cf75f2c5e588a70004321c8d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 30 Jun 2016 17:37:41 +0200 Subject: Minor manpage fixes. --- lacme-accountd.md | 12 ++++++------ lacme.md | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lacme-accountd.md b/lacme-accountd.md index 81c0802..4d3e1a5 100644 --- a/lacme-accountd.md +++ b/lacme-accountd.md @@ -58,15 +58,15 @@ Options The following command can be used to generate a new 4096-bits RSA key in PEM format with mode 0600: - openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key + openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/account.key -`-socket=`*path* +`--socket=`*path* : Use *path* as the UNIX-domain socket to bind against for signature requests from the [ACME] client. `lacme-accountd` aborts if *path* exists or if its parent directory is writable by other users. -`-?`, `--help` +`-h`, `--help` : Display a brief help and exit. @@ -117,17 +117,17 @@ Examples Run `lacme-accountd` in a first terminal: - ~$ lacme-accountd --privkey=file:/path/to/priv.key --socket=/run/user/1000/S.lacme + ~$ lacme-accountd --privkey=file:/path/to/account.key --socket=$XDG_RUNTIME_DIR/S.lacme Then, while `lacme-accountd` is running, execute locally [`lacme`(1)] in another terminal: - ~$ sudo lacme --socket=/run/user/1000/S.lacme new-cert + ~$ sudo lacme --socket=$XDG_RUNTIME_DIR/S.lacme new-cert Alternatively, use [OpenSSH] 6.7 or later to forward the socket and execute [`lacme`(1)] remotely: - ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:/run/user/1000/S.lacme user@example.org \ + ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:$XDG_RUNTIME_DIR/S.lacme user@example.org \ sudo lacme --socket=/path/to/remote.sock new-cert See also diff --git a/lacme.md b/lacme.md index b7a7f49..ebf54d2 100644 --- a/lacme.md +++ b/lacme.md @@ -117,7 +117,7 @@ Generic options *socket* option of the [`[client]` section](#client-section) of the configuration file. -`-?`, `--help` +`-h`, `--help` : Display a brief help and exit. @@ -282,7 +282,7 @@ Valid options are: following command can be used to generate a new 4096-bits RSA key in PEM format with mode 0600: - openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key + openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/srv.key *min-days* -- cgit v1.2.3 From 968c5f4bb6f6d923b306498dee2c21c418326513 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 30 Jun 2016 19:11:47 +0200 Subject: More useful message upon Validation Challenge failure. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Format the problem document if the JSON has an “error” key. Cf. section 7 “Identifier Validation Challenges”. --- client | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client b/client index 2566c9b..46f388f 100755 --- a/client +++ b/client @@ -284,10 +284,17 @@ elsif ($COMMAND eq 'new-cert') { keyAuthorization => $keyAuthorization }); # wait until the status become 'valid' - for ( my $i = 0, my $status; - $status = request_json_decode($r)->{status} // 'pending', + for ( my $i = 0, my $content, my $status; + $content = request_json_decode($r), + $status = $content->{status} // 'pending', $status ne 'valid'; $r = request('GET' => $challenge->{uri}), $i++ ) { + if (defined (my $problem = $content->{error})) { # problem document (RFC 7807) + my $msg = $problem->{status}; + $msg .= " " .$problem->{title} if defined $problem->{title}; + $msg .= " (".$problem->{detail}.")" if defined $problem->{detail}; + die $msg, "\n"; + } die "Error: Invalid challenge for $domain (status: ".$status.")\n" if $status ne 'pending'; die "Timeout exceeded while waiting for challenge to pass ($domain)\n" if $i >= ($CONFIG->{timeout} // 10); -- cgit v1.2.3 From 79e06aeb557e0af7564d64987600ca2138ce04c3 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 30 Jun 2016 19:22:35 +0200 Subject: Add link to Boulder issue #359 (Implement Certificate Refresh). --- lacme | 1 + 1 file changed, 1 insertion(+) diff --git a/lacme b/lacme index f6d2b7a..3d98760 100755 --- a/lacme +++ b/lacme @@ -516,6 +516,7 @@ if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) { # TODO: renewal without the account key, see # https://github.com/letsencrypt/acme-spec/pull/168 # https://github.com/letsencrypt/acme-spec/issues/191 +# https://github.com/letsencrypt/boulder/issues/359 # elsif ($COMMAND eq 'new-cert') { my $conf; -- cgit v1.2.3 From a9a6208f5c7e0f73fd8b370f99f5c8f58e6b381c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 30 Jun 2016 19:57:12 +0200 Subject: Honor Retry-After headers for certificate issuance and challenge responses. --- client | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/client b/client index 46f388f..3bf0bad 100755 --- a/client +++ b/client @@ -257,6 +257,7 @@ elsif ($COMMAND =~ /\Areg=(\p{Print}+)\Z/) { # elsif ($COMMAND eq 'new-cert') { die unless @ARGV; + my $timeout = $CONFIG->{timeout} // 10; foreach my $domain (@ARGV) { print STDERR "Processing new DNS authz for $domain\n" if $ENV{DEBUG}; my $r = acme_resource('new-authz', identifier => {type => 'dns', value => $domain}); @@ -288,7 +289,7 @@ elsif ($COMMAND eq 'new-cert') { $content = request_json_decode($r), $status = $content->{status} // 'pending', $status ne 'valid'; - $r = request('GET' => $challenge->{uri}), $i++ ) { + $r = request('GET' => $challenge->{uri})) { if (defined (my $problem = $content->{error})) { # problem document (RFC 7807) my $msg = $problem->{status}; $msg .= " " .$problem->{title} if defined $problem->{title}; @@ -296,9 +297,16 @@ elsif ($COMMAND eq 'new-cert') { die $msg, "\n"; } die "Error: Invalid challenge for $domain (status: ".$status.")\n" if $status ne 'pending'; - die "Timeout exceeded while waiting for challenge to pass ($domain)\n" - if $i >= ($CONFIG->{timeout} // 10); - sleep 1; + + my $sleep = 1; + if (defined (my $retry_after = $r->header('Retry-After'))) { + print STDERR "Retrying after $retry_after seconds...\n"; + $sleep = $retry_after; + } + + $i += $sleep; + die "Timeout exceeded while waiting for challenge to pass ($domain)\n" if $timeout > 0 and $i >= $timeout; + sleep $sleep; } } @@ -309,12 +317,17 @@ elsif ($COMMAND eq 'new-cert') { # https://acme-v01.api.letsencrypt.org/acme/cert/$serial print STDERR "Certificate URI: $uri\n"; - # wait for the cert - for (my $i = 0; $r->decoded_content() eq ''; $r = request('GET' => $uri), $i++) { - die request_status_line($r), "\n" unless $r->is_success(); - die "Timeout exceeded while waiting for certificate\n" - if $i >= ($CONFIG->{timeout} // 10); - sleep 1; + if ($r->decoded_content() eq '') { # wait for the cert + for (my $i = 0;;) { + $r = request('GET' => $uri); + die request_status_line($r), "\n" unless $r->is_success(); + last unless $r->code == 202; # Accepted + my $retry_after = $r->header('Retry-After') // 1; + print STDERR "Retrying after $retry_after seconds...\n"; + $i += $retry_after; + die "Timeout exceeded while waiting for certificate\n" if $timeout > 0 and $i >= $timeout; + sleep $retry_after; + } } my $der = $r->decoded_content(); -- cgit v1.2.3 From 880b0f357bbc48589db0de8b9800956801fbe35e Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 10 Oct 2016 13:30:38 +0200 Subject: Manpage: update Subscriber Agreement URL to v1.1.1. Cf. https://letsencrypt.org/repository/ . --- lacme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lacme.md b/lacme.md index ebf54d2..f29f24f 100644 --- a/lacme.md +++ b/lacme.md @@ -339,7 +339,7 @@ Examples ======== ~$ sudo lacme new-reg mailto:noreply@example.com - ~$ sudo lacme reg=/acme/reg/137760 --agreement-uri=https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf + ~$ sudo lacme reg=/acme/reg/137760 --agreement-uri=https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf ~$ sudo lacme new-cert ~$ sudo lacme revoke-cert /path/to/server/certificate.pem -- cgit v1.2.3 From ec03eae5c2a39f4a878ad910eda3c414c6d2d51f Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 30 Nov 2016 21:55:19 +0100 Subject: Stop mentioning GET-based renewal, as it was removed from the ACME IETF draft. https://github.com/ietf-wg-acme/acme/issues/62 https://github.com/ietf-wg-acme/acme/pull/67 :-( --- lacme | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lacme b/lacme index 3d98760..13bff78 100755 --- a/lacme +++ b/lacme @@ -513,10 +513,6 @@ if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) { ############################################################################# # new-cert [SECTION ..] -# TODO: renewal without the account key, see -# https://github.com/letsencrypt/acme-spec/pull/168 -# https://github.com/letsencrypt/acme-spec/issues/191 -# https://github.com/letsencrypt/boulder/issues/359 # elsif ($COMMAND eq 'new-cert') { my $conf; -- cgit v1.2.3 From dd1da2ac44a7eab89e9a17135367aa0915efad0b Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 30 Nov 2016 22:07:09 +0100 Subject: typo --- config/lacme-accountd.conf | 4 ++-- config/lacme-certs.conf | 2 +- config/lacme.conf | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf index 0a8b81a..443b4f1 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -11,11 +11,11 @@ # gpg(1) to use, as well as some default options. Default: "gpg # --quiet". # -#gpg = gpg2 --quiet --no-auto-check-trustdb +#gpg = gpg --quiet --no-auto-check-trustdb # The value of "socket" specifies the UNIX-domain socket to bind against # for signature requests from the ACME client. An error is raised if -# the path exists exists or if its parent directory is writable by other +# the path exists or if its parent directory is writable by other # users. # Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR # environment variable is set. diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf index fbce5e2..9b9df2f 100644 --- a/config/lacme-certs.conf +++ b/config/lacme-certs.conf @@ -32,7 +32,7 @@ # required. #subject = /CN=example.org -# Comma-separated list of Subject Alternative Names. +# Comma-separated list of Subject Alternative Names. #subjectAltName = DNS:example.org,DNS:www.example.org # username[:groupname] to chown the issued certificate and diff --git a/config/lacme.conf b/config/lacme.conf index c5c643d..d64276c 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -1,7 +1,7 @@ # For certificate issuance (new-cert command), specify the certificate # configuration file to use # -#config-certs = config/lacme-certs.conf +#config-certs = /etc/lacme/lacme-certs.conf [client] # The value of "socket" specifies the lacme-accountd(1) UNIX-domain @@ -29,7 +29,7 @@ #command = /usr/lib/lacme/client # Root URI of the ACME server. NOTE: Use the staging server for testing -# as it has relaxed ratelimit. +# as it has relaxed rate-limiting. # #server = https://acme-v01.api.letsencrypt.org/ #server = https://acme-staging.api.letsencrypt.org/ @@ -72,7 +72,7 @@ # and also setting the list of supplementary gids to that single group). # Preserve root privileges if the value is empty (not recommended). # -#user = www-data +#group = www-data # Path to the ACME webserver executable. #command = /usr/lib/lacme/webserver -- cgit v1.2.3 From 27788fd4a399642eddbdb1934ccaa13f7fd00124 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 1 Dec 2016 00:16:18 +0100 Subject: Make lacme able to spawn lacme-accountd. --- config/lacme.conf | 41 ++++++++++++++++++++++++++--- lacme | 77 ++++++++++++++++++++++++++++++++++++++++--------------- lacme-accountd | 37 ++++++++++++++++---------- lacme.md | 42 +++++++++++++++++++++++++++++- 4 files changed, 159 insertions(+), 38 deletions(-) diff --git a/config/lacme.conf b/config/lacme.conf index d64276c..23313c7 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -4,12 +4,15 @@ #config-certs = /etc/lacme/lacme-certs.conf [client] -# The value of "socket" specifies the lacme-accountd(1) UNIX-domain -# socket to connect to for signature requests from the ACME client. -# lacme(1) aborts if the socket is readable or writable by other users, -# or if its parent directory is writable by other users. +# The value of "socket" specifies the path to the lacme-accountd(1) +# UNIX-domain socket to connect to for signature requests from the ACME +# client. lacme(1) aborts if the socket is readable or writable by +# other users, or if its parent directory is writable by other users. # Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR environment # variable is set. +# This option is ignored when lacme-accountd(1) is spawned by lacme(1), +# since the two processes communicate through a socket pair. See the +# "accountd" section below for details. # #socket = /run/user/1000/S.lacme @@ -83,4 +86,34 @@ # #iptables = Yes + +# lacme-accound(1) section. Comment out the following section to make +# lacme(1) connect to an existing UNIX-domain socket bound by a running +# acme-accountd(1). +[accountd] + +# username to drop privileges to (setting both effective and real uid). +# Preserve root privileges if the value is empty. +# +#user = root + +# groupname to drop privileges to (setting both effective and real gid, +# and also setting the list of supplementary gids to that single group). +# Preserve root privileges if the value is empty. +# +#group = root + +# Path to the lacme-accountd(1) executable. +#command = /usr/bin/lacme-accountd + +# Path to the lacme-accountd(1) configuration file. +#config = /etc/lacme/lacme-accountd.conf + +# The (private) account key to use for signing requests. See +# lacme-accountd(1) for details. +#privkey = file:/path/to/account.key + +# Be quiet. +#quiet = Yes + ; vim:ft=dosini diff --git a/lacme b/lacme index 13bff78..05b4b18 100755 --- a/lacme +++ b/lacme @@ -30,7 +30,7 @@ use File::Temp (); use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; use POSIX (); -use Socket qw/PF_INET PF_INET6 PF_UNIX INADDR_ANY IN6ADDR_ANY +use Socket qw/AF_UNIX PF_INET PF_INET6 PF_UNIX PF_UNSPEC INADDR_ANY IN6ADDR_ANY SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/; use Config::Tiny (); @@ -82,6 +82,7 @@ do { my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; my $defaults = delete $h->{_} // {}; + my $accountd = exists $h->{accountd} ? 1 : 0; my %valid = ( client => { socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), @@ -99,6 +100,14 @@ do { command => '/usr/lib/lacme/webserver', iptables => 'Yes' + }, + accountd => { + user => '', + group => '', + command => '/usr/bin/lacme-accountd', + config => '/etc/lacme/lacme-accountd.conf', + privkey => undef, + quiet => 'Yes', } ); foreach my $s (keys %valid) { @@ -110,6 +119,7 @@ do { } die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; $CONFIG->{_} = $defaults; + delete $CONFIG->{accountd} unless $accountd; }; # Regular expressions for domain validation @@ -388,31 +398,58 @@ sub acme_client($@) { my $args = shift; my @args = @_; - my @stat; + my $client; my $conf = $CONFIG->{client}; - my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; - $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname - - # ensure we're the only user with write access to the parent dir - my $dirname = $sockname =~ s/[^\/]+$//r; - @stat = stat($dirname) or die "Can't stat $dirname: $!"; - die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; - - # ensure we're the only user with read/write access to the socket - @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n"; - die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; - - # connect(2) to the socket - socket(my $client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; - my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n"; - until (connect($client, $sockaddr)) { - next if $! == EINTR; # try again if connect(2) was interrupted by a signal - die "connect: $!"; + if (defined (my $accountd = $CONFIG->{accountd})) { + socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; + my $pid = fork() // "fork: $!"; + unless ($pid) { + drop_privileges($accountd->{user}, $accountd->{group}, '/'); + set_FD_CLOEXEC($s, 0); + $client->close() or die "Can't close: $!"; + my @cmd = ($accountd->{command}, '--fdopen='.fileno($s)); + push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; + push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; + push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no'; + push @cmd, '--debug' if $OPTS{debug}; + exec { $cmd[0] } @cmd or die; + } + print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; + $s->close() or die "Can't close: $!"; + push @CLEANUP, sub() { + print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug}; + shutdown($client, SHUT_RDWR) or warn "shutdown: $!"; + kill 15 => $pid; + waitpid $pid => 0; + }; + } + else { + my @stat; + my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; + $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname + + # ensure we're the only user with write access to the parent dir + my $dirname = $sockname =~ s/[^\/]+$//r; + @stat = stat($dirname) or die "Can't stat $dirname: $!"; + die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + + # ensure we're the only user with read/write access to the socket + @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n"; + die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; + + # connect(2) to the socket + socket($client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; + my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n"; + until (connect($client, $sockaddr)) { + next if $! == EINTR; # try again if connect(2) was interrupted by a signal + die "connect: $!"; + } } # use execve(2) rather than a Perl pseudo-process to ensure that the # child doesn't have access to the parent's memory my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno + set_FD_CLOEXEC($client, 1); spawn({%$args{qw/in out/}, child => sub() { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); diff --git a/lacme-accountd b/lacme-accountd index fbf1bcb..411538d 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -59,7 +59,7 @@ sub usage(;$$) { } exit $rv; } -usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s quiet|q debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s fdopen=i quiet|q debug help|h/); usage(0) if $OPTS{help}; do { @@ -137,7 +137,10 @@ $JWK = JSON::->new->encode($JWK); # to support the abstract namespace.) The downside is that we have to # delete the file manually. # -do { +if (defined $OPTS{fdopen}) { + die "Invalid file descriptor" unless $OPTS{fdopen} =~ /\A(\d+)\z/; + open $S, '+<&=', $1 or die "fdopen $1: $!"; +} else { my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef); die "Missing socket option\n" unless defined $sockname; $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname @@ -165,26 +168,34 @@ do { # For each new connection, send the protocol version and the account key's # public parameters, then sign whatever comes in # -$SIG{PIPE} = 'IGNORE'; # ignore broken pipes -for (my $count = 0;; $count++) { - accept(my $conn, $S) or do { - next if $! == EINTR; # try again if accept(2) was interrupted by a signal - die "accept: $!"; - }; - print STDERR "[$count]>> Accepted new connection\n" unless $OPTS{quiet}; - +sub conn($;$) { + my $conn = shift; + my $count = shift; $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK, "\r\n" ); # sign whatever comes in while (defined (my $data = $conn->getline())) { $data =~ s/\r\n\z// or die; - print STDERR "[$count]>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; + print STDERR "[$count] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; my $sig = $SIGN->($data); $conn->printflush( encode_base64url($sig), "\r\n" ); } +} - print STDERR "[$count]>> Connection terminated\n" unless $OPTS{quiet}; - close $conn or warn "Can't close: $!"; +if (defined $OPTS{fdopen}) { + conn($S, $$); +} else { + $SIG{PIPE} = 'IGNORE'; # ignore broken pipes + for (my $count = 0;; $count++) { + accept(my $conn, $S) or do { + next if $! == EINTR; # try again if accept(2) was interrupted by a signal + die "accept: $!"; + }; + print STDERR "[$count] >>> Accepted new connection\n" unless $OPTS{quiet}; + conn($conn, $count); + print STDERR "[$count] >>> Connection terminated\n" unless $OPTS{quiet}; + close $conn or warn "Can't close: $!"; + } } diff --git a/lacme.md b/lacme.md index f29f24f..93f348c 100644 --- a/lacme.md +++ b/lacme.md @@ -26,7 +26,9 @@ with its own executable: the [ACME] client.) One can use the UNIX-domain socket forwarding facility of OpenSSH 6.7 and later to run [`lacme-accountd`(1)] and `lacme` on different - hosts. + hosts. Alternatively, the [`lacme-accountd`(1)] process can be + spawned by the “master” `lacme` process below; the communication + between the two then goes through a socket pair. 2. A “master” `lacme` process, which runs as root and is the only component with access to the private key material of the server @@ -249,6 +251,44 @@ This section is used for configuring the [ACME] webserver. automatically removed once `lacme` exits. Default: `Yes`. +`[accountd]` section +--------------------- + +This section is used for configuring the [`lacme-accountd`(1)] process. +If the section (including its header) is absent or commented out, +`lacme` connects to an existing UNIX-domain socket bound by a running +[`lacme-accountd`(1)]. + +*user* + +: The username to drop privileges to (setting both effective and real + uid). Preserve root privileges if the value is empty. + +*group* + +: The groupname to drop privileges to (setting both effective and real + gid, and also setting the list of supplementary gids to that single + group). + +*command* + +: Path to the [`lacme-accountd`(1)] executable. + Default: `/usr/bin/lacme-accountd`. + +*config* + +: Path to the [`lacme-accountd`(1)] configuration file. + Default: `/etc/lacme/lacme-accountd.conf`. + +*privkey* + +: The (private) account key to use for signing requests. See + [`lacme-accountd`(1)] for details. + +*quiet* + +: Be quiet. Possible values: `Yes`/`No`. + Certificate configuration file ============================== -- cgit v1.2.3 From 458acba80be362c9f59e976a62b9749a7809c4aa Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 1 Dec 2016 00:19:11 +0100 Subject: wibble --- config/lacme.conf | 2 +- lacme.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/lacme.conf b/config/lacme.conf index 23313c7..39cfd36 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -89,7 +89,7 @@ # lacme-accound(1) section. Comment out the following section to make # lacme(1) connect to an existing UNIX-domain socket bound by a running -# acme-accountd(1). +# acme-accountd(1) process. [accountd] # username to drop privileges to (setting both effective and real uid). diff --git a/lacme.md b/lacme.md index 93f348c..550aa49 100644 --- a/lacme.md +++ b/lacme.md @@ -27,8 +27,8 @@ with its own executable: One can use the UNIX-domain socket forwarding facility of OpenSSH 6.7 and later to run [`lacme-accountd`(1)] and `lacme` on different hosts. Alternatively, the [`lacme-accountd`(1)] process can be - spawned by the “master” `lacme` process below; the communication - between the two then goes through a socket pair. + spawned by the “master” `lacme` process below; in that case, the + two processes communicate through a socket pair. 2. A “master” `lacme` process, which runs as root and is the only component with access to the private key material of the server @@ -257,7 +257,7 @@ This section is used for configuring the [ACME] webserver. This section is used for configuring the [`lacme-accountd`(1)] process. If the section (including its header) is absent or commented out, `lacme` connects to an existing UNIX-domain socket bound by a running -[`lacme-accountd`(1)]. +[`lacme-accountd`(1)] process. *user* -- cgit v1.2.3 From e33088685aa300b903cb4216585a108d1f638ce4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 1 Dec 2016 00:21:03 +0100 Subject: wibble --- lacme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lacme.md b/lacme.md index 550aa49..ea744b0 100644 --- a/lacme.md +++ b/lacme.md @@ -268,7 +268,7 @@ If the section (including its header) is absent or commented out, : The groupname to drop privileges to (setting both effective and real gid, and also setting the list of supplementary gids to that single - group). + group). Preserve root privileges if the value is empty. *command* -- cgit v1.2.3 From aa514ea28e997765a40f5efdebbf24e9a6e83cef Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 1 Dec 2016 00:23:01 +0100 Subject: s/priv.key/account.key/ --- config/lacme-accountd.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf index 443b4f1..94d2556 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -4,8 +4,8 @@ # - file:FILE, to specify an encrypted private key (in PEM format) # - gpg:FILE, to specify a gpg-encrypted private key (in PEM format) # -#privkey = gpg:/path/to/encrypted/priv.key.gpg -#privkey = file:/path/to/priv.key +#privkey = gpg:/path/to/encrypted/account.key.gpg +#privkey = file:/path/to/account.key # For a gpg-encrypted private account key, "gpg" specifies the binary # gpg(1) to use, as well as some default options. Default: "gpg -- cgit v1.2.3 From 844edd3dd60590bafcaa863eedb6cda94a0e07a3 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 1 Dec 2016 00:37:52 +0100 Subject: lacme: add an option --quiet to avoid mentioning valid certs. --- lacme | 5 +++-- lacme.md | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lacme b/lacme index 05b4b18..839d53d 100755 --- a/lacme +++ b/lacme @@ -60,7 +60,7 @@ sub usage(;$$) { } exit $rv; } -usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s quiet|q debug help|h/); usage(0) if $OPTS{help}; $COMMAND = shift(@ARGV) // usage(1, "Missing command"); @@ -120,6 +120,7 @@ do { die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; $CONFIG->{_} = $defaults; delete $CONFIG->{accountd} unless $accountd; + $OPTS{quiet} = 0 if $OPTS{debug}; }; # Regular expressions for domain validation @@ -592,7 +593,7 @@ elsif ($COMMAND eq 'new-cert') { my $d = $conf->{'min-days'} // 10; if ($d > 0 and $t - time > $d*86400) { my $d = POSIX::strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($t)); - print STDERR "[$s] Valid until $d, skipping\n"; + print STDERR "[$s] Valid until $d, skipping\n" unless $OPTS{quiet}; next; } } diff --git a/lacme.md b/lacme.md index ea744b0..b086fe7 100644 --- a/lacme.md +++ b/lacme.md @@ -123,6 +123,10 @@ Generic options : Display a brief help and exit. +`-q`, `--quiet` + +: Be quiet. + `--debug` : Turn on debug mode. -- cgit v1.2.3 From 8faab5db6571972156f45b5838b23dbb0fadd5c4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 1 Dec 2016 10:41:33 +0100 Subject: lacme: avoid spawning multiple accountd processes. --- lacme | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lacme b/lacme index 839d53d..cf2f9eb 100755 --- a/lacme +++ b/lacme @@ -395,6 +395,7 @@ sub spawn_webserver() { # If $args->{in} is defined, the data is written to the client's STDIN. # If $args->{out} is defined, its value is set to client's STDOUT data. # +my $ACCOUNTD = 0; sub acme_client($@) { my $args = shift; my @args = @_; @@ -402,27 +403,30 @@ sub acme_client($@) { my $client; my $conf = $CONFIG->{client}; if (defined (my $accountd = $CONFIG->{accountd})) { - socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; - my $pid = fork() // "fork: $!"; - unless ($pid) { - drop_privileges($accountd->{user}, $accountd->{group}, '/'); - set_FD_CLOEXEC($s, 0); - $client->close() or die "Can't close: $!"; - my @cmd = ($accountd->{command}, '--fdopen='.fileno($s)); - push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; - push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; - push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no'; - push @cmd, '--debug' if $OPTS{debug}; - exec { $cmd[0] } @cmd or die; + unless ($ACCOUNTD) { + socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; + my $pid = fork() // "fork: $!"; + unless ($pid) { + drop_privileges($accountd->{user}, $accountd->{group}, '/'); + set_FD_CLOEXEC($s, 0); + $client->close() or die "Can't close: $!"; + my @cmd = ($accountd->{command}, '--fdopen='.fileno($s)); + push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; + push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; + push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no'; + push @cmd, '--debug' if $OPTS{debug}; + exec { $cmd[0] } @cmd or die; + } + print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; + $ACCOUNTD = $pid; + $s->close() or die "Can't close: $!"; + push @CLEANUP, sub() { + print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug}; + shutdown($client, SHUT_RDWR) or warn "shutdown: $!"; + kill 15 => $pid; + waitpid $pid => 0; + }; } - print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; - $s->close() or die "Can't close: $!"; - push @CLEANUP, sub() { - print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug}; - shutdown($client, SHUT_RDWR) or warn "shutdown: $!"; - kill 15 => $pid; - waitpid $pid => 0; - }; } else { my @stat; -- cgit v1.2.3