From 05aafaa0c740d7a17e6e3f17634ae6990187f22d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 19 Feb 2017 12:28:39 +0100 Subject: wibble --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index cb49818..1951ed7 100755 --- a/lacme +++ b/lacme @@ -77,7 +77,7 @@ do { die "Error: Can't find configuration file\n" unless defined $CONFFILENAME; print STDERR "Using configuration file: $CONFFILENAME\n" if $OPTS{debug}; open $CONFFILE, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n"; - my $conf = do { local $/ = undef, <$CONFFILE> }; + my $conf = do { local $/ = undef; <$CONFFILE> }; # don't close $CONFFILE so we can pass it to the client my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; -- cgit v1.2.3 From fc117d6513dfa1e6287927a9b95ac0558eaea951 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 19 Feb 2017 13:21:38 +0100 Subject: config-cert: import the default section of files already read. --- lacme | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 1951ed7..6c7f48d 100755 --- a/lacme +++ b/lacme @@ -562,7 +562,7 @@ elsif ($COMMAND eq 'new-cert') { my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'} : defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ] : [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ]; - my $conf; + my ($conf, %defaults); foreach my $conffile (@$conffiles) { $conffile = ($CONFFILENAME =~ s#[^/]+\z##r).$conffile unless $conffile =~ /\A\//; my @filenames; @@ -582,13 +582,14 @@ elsif ($COMMAND eq 'new-cert') { foreach my $filename (sort @filenames) { print STDERR "Reading $filename\n" if $OPTS{debug}; my $h = Config::Tiny::->read($filename) or die Config::Tiny::->errstr()."\n"; - my $defaults = delete $h->{_} // {}; + my $def = delete $h->{_} // {}; + $defaults{$_} = $def->{$_} foreach keys %$def; my @valid = qw/certificate certificate-chain certificate-key min-days CAfile hash keyUsage subject subjectAltName chown chmod notify/; foreach my $s (keys %$h) { $conf->{$s} = { map { $_ => delete $h->{$s}->{$_} } @valid }; die "Unknown option(s) in [$s]: ".join(', ', keys %{$h->{$s}})."\n" if %{$h->{$s}}; - $conf->{$s}->{$_} //= $defaults->{$_} foreach keys %$defaults; + $conf->{$s}->{$_} //= $defaults{$_} foreach keys %defaults; } } } @@ -602,6 +603,11 @@ elsif ($COMMAND eq 'new-cert') { next; }; + if ($OPTS{debug}) { + print STDERR "Configuration option for $s:\n"; + print " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); + } + my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; unless (defined $certtype) { print STDERR "[$s] Warning: Missing 'certificate' and 'certificate-chain', skipping\n"; -- cgit v1.2.3 From bbbd329e9a1274d0a7bfb7b741894f5417b43538 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 19 Feb 2017 13:23:51 +0100 Subject: Ensure lacme's config file descriptor has the FD_CLOEXEC bit set. --- lacme | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lacme') diff --git a/lacme b/lacme index 6c7f48d..8cbed17 100755 --- a/lacme +++ b/lacme @@ -68,6 +68,7 @@ $COMMAND = $COMMAND =~ /\A(new-reg|reg=\p{Print}*|new-cert|revoke-cert)\z/ ? $1 : usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND @ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV +sub set_FD_CLOEXEC($$); my $CONFFILENAME = $OPTS{config} // first { -f $_ } ( "./$NAME.conf" , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME.conf" @@ -79,6 +80,7 @@ do { open $CONFFILE, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n"; my $conf = do { local $/ = undef; <$CONFFILE> }; # don't close $CONFFILE so we can pass it to the client + set_FD_CLOEXEC($CONFFILE, 1); my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; my $defaults = delete $h->{_} // {}; -- cgit v1.2.3 From de585094c458a36a387277544bda5f4004bbb03c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 19 Feb 2017 13:24:07 +0100 Subject: new-cert: sort section names if not passed explicitely. --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index 8cbed17..f9b3530 100755 --- a/lacme +++ b/lacme @@ -598,7 +598,7 @@ elsif ($COMMAND eq 'new-cert') { my $challenge_dir; my $rv = 0; - foreach my $s (@ARGV ? @ARGV : keys %$conf) { + foreach my $s (@ARGV ? @ARGV : sort (keys %$conf)) { my $conf = $conf->{$s} // do { print STDERR "Warning: No such section $s, skipping\n"; $rv = 1; -- cgit v1.2.3 From 84f6363da57ccc3a58fc72f60cf51ca70cea34f6 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 19 Feb 2017 13:36:11 +0100 Subject: new-cert: new CLI option "min-days" --- lacme | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index f9b3530..75c1465 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 quiet|q debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s agreement-uri=s min-days=i quiet|q debug help|h/); usage(0) if $OPTS{help}; $COMMAND = shift(@ARGV) // usage(1, "Missing command"); @@ -619,8 +619,8 @@ elsif ($COMMAND eq 'new-cert') { # skip certificates that expire at least $conf->{'min-days'} days in the future if (-f $conf->{$certtype} and defined (my $t = x509_enddate($conf->{$certtype}))) { - my $d = $conf->{'min-days'} // 10; - if ($d > 0 and $t - time > $d*86400) { + my $d = $OPTS{'min-days'} // $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" unless $OPTS{quiet}; next; -- cgit v1.2.3 From 1426a858ae1c4da30f777110e1253fa36bac2b41 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 22 Feb 2017 10:19:56 +0100 Subject: new-cert: mark basicConstraints and keyUsage x509v3 extensions as critical in the CSR. Boulder's issue #565 "Golang errors on extensions marked critical" was fixed upstream, cf. https://github.com/letsencrypt/boulder/issues/565 . --- lacme | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 75c1465..b654c7d 100755 --- a/lacme +++ b/lacme @@ -147,14 +147,10 @@ sub gen_csr(%) { "[ req_distinguished_name ]\n", "[ v3_req ]\n", - # XXX Golang errors on extensions marked critical - # https://github.com/letsencrypt/boulder/issues/565 - #"basicConstraints = critical, CA:FALSE\n", - "basicConstraints = CA:FALSE\n", + "basicConstraints = critical, CA:FALSE\n", "subjectKeyIdentifier = hash\n" ); - #$config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; - $config->print("keyUsage = $args{keyUsage}\n") if defined $args{keyUsage}; + $config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; $config->close() or die "Can't close: $!"; -- cgit v1.2.3 From f4af28d7e526bd56a78225daf84d11cdf96bd611 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 22 Feb 2017 10:51:08 +0100 Subject: new-cert: create certificate files atomically. --- lacme | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index b654c7d..a8c25fe 100755 --- a/lacme +++ b/lacme @@ -524,16 +524,25 @@ sub spawn($@) { sub install_cert($$@) { my $filename = shift; my $x509 = shift; - - open my $fh, '>', $filename or die "Can't open $filename: $!"; - print $fh $x509; - foreach (@_) { # append the chain - open my $fh2, '<', $_ or die "Can't open $_: $!"; - my $ca = do { local $/ = undef; $fh2->getline() }; - print $fh $ca; - close $fh2 or die "Can't close: $!"; + my @chain = @_; + + my $tmp = "$filename.new"; + open my $fh, '>', $tmp or die "Can't open $tmp: $!"; + eval { + $fh->print($x509) or die "Can't print: $!"; + foreach (@chain) { # append the chain + open my $fh2, '<', $_ or die "Can't open $_: $!"; + my $ca = do { local $/ = undef; $fh2->getline() }; + $fh2->close() or die "Can't close: $!"; + $fh->print($ca) or die "Can't print: $!"; + } + $fh->close() or die "Can't close: $!"; + }; + if ($@) { + unlink $tmp or warn "Can't unlink $tmp: $!"; + die $@; } - close $fh or die "Can't close: $!"; + rename($tmp, $filename) or die "Can't rename $tmp to $filename: $!"; } -- cgit v1.2.3 From 944407621f313c15f6cfd53267da1ddbdaceec9f Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 28 Jun 2017 17:19:46 +0200 Subject: webserver: allow listening to multiple addresses. (Useful when dual-stack IPv4/IPv6 is not supported.) Also, change the default to listen to a UNIX-domain socket . Moreover temporary iptables rules are no longer installed. Hosts without a public HTTP daemon listening on port 80 need to set the 'listen' option to [::] and/or 0.0.0.0, and possibly set the 'iptables' option to Yes. --- lacme | 225 +++++++++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 154 insertions(+), 71 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index a8c25fe..b4d09e8 100755 --- a/lacme +++ b/lacme @@ -30,7 +30,8 @@ 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/AF_UNIX PF_INET PF_INET6 PF_UNIX PF_UNSPEC INADDR_ANY IN6ADDR_ANY +use Socket qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC + INADDR_ANY IN6ADDR_ANY IPPROTO_IPV6 SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/; use Config::Tiny (); @@ -95,12 +96,12 @@ do { map {$_ => undef} qw/server timeout SSL_verify SSL_version SSL_cipher_list/ }, webserver => { - listen => ':80', - 'challenge-directory' => '/var/www/acme-challenge', + listen => '/var/run/lacme.socket', + 'challenge-directory' => undef, user => 'www-data', group => 'www-data', command => '/usr/lib/lacme/webserver', - iptables => 'Yes' + iptables => 'No' }, accountd => { @@ -270,12 +271,9 @@ sub set_FD_CLOEXEC($$) { ############################################################################# -# Try to spawn a webserver to serve ACME challenges, and return the -# temporary challenge directory. -# If a webserver is already listening, symlink the 'challenge-directory' -# configuration option to the temporary challenge directory. -# Otherwise, bind(2) a socket, pass its fileno to the webserver -# component, and optionally install iptables rules. +# If 'listen' is not empty, bind socket(s) to the given addresse(s) and +# spawn webserver(s) to serve ACME challenge reponses. +# The temporary challenge directory is returned. # sub spawn_webserver() { # create a temporary directory; give write access to the ACME client @@ -283,73 +281,151 @@ sub spawn_webserver() { my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; chmod 0755, $tmpdir or die "Can't chmod: $!"; if ((my $username = $CONFIG->{client}->{user}) ne '') { - my $uid = getpwnam($username) // die "Can't getgrnam($username): $!"; + my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; chown($uid, -1, $tmpdir) or die "Can't chown: $!"; } my $conf = $CONFIG->{webserver}; - my ($fam, $addr, $port) = (PF_INET, $conf->{listen}, 80); - $port = $1 if $addr =~ s/:(\d+)$//; - $addr = Socket::inet_ntop(PF_INET, INADDR_ANY) if $addr eq ''; - $fam = PF_INET6 if $addr =~ s/^\[(.+)\]$/$1/; - - my $proto = getprotobyname("tcp") // die; - socket(my $srv, $fam, SOCK_STREAM, $proto) or die "socket: $!"; - setsockopt($srv, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) or die "setsockopt: $!"; - $addr = Socket::inet_pton($fam, $addr) // die "Invalid address $conf->{listen}\n"; - my $sockaddr = $fam == PF_INET ? Socket::pack_sockaddr_in($port, $addr) - : $fam == PF_INET6 ? Socket::pack_sockaddr_in6($port, $addr) - : die; - - # try to bind aginst the specified address:port - bind($srv, $sockaddr) or do { - die "Can't bind to $conf->{listen}: $!" if $! != EADDRINUSE; - print STDERR "[$$] Using existing webserver on $conf->{listen}\n" if $OPTS{debug}; - my $dir = $conf->{'challenge-directory'}; + + # parse and pack addresses to listen to + my @sockaddr; + foreach my $a (split /[[:blank:],]\s*/, $conf->{listen}) { + my $sockaddr; + if ($a =~ /\A\//) { # absolute path to a unix domain socket + $sockaddr = Socket::pack_sockaddr_un($a); + } elsif ($a =~ /\A(\d+(?:\.\d+){3})(?::(\d+))?\z/) { + my $n = Socket::inet_pton(AF_INET, $1); + $sockaddr = Socket::pack_sockaddr_in($2 // 80, $n) if defined $n; + } elsif ($a =~ /\A\[([[:xdigit:]:.]{2,39})\](?::(\d+))?\z/) { + my $n = Socket::inet_pton(AF_INET6, $1); + $sockaddr = Socket::pack_sockaddr_in6($2 // 80, $n) if defined $n; + } + die "Invalid address: $a\n" unless defined $sockaddr; + push @sockaddr, $sockaddr; + } + + # symlink the 'challenge-directory' configuration option to the + # temporary challenge directory (so an existing httpd can directly + # serve ACME challenge reponses). + if (defined (my $dir = $conf->{'challenge-directory'})) { + print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; push @CLEANUP, sub() { print STDERR "Unlinking $dir\n" if $OPTS{debug}; unlink $dir or warn "Warning: Can't unlink $dir: $!"; + } + } + elsif (!@sockaddr) { + die "'challenge-directory' option of section [webserver] is required when 'listen' is empty\n"; + } + + # create socket(s) and spawn webserver(s) + my @sockaddr4; + foreach my $sockaddr (@sockaddr) { + my $domain = Socket::sockaddr_family($sockaddr) // die; + socket(my $sock, $domain, SOCK_STREAM, 0) or die "socket: $!"; + setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) + if $domain == AF_INET or $domain == AF_INET6; + + my $p; # pretty-print the address/port + if ($domain == AF_UNIX) { + $p = Socket::unpack_sockaddr_un($sockaddr); + } elsif ($domain == AF_INET) { + my ($port, $addr) = Socket::unpack_sockaddr_in($sockaddr); + $p = Socket::inet_ntop($domain, $addr).":$port"; + } elsif ($domain == AF_INET6) { + my ($port, $addr) = Socket::unpack_sockaddr_in6($sockaddr); + $p = "[".Socket::inet_ntop($domain, $addr)."]:$port"; + } + + if ($domain == AF_UNIX) { + # bind(2) with a loose umask(2) to allow anyone to connect + my $umask = umask(0111) // die "umask: $!"; + my $path = Socket::unpack_sockaddr_un($sockaddr); + bind($sock, $sockaddr) or die "Couldn't bind to $p: $!"; + push @CLEANUP, sub() { + print STDERR "Unlinking $path\n" if $OPTS{debug}; + unlink $path or warn "Warning: Can't unlink $path: $!"; + }; + umask($umask) // die "umask: $!"; + } + else { + bind($sock, $sockaddr) or die "Couldn't bind to $p: $!"; + } + + listen($sock, 5) or die "listen: $!"; + + # spawn a webserver component bound to the given socket + my $pid = fork() // "fork: $!"; + unless ($pid) { + drop_privileges($conf->{user}, $conf->{group}, $tmpdir); + set_FD_CLOEXEC($sock, 0); + $ENV{DEBUG} = $OPTS{debug}; + # use execve(2) rather than a Perl pseudo-process to ensure that + # the child doesn't have access to the parent's memory + exec $conf->{command}, fileno($sock) or die; + } + + print STDERR "[$$] Forking ACME webserver bound to $p, child PID $pid\n" if $OPTS{debug}; + set_FD_CLOEXEC($sock, 1); + push @CLEANUP, sub() { + print STDERR "[$$] Shutting down ACME webserver bound to $p\n" if $OPTS{debug}; + shutdown($sock, SHUT_RDWR) or warn "shutdown: $!"; + kill 15 => $pid; + waitpid $pid => 0; }; - return $tmpdir; - }; - listen($srv, 5) or die "listen: $!"; - # spawn the webserver component - my $pid = fork() // "fork: $!"; - unless ($pid) { - drop_privileges($conf->{user}, $conf->{group}, $tmpdir); - set_FD_CLOEXEC($srv, 0); - $ENV{DEBUG} = $OPTS{debug}; - # use execve(2) rather than a Perl pseudo-process to ensure that - # the child doesn't have access to the parent's memory - exec $conf->{command}, fileno($srv) or die; + # on dual-stack ipv4/ipv6, we'll need to open the port for the + # v4-mapped address as well + if ($domain == AF_INET6) { + my $v6only = getsockopt($sock, Socket::IPPROTO_IPV6, Socket::IPV6_V6ONLY) + // die "getsockopt(IPV6_V6ONLY): $!"; + my ($port, $addr) = Socket::unpack_sockaddr_in6($sockaddr); + my $mask = "\xFF" x 12 . "\x00" x 4; + my $prefix = "\x00" x 10 . "\xFF" x 2 . "\x00" x 4; + + if (unpack('i', $v6only) == 0) { + if ($addr eq IN6ADDR_ANY) { + push @sockaddr4, Socket::pack_sockaddr_in($port, INADDR_ANY); + } elsif (($addr & $mask) eq $prefix) { + my $v4 = substr($addr, 12); + push @sockaddr4, Socket::pack_sockaddr_in($port, $v4); + } + } + } } - print STDERR "[$$] Forking ACME webserver, child PID $pid\n" if $OPTS{debug}; - set_FD_CLOEXEC($srv, 1); - push @CLEANUP, sub() { - print STDERR "[$$] Shutting down ACME webserver\n" if $OPTS{debug}; - shutdown($srv, SHUT_RDWR) or warn "shutdown: $!"; - kill 15 => $pid; - waitpid $pid => 0; - }; + # allow incoming traffic on the given addresses + if (lc ($conf->{iptables} // 'No') eq 'yes') { + iptables_save(AF_INET, @sockaddr, @sockaddr4); + iptables_save(AF_INET6, @sockaddr); + } + + return $tmpdir; +} + - return $tmpdir if lc ($conf->{iptables} // 'Yes') eq 'no'; +############################################################################# +# Save current iptables/ipv6tables to a temporary file and install +# temporary rules to open the given addresses/ports. +sub iptables_save($@) { + my $domain = shift; + my @sockaddr = grep { Socket::sockaddr_family($_) == $domain } @_; + return unless @sockaddr; # no address in that domain # install iptables - my $iptables_bin = $fam == PF_INET ? 'iptables' : $fam == PF_INET6 ? 'ip6tables' : die; + my $iptables_bin = $domain == AF_INET ? 'iptables' : $domain == AF_INET6 ? 'ip6tables' : die; my $iptables_tmp = File::Temp::->new(TMPDIR => 1) // die; set_FD_CLOEXEC($iptables_tmp, 1); - my $pid2 = fork() // die "fork: $!"; - unless ($pid2) { + my $pid = fork() // die "fork: $!"; + unless ($pid) { open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; open STDOUT, '>&', $iptables_tmp or die "Can't dup: $!"; $| = 1; # turn off buffering for STDOUT exec "/sbin/$iptables_bin-save", "-c" or die; } - waitpid $pid2 => 0; + waitpid $pid => 0; die "Error: /sbin/$iptables_bin-save exited with value ".($? >> 8) if $? > 0; # seek back to the begining, as we'll restore directly from the @@ -359,32 +435,39 @@ sub spawn_webserver() { seek($iptables_tmp, SEEK_SET, 0) or die "Can't seek: $!"; push @CLEANUP, sub() { - print STDERR "[$$] Restoring iptables\n" if $OPTS{debug}; - my $pid2 = fork() // die "fork: $!"; - unless ($pid2) { + print STDERR "[$$] Restoring $iptables_bin\n" if $OPTS{debug}; + my $pid = fork() // die "fork: $!"; + unless ($pid) { open STDIN, '<&', $iptables_tmp or die "Can't dup: $!"; open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; exec "/sbin/$iptables_bin-restore", "-c" or die; } - waitpid $pid2 => 0; + waitpid $pid => 0; warn "Warning: /sbin/$iptables_bin-restore exited with value ".($? >> 8) if $? > 0; }; - # it's safe to install the new iptables to open $port now that the - # restore hook is in place - my $mask = $fam == PF_INET ? ($addr eq INADDR_ANY ? '0' : '32') - : $fam == PF_INET6 ? ($addr eq IN6ADDR_ANY ? '0' : '128') - : die; - my $dest = Socket::inet_ntop($fam, $addr) .'/'. $mask; - system ("/sbin/$iptables_bin", qw/-I INPUT -p tcp -m tcp -m state/, - '-d', $dest, '--dport', $port, - '--state', 'NEW,ESTABLISHED', '-j', 'ACCEPT') == 0 or die; - system ("/sbin/$iptables_bin", qw/-I OUTPUT -p tcp -m tcp -m state/, - '-s', $dest, '--sport', $port, - '--state', 'ESTABLISHED', '-j', 'ACCEPT') == 0 or die; + # it's safe to install the new iptables to open $addr:$port now that + # the restore hook is in place - return $tmpdir; + foreach my $sockaddr (@sockaddr) { + my ($port, $addr, $mask); + if ($domain == AF_INET) { + ($port, $addr) = Socket::unpack_sockaddr_in($sockaddr); + $mask = $addr eq INADDR_ANY ? '0' : '32'; + } elsif ($domain == AF_INET6) { + ($port, $addr) = Socket::unpack_sockaddr_in6($sockaddr); + $mask = $addr eq IN6ADDR_ANY ? '0' : '128'; + } + + my $dest = Socket::inet_ntop($domain, $addr) .'/'. $mask; + system ("/sbin/$iptables_bin", qw/-I INPUT -p tcp -m tcp -m state/, + '-d', $dest, '--dport', $port, + '--state', 'NEW,ESTABLISHED', '-j', 'ACCEPT') == 0 or die; + system ("/sbin/$iptables_bin", qw/-I OUTPUT -p tcp -m tcp -m state/, + '-s', $dest, '--sport', $port, + '--state', 'ESTABLISHED', '-j', 'ACCEPT') == 0 or die; + } } -- cgit v1.2.3 From 80c3a95a95ed268905fa87a398748f94628eed44 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 28 Jun 2017 21:26:00 +0200 Subject: new-cert: use File::Temp for the temporary cert filename. This ensures we aren't overwritting existing /path/to/srv.pem.new files. --- lacme | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index b4d09e8..fd90d1e 100755 --- a/lacme +++ b/lacme @@ -604,14 +604,19 @@ sub spawn($@) { ############################################################################# # Install the certificate # -sub install_cert($$@) { +sub install_cert($$;$) { my $filename = shift; my $x509 = shift; - my @chain = @_; + my @chain = grep !/\A\s*\z/, @_; # ignore empty CAfile + + my ($dirname, $basename) = + $filename =~ /\A(.*)\/([^\/]+)\z/ ? ($1, $2) : ('.', $filename); + my $fh = File::Temp::->new(UNLINK => 0, DIR => $dirname, + TEMPLATE => "$basename.XXXXXX") // die; - my $tmp = "$filename.new"; - open my $fh, '>', $tmp or die "Can't open $tmp: $!"; eval { + my $umask = umask() // die "umask: $!"; + chmod(0644 &~ $umask, $fh) or die "chmod: $!"; $fh->print($x509) or die "Can't print: $!"; foreach (@chain) { # append the chain open my $fh2, '<', $_ or die "Can't open $_: $!"; @@ -621,11 +626,13 @@ sub install_cert($$@) { } $fh->close() or die "Can't close: $!"; }; + my $path = $fh->filename(); if ($@) { - unlink $tmp or warn "Can't unlink $tmp: $!"; + print STDERR "Unlinking $path\n" if $OPTS{debug}; + unlink $path or warn "Can't unlink $path: $!"; die $@; } - rename($tmp, $filename) or die "Can't rename $tmp to $filename: $!"; + rename($path, $filename) or die "Can't rename $path to $filename: $!"; } -- cgit v1.2.3 From 40a54d2ad35630b1c8a7cd88791db032a7983d4d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 28 Jun 2017 21:33:40 +0200 Subject: Change the default 'min-days' from 10 to 21. This avoids expiration notices from Let's Encrypt when auto-renewal is done by a cronjob: Let's Encrypt sends a notice 19 (then 9) days before expiration. --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index fd90d1e..d7a416e 100755 --- a/lacme +++ b/lacme @@ -714,7 +714,7 @@ elsif ($COMMAND eq 'new-cert') { # skip certificates that expire at least $conf->{'min-days'} days in the future if (-f $conf->{$certtype} and defined (my $t = x509_enddate($conf->{$certtype}))) { - my $d = $OPTS{'min-days'} // $conf->{'min-days'} // 10; + my $d = $OPTS{'min-days'} // $conf->{'min-days'} // 21; 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" unless $OPTS{quiet}; -- cgit v1.2.3