From dcf25ee7dd6762d25b59e5f87eaa320a8f2be05c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 28 Jun 2017 22:38:39 +0200 Subject: Update README. --- README | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README b/README index ea8cc66..5e17a8d 100644 --- a/README +++ b/README @@ -22,14 +22,12 @@ own executable: of the account key manager to the ACME client: data signatures are requested by writing the data to be signed to the socket. - * For certificate issuances, an optional webserver, which is spawned - by the "master" process when no service is listening on the HTTP - port. (The only challenge type currently supported is "http-01", - which requires a webserver to answer challenges.) That webserver - only processes GET and HEAD requests under the - "/.well-known/acme-challenge/" URI. By default some iptables(8) - rules are automatically installed to open the HTTP port, and removed - afterwards. + * For certificate issuances, an optional webserver which is spawned by + the "master". (The only challenge type currently supported is + "http-01", which requires a webserver to answer challenges.) That + webserver only processes GET and HEAD requests under the + "/.well-known/acme-challenge/" URI. Moreover temporary iptables(8) + rules can be automatically installed to open the HTTP port. Consult the manuals for more information. -- cgit v1.2.3 From 8fd46f8f562345bb6c26b3eb8307994378732b94 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 28 Jun 2017 22:45:12 +0200 Subject: Improve docs. --- lacme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lacme.md b/lacme.md index 4146515..7f2e616 100644 --- a/lacme.md +++ b/lacme.md @@ -223,7 +223,8 @@ of [ACME] commands and dialogues with the remote [ACME] server). `[webserver]` section --------------------- -This section is used for configuring the [ACME] webserver. +This section is used to configure how [ACME] challenge responses are +served. *listen* @@ -258,7 +259,8 @@ This section is used for configuring the [ACME] webserver. *command* -: Path to the [ACME] webserver executable. +: Path to the [ACME] webserver executable. A separate process is + spawned for each address to *listen* on. Default: `/usr/lib/lacme/webserver`. *iptables* -- cgit v1.2.3 From 73ac1dd0d4d47905e8a407bcb1bf2ac494c34c86 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 09:44:43 +0200 Subject: Improve docs. --- config/nginx.conf | 6 +++--- lacme.md | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/config/nginx.conf b/config/nginx.conf index f842c12..e4ceb0c 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -1,10 +1,10 @@ -# Let nginx serve ACME requests directly, or pass them to lacme's -# webserver component. +# Use nginx to serve ACME requests; either directly, or by passing them +# over to a locally-bound lacme webserver component. # # This file needs to be sourced to the server directives (at least the # non-ssl one) of each virtual host requiring authorization. -location /.well-known/acme-challenge/ { +location ^~ /.well-known/acme-challenge/ { # Pass ACME requests to lacme's webserver component proxy_pass http://unix:/var/run/lacme.socket; diff --git a/lacme.md b/lacme.md index 7f2e616..3ba4a44 100644 --- a/lacme.md +++ b/lacme.md @@ -224,7 +224,7 @@ of [ACME] commands and dialogues with the remote [ACME] server). --------------------- This section is used to configure how [ACME] challenge responses are -served. +served during certificate issuance. *listen* @@ -232,6 +232,12 @@ served. addresses are of the form `IPV4:PORT`, `[IPV6]:PORT` (where the `:PORT` suffix is optional and defaults to the HTTP port 80), or an absolute path of a UNIX-domain socket (created with mode `0666`). + Since the webserver component listens to a UNIX-domain socket by + default, it is only suitable when an external HTTP daemon is + publicly reachable and passes all ACME challenge requests to that + socket; if that's not the case, one needs to set *listen* to `[::]` + (or `0.0.0.0 [::]` when dual stack IPv4/IPv6 is disabled or + unavailable), and possibly also set *iptables* to `Yes`. Default: `/var/run/lacme.socket`. *challenge-directory* -- cgit v1.2.3 From 7da82bf4ce1d40b730c4ace0817ccbcb862221ee Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 09:49:05 +0200 Subject: wibble --- lacme.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lacme.md b/lacme.md index 3ba4a44..d2a3b46 100644 --- a/lacme.md +++ b/lacme.md @@ -232,14 +232,15 @@ served during certificate issuance. addresses are of the form `IPV4:PORT`, `[IPV6]:PORT` (where the `:PORT` suffix is optional and defaults to the HTTP port 80), or an absolute path of a UNIX-domain socket (created with mode `0666`). - Since the webserver component listens to a UNIX-domain socket by - default, it is only suitable when an external HTTP daemon is - publicly reachable and passes all ACME challenge requests to that - socket; if that's not the case, one needs to set *listen* to `[::]` - (or `0.0.0.0 [::]` when dual stack IPv4/IPv6 is disabled or - unavailable), and possibly also set *iptables* to `Yes`. Default: `/var/run/lacme.socket`. + Note: The default value is only suitable when an external HTTP + daemon is publicly reachable and passes all ACME challenge requests + to the webserver component through the UNIX-domain socket + `/var/run/lacme.socket`; if that's not the case, one needs to set + *listen* to `[::]` (or `0.0.0.0 [::]` when dual stack IPv4/IPv6 is + disabled or unavailable), and possibly also set *iptables* to `Yes`. + *challenge-directory* : Specify a non-existent directory under which an external HTTP daemon -- cgit v1.2.3 From 3dde7848732e6fe3f0323866b7fe06cc12748bf5 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 10:01:21 +0200 Subject: wibble --- lacme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lacme.md b/lacme.md index d2a3b46..d18b176 100644 --- a/lacme.md +++ b/lacme.md @@ -238,7 +238,7 @@ served during certificate issuance. daemon is publicly reachable and passes all ACME challenge requests to the webserver component through the UNIX-domain socket `/var/run/lacme.socket`; if that's not the case, one needs to set - *listen* to `[::]` (or `0.0.0.0 [::]` when dual stack IPv4/IPv6 is + *listen* to `[::]` (or `0.0.0.0 [::]` when dual IPv4/IPv6 stack is disabled or unavailable), and possibly also set *iptables* to `Yes`. *challenge-directory* -- cgit v1.2.3 From 15639f5b1aa607ccb4fec1a41643a3b916e0e44a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 10:48:35 +0200 Subject: webserver: refuse to follow symlink when serving ACME challenge responses. --- Changelog | 7 ++++++- lacme | 2 +- webserver | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 59d5153..9b13c44 100644 --- a/Changelog +++ b/Changelog @@ -4,7 +4,7 @@ lacme (0.3) upstream; lacme-certs.conf.d"), import the default section of files read earlier. + new-cert: create certificate files atomically. + webserver: allow listening to multiple addresses (useful when - dual-stack IPv4/IPv6 is not supported). Listen to a UNIX-domain + dual IPv4/IPv6 stack is not supported). Listen to a UNIX-domain socket by default . + webserver: don't install temporary iptables by default. Hosts without a public HTTP daemon listening on port 80 need to set the @@ -21,6 +21,11 @@ lacme (0.3) upstream; - new-cert: mark the basicConstraints (CA:FALSE) and keyUsage x509v3 extensions as critical in the CSR, following upstream fix of Boulder's issue #565. + - webserver: refuse to follow symlink when serving ACME challenge + responses. When dropping privileges to a dedicated UID + (recommended) only the ACME client could write to its current + directory anyway, so following symlinks was not a serious + vulnerability. -- Guilhem Moulin Sun, 19 Feb 2017 13:08:41 +0100 diff --git a/lacme b/lacme index d7a416e..7adc972 100755 --- a/lacme +++ b/lacme @@ -24,7 +24,7 @@ use warnings; our $VERSION = '0.0.1'; my $NAME = 'lacme'; -use Errno qw/EADDRINUSE EINTR/; +use Errno 'EINTR'; use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; use File::Temp (); use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; diff --git a/webserver b/webserver index 7914762..21486ae 100755 --- a/webserver +++ b/webserver @@ -91,7 +91,11 @@ while (1) { while (defined (my $h = $conn->getline())) { last if $h eq "\r\n" }; my ($status_line, $content_type, $content); - if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and -f $1) { + if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and + ! -l $1 and -f _) { # reuse previous stat structure and save a syscall + # XXX calling lstat(2) before open(2) is racy; if O_NOFOLLOW was + # exposed to perl we would instead use it and later fstat(2) the + # file descriptor if (open my $fh, '<', $1) { # only open files in the cwd ($status_line, $content_type) = ('200 OK', 'application/jose+json'); $content = do { local $/ = undef; $fh->getline() }; -- cgit v1.2.3 From a528bcffe2480245185a3b8d6e6c51307635a4ea Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 10:52:01 +0200 Subject: lacme(1), lacme-accountd(1): fix version number. --- Changelog | 2 ++ lacme | 2 +- lacme-accountd | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 9b13c44..1fd6762 100644 --- a/Changelog +++ b/Changelog @@ -26,6 +26,8 @@ lacme (0.3) upstream; (recommended) only the ACME client could write to its current directory anyway, so following symlinks was not a serious vulnerability. + - lacme(1), lacme-accountd(1): fix version number shown with + --version. -- Guilhem Moulin Sun, 19 Feb 2017 13:08:41 +0100 diff --git a/lacme b/lacme index 7adc972..1065e67 100755 --- a/lacme +++ b/lacme @@ -21,7 +21,7 @@ use strict; use warnings; -our $VERSION = '0.0.1'; +our $VERSION = '0.3'; my $NAME = 'lacme'; use Errno 'EINTR'; diff --git a/lacme-accountd b/lacme-accountd index 00d6ccd..547af59 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -22,7 +22,7 @@ use strict; use warnings; -our $VERSION = '0.0.1'; +our $VERSION = '0.3'; my $PROTOCOL_VERSION = 1; my $NAME = 'lacme-accountd'; -- cgit v1.2.3 From 7c7e01fa8d8623145078cc352c3617ad43ebe326 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 10:52:01 +0200 Subject: Remove potential race when creating ACME challenge response files. --- Changelog | 2 ++ client | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Changelog b/Changelog index 1fd6762..5252fd2 100644 --- a/Changelog +++ b/Changelog @@ -28,6 +28,8 @@ lacme (0.3) upstream; vulnerability. - lacme(1), lacme-accountd(1): fix version number shown with --version. + - client: remove potential race when creating ACME challenge response + files. -- Guilhem Moulin Sun, 19 Feb 2017 13:08:41 +0100 diff --git a/client b/client index cd94ed8..333ae3b 100755 --- a/client +++ b/client @@ -44,10 +44,13 @@ use warnings; my $PROTOCOL_VERSION = 1; -use LWP::UserAgent (); +use Errno 'EEXIST'; +use Fcntl qw/O_CREAT O_EXCL O_WRONLY/; +use Digest::SHA qw/sha256 sha256_hex/; use MIME::Base64 qw/encode_base64 encode_base64url/; + +use LWP::UserAgent (); use JSON (); -use Digest::SHA qw/sha256 sha256_hex/; use Config::Tiny (); @@ -266,18 +269,19 @@ elsif ($COMMAND eq 'new-cert') { @{request_json_decode($r)->{challenges} // []}; die "Missing 'http-01' challenge in server response" unless defined $challenge; die "Invalid challenge token ".($challenge->{token} // '')."\n" + # ensure we don't write outside the cwd unless ($challenge->{token} // '') =~ /\A[A-Za-z0-9_\-]+\z/; my $keyAuthorization = $challenge->{token}.'.'.$JWK_thumbprint; # serve $keyAuthorization at http://$domain/.well-known/acme-challenge/$challenge->{token} - if (-e $challenge->{token}) { - print STDERR "WARNING: File exists: $challenge->{token}\n"; - } - else { - open my $fh, '>', $challenge->{token} or die "Can't open $challenge->{token}: $!"; + if (sysopen(my $fh, $challenge->{token}, O_CREAT|O_EXCL|O_WRONLY, 0644)) { $fh->print($keyAuthorization); $fh->close() or die "Can't close: $!"; + } elsif ($! == EEXIST) { + print STDERR "WARNING: File exists: $challenge->{token}\n"; + } else { + die "Can't open $challenge->{token}: $!"; } $r = acme($challenge->{uri}, { -- cgit v1.2.3 From a71cce0e99270492dbfa1567c046cd7db79ffd64 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 12:06:35 +0200 Subject: webserver: open ACME challenge files with O_NOFOLLOW. --- webserver | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/webserver b/webserver index 21486ae..211f646 100755 --- a/webserver +++ b/webserver @@ -38,6 +38,7 @@ use warnings; # not a problem since FD can be bound as root prior to the execve(2). use Errno 'EINTR'; +use Fcntl qw/O_NOFOLLOW O_RDONLY/; use Socket qw/AF_UNIX AF_INET AF_INET6/; # Untaint and fdopen(3) the listening socket @@ -91,12 +92,9 @@ while (1) { while (defined (my $h = $conn->getline())) { last if $h eq "\r\n" }; my ($status_line, $content_type, $content); - if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and - ! -l $1 and -f _) { # reuse previous stat structure and save a syscall - # XXX calling lstat(2) before open(2) is racy; if O_NOFOLLOW was - # exposed to perl we would instead use it and later fstat(2) the - # file descriptor - if (open my $fh, '<', $1) { # only open files in the cwd + if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and -f $1) { + # only open files in the cwd, and refuse to follow symlinks + if (sysopen(my $fh, $1, O_NOFOLLOW|O_RDONLY)) { ($status_line, $content_type) = ('200 OK', 'application/jose+json'); $content = do { local $/ = undef; $fh->getline() }; $fh->close() or die "close: $!"; -- cgit v1.2.3 From 97b4aad955ea816d7cc2273c1fd85fe139ec6207 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 14:53:40 +0200 Subject: webserver: improve serving logic for ACME challenge responses. In particular, we now return "403 Forbidden" for /.well-known/acme-challenge/ --- webserver | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/webserver b/webserver index 211f646..90be70c 100755 --- a/webserver +++ b/webserver @@ -37,7 +37,7 @@ use warnings; # as "www-data:www-data"; bind(2)'ing to a privileged port such as 80 is # not a problem since FD can be bound as root prior to the execve(2). -use Errno 'EINTR'; +use Errno qw/EINTR ENOENT/; use Fcntl qw/O_NOFOLLOW O_RDONLY/; use Socket qw/AF_UNIX AF_INET AF_INET6/; @@ -92,19 +92,23 @@ while (1) { while (defined (my $h = $conn->getline())) { last if $h eq "\r\n" }; my ($status_line, $content_type, $content); - if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and -f $1) { - # only open files in the cwd, and refuse to follow symlinks - if (sysopen(my $fh, $1, O_NOFOLLOW|O_RDONLY)) { + if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/) { + # only serve base64-encoded filenames (tokens) in the cwd + # XXX stat(2) followed by open(2) is racy; open(2) would hang if + # an attacker manages to replace a regular file with a FIFO + # between the syscalls, leading to denial of service; however + # there shouldn't be any risk of information disclosure as we're + # not following symlinks (and the cwd is not owned by us) + if (-f $1 and sysopen(my $fh, $1, O_NOFOLLOW|O_RDONLY)) { ($status_line, $content_type) = ('200 OK', 'application/jose+json'); $content = do { local $/ = undef; $fh->getline() }; $fh->close() or die "close: $!"; - } - else { - $status_line = '403 Forbidden'; + } elsif ($! == ENOENT) { # errno from either stat(2) or open(2) + $status_line = '404 Not Found'; } } - $conn->print( "HTTP/$proto ", ($status_line // '404 Not Found'), "\r\n" ); + $conn->print( "HTTP/$proto ", ($status_line // '403 Forbidden'), "\r\n" ); $conn->print( "Content-Type: $content_type\r\n" ) if defined $content_type; $conn->print( "Content-Length: ".length($content)."\r\n" ) if defined $content; $conn->print( "Connection: close\r\n" ); -- cgit v1.2.3 From 96dc4add445c5a48632bef6f8a4f0440c70f03d0 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 22:23:38 +0200 Subject: Provide apache2 configuration snippet. --- Changelog | 2 +- config/apache2.conf | 12 ++++++++++++ config/nginx.conf | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 config/apache2.conf diff --git a/Changelog b/Changelog index 5252fd2..0674c4a 100644 --- a/Changelog +++ b/Changelog @@ -12,7 +12,7 @@ lacme (0.3) upstream; 'iptables' option to Yes. + Change 'min-days' default from 10 to 21, to avoid expiration notices from Let's Encrypt when auto-renewal is done by a cronjob. - + Provide nginx configuration snippet. + + Provide nginx and apache2 configuration snippets. - Ensure lacme's config file descriptor is not passed to the accountd or webserver components. - new-cert: sort section names if not passed explicitely. diff --git a/config/apache2.conf b/config/apache2.conf new file mode 100644 index 0000000..20927fa --- /dev/null +++ b/config/apache2.conf @@ -0,0 +1,12 @@ +# Use Apache2 to serve ACME requests by passing them over to a +# locally-bound lacme webserver component. +# +# This file needs to be sourced to the server directives (at least the +# non-ssl one) of each virtual host requiring authorization. + + + ProxyPass unix:///var/run/lacme.socket|http://127.0.0.1/.well-known/acme-challenge/ + Order allow,deny + Allow from all + + diff --git a/config/nginx.conf b/config/nginx.conf index e4ceb0c..6753ff9 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -1,4 +1,4 @@ -# Use nginx to serve ACME requests; either directly, or by passing them +# Use Nginx to serve ACME requests; either directly, or by passing them # over to a locally-bound lacme webserver component. # # This file needs to be sourced to the server directives (at least the -- cgit v1.2.3 From d93660085ceba3f81631bba4744b23af7984cd9d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 29 Jun 2017 22:43:33 +0200 Subject: Improve docs. --- lacme.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lacme.md b/lacme.md index d18b176..0f6f3ee 100644 --- a/lacme.md +++ b/lacme.md @@ -234,12 +234,15 @@ served during certificate issuance. absolute path of a UNIX-domain socket (created with mode `0666`). Default: `/var/run/lacme.socket`. - Note: The default value is only suitable when an external HTTP + **Note**: The default value is only suitable when an external HTTP daemon is publicly reachable and passes all ACME challenge requests to the webserver component through the UNIX-domain socket - `/var/run/lacme.socket`; if that's not the case, one needs to set - *listen* to `[::]` (or `0.0.0.0 [::]` when dual IPv4/IPv6 stack is - disabled or unavailable), and possibly also set *iptables* to `Yes`. + `/var/run/lacme.socket` (for instance using the provided + `/etc/lacme/apache2.conf` or `/etc/lacme/nginx.conf` configuration + snippets for each virtual host requiring authorization). If there + is no HTTP daemon bound to port 80 one needs to set *listen* to + `[::]` (or `0.0.0.0 [::]` when dual IPv4/IPv6 stack is disabled or + unavailable), and possibly also set *iptables* to `Yes`. *challenge-directory* @@ -267,7 +270,8 @@ served during certificate issuance. *command* : Path to the [ACME] webserver executable. A separate process is - spawned for each address to *listen* on. + spawned for each address to *listen* on. (In particular no + webserver process is forked when the *listen* option is empty.) Default: `/usr/lib/lacme/webserver`. *iptables* -- cgit v1.2.3