diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2017-02-12 21:18:08 +0100 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2017-02-12 21:18:08 +0100 |
commit | 24157694f0c98740e9e5660c0b521d8e83594cc5 (patch) | |
tree | bfc42fdab15662a6c3c98febe7eed31aa99e5ff6 /acmeman | |
parent | 28d06bca0b9f2a64a92e54aeafcae1140c69a09c (diff) | |
download | acmeman-24157694f0c98740e9e5660c0b521d8e83594cc5.tar.gz acmeman-24157694f0c98740e9e5660c0b521d8e83594cc5.tar.bz2 |
Support creation of SAN certificates shared between several virtual hosts
* acmeman: New option --alt-names
New macro LetsEncryptReference
Coalesce virtual hosts referring to the same server name.
Diffstat (limited to 'acmeman')
-rwxr-xr-x | acmeman | 215 |
1 files changed, 179 insertions, 36 deletions
@@ -40,10 +40,11 @@ acmeman - manages ACME certificates =head1 SYNOPSIS B<acmeman> -[B<-Fdn>] +[B<-Fadn>] [B<-D> I<N>] [B<-f> I<FILE>] [B<-l> B<slackware>|B<debian>|B<rh>] +[B<--alt-names>] [B<--config-file=>I<FILE>] [B<--debug>] [B<--dry-run>] @@ -134,13 +135,13 @@ B<acmeman> is run. To use the created certificate, create a new B<VirtualHost> block that contains the following statement: -Use LetsEncryptServer I<DOMAIN> + Use LetsEncryptServer DOMAIN where I<DOMAIN> is the name used in the B<ServerName> statement of the plain HTTP configuration. Copy the B<ServerAlias> statements (if any), and add the -rest of configuration statements. Note, that you need not use the B<ServerName> -statement, as it will be included when the B<LetsEncryptServer> macro is -expanded. +rest of configuration statements. Note, that you need not use the +B<ServerName> statement, as it will be included when the B<LetsEncryptServer> +macro is expanded. Example: @@ -156,23 +157,47 @@ Example: ServerAlias www.example.com ... </VirtualHost> - -=head1 OPTIONS -=over 4 +Alternatively, you can use the B<LetsEncryptSSL> macro, which differs from +B<LetsEncryptServer> in that it configures only SSL settings, without the +B<ServerName> statement, which therefore must be included explicitly: -=item B<-h> + <VirtualHost *:443> + ServerName example.org + ServerAlias www.example.com + Use LetsEncryptSSL example.org + ... + </VirtualHost> -Prints a short usage summary. - -=item B<--help> +LetsEncrypt limits the number of certificates requested for a single +registered domain per week (at the time of this writing - 20). To avoid +hitting that limit, you may wish to use the same certificate for different +virtual hosts. The special macro B<LetsEncryptReference> is provided for +that purpose. Suppose, for example, that you wish to configure server +name B<git.example.org> to use the same certificate as B<example.org> +(configured in the example above). You then declare the virtual host +for the plain HTTP as follows: -Prints detailed user manual. + <VirtualHost *:80> + ServerName git.example.org + Use LetsEncryptReference example.org + ... + </VirtualHost> -=item B<--usage> +The argument to the B<LetsEncryptReference> macro indicates the CN name of +the certificate to which the current server name (and aliases, if any) are +to be added as alternative names. The corresponding virtual host for SSL +will use the B<LetsEncryptSSL> macro to configure the correct certificate: -Outputs a terse reminder of the command line syntax along with a -list of available options. + <VirtualHost *:80> + ServerName git.example.org + Use LetsEncryptSSL example.org + ... + </VirtualHost> + +=head1 OPTIONS + +=over 4 =item B<-D>, B<--tile-delta=>I<N> @@ -187,6 +212,14 @@ Force renewal of certificates, no matter their expire date. With B<--setup>, force installing the B<httpd-letsencrypt.conf> file even if it already exists. +=item B<-a>, B<--alt-names> + +Compare the list of alternative names of each certificate with the one +gathered from the Apache configuration, and reissue the certificate +if the two lists don't match. This uses an ad-hoc logic, due to the +deficiency of the underlying X509 module, and therefore is not enabled +by default. + =item B<-d>, B<--debug> Increase debugging level. Multiple options accumulate. Three debugging @@ -230,6 +263,25 @@ production. Implies B<--debug>. Set up the B<acmeman> infrastructure files. =back + +The following options are informational: + +=over 4 + +=item B<-h> + +Prints a short usage summary. + +=item B<--help> + +Prints detailed user manual. + +=item B<--usage> + +Outputs a terse reminder of the command line syntax along with a +list of available options. + +=back =head1 AUTHOR @@ -248,6 +300,7 @@ my $letsencrypt_root_cert_url = 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem'; my $time_delta = 86400; my $force = 0; +my $check_alt_names = 0; my %select; # Hash of selected domain names # Variables read from the Apache configuration files @@ -368,6 +421,41 @@ sub domain_cert_expires { my $crt = make_filename('cert', $domain); if (-f $crt) { my $x509 = Crypt::OpenSSL::X509->new_from_file($crt); + + my $exts = $x509->extensions_by_name(); + if (exists($exts->{subjectAltName})) { + my $msg = $check_alt_names + ? 'will renew' : 'use -a to trigger renewal'; + + # FIXME: Crypt::OpenSSL::X509 returns extensions as strings, + # instead of as ASN.1 objects. Until it is fixed, the + # following naive logic is implemented to split the string into + # names: + my @names = split /\.\.+/, $exts->{subjectAltName}->value(); + # First value is irrelevant + shift @names; + # Prepare sorted arrays of alternative names and requested + # names (@vh). + @names = sort @names; + my @vh = sort ($domain, @_); + # Compare them + if ($#vh != $#names) { + debug(1, "$crt: number of subject names changed; $msg"); + return 1 if $check_alt_names; + } else { + for (my $i = 0; $i <= $#names; $i++) { + if ($names[$i] ne $vh[$i]) { + debug(1, "$crt: subject names differ; $msg"); + if ($check_alt_names) { + return 1; + } else { + last; + } + } + } + } + } + my $expiry = $x509->notAfter(); my $strp = DateTime::Format::Strptime->new( @@ -484,8 +572,9 @@ sub examine_http_config { STATE_INITIAL => 0, # Initial state STATE_VIRTUAL_HOST => 1, # In VirtualHost block STATE_USE_CHALLENGE => 2, # Ditto, but challenge macro was used - STATE_MACRO_CHALLENGE => 3, # In LetsEncryptChallenge macro - STATE_MACRO_SERVER => 4 + STATE_USE_REFERENCE => 3, # Ditto, but reference macro was used + STATE_MACRO_CHALLENGE => 4, # In LetsEncryptChallenge macro + STATE_MACRO_SSL => 5 }; state $state = STATE_INITIAL; @@ -494,6 +583,7 @@ sub examine_http_config { if (open(my $fd, '<', $file)) { my $server_name; my @server_aliases; + my $reference; my $line; while (<$fd>) { @@ -512,33 +602,53 @@ sub examine_http_config { $state = STATE_VIRTUAL_HOST; $server_name = undef; @server_aliases = (); + $reference = undef; } elsif (/^ServerRoot\s+(.+)/i) { $server_root = dequote($1); } elsif (/^<(?:(?i)Macro)\s+LetsEncryptChallenge/) { $state = STATE_MACRO_CHALLENGE; - } elsif (/^<(?:(?i)Macro)\s+LetsEncryptServer\s+(.+?)\s*>/) { - $state = STATE_MACRO_SERVER; + } elsif (/^<(?:(?i)Macro)\s+LetsEncryptSSL\s+(.+?)\s*>/) { + $state = STATE_MACRO_SSL; $filename_arg = $1; $filename_arg =~ s{\$}{\\\$}; } } elsif ($state == STATE_VIRTUAL_HOST - || $state == STATE_USE_CHALLENGE) { + || $state == STATE_USE_CHALLENGE + || $state == STATE_USE_REFERENCE) { if (m{</VirtualHost}i) { unshift @server_aliases, $server_name if defined $server_name; - if ($state == STATE_USE_CHALLENGE - && @server_aliases + if (@server_aliases && (!%select || exists($select{$server_name}))) { - my @names = uniq(@server_aliases); - debug(3, "$file:$line: will handle @names"); - push @virthost, \@names; - } + if ($state == STATE_USE_CHALLENGE) { + my @names = uniq(@server_aliases); + debug(3, "$file:$line: will handle @names"); + push @virthost, \@names; + } elsif ($state == STATE_USE_REFERENCE) { + my @names = uniq(@server_aliases); + unshift @names, $reference; + debug(3, "$file:$line: reference: @names"); + push @virthost, \@names; + } + } $state = STATE_INITIAL; } elsif (/^(?:(?i)Use)\s+LetsEncryptChallenge/) { - $state = STATE_USE_CHALLENGE; - # } elsif (/^Use\s+LetsEncryptSSL\s+(\S+)/i) { - # debug(3, "$file: $1"); - # push @virthost, [ $1 ]; + if ($state == STATE_VIRTUAL_HOST) { + $state = STATE_USE_CHALLENGE; + } elsif ($state == STATE_USE_CHALLENGE) { + error("$file:$line: duplicate use of LetsEncryptChallenge"); + } else { + error("$file:$line: LetsEncryptChallenge and LetsEncryptReference can't be used together"); + } + } elsif (/^(?:(?i)Use)\s+LetsEncryptReference\s+(.+)\s*$/) { + if ($state == STATE_VIRTUAL_HOST) { + $state = STATE_USE_REFERENCE; + $reference = $1; + } elsif ($state == STATE_USE_REFERENCE) { + error("$file:$line: duplicate use of LetsEncryptReference"); + } else { + error("$file:$line: LetsEncryptChallenge and LetsEncryptReference can't be used together"); + } } elsif (/^(?:(?i)ServerName)\s+(\S+)/) { $server_name = dequote($1); } elsif (/^(?:(?i)ServerAlias)\s+(.+)\s*$/) { @@ -554,7 +664,7 @@ sub examine_http_config { $www_root = $dir; debug(3, "ACME challenge root dir: $www_root"); } - } elsif ($state == STATE_MACRO_SERVER) { + } elsif ($state == STATE_MACRO_SSL) { if (m{^</macro}i) { $state = STATE_INITIAL; } elsif (/(?:(?i)SSLCertificate((?:Key)|(?:Chain))?File)\s+(.+)/) { @@ -650,10 +760,18 @@ sub initial_setup { Options None Require all granted </Directory> + <IfModule mod_rewrite.c> + RewriteEngine On + RewriteRule /.well-known/acme-challenge - [L] + </IfModule> </Macro> -<Macro LetsEncryptServer \$domain> - ServerName \$domain +<Macro LetsEncryptReference \$domain> + Use LetsEncryptChallenge + Alias /.dummy/\$domain /dev/null +</Macro> + +<Macro LetsEncryptSSL \$domain> SSLEngine on SSLProtocol all -SSLv2 -SSLv3 SSLHonorCipherOrder on @@ -662,6 +780,12 @@ sub initial_setup { SSLCertificateKeyFile /etc/ssl/acme/\$domain/privkey.pem SSLCACertificateFile /etc/ssl/acme/lets-encrypt-x3-cross-signed.pem </Macro> + +<Macro LetsEncryptServer \$domain> + ServerName \$domain + Use LetsEncryptSSL \$domain +</Macro> + EOT ; close $fd; @@ -677,6 +801,23 @@ EOT exit(EX_OK); } +sub coalesce { + my $ref = shift; + debug(1, "coalescing virtual hosts"); + my $i = 0; + my @vhost; + foreach my $ent (sort { $a->{names}[0] cmp $b->{names}[0] } + map { { ord => $i++, names => $_ } } @{$ref}) { + if (@vhost && $vhost[-1]->{names}[0] eq $ent->{names}[0]) { + shift @{$ent->{names}}; + push $vhost[-1]->{names}, @{$ent->{names}}; + } else { + push @vhost, $ent; + } + } + @{$ref} = map { $_->{names} } sort { $a->{ord} <=> $b->{ord} } @vhost; +} + my %apache_layout_tab = ( slackware => { config => '/etc/httpd/httpd.conf', incdir => '/etc/httpd/extra' @@ -730,7 +871,8 @@ GetOptions("h" => sub { "setup|s" => \$setup, "config-file|f=s" => sub { $apache_layout = { config => $_[1] } - } + }, + "alt-names|a" => \$check_alt_names ) or exit(EX_USAGE); if ($dry_run) { @@ -764,6 +906,7 @@ if ($setup) { examine_http_config($apache_layout->{config}) or exit(EX_OSFILE); } debug(1, "nothing to do") unless @virthost; +coalesce \@virthost; # Check challenge root directory if (defined($www_root)) { @@ -779,7 +922,7 @@ $account_key = Crypt::OpenSSL::RSA->generate_key(4096); $challenge = Protocol::ACME::Challenge::LocalFile->new({www_root => $www_root}); foreach my $vhost (@virthost) { - if ($force || domain_cert_expires(${$vhost}[0])) { + if ($force || domain_cert_expires(@{$vhost})) { register_domain_certificate(@$vhost); } } |