diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2017-09-13 15:13:34 +0200 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2017-09-13 15:23:44 +0200 |
commit | 6599db656e097d8eb22921d2c2ce3451c8147563 (patch) | |
tree | 2b04a44ba67b088e0c62ac364c75b72ef928b8a9 /acmeman | |
parent | 36f66056a4ddbba8f300ef2fd15955e292e2755f (diff) | |
download | acmeman-6599db656e097d8eb22921d2c2ce3451c8147563.tar.gz acmeman-6599db656e097d8eb22921d2c2ce3451c8147563.tar.bz2 |
Rewrite
This patch introduces acmeman configuration file, which can be used to
direct its action if a server other than Apache is used. It also can
be instructed to store certificate, certificate chain, and certificate
key in a single file, instead of three different ones. This can be used
with such servers as pound(8).
In the absense of a configuration file, the program operates as in
previous versions.
* MANIFEST: Update.
* Makefile.PL: Update.
* Changes: Update.
* acmeman: Use configuration file if present. Apache configuration
remains as a default source of TLS domains. Configuration file can
override or complement it.
* lib/App/Acmeman/Domain.pm: New file.
* lib/App/Acmeman/Config.pm: New file.
* lib/App/Acmeman/Source/Apache.pm: New file.
* lib/App/Acmeman/Apache/Layout.pm: New file.
Diffstat (limited to 'acmeman')
-rwxr-xr-x | acmeman | 977 |
1 files changed, 554 insertions, 423 deletions
@@ -30,9 +30,12 @@ use Pod::Usage; use Pod::Man; use Getopt::Long qw(:config gnu_getopt no_ignore_case); use POSIX qw(strftime time floor); +use App::Acmeman::Config; +use App::Acmeman::Domain qw(:files); use Data::Dumper; +use Text::ParseWords; -our $VERSION = '1.03'; +our $VERSION = '1.04'; =head1 NAME @@ -43,32 +46,22 @@ acmeman - manages ACME certificates B<acmeman> [B<-Fadns>] [B<-D> I<N>] -[B<-I> I<DIR>] [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>] [B<--force>] -[B<--incdir=>I<DIR>] -[B<--include-directory=>I<DIR>] -[B<--layout=>B<slackware>|B<debian>|B<rh>] -[B<--restart=>I<COMMAND>] [B<--stage>] [B<--time-delta=>I<N>] [I<DOMAIN>...] B<acmeman> B<--setup> | B<-S> [B<-Fdn>] -[B<-f> I<FILE>] -[B<-l> B<slackware>|B<debian>|B<rh>] [B<--config-file=>I<FILE>] [B<--debug>] [B<--dry-run>] [B<--force>] -[B<--layout=>B<slackware>|B<debian>|B<rh>] -[I<DIR>] B<acmeman> [B<-h>] @@ -78,21 +71,258 @@ B<acmeman> =head1 DESCRIPTION A tool for automatic creation and renewal of ACME (LetsEncrypt) SSL -certificates. It assumes that HTTP is served by Apache server, version -2.4 or later (although only minor changes are necessary to make it work -with version 2.2). Three most popular layouts of Apache configuration -files are supported: Debian, Slackware, and Red Hat. A special -directory should be configured for receiving ACME challenges. +certificates. The list of domains to handle can be obtained from +B<acmeman> or B<apache> configuration files, or from both. If the +default B<acmeman> configuration file doesn't exist, the program +scans B<apache> configuration files for a list of domains. + +B<Acmeman> is normally run periodically as a cronjob. + +If you plan to serve SSL protected domains using apache, you can skip +right to the B<APACHE> section. + +The following is a short introduction to the B<acmeman> configuration. For +a detailed discussion, see the B<CONFIGURATION> section below. + +The configuration file, B</etc/acmeman.conf>, consists of statements, +which have the form B<I<KW>=I<VAL>>, grouped into sections, declared +as B<[I<NAME>]> (square brackets are part of the syntax). Empty lines +and comments (introduced by a hash sign) are ignored. + +Domains which require LetsEncrypt certificates are declared in B<domain> +section. Each section introduces a single domain. E.g.: + + [domain example.com] + alt = www.example.com + files = default + +This section instructs B<acmeman> that a certificate is needed for +domain B<example.com>, using B<www.example.com> as its alternative name, +The B<files> statement identifies the name of a B<files> section containing +rules for creating certificate files for that domain. This section must be +defined elsewhere in the configuration file. For example: + + [files default] + type = split + certificate-file = /etc/ssl/acme/$domain/cert.pem + key-file = /etc/ssl/acme/$domain/privkey.pem + ca-file = /etc/ssl/acme/$domain/ca.pem + argument = $domain + +This definition tells B<acmeman> that it should store certificate, certficate +key, and certificate authority chain in three separate files. Names of these +files will be created by replacing the B<$domain> string in the corresponding +definition with the domain name from the B<domain> section, + +In fact, the B<files> section above is the default one. It will be created +implicitly if no other B<files> section is defined in the configuration file. +Moreover, the string B<default> is the default identifier, which is used if +the B<domain> section lacks the B<files> keyword. + +The special section B<[core]> contains basic settings that control the +program behavior. One of the important settings is B<source>, which declares +an external source from which domain settings must be obtained. As +of B<acmeman> version 2.00, the only available external source is B<apache>. +Consider the following configuration: + + [core] + source = apache + +It instructs B<acmeman> to read domain settings from Apache configuration +files. This is basically the configuration that is used in the absense of +the configuration file. See the B<APACHE> section for a detailed discussion +of this operation mode. + +=head1 CONFIGURATION + +Configuration file controls the operation of B<acmeman>. By default, +its name is B</etc/acmeman.conf>. If it is absent, B<acmeman> falls +back to the legacy operation mode, scanning Apache configuration files +for domains that use LetsEncrypt SSL certificates. See the B<APACHE> +section below for a detailed description. + +The configuration file has a traditional line-oriented syntax. Comments +are introduced with a hash sign. Empty lines are ignored. Leading and +trailing whitespace is removed prior to parsing. Long statements can be +split over several physical lines by ending each line excepting the last +one with a backslash immediately followed by a newline character. + +Statements have the following syntax: + + KEYWORD = VALUES + +where I<KEYWORD> stands for a symbolic name consisting of alphanumeric +characters, dashes and underscores, and I<VALUE> stands for any sequence +of characters. + +Statements are grouped in sections. Each section is identified by its name +and optional arguments. The section begins with the construct + + [NAME] + +or, if arguments are present: + + [NAME ARG1 ARG2 ...] + +The square brackets are part of the syntax. + +The statements in the configuration file form a directed graph. Often +in this document we will identify the statement by its I<path>, i.e. a +list of section name, arguments and the keyword, separated by dots. For +example, the path B<files.apache.type> corresponds to the following +configuration file fragment: + + [file apache] + type = single + +The following describes the available sections and keywords + +=head2 B<[core]> + +This section defines the behavior of the program as a whole. + +=over 4 + +=item B<rootdir=>I<DIR> + +Defines the root directory to use instead of the default </var/www/acme>. +Root directory is the directory under which the +F<.well-known/acme-challenge> subdirectory is located. + +=item B<time-delta=>I<SECONDS> + +Sets the time window before the actual expiration time, when the certificate +becomes eligible for renewal. I<N> is time in seconds. The default +value is 86400, which means that B<acmeman> will attempt to renew any +certificate that expires within 24 hours. + +The command line option B<--time-delta> overrides this setting. + +=item B<restart=>I<COMMAND> + +Defines the command to be run at the end of the run if at least one +certificate has been updated. Normally this command reloads the httpd +server (or whatever server is using the certificates). If more than one +B<restart> statements are defined, they will be run in sequenct in the +same order as they appeared in the configuration file. + +=item B<source=>I<ID> [I<LAYOUT>] + +Defines additional source of information. As of B<acmeman> version 2.00, +only one I<ID> is supported: B<apache>. This source module scans apache +configuration files as described in section B<APACHE>. The optional +I<LAYOUT> argument defines the apache configuration layout. Allowed values +are: B<debian>, B<slackware>, B<suse> and B<rh> (for Red Hat). If I<LAYOUT> +is absent, it will be autodetected. + +=item B<files=>I<NAME> + +Identifies the B<[files]> section which describes how to create certificate +files for domains which lack explicit B<files> keyword. Default I<NAME> is +B<default>. See the description of the B<files> statement in B<domain> +section. + +=item B<check-alt-names=>I<BOOL> + +When set to B<true>, it instructs the program to 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. + +Valid values for I<BOOL> are: B<1>, B<on>, B<true>, or B<yes>, for +true, and B<0>, B<off>, B<false>, or B<no> for false (all values +case-insensitive). + +=back + +=head2 B<[domain I<CN>]> + +Declares the domain for which a certificate should be maintained. I<CN> is +the canonical name for the domain. Alternative names can be specified using +the B<alt> keyword. + +=over 4 + +=item B<files=>I<ID> + +Identifies the B<[files]> section which describes how to create certificate +files for this domain. In the absense of this statement, the B<files> +statement from the B<[core]> section will be used. + +=item B<alt=>I<NAME> + +Defines alternative name for the certificate. Multiple B<alt> statements +are allowed. + +=back + +=head2 B<[files I<ID>]> + +The B<files> section instructs B<acmeman> how to create certificate files. +It is applied to particular domains by placing the B<files=I<ID>> statement +in the coresponding domain sections. + +The I<FILENAME> arguments to the keywords below can contain references to +a I<meta-variable>, which will be replaced by the actual domain name when +handling this section for a particular domain. By default, this meta-variable +is B<$domain>. + +=over 4 + +=item B<type=>B<single>|B<split> + +Type of the certificate to create. When set to B<single>, a single +certificate file will be created. Its name is determined by the +B<certificate-file> statement. The file will contain the certificate, +certificate chain and the signature, in this order. + +When set to B<split>, the certificate, certificate chain and the signature +will be saved to three distinct files, whose names are defined by +B<certificate-file>, B<ca-file>, and B<key-file>, correspondingly. If +B<ca-file> is not defined, only certificate and key files will be created. + +The default is B<split>. + +=item B<certificate-file=>I<FILENAME> + +Defines the name of the certificate file for this domain. This statement +is mandatory. + +=item B<key-file=>I<FILENAME> + +Defines the name of the certificate key file. This statement must be present +if B<type> is set to B<split>. + +=item B<ca-file=>I<FILENAME> + +Defines the name of the certificate authority file. This statement may +be present if B<type> is set to B<split>. + +=item B<argument=>I<STRING> + +Defines the name of the meta-variable in I<FILENAME> arguments, which will +be replaced with the actual domain name. Default is B<$domain>. + +=back + +=head1 APACHE + +This is the default mode. It assumes Apache httpd, version 2.4 or later +(although only minor changes are necessary to make it work with version 2.2). +Four most popular layouts of Apache configuration files are supported: +Debian, Slackware, SuSe, and Red Hat. A special directory should be +configured for receiving ACME challenges. The package provides two Apache macros: for serving ACME challenges and declaring SSL virtual hosts. -B<Acmeman> should be started periodically, as a cronjob. Upon startup, -it scans Apache configuration for virtual hosts using ACME certificates, -checks their expiration times, and renews those of the certificates that -are nearing their expiration times within a predefined number of seconds -(24 hours by default). If any of the certificates were updated during -the run, B<acmeman> will restart the B<httpd> server. +Upon startup the program scans Apache configuration for virtual hosts +that use ACME certificates, checks their expiration times, and renews those +of the certificates that are nearing their expiration times within a +predefined number of seconds (24 hours by default). If any of the +certificates were updated during the run, B<acmeman> will restart the +B<httpd> server. =head2 Setup @@ -114,12 +344,6 @@ will be created for each domain name needing SSL, and two files will be placed there: F<cert.pem>, containing the leaf and intermediate certificates for that domain, and F<privkey.pem>, containing the private key for that domain. -If the program is unable to determine the Apache configuration layout, you can -declare it using the B<--layout> option. It takes a single argument, one of: -B<debian>, B<slackware>, or B<rh> (all lower case). Alternatively, you can -specify the location of the main Apache configuration file, using the -B<--config-file> option. - The program will refuse to overwrite existing files B<httpd-letsencrypt.conf>, unless given the B<--force> (B<-F>) option. @@ -206,12 +430,14 @@ will use the B<LetsEncryptSSL> macro to configure the correct certificate: =over 4 -=item B<-D>, B<--tile-delta=>I<N> +=item B<-D>, B<--time-delta=>I<N> Sets the time window before the actual expiration time, when the certificate becomes eligible for renewal. I<N> is time in seconds. The default value is 86400, which means that B<acmeman> will attempt to renew any certificate that expires within 24 hours. + +This option overrides the B<core.time-delta> configuration setting. =item B<-F>, B<--force> @@ -219,11 +445,6 @@ 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<-I>, B<--incdir>, B<--include-directory=>I<DIR> - -Specifies base directory for Apache B<Include> and B<IncludeOptional> -statements with relative pathnames. - =item B<-a>, B<--alt-names> Compare the list of alternative names of each certificate with the one @@ -231,6 +452,8 @@ 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. + +This option overrides the B<core.check-alt-names> configuration setting. =item B<-d>, B<--debug> @@ -257,22 +480,13 @@ debugging information about ACME transactions for each certificate. =item B<-f>, B<--config-file=>I<FILE> -Read I<FILE> as main Apache configuration file. - -=item B<-l>, B<--layout=>I<NAME> - -Defines Apache configuration file layout. Valid values for I<NAME> are: -B<slackware>, B<debian>, and B<rh> (for Red Hat). +Read configuration from I<FILE>, instead of the default F</etc/acmeman.conf>. =item B<-n>, B<--dry-run> Don't modify any files, just print what would have been done. Implies B<--debug>. -=item B<--restart=>I<COMMAND> - -Specifies the command to restart Apache daemon. - =item B<-S>, B<--setup> Set up the B<acmeman> infrastructure files. @@ -317,18 +531,11 @@ my %acme_endpoint = (prod => 'acme-v01.api.letsencrypt.org', staging => 'acme-staging.api.letsencrypt.org'); 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 -my $server_root; -my $www_root; # Root directory for ACME challenges -my @virthost; # List of virtual hosts using ACME certificates -my $filename_arg; # Name of the formal argument to the LetsEncrypServer -my %filename_pattern; # File name pattern. - # Valid keys are: cert, key and chain. +my $config_file = '/etc/acmeman.conf'; +my $config; my $account_key; my $challenge; @@ -365,15 +572,6 @@ sub abend { exit $code; } -sub make_filename { - my ($type, $arg) = @_; - abend(EX_SOFTWARE, "no $type in \%filename_pattern") - unless exists $filename_pattern{$type}; - my $res = $filename_pattern{$type}; - $res =~ s{$filename_arg}{$arg}g; - return $res; -} - sub prep_dir { my $dir = dirname(shift); if (! -d $dir) { @@ -385,7 +583,7 @@ sub prep_dir { if ($file eq '') { error($message); } else { - error("problem unlinking $file: $message"); + error("mkdir $file: $message"); } } exit(EX_CANTCREAT); @@ -400,50 +598,51 @@ sub debug_to_loglevel { } sub make_csr { - my $cn = shift; + my $dom = shift; my $req = Crypt::OpenSSL::PKCS10->new(4096); - $req->set_subject("/CN=$cn"); + $req->set_subject("/CN=".$dom->cn); $req->add_ext(Crypt::OpenSSL::PKCS10::NID_subject_alt_name, - join(',', map { "DNS:$_" } @_)) if @_; + join(',', map { "DNS:$_" } $dom->alt)) + if $dom->alt > 0; $req->add_ext_final(); $req->sign(); - if (exists($filename_pattern{key})) { - my $filename = make_filename('key', $cn); - debug(3, "writing $filename"); - prep_dir($filename); - my $u = umask(077); - $req->write_pem_pk($filename); - umask($u); - } - return $req->get_pem_req(); + return $req; } sub save_crt { - my $type = shift; my $domain = shift; + my $type = shift; - my $filename = make_filename($type, $domain); - debug(3, "writing $filename"); - prep_dir($filename); - open(my $fd, '>', $filename); + if (my $filename = $domain->file($type)) { + debug(3, "writing $filename"); + prep_dir($filename); + open(my $fd, '>', $filename); - foreach my $der (@_) { - my $pem = Crypt::Format::der2pem($der, 'CERTIFICATE'); - print $fd $pem; - print $fd "\n"; + foreach my $der (@_) { + my $pem = Crypt::Format::der2pem($der, 'CERTIFICATE'); + print $fd $pem; + print $fd "\n"; + } + close $fd; + return $filename; } - close $fd; +} + +sub selected_domain { + my $dom = shift; + return 1 unless %select; + return grep { $select{$_} } $dom->names; } sub domain_cert_expires { my $domain = shift; - my $crt = make_filename('cert', $domain); + my $crt = $domain->certificate_file; 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 + my $msg = $config->get(qw(core 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 @@ -451,8 +650,7 @@ sub domain_cert_expires { # names: my $blob = $exts->{subjectAltName}->value(); my @missing; - foreach my $vh (map { lc } sort { length($b) <=> length($a) } - uniq($domain, @_)) { + foreach my $vh (sort { length($b) <=> length($a) } $domain->names) { unless ($blob =~ s/\Q$vh\E\b//) { push @missing, $vh; } @@ -461,7 +659,7 @@ sub domain_cert_expires { debug(1, "$crt: the following SANs are missing: " . join(', ', @missing) ."; $msg"); - return 1 if $check_alt_names; + return 1 if $config->get(qw(core check-alt-names)); } } @@ -485,7 +683,7 @@ sub domain_cert_expires { $in = "today"; } debug(2, "$crt expires on $expiry, $in"); - if ($now + $time_delta < $ts) { + if ($now + $config->get(qw(core time-delta)) < $ts) { return 0; } else { debug(2, "will renew $crt (expires on $expiry, $in)"); @@ -501,20 +699,19 @@ sub register_domain_certificate { my $domain = shift; if ($debug) { - my $crt = make_filename('cert', $domain); + my $crt = $domain->certificate_file; + my $alt = join(',', $domain->alt); if (-f $crt) { - debug(1, "renewing $crt: CN=$domain, alternatives=@_"); + debug(1, "renewing $crt: CN=$domain, alternatives=$alt"); } else { - debug(1, "issuing $crt: CN=$domain, alternatives=@_"); + debug(1, "issuing $crt: CN=$domain, alternatives=$alt"); } } return 1 if $dry_run; my $acme = Protocol::ACME->new( host => $acme_endpoint{$acme_host}, - account_key => { - buffer => $account_key->get_private_key_string(), - format => 'PEM' }, + account_key => { buffer => $account_key->get_private_key_string(), format => 'PEM' }, loglevel => debug_to_loglevel() ); @@ -523,21 +720,43 @@ sub register_domain_certificate { $acme->register(); $acme->accept_tos(); - foreach my $name ($domain, @_) { + foreach my $name ($domain->names) { $acme->authz($name); $acme->handle_challenge($challenge); $acme->check_challenge(); $acme->cleanup_challenge($challenge); } - my $csr = make_csr($domain, @_); - my $cert = $acme->sign({ format => 'PEM', buffer => $csr }); + + my $csr = make_csr($domain); + my $cert = $acme->sign({ format => 'PEM', buffer => $csr->get_pem_req() }); my $chain = $acme->chain(); - if (exists($filename_pattern{chain})) { - save_crt('cert', $domain, $cert); - save_crt('chain', $domain, $chain); + + if (my $filename = $domain->file(KEY_FILE)) { + debug(3, "writing $filename"); + prep_dir($filename); + my $u = umask(077); + $csr->write_pem_pk($filename); + umask($u); + + if ($filename = $domain->file(CA_FILE)) { + save_crt($domain, CA_FILE, $chain); + } + save_crt($domain, CERT_FILE, $cert); } else { - save_crt('cert', $domain, $cert, $chain); - } + $filename = $domain->certificate_file; + debug(3, "writing $filename"); + prep_dir($filename); + my $u = umask(077); + open(my $fd, '>', $filename) + or abend(EX_CANTCREAT, "can't open $filename for writing: $!"); + print $fd Crypt::Format::der2pem($cert, 'CERTIFICATE'); + print $fd "\n"; + print $fd Crypt::Format::der2pem($chain, 'CERTIFICATE'); + print $fd "\n"; + print $fd $csr->get_pem_pk(); + print $fd "\n"; + umask($u); + } }; if ($@) { if (UNIVERSAL::isa($@, 'Protocol::ACME::Exception')) { @@ -559,251 +778,44 @@ sub register_domain_certificate { return 1; } -sub dequote { - my ($arg) = @_; - $arg =~ s{^"(.*?)"$}{$1}; - return $arg; -} - -sub uniq { - my %h; - map { - if (exists($h{$_})) { - () - } else { - $h{$_} = 1; - $_ - } - } @_; -} - -sub examine_http_config { - my ($file) = @_; - - use constant { - STATE_INITIAL => 0, # Initial state - STATE_VIRTUAL_HOST => 1, # In VirtualHost block - STATE_USE_CHALLENGE => 2, # Ditto, but challenge macro was used - 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; - - debug(3, "reading apache configuration file \"$file\""); - if (open(my $fd, '<', $file)) { - my $server_name; - my @server_aliases; - my $reference; - my $line; - - while (<$fd>) { - ++$line; - chomp; - s/^\s+//; - next if /^(#.*)?$/; - if (/^include(optional)?\s+(.+?)\s*$/i) { - debug(3, "$file:$line: state $state: Include$1 $2"); - http_include(dequote($2), $1 ne ''); - next; - } - - if ($state == STATE_INITIAL) { - if (/^<VirtualHost/i) { - $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+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_REFERENCE) { - if (m{</VirtualHost}i) { - unshift @server_aliases, $server_name - if defined $server_name; - if (@server_aliases - && (!%select || exists($select{$server_name}))) { - 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/) { - 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*$/) { - push @server_aliases, - map { /^\*\./ ? () : dequote($_) } split /\s+/, $1; - } - } elsif ($state == STATE_MACRO_CHALLENGE) { - if (m{^</macro}i) { - $state = STATE_INITIAL; - } elsif (m{^(?:(?i)Alias)\s+/.well-known/acme-challenge\s+(.+)}) { - my $dir = dequote($1); - $dir =~ s{/.well-known/acme-challenge$}{}; - $www_root = $dir; - debug(3, "ACME challenge root dir: $www_root"); - } - } elsif ($state == STATE_MACRO_SSL) { - if (m{^</macro}i) { - $state = STATE_INITIAL; - } elsif (/(?:(?i)SSLCertificate((?:Key)|(?:Chain))?File)\s+(.+)/) { - $filename_pattern{lc($1 ? $1 : 'cert')} = $2; - } - } - } - close $fd; - } else { - error("can't open file \"$file\": $!"); - return 0; - } - return 1; -} - -sub http_include { - my ($pattern, $optional) = @_; - $pattern = "$server_root/$pattern" unless $pattern =~ m{^/}; - $pattern =~ s{/*$}{}; - $pattern .= '/*' if -d $pattern; - foreach my $file (glob $pattern) { - if ($optional && ! -e $file) { - debug(1, "optional include file \"$file\" doesn't exist"); - next; - } - examine_http_config($file); - } -} -# ## -my $apache_layout; - -sub incdir { - if (exists($apache_layout->{incdir})) { - if (ref($apache_layout->{incdir}) eq 'CODE') { - return &{$apache_layout->{incdir}}; - } else { - return $apache_layout->{incdir}; - } - } - return dirname($apache_layout->{config}); -} - sub get_root_cert { my $name = shift; - prep_dir($name); + + prep_dir($name) unless $dry_run; debug(1, "downloading $letsencrypt_root_cert_url to \"$name\""); my $ua = LWP::UserAgent->new; my $response = $ua->get($letsencrypt_root_cert_url); if ($response->is_success) { - open(my $fd, '>', $name) - or abend(EX_CANTCREAT, "can't open \"$name\" for writing: $!"); - print $fd $response->decoded_content; - close $fd; + unless ($dry_run) { + open(my $fd, '>', $name) + or abend(EX_CANTCREAT, "can't open \"$name\" for writing: $!"); + print $fd $response->decoded_content; + close $fd; + } } else { error("error downloading certificate from $letsencrypt_root_cert_url"); abend(EX_NOINPUT, $response->status_line); } } - + sub initial_setup { - if (defined($www_root)) { - error("acmeman macros seem to be already installed"); - abend(EX_NOPERM, "use --force to continue") unless $force; + get_root_cert('/etc/ssl/acme/lets-encrypt-x3-cross-signed.pem'); + my $source = $config->get(qw(core source)); + unless (defined($source)) { + require App::Acmeman::Source::Apache; + $source = new App::Acmeman::Source::Apache; + $source->configure($config); + $source = undef unless $config->success; + $config->clrerr; } - if ($#ARGV == -1) { - $www_root = '/var/www/acme'; - } elsif ($#ARGV == 0) { - $www_root = $ARGV[0]; - } else { - abend(EX_USAGE, "too many arguments in setup mode"); - } - my $filename = incdir() . "/httpd-letsencrypt.conf"; - if (-e $filename) { - error("the file \"$filename\" already exists"); - abend(EX_NOPERM, "use --force to continue") unless $force; - } - debug(2, "writing $filename"); - unless ($dry_run) { - prep_dir($filename); - open(my $fd, '>', $filename) - or abend(EX_CANTCREAT, "can't open \"$filename\" for writing: $!"); - print $fd <<EOT; -<Macro LetsEncryptChallenge> - Alias /.well-known/acme-challenge $www_root/.well-known/acme-challenge - <Directory $www_root/.well-known/acme-challenge> - Options None - Require all granted - </Directory> - <IfModule mod_rewrite.c> - RewriteEngine On - RewriteRule /.well-known/acme-challenge - [L] - </IfModule> -</Macro> - -<Macro LetsEncryptReference \$domain> - Use LetsEncryptChallenge - Alias /.dummy/\$domain /dev/null -</Macro> - -<Macro LetsEncryptSSL \$domain> - SSLEngine on - SSLProtocol all -SSLv2 -SSLv3 - SSLHonorCipherOrder on - SSLCipherSuite EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:EECDH+RC4:RSA+RC4:!MD5 - SSLCertificateFile /etc/ssl/acme/\$domain/cert.pem - 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; - - get_root_cert('/etc/ssl/acme/lets-encrypt-x3-cross-signed.pem'); - if (exists($apache_layout->{post_setup})) { - &{$apache_layout->{post_setup}}($filename); + if ($source) { + unless ($source->setup(dry_run => $dry_run, force => $force)) { + exit(1); } } - error("created file \"$filename\"", prefix => 'NOTE'); - error("please, enable mod_macro and make sure your Apache configuration includes this file", prefix => 'NOTE'); + exit(EX_OK); } @@ -811,51 +823,51 @@ sub coalesce { my $ref = shift; debug(2, "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}}; + my @domlist; + foreach my $ent (sort { $a->{domain} cmp $b->{domain} } + map { { ord => $i++, domain => $_ } } @{$ref}) { + if (@domlist && $domlist[-1]->cn eq $ent->cn) { + $domlist[-1] += $ent; } else { - push @vhost, $ent; + push @domlist, $ent; } } - @{$ref} = map { $_->{names} } sort { $a->{ord} <=> $b->{ord} } @vhost; + @{$ref} = map { $_->{domain} } sort { $a->{ord} <=> $b->{ord} } @domlist; } - -my %apache_layout_tab = ( - slackware => { config => '/etc/httpd/httpd.conf', - incdir => '/etc/httpd/extra', - restart => '/etc/rc.d/rc.httpd restart' - }, - debian => { config => '/etc/apache2/apache2.conf', - incdir => sub { - for my $dir ('/etc/apache2/conf-available', - '/etc/apache2/conf.d') { - return $dir if -d $dir; - } - warn 'none of the expected configuration directories found; falling back to /etc/apache2'; - return '/etc/apache2'; - }, - restart => 'service apache2 restart', - post_setup => sub { - my ($filename) = @_; - my $dir = dirname($filename); - my $name = basename($filename); - if ($dir eq '/etc/apache2/conf-available') { - chdir('/etc/apache2/conf-enabled'); - symlink "../conf-available/$name", $name; - } - } - }, - rh => { config => '/etc/httpd/conf/httpd.conf', - incdir => '/etc/httpd/conf.d', - restart => 'service httpd restart' + +sub collect { + my $aref = shift; + return unless $config->isset('domain'); + my $err; + while (my ($k, $v) = each %{$config->get('domain')}) { + my $dom; + my $ft; + + if (exists($v->{files})) { + if (my $fref = $config->get('files', $v->{files})) { + $dom = new App::Acmeman::Domain( + cn => $k, + alt => $v->{alt}, + %{$fref}); + } else { + error("files.$k->{files} is referenced from [domain $k], but never declared"); + ++$err; + next; + } + } else { + $dom = new App::Acmeman::Domain( + cn => $k, + alt => $v->{alt}, + %{$config->get('files', $config->get(qw(core files)))}); + } + push @$aref, $dom; } -); + exit(1) if $err; +} my $setup; +my $time_delta; +my $check_alt_names; GetOptions("h" => sub { pod2usage(-message => "$progname: $progdescr", @@ -872,81 +884,200 @@ GetOptions("h" => sub { "stage|s" => sub { $acme_host = 'staging' }, "force|F" => \$force, "time-delta|D=n" => \$time_delta, - "layout|l=s" => sub { - my ($opt, $arg) = @_; - abend(EX_USAGE, "unknown layout: $arg") - unless exists $apache_layout_tab{$arg}; - $apache_layout = $apache_layout_tab{$arg}; - }, "setup|S" => \$setup, - "config-file|f=s" => sub { - $apache_layout ||= {}; - $apache_layout->{config} = $_[1] - }, - "restart=s" => sub { - $apache_layout ||= {}; - $apache_layout->{restart} = $_[1] |