diff options
author | Guilhem Moulin <guilhem@fripost.org> | 2017-06-29 22:47:24 +0200 |
---|---|---|
committer | Guilhem Moulin <guilhem@fripost.org> | 2017-06-29 22:47:24 +0200 |
commit | 3a9a58b9556c4ccd07c10429c040e6c98781fd45 (patch) | |
tree | 7058ff1bfebb25d247111428d194a828e1cac253 | |
parent | c8e2cd230a90b58b7e962f658fafb2d1306a579d (diff) | |
parent | d93660085ceba3f81631bba4744b23af7984cd9d (diff) |
Merge branch 'master' into debian
-rw-r--r-- | Changelog | 13 | ||||
-rw-r--r-- | README | 14 | ||||
-rwxr-xr-x | client | 18 | ||||
-rw-r--r-- | config/apache2.conf | 12 | ||||
-rw-r--r-- | config/nginx.conf | 6 | ||||
-rwxr-xr-x | lacme | 4 | ||||
-rwxr-xr-x | lacme-accountd | 2 | ||||
-rw-r--r-- | lacme.md | 17 | ||||
-rwxr-xr-x | webserver | 20 |
9 files changed, 74 insertions, 32 deletions
@@ -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 </var/run/lacme.socket>. + webserver: don't install temporary iptables by default. Hosts without a public HTTP daemon listening on port 80 need to set the @@ -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. @@ -21,6 +21,15 @@ 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. + - lacme(1), lacme-accountd(1): fix version number shown with + --version. + - client: remove potential race when creating ACME challenge response + files. -- Guilhem Moulin <guilhem@guilhem.org> Sun, 19 Feb 2017 13:08:41 +0100 @@ -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. @@ -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}, { 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. + +<Location /.well-known/acme-challenge/> + ProxyPass unix:///var/run/lacme.socket|http://127.0.0.1/.well-known/acme-challenge/ + Order allow,deny + Allow from all +</Location> + diff --git a/config/nginx.conf b/config/nginx.conf index f842c12..6753ff9 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; @@ -21,10 +21,10 @@ use strict; use warnings; -our $VERSION = '0.0.1'; +our $VERSION = '0.3'; 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/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'; @@ -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 during certificate issuance. *listen* @@ -233,6 +234,16 @@ This section is used for configuring the [ACME] webserver. 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 + daemon is publicly reachable and passes all ACME challenge requests + to the webserver component through the UNIX-domain socket + `/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* : Specify a non-existent directory under which an external HTTP daemon @@ -258,7 +269,9 @@ 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. (In particular no + webserver process is forked when the *listen* option is empty.) Default: `/usr/lib/lacme/webserver`. *iptables* @@ -37,7 +37,8 @@ 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/; # Untaint and fdopen(3) the listening socket @@ -91,18 +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) { - if (open my $fh, '<', $1) { # only open files in the cwd + 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" ); |