diff options
author | Hugo Hörnquist <hugo@lysator.liu.se> | 2023-01-10 12:56:33 +0100 |
---|---|---|
committer | Hugo Hörnquist <hugo@lysator.liu.se> | 2023-01-12 15:07:57 +0100 |
commit | 0a07215d422f8f606a41d822436e6c6dd93d001f (patch) | |
tree | 3e335e7fb5e3b03b90fdef953bf7be8afef73ff8 | |
parent | Convert to pdk module. (diff) | |
download | hugonikanor-letsencrypt-0a07215d422f8f606a41d822436e6c6dd93d001f.tar.gz hugonikanor-letsencrypt-0a07215d422f8f606a41d822436e6c6dd93d001f.tar.xz |
Working product.
28 files changed, 316 insertions, 151 deletions
diff --git a/data/Archlinux.yaml b/data/Archlinux.yaml deleted file mode 100644 index 386801e..0000000 --- a/data/Archlinux.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -letsencrypt::nginx::certbot_plugin_package: certbot-nginx -letsencrypt::apache::certbot_plugin_package: certbot-apache diff --git a/data/common.yaml b/data/common.yaml new file mode 100644 index 0000000..ae7a581 --- /dev/null +++ b/data/common.yaml @@ -0,0 +1,2 @@ +--- +letsencrypt::renewal_provider: cron diff --git a/data/os/Archlinux.yaml b/data/os/Archlinux.yaml new file mode 100644 index 0000000..671637f --- /dev/null +++ b/data/os/Archlinux.yaml @@ -0,0 +1,5 @@ +--- +letsencrypt::authenticator::nginx::certbot_plugin_package: certbot-nginx +letsencrypt::authenticator::apache::certbot_plugin_package: certbot-apache + +letsencrypt::renewal_provider: systemd diff --git a/data/os/Debian.yaml b/data/os/Debian.yaml new file mode 100644 index 0000000..0d2e358 --- /dev/null +++ b/data/os/Debian.yaml @@ -0,0 +1,5 @@ +--- +letsencrypt::authenticator::nginx::certbot_plugin_package: python3-certbot-nginx +letsencrypt::authenticator::apache::certbot_plugin_package: python3-certbot-apache + +letsencrypt::renew::setup::provider: systemd diff --git a/data/FreeBSD.yaml b/data/os/FreeBSD.yaml index 6e2fe58..6e2fe58 100644 --- a/data/FreeBSD.yaml +++ b/data/os/FreeBSD.yaml diff --git a/data/os/RedHat.yaml b/data/os/RedHat.yaml new file mode 100644 index 0000000..0d2e358 --- /dev/null +++ b/data/os/RedHat.yaml @@ -0,0 +1,5 @@ +--- +letsencrypt::authenticator::nginx::certbot_plugin_package: python3-certbot-nginx +letsencrypt::authenticator::apache::certbot_plugin_package: python3-certbot-apache + +letsencrypt::renew::setup::provider: systemd diff --git a/data/os/RedHat/7.yaml b/data/os/RedHat/7.yaml new file mode 100644 index 0000000..0869a58 --- /dev/null +++ b/data/os/RedHat/7.yaml @@ -0,0 +1,3 @@ +--- +letsencrypt::authenticator::nginx::certbot_plugin_package: python2-certbot-nginx +letsencrypt::authenticator::apache::certbot_plugin_package: python2-certbot-apache diff --git a/files/letsencrypt-renew.service b/files/letsencrypt-renew.service index 253f260..f8f2c18 100644 --- a/files/letsencrypt-renew.service +++ b/files/letsencrypt-renew.service @@ -4,5 +4,4 @@ Documentation=man:certbot(1) [Service] Type=oneshot -EnvironmentFile=/etc/letsencrypt/env/%i -ExecStart=certbot --text --agree-tos --non-interactive certonly --rsa-key-size 4086 --cert-name '%i' -a $AUTHENTICATOR $DOMAINS --post-hook $POST_HOOK --quiet --keep-until-expiring +ExecStart=/etc/letsencrypt/renew_cert %i diff --git a/files/run_certbot.py b/files/run_certbot.py new file mode 100644 index 0000000..f81f707 --- /dev/null +++ b/files/run_certbot.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +""" +Gathers domain names to give to certbot, and then execs +certbot. "Required" to send multiple domain names + +File managed by Puppet +""" + +# Script should be compatible with both Python2 and Python3 + +from __future__ import print_function +import sys +import os + +if len(sys.argv) != 2: + print('Takes exactly one argument: the certificates name', + file=sys.stderr) + os.exit(1) + + +cert_name = sys.argv[1] +here = os.path.dirname(sys.argv[0]) + +cmdline = ['certbot', '--config', os.path.join(here, cert_name + ".ini")] +with open(os.path.join(here, cert_name + '.domains')) as f: + for line in f: + if not line: + continue + if line[0] == '#': + continue + cmdline += ['-d', line.strip()] +cmdline += ['certonly'] + +os.execvp('certbot', cmdline) diff --git a/functions/conf/nginx.pp b/functions/conf/nginx.pp new file mode 100644 index 0000000..aa5f30e --- /dev/null +++ b/functions/conf/nginx.pp @@ -0,0 +1,17 @@ +function letsencrypt::conf::nginx ( + String $cert_name, +) >> Letsencrypt::Ssl_conf::Nginx { + $cert_path = $facts['letsencrypt_directory'][$cert_name] + if $cert_path == undef { + { + ssl => false, + } + } else { + { + ssl => true, + ssl_redirect => true, + ssl_cert => "${cert_path}/fullchain.pem", + ssl_key => "${cert_path}/privkey.pem", + } + } +} diff --git a/functions/conf/nginx/location.pp b/functions/conf/nginx/location.pp new file mode 100644 index 0000000..84d0e82 --- /dev/null +++ b/functions/conf/nginx/location.pp @@ -0,0 +1,16 @@ +function letsencrypt::conf::nginx::location ( + String $cert_name, +) >> Letsencrypt::Ssl_conf::Nginx::Location { + $cert_path = $facts['letsencrypt_directory'][$cert_name] + + if $cert_path == undef { + { + ssl => false, + } + } else { + { + ssl => true, + ssl_only => true, + } + } +} diff --git a/hiera.yaml b/hiera.yaml new file mode 100644 index 0000000..545fff3 --- /dev/null +++ b/hiera.yaml @@ -0,0 +1,21 @@ +--- +version: 5 + +defaults: # Used for any hierarchy level that omits these keys. + datadir: data # This path is relative to hiera.yaml's directory. + data_hash: yaml_data # Use the built-in YAML backend. + +hierarchy: + - name: "osfamily/major release" + paths: + # Used to distinguish between Debian and Ubuntu + - "os/%{facts.os.name}/%{facts.os.release.major}.yaml" + - "os/%{facts.os.family}/%{facts.os.release.major}.yaml" + # Used for Solaris + - "os/%{facts.os.family}/%{facts.kernelrelease}.yaml" + - name: "osfamily" + paths: + - "os/%{facts.os.name}.yaml" + - "os/%{facts.os.family}.yaml" + - name: 'common' + path: 'common.yaml' diff --git a/manifests/authenticator/nginx.pp b/manifests/authenticator/nginx.pp new file mode 100644 index 0000000..971c4ed --- /dev/null +++ b/manifests/authenticator/nginx.pp @@ -0,0 +1,22 @@ +# Sets up nginx specific configuration, and provides access to +# variables for enterpolating into nginx configurations +# +# These use the default cert name +# @example +# nginx::resource::server { 'servername': +# * => $letsescrypt::nginx::server_ssl +# } +# $letsencrypt::nginx::location_ssl +# @param certbot_plugin_package +# Name of the system package providing this plugin. +# Populated through hiera. +# @param manage_package +# If this class should manage the package. +class letsencrypt::authenticator::nginx ( + String $certbot_plugin_package, + Boolean $manage_package = true, +) { + if $manage_package { + ensure_packages([$certbot_plugin_package]) + } +} diff --git a/manifests/cert.pp b/manifests/cert.pp index 061ace1..13e1c82 100644 --- a/manifests/cert.pp +++ b/manifests/cert.pp @@ -1,44 +1,78 @@ # @summary A single certificate -# TODO possibly default cert_name to $::fqdn instead -# @param cert_name Name of the certificate +# @param cert_name +# Name of the certificate, can be anything, but $::fqdn is recommended # @param ensure Present or absent (currently does nothing) -# @param include_self Should the certificates name be one of its domains? +# @param include_self +# Should the certificates name be one of its domains? +# @param authenticator +# How should the challenge be handled. +# @param domains +# List of domains to add to certificate +# @param config +# Additional config for this entry define letsencrypt::cert ( + Letsencrypt::Authenticator $authenticator, String $cert_name = $name, Enum['present', 'absent'] $ensure = 'present', Boolean $include_self = true, + Array[String] $domains = [], + Hash[String, Any] $config = {}, ) { - # TODO these env files are systemd specific - # TODO concat::fragment is clumsy, look at re-implementing the - # functionallity internally + $conf_file = "${letsencrypt::config_dir}/${cert_name}.ini" + $domain_file = "${letsencrypt::config_dir}/${cert_name}.domains" - concat { "${letsencrypt::config_dir}/env/${cert_name}": - ensure => present, - warn => true, + include "::letsencrypt::authenticator::${authenticator}" + + $local_conf = { + 'cert-name' => $cert_name, + 'rsa-key-size' => 4096, + 'authenticator' => $authenticator, + 'agree-tos' => true, + 'quiet' => true, + 'keep-until-expiring' => true, + 'non-interactive' => true, } - $cert_preamble = @(EOF) - AUTHENTICATOR = '' - POST_HOOK = '' - DOMAINS = - |- EOF + $conf = $letsencrypt::config_ + $local_conf + $config - concat::fragment { "letsencrypt ${cert_name} preamble": - target => "${letsencrypt::config_dir}/env/${cert_name}", - order => '0', - content => $cert_preamble, + file { $conf_file: + ensure => file, + content => epp("${module_name}/ini.epp", { 'values' => $conf }), } - concat::fragment { "letsencrypt ${cert_name} postamble": - target => "${letsencrypt::config_dir}/env/${cert_name}", - order => '99', - content => "\n\n", + concat { $domain_file: + ensure_newline => true, + warn => true, } - if $include_self { - letsencrypt::domain { $cert_name: } + $domains.each |$domain| { + letsencrypt::domain { $domain: + cert_name => $cert_name, + } + } + if $include_self and ! $cert_name in $domains { + letsencrypt::domain { $cert_name: + cert_name => $cert_name, + } } letsencrypt::renew { $cert_name: } + + # This might be incorrect. If a certificate of that name already + # exists then the new certificate will instead be called + # ${cert-name}-0001. See + # https://eff-certbot.readthedocs.io/en/stable/using.html#where-are-my-certificates + exec { "letsencrypt - get initial ${cert_name}": + creates => "${letsencrypt::cert_dir}/${cert_name}", + command => [$letsencrypt::renew::setup::renew_script, $cert_name], + require => File[$letsencrypt::renew::setup::renew_script], + } + + exec { "letsencrypt - refresh ${cert_name}": + command => [$letsencrypt::renew::setup::renew_script, $cert_name], + subscribe => [File[$conf_file], Concat[$domain_file]], + refreshonly => true, + require => File[$letsencrypt::renew::setup::renew_script], + } } diff --git a/manifests/domain.pp b/manifests/domain.pp index 9e6b377..1f9fa40 100644 --- a/manifests/domain.pp +++ b/manifests/domain.pp @@ -1,20 +1,15 @@ -# A single domain belonging to a certificate -# @example -# letsencrypt::domain { 'www.example.com': -# cert_name => 'example.com', -# } -# @param domain_name Hostname which should be included in the target certificate -# @param cert_name Certificate to add the hostname to +# @summary +# A single domain name which should be part of a certificate +# @param cert_name +# Which certificate this domain name belongs to +# @param domain_name +# The domain name to be added define letsencrypt::domain ( + String $cert_name, String $domain_name = $name, - String $cert_name = $::facts['fqdn'], ) { - ensure_resource('letsencrypt::cert', $cert_name, { - ensure => present, - }) - - concat::fragment { "letsencrypt ${cert_name} - ${domain_name}": - target => "${letsencrypt::config_dir}/env/${cert_name}", - content => " -d ${domain_name}", + concat::fragment { "Letsencrypt::Domain - ${cert_name} - ${domain_name}": + target => "${letsencrypt::config_dir}/${cert_name}.domains", + content => $domain_name, } } diff --git a/manifests/init.pp b/manifests/init.pp index cc72b32..d6fb5f6 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -1,38 +1,53 @@ # @summary Sets up letsencrypt for other classes # @param email Contact email sent to letsencrypt -# @param config_dir Location of configuration files -# @param default_cert Should a certificate be automatically configured -# @param default_cert_name -# The name (and domain) of the automatically configured centificate. +# @param manage_package +# Should the certbot package resource be managed by this class +# @param certbot_package +# Name of the certbot package. Should be automatically set through hiera. +# @param server +# Server providing ACME challenge +# @param renewal_provider +# Service responsible for periodically renewing the certificate +# @param config +# Default configuration values to pass to certbot. $server and +# $email is added here if not explicitly set. It's later merged with +# a specific instance for each certificate. class letsencrypt ( String $email, - Stdlib::Unixpath $config_dir = '/etc/letsencrypt', - String $default_cert_name = $::facts['fqdn'], - Boolean $default_cert = true, - # TODO renewal provider here? + Letsencrypt::Renewal_provider $renewal_provider, # hiera + String $certbot_package = 'certbot', + Boolean $manage_package = true, + String $server = 'https://acme-v02.api.letsencrypt.org/directory', + Hash[String, Any] $config = {}, ) { - if $default_cert { - letsencrypt::cert { $default_cert_name: - ensure => present, - } - } + # if $default_cert { + # letsencrypt::cert { $default_cert_name: + # ensure => present, + # } + # } - file { $config_dir: - ensure => directory, - } + # These are internal instead of parameters, since certbot appears to + # not accept them in other places. This might prove wrong (BSD?), in + # that case: make them parameters again, and resolve the few remaining + # instances where they are hard coded. + $config_dir = '/etc/letsencrypt' + $cert_dir = "${config_dir}/live" - $cli_conf = @("EOF") - email = ${email} - | EOF + # Used by letsencrypt::cert + $config_ = { + 'server' => $server, + 'email' => $email, + } + $config - file { "${config_dir}/cli.ini": - content => $cli_conf, + file { $config_dir: + ensure => directory, } include letsencrypt::renew::setup - # Boolean indicating if ssl is configured. Mainly used by - # letsencrypt::nginx and similar classes to determine their export - # of their variable $ssl. - $ssl_configured = 'letsencrypt_director$' in keys($facts) + if $manage_package { + package { $certbot_package: + ensure => installed, + } + } } diff --git a/manifests/nginx.pp b/manifests/nginx.pp deleted file mode 100644 index 75b5b48..0000000 --- a/manifests/nginx.pp +++ /dev/null @@ -1,52 +0,0 @@ -# Sets up nginx specific configuration, and provides access to -# variables for enterpolating into nginx configurations -# -# These use the default cert name -# @example -# nginx::resource::server { 'servername': -# * => $letsescrypt::nginx::server_ssl -# } -# $letsencrypt::nginx::location_ssl -# -# @param certbot_plugin_package -# Name of the system package providing this plugin. -# Populated through hiera. -# @param manage_package -# If this class should manage the package. -class letsencrypt::nginx ( - String $certbot_plugin_package, - Boolean $manage_package = true, -) { - # TODO $cert_path should use the default certificate name. - # There should however also be a hash of all configured - # certificates. - $cert_path = "${letsencrypt::config_dir}/live/${letsencrypt::config_dir::default_cert_name}" - - $server_ssl = if $letsencrypt::ssl_configured { - { - ssl => true, - ssl_redirect => true, - ssl_cert => "${cert_path}/fullchain.pem", - ssl_key => "${cert_path}/privkey.pem", - } - } else { - { - ssl => false, - } - } - - $location_ssl = if $letsencrypt::ssl_configured { - { - ssl => true, - ssl_only => true, - } - } else { - { - ssl => false, - } - } - - if $manage_package { - ensure_packages([$certbot_plugin_package]) - } -} diff --git a/manifests/renew.pp b/manifests/renew.pp index 97cf5e9..ce6fbee 100644 --- a/manifests/renew.pp +++ b/manifests/renew.pp @@ -6,10 +6,7 @@ define letsencrypt::renew ( String $cert_name = $name, ) { - # TODO this is systemd specific - # TODO ensure letsencrypt::renew::setup is included beforehand - service { "${letsencrypt::renew::systemd::service_name}@${cert_name}.timer": - ensure => 'running', - enable => true, + Resource["letsencrypt::renew::${letsencrypt::renew::setup::provider}"] { $cert_name: + cert_name => $cert_name, } } diff --git a/manifests/renew/cron.pp b/manifests/renew/cron/setup.pp index 37aa3fb..d6cb51b 100644 --- a/manifests/renew/cron.pp +++ b/manifests/renew/cron/setup.pp @@ -1,6 +1,6 @@ # Handles renewal certificates through CRON -# private -class letsencrypt::renew::cron ( +# @api private +class letsencrypt::renew::cron::setup ( ) { fail('Not yet implemented') } diff --git a/manifests/renew/setup.pp b/manifests/renew/setup.pp index 8b4708b..7ba6a1b 100644 --- a/manifests/renew/setup.pp +++ b/manifests/renew/setup.pp @@ -1,18 +1,17 @@ # Sets up timers for automatically renewing certificates -# TODO -# - make provider OS dependant -# - is provider the correct name? # @param provider # How the renewal should be managed. # @api private class letsencrypt::renew::setup ( - Enum['systemd', 'cron'] $provider = 'systemd', + Letsencrypt::Renewal_provider $provider = $letsencrypt::renewal_provider, ) { - file { [ - '/etc/letsencrypt/env', - ]: - ensure => directory, - } + include "::letsencrypt::renew::${provider}::setup" + + # also used by letsencrypt::cert + $renew_script = "${letsencrypt::config_dir}/renew_cert" - include "::letsencrypt::renew::${provider}" + file { $renew_script: + source => "puppet:///modules/${module_name}/run_certbot.py", + mode => '0555', + } } diff --git a/manifests/renew/systemd.pp b/manifests/renew/systemd.pp index 8c63f23..f64e7e5 100644 --- a/manifests/renew/systemd.pp +++ b/manifests/renew/systemd.pp @@ -1,16 +1,11 @@ -# Handles renewal certificates through systemd timers -# @param service_name Target name of the service file -# @param service_path Where the service file should be installed # @api private -class letsencrypt::renew::systemd ( - String $service_name = 'letsencrypt-renew', - String $service_path = '/etc/systemd/system', +define letsencrypt::renew::systemd ( + String $cert_name = $name ) { - file { "${service_path}/${service_name}@.service": - source => "puppet:///modules/${module_name}/letsencrypt-renew.service", - } - - file { "${service_path}/${service_name}@.timer": - source => "puppet:///modules/${module_name}/letsencrypt-renew.timer", + require letsencrypt::renew::systemd::setup + $service = $letsencrypt::renew::systemd::setup::service_name + service { "${service}@${cert_name}.timer": + ensure => 'running', + enable => true, } } diff --git a/manifests/renew/systemd/setup.pp b/manifests/renew/systemd/setup.pp new file mode 100644 index 0000000..5839efc --- /dev/null +++ b/manifests/renew/systemd/setup.pp @@ -0,0 +1,23 @@ +# Handles renewal certificates through systemd timers +# @param service_name Target name of the service file +# @param service_path Where the service file should be installed +# @api private +class letsencrypt::renew::systemd::setup ( + String $service_name = 'letsencrypt-renew', + String $service_path = '/etc/systemd/system', +) { + file { "${service_path}/${service_name}@.service": + source => "puppet:///modules/${module_name}/letsencrypt-renew.service", + notify => Exec['systemctl daemon-reload'], + } + + file { "${service_path}/${service_name}@.timer": + source => "puppet:///modules/${module_name}/letsencrypt-renew.timer", + notify => Exec['systemctl daemon-reload'], + } + + exec { 'systemctl daemon-reload': + refreshonly => true, + provider => shell, + } +} diff --git a/metadata.json b/metadata.json index 834628d..0416520 100644 --- a/metadata.json +++ b/metadata.json @@ -4,7 +4,7 @@ "author": "HugoNikanor", "summary": "", "license": "Apache-2.0", - "source": "", + "source": "https://git.hornquist.se/puppet/hugonikanor-letsencrypt", "operatingsystem_support": [ { "operatingsystem": "Archlinux" diff --git a/templates/ini.epp b/templates/ini.epp new file mode 100644 index 0000000..7d5cc92 --- /dev/null +++ b/templates/ini.epp @@ -0,0 +1,10 @@ +<%- | Hash[String, Any] $values +| -%> +<%# This is technically not a proper ini-file, since it lacks headersa. + Certbot however calls them ini files, so we do to. -%> +# File managed by Puppet +<%- $values.each |$key, $value| { -%> +<%- unless $value == undef { -%> +<%= $key %> = <%= $value %> +<%- } -%> +<%- } -%> diff --git a/types/authenticator.pp b/types/authenticator.pp new file mode 100644 index 0000000..8593213 --- /dev/null +++ b/types/authenticator.pp @@ -0,0 +1 @@ +type Letsencrypt::Authenticator = Enum['nginx'] diff --git a/types/renewal_provider.pp b/types/renewal_provider.pp new file mode 100644 index 0000000..bfa3077 --- /dev/null +++ b/types/renewal_provider.pp @@ -0,0 +1 @@ +type Letsencrypt::Renewal_provider = Enum['systemd', 'cron'] diff --git a/types/ssl_conf/nginx.pp b/types/ssl_conf/nginx.pp new file mode 100644 index 0000000..fb2187d --- /dev/null +++ b/types/ssl_conf/nginx.pp @@ -0,0 +1,11 @@ +type Letsencrypt::Ssl_conf::Nginx = Variant[ + Struct[{ + ssl => Boolean, + }], + Struct[{ + ssl => Boolean, + ssl_redirect => Boolean, + ssl_cert => String, + ssl_key => String, + }], +] diff --git a/types/ssl_conf/nginx/location.pp b/types/ssl_conf/nginx/location.pp new file mode 100644 index 0000000..25941d0 --- /dev/null +++ b/types/ssl_conf/nginx/location.pp @@ -0,0 +1,9 @@ +type Letsencrypt::Ssl_conf::Nginx::Location = Variant[ + Struct[{ + ssl => Boolean, + }], + Struct[{ + ssl => Boolean, + ssl_only => Boolean, + }], +] |