aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2017-02-12 21:18:08 +0100
committerSergey Poznyakoff <gray@gnu.org.ua>2017-02-12 21:18:08 +0100
commit24157694f0c98740e9e5660c0b521d8e83594cc5 (patch)
treebfc42fdab15662a6c3c98febe7eed31aa99e5ff6
parent28d06bca0b9f2a64a92e54aeafcae1140c69a09c (diff)
downloadacmeman-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.
-rwxr-xr-xacmeman215
1 files changed, 179 insertions, 36 deletions
diff --git a/acmeman b/acmeman
index 59af82c..a747a47 100755
--- a/acmeman
+++ b/acmeman
@@ -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);
}
}

Return to:

Send suggestions and report system problems to the System administrator.