diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..cacadf2 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Vox Pupuli Security Policy + +Our vulnerabilities reporting process is at https://voxpupuli.org/security/ diff --git a/.msync.yml b/.msync.yml index 8864fc0..4c7999c 100644 --- a/.msync.yml +++ b/.msync.yml @@ -1 +1 @@ -modulesync_config_version: '2.12.0' +modulesync_config_version: '3.0.0' diff --git a/.rubocop.yml b/.rubocop.yml index c2ebc88..316e4ec 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,546 +1,549 @@ require: rubocop-rspec AllCops: # Puppet Server 5 defaults to jruby 1.7 so TargetRubyVersion must stay at 1.9 until we drop support for puppet 5 TargetRubyVersion: 1.9 Include: - ./**/*.rb Exclude: - files/**/* - vendor/**/* - .vendor/**/* - pkg/**/* - spec/fixtures/**/* - Gemfile - Rakefile - Guardfile - Vagrantfile Lint/ConditionPosition: Enabled: True Lint/ElseLayout: Enabled: True Lint/UnreachableCode: Enabled: True Lint/UselessComparison: Enabled: True Lint/EnsureReturn: Enabled: True Lint/HandleExceptions: Enabled: True Lint/LiteralInCondition: Enabled: True Lint/ShadowingOuterLocalVariable: Enabled: True Lint/LiteralInInterpolation: Enabled: True Style/HashSyntax: Enabled: True Style/RedundantReturn: Enabled: True Layout/EndOfLine: Enabled: False Lint/AmbiguousOperator: Enabled: True Lint/AssignmentInCondition: Enabled: True Layout/SpaceBeforeComment: Enabled: True Style/AndOr: Enabled: True Style/RedundantSelf: Enabled: True Metrics/BlockLength: Enabled: False # Method length is not necessarily an indicator of code quality Metrics/MethodLength: Enabled: False # Module length is not necessarily an indicator of code quality Metrics/ModuleLength: Enabled: False Style/WhileUntilModifier: Enabled: True Lint/AmbiguousRegexpLiteral: Enabled: True Security/Eval: Enabled: True Lint/BlockAlignment: Enabled: True Lint/DefEndAlignment: Enabled: True Lint/EndAlignment: Enabled: True Lint/DeprecatedClassMethods: Enabled: True Lint/Loop: Enabled: True Lint/ParenthesesAsGroupedExpression: Enabled: True Lint/RescueException: Enabled: True Lint/StringConversionInInterpolation: Enabled: True Lint/UnusedBlockArgument: Enabled: True Lint/UnusedMethodArgument: Enabled: True Lint/UselessAccessModifier: Enabled: True Lint/UselessAssignment: Enabled: True Lint/Void: Enabled: True Layout/AccessModifierIndentation: Enabled: True Style/AccessorMethodName: Enabled: True Style/Alias: Enabled: True Layout/AlignArray: Enabled: True Layout/AlignHash: Enabled: True Layout/AlignParameters: Enabled: True Metrics/BlockNesting: Enabled: True Style/AsciiComments: Enabled: True Style/Attr: Enabled: True Style/BracesAroundHashParameters: Enabled: True Style/CaseEquality: Enabled: True Layout/CaseIndentation: Enabled: True Style/CharacterLiteral: Enabled: True Style/ClassAndModuleCamelCase: Enabled: True Style/ClassAndModuleChildren: Enabled: False Style/ClassCheck: Enabled: True # Class length is not necessarily an indicator of code quality Metrics/ClassLength: Enabled: False Style/ClassMethods: Enabled: True Style/ClassVars: Enabled: True Style/WhenThen: Enabled: True Style/WordArray: Enabled: True Style/UnneededPercentQ: Enabled: True Layout/Tab: Enabled: True Layout/SpaceBeforeSemicolon: Enabled: True Layout/TrailingBlankLines: Enabled: True Layout/SpaceInsideBlockBraces: Enabled: True Layout/SpaceInsideBrackets: Enabled: True Layout/SpaceInsideHashLiteralBraces: Enabled: True Layout/SpaceInsideParens: Enabled: True Layout/LeadingCommentSpace: Enabled: True Layout/SpaceBeforeFirstArg: Enabled: True Layout/SpaceAfterColon: Enabled: True Layout/SpaceAfterComma: Enabled: True Layout/SpaceAfterMethodName: Enabled: True Layout/SpaceAfterNot: Enabled: True Layout/SpaceAfterSemicolon: Enabled: True Layout/SpaceAroundEqualsInParameterDefault: Enabled: True Layout/SpaceAroundOperators: Enabled: True Layout/SpaceBeforeBlockBraces: Enabled: True Layout/SpaceBeforeComma: Enabled: True Style/CollectionMethods: Enabled: True Layout/CommentIndentation: Enabled: True Style/ColonMethodCall: Enabled: True Style/CommentAnnotation: Enabled: True # 'Complexity' is very relative Metrics/CyclomaticComplexity: Enabled: False Style/ConstantName: Enabled: True Style/Documentation: Enabled: False Style/DefWithParentheses: Enabled: True Style/PreferredHashMethods: Enabled: True Layout/DotPosition: EnforcedStyle: trailing Style/DoubleNegation: Enabled: True Style/EachWithObject: Enabled: True Layout/EmptyLineBetweenDefs: Enabled: True Layout/IndentArray: Enabled: True Layout/IndentHash: Enabled: True Layout/IndentationConsistency: Enabled: True Layout/IndentationWidth: Enabled: True Layout/EmptyLines: Enabled: True Layout/EmptyLinesAroundAccessModifier: Enabled: True Style/EmptyLiteral: Enabled: True # Configuration parameters: AllowURI, URISchemes. Metrics/LineLength: Enabled: False Style/MethodCallWithoutArgsParentheses: Enabled: True Style/MethodDefParentheses: Enabled: True Style/LineEndConcatenation: Enabled: True Layout/TrailingWhitespace: Enabled: True Style/StringLiterals: Enabled: True Style/TrailingCommaInArguments: Enabled: True Style/TrailingCommaInLiteral: Enabled: True Style/GlobalVars: Enabled: True Style/GuardClause: Enabled: True Style/IfUnlessModifier: Enabled: True Style/MultilineIfThen: Enabled: True Style/NegatedIf: Enabled: True Style/NegatedWhile: Enabled: True Style/Next: Enabled: True Style/SingleLineBlockParams: Enabled: True Style/SingleLineMethods: Enabled: True Style/SpecialGlobalVars: Enabled: True Style/TrivialAccessors: Enabled: True Style/UnlessElse: Enabled: True Style/VariableInterpolation: Enabled: True Style/VariableName: Enabled: True Style/WhileUntilDo: Enabled: True Style/EvenOdd: Enabled: True Style/FileName: Enabled: True Style/For: Enabled: True Style/Lambda: Enabled: True Style/MethodName: Enabled: True Style/MultilineTernaryOperator: Enabled: True Style/NestedTernaryOperator: Enabled: True Style/NilComparison: Enabled: True Style/FormatString: Enabled: True Style/MultilineBlockChain: Enabled: True Style/Semicolon: Enabled: True Style/SignalException: Enabled: True Style/NonNilCheck: Enabled: True Style/Not: Enabled: True Style/NumericLiterals: Enabled: True Style/OneLineConditional: Enabled: True Style/OpMethod: Enabled: True Style/ParenthesesAroundCondition: Enabled: True Style/PercentLiteralDelimiters: Enabled: True Style/PerlBackrefs: Enabled: True Style/PredicateName: Enabled: True Style/RedundantException: Enabled: True Style/SelfAssignment: Enabled: True Style/Proc: Enabled: True Style/RaiseArgs: Enabled: True Style/RedundantBegin: Enabled: True Style/RescueModifier: Enabled: True # based on https://github.com/voxpupuli/modulesync_config/issues/168 Style/RegexpLiteral: EnforcedStyle: percent_r Enabled: True Lint/UnderscorePrefixedVariableName: Enabled: True Metrics/ParameterLists: Enabled: False Lint/RequireParentheses: Enabled: True Style/ModuleFunction: Enabled: True Lint/Debugger: Enabled: True Style/IfWithSemicolon: Enabled: True Style/Encoding: Enabled: True Style/BlockDelimiters: Enabled: True Layout/MultilineBlockLayout: Enabled: True # 'Complexity' is very relative Metrics/AbcSize: Enabled: False # 'Complexity' is very relative Metrics/PerceivedComplexity: Enabled: False Lint/UselessAssignment: Enabled: True Layout/ClosingParenthesisIndentation: Enabled: True # RSpec RSpec/BeforeAfterAll: Exclude: - spec/acceptance/**/* # We don't use rspec in this way RSpec/DescribeClass: Enabled: False # Example length is not necessarily an indicator of code quality RSpec/ExampleLength: Enabled: False RSpec/NamedSubject: Enabled: False # disabled for now since they cause a lot of issues # these issues aren't easy to fix RSpec/RepeatedDescription: Enabled: False RSpec/NestedGroups: Enabled: False +RSpec/MultipleExpectations: + Enabled: false + # this is broken on ruby1.9 Layout/IndentHeredoc: Enabled: False # disable Yaml safe_load. This is needed to support ruby2.0.0 development envs Security/YAMLLoad: Enabled: false # This affects hiera interpolation, as well as some configs that we push. Style/FormatStringToken: Enabled: false # This is useful, but sometimes a little too picky about where unit tests files # are located. RSpec/FilePath: Enabled: false diff --git a/.travis.yml b/.travis.yml index 694b2d9..a8b0d9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,103 +1,104 @@ --- +os: linux dist: bionic language: ruby cache: bundler before_install: - yes | gem update --system - bundle --version script: - 'bundle exec rake $CHECK' -matrix: +jobs: fast_finish: true include: - rvm: 2.4.4 bundler_args: --without system_tests development release env: PUPPET_VERSION="~> 5.0" CHECK=test - rvm: 2.5.3 bundler_args: --without system_tests development release env: PUPPET_VERSION="~> 6.0" CHECK=test_with_coveralls - rvm: 2.5.3 bundler_args: --without system_tests development release env: PUPPET_VERSION="~> 6.0" CHECK=rubocop - rvm: 2.4.4 bundler_args: --without system_tests development release env: PUPPET_VERSION="~> 5.0" CHECK=build DEPLOY_TO_FORGE=yes - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=centos7-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=centos7-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=debian9-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=debian9-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=debian10-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=debian10-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=fedora30-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=fedora30-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=fedora31-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=fedora31-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=ubuntu1604-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=ubuntu1604-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_setfile=ubuntu1804-64 CHECK=beaker services: docker - rvm: 2.5.3 bundler_args: --without development release env: BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_setfile=ubuntu1804-64 CHECK=beaker services: docker branches: only: - master - /^v\d/ notifications: email: false webhooks: https://voxpupu.li/incoming/travis irc: on_success: always on_failure: always channels: - "chat.freenode.org#voxpupuli-notifications" deploy: provider: puppetforge - user: puppet + username: puppet password: secure: "n/CVUS2upgv5DifCm/YsjBxR/11bdTRDYi1x9QK6ILa32+6ngVV2RQaIMXXJfJKYIPT8/O21tpc9C7fOBRGhRpNbl0usfvqsZS0C9UkpEx6AqT7lcRzj6pLrNn3IuChhZ0tQjNiKp9LxVTzltxr5uTFwKKCE4o534v/DLAzkzq5EAZuBWpRS1rcVHQA3o0767Gu3601yyYkZj9ySDH5RpbSdTCcNkTzwtFhr2NEvVb+2FI0RhchDSqPBfNWHV4Hn3dKuL42MNC2zjd2FYFpC8F27OXk/erUZIOZFfpZuIWypjSimfVC95a2Nb8kfQotTvQxUI1fwiB01ibUQGGkJj+mh7Utg/byBrbijpJnWRR7TT6oQ1NbIUHVXcqE1tfpbCBZ4Ws2Hqji0QoGc0fMrkt1NVlZlgbVrb9t+ctb1QcLaEPI+1Zf2a3AZhXOKA1EGx2W5DTQSWSPv57BUtFPICZENQi/ats30h+0FwtN7rjfx8Q6BIGO2D5JODI6eJC0nLNaL5UaPA0pjGRsNlZWoUzieuKG08G/rQtj/8jLq/3eLICv1cbfvj7lDPc0thPwXdrPIC9nLSisb/wdLufpqXsFku9TeBqrRHfQWImybcv7JRAeZZeHEo+tvFvZg5MSAEiPmz8zIOJOKhuMKEXlOmALmSjEhW6Ca5r7xuQ5Qwm0=" on: tags: true # all_branches is required to use tags all_branches: true # Only publish the build marked with "DEPLOY_TO_FORGE" condition: "$DEPLOY_TO_FORGE = yes" diff --git a/Gemfile b/Gemfile index 32c3114..8592cd6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,48 +1,48 @@ source ENV['GEM_SOURCE'] || "https://rubygems.org" def location_for(place, fake_version = nil) if place =~ /^(git[:@][^#]*)#(.*)/ [fake_version, { :git => $1, :branch => $2, :require => false }].compact elsif place =~ /^file:\/\/(.*)/ ['>= 0', { :path => File.expand_path($1), :require => false }] else [place, { :require => false }] end end group :test do - gem 'voxpupuli-test', '>= 1.0.0', :require => false - gem 'coveralls', :require => false - gem 'simplecov-console', :require => false + gem 'voxpupuli-test', '~> 2.0', :require => false + gem 'coveralls', :require => false + gem 'simplecov-console', :require => false end group :development do gem 'travis', :require => false gem 'travis-lint', :require => false gem 'guard-rake', :require => false gem 'overcommit', '>= 0.39.1', :require => false end group :system_tests do gem 'voxpupuli-acceptance', :require => false end group :release do gem 'github_changelog_generator', :require => false, :git => 'https://github.com/voxpupuli/github-changelog-generator', :branch => 'voxpupuli_essential_fixes' gem 'puppet-blacksmith', :require => false gem 'voxpupuli-release', :require => false gem 'puppet-strings', '>= 2.2', :require => false end if facterversion = ENV['FACTER_GEM_VERSION'] gem 'facter', facterversion.to_s, :require => false, :groups => [:test] else gem 'facter', :require => false, :groups => [:test] end ENV['PUPPET_VERSION'].nil? ? puppetversion = '~> 6.0' : puppetversion = ENV['PUPPET_VERSION'].to_s gem 'puppet', puppetversion, :require => false, :groups => [:test] # vim: syntax=ruby diff --git a/manifests/certonly.pp b/manifests/certonly.pp index 068ff56..b639fc9 100644 --- a/manifests/certonly.pp +++ b/manifests/certonly.pp @@ -1,221 +1,219 @@ # @summary Request a certificate using the `certonly` installer # # This type can be used to request a certificate using the `certonly` installer. # # @param ensure # Intended state of the resource # Will remove certificates for specified domains if set to 'absent'. Will # also remove cronjobs and renewal scripts if `manage_cron` is set to 'true'. # @param domains # An array of domains to include in the CSR. # @param custom_plugin Whether to use a custom plugin in additional_args and disable -a flag. # @param plugin The authenticator plugin to use when requesting the certificate. # @param webroot_paths # An array of webroot paths for the domains in `domains`. # Required if using `plugin => 'webroot'`. If `domains` and # `webroot_paths` are not the same length, the last `webroot_paths` # element will be used for all subsequent domains. # @param letsencrypt_command Command to run letsencrypt # @param additional_args An array of additional command line arguments to pass to the `letsencrypt-auto` command. # @param environment An optional array of environment variables (in addition to VENV_PATH). # @param key_size Size for the RSA public key # @param manage_cron # Indicating whether or not to schedule cron job for renewal. # Runs daily but only renews if near expiration, e.g. within 10 days. # @param suppress_cron_output Redirect cron output to devnull # @param cron_before_command Representation of a command that should be run before renewal command # @param cron_success_command Representation of a command that should be run if the renewal command succeeds. # @param cron_hour # Optional hour(s) that the renewal command should execute. # e.g. '[0,12]' execute at midnight and midday. Default - seeded random hour. # @param cron_minute # Optional minute(s) that the renewal command should execute. # e.g. 0 or '00' or [0,30]. Default - seeded random minute. # @param cron_monthday # Optional string, integer or array of monthday(s) the renewal command should # run. E.g. '2-30/2' to run on even days. Default: Every day. # @param config_dir The path to the configuration directory. # @param pre_hook_commands Array of commands to run in a shell before attempting to obtain/renew the certificate. # @param post_hook_commands Array of command(s) to run in a shell after attempting to obtain/renew the certificate. # @param deploy_hook_commands # Array of command(s) to run in a shell once if the certificate is successfully issued. # Two environmental variables are supplied by certbot: # - $RENEWED_LINEAGE: Points to the live directory with the cert files and key. # Example: /etc/letsencrypt/live/example.com # - $RENEWED_DOMAINS: A space-delimited list of renewed certificate domains. # Example: "example.com www.example.com" # define letsencrypt::certonly ( Enum['present','absent'] $ensure = 'present', Array[String[1]] $domains = [$title], String[1] $cert_name = $title, Boolean $custom_plugin = false, Letsencrypt::Plugin $plugin = 'standalone', Array[Stdlib::Unixpath] $webroot_paths = [], String[1] $letsencrypt_command = $letsencrypt::command, Integer[2048] $key_size = $letsencrypt::key_size, Array[String[1]] $additional_args = [], Array[String[1]] $environment = [], Boolean $manage_cron = false, Boolean $suppress_cron_output = false, Optional[String[1]] $cron_before_command = undef, Optional[String[1]] $cron_success_command = undef, Array[Variant[Integer[0, 59], String[1]]] $cron_monthday = ['*'], Variant[Integer[0,23], String, Array] $cron_hour = fqdn_rand(24, $title), Variant[Integer[0,59], String, Array] $cron_minute = fqdn_rand(60, fqdn_rand_string(10, $title)), Stdlib::Unixpath $config_dir = $letsencrypt::config_dir, Variant[String[1], Array[String[1]]] $pre_hook_commands = [], Variant[String[1], Array[String[1]]] $post_hook_commands = [], Variant[String[1], Array[String[1]]] $deploy_hook_commands = [], ) { - if $plugin == 'webroot' and empty($webroot_paths) { fail("The 'webroot_paths' parameter must be specified when using the 'webroot' plugin") } # Wildcard-less title for use in file paths $title_nowc = regsubst($title, '^\*\.', '') if $ensure == 'present' { if ($custom_plugin) { $default_args = "--text --agree-tos --non-interactive certonly --rsa-key-size ${key_size}" } else { $default_args = "--text --agree-tos --non-interactive certonly --rsa-key-size ${key_size} -a ${plugin}" } } else { $default_args = '--text --agree-tos --non-interactive delete' } case $plugin { - 'webroot': { $_plugin_args = zip($domains, $webroot_paths).map |$domain| { if $domain[1] { "--webroot-path ${domain[1]} -d '${domain[0]}'" } else { "-d '${domain[0]}'" } } $plugin_args = ["--cert-name '${cert_name}'"] + $_plugin_args } 'dns-rfc2136': { require letsencrypt::plugin::dns_rfc2136 $_domains = join($domains, '\' -d \'') $plugin_args = [ "--cert-name '${cert_name}' -d", "'${_domains}'", "--dns-rfc2136-credentials ${letsencrypt::plugin::dns_rfc2136::config_dir}/dns-rfc2136.ini", "--dns-rfc2136-propagation-seconds ${letsencrypt::plugin::dns_rfc2136::propagation_seconds}", ] } 'dns-route53': { - require letsencrypt::plugin::dns_route53 - $_domains = join($domains, '\' -d \'') - $plugin_args = [ - "--cert-name '${cert_name}' -d '${_domains}'", - "--dns-route53-propagation-seconds ${letsencrypt::plugin::dns_route53::propagation_seconds}", - ] + require letsencrypt::plugin::dns_route53 + $_domains = join($domains, '\' -d \'') + $plugin_args = [ + "--cert-name '${cert_name}' -d '${_domains}'", + "--dns-route53-propagation-seconds ${letsencrypt::plugin::dns_route53::propagation_seconds}", + ] } default: { if $ensure == 'present' { $_domains = join($domains, '\' -d \'') $plugin_args = "--cert-name '${cert_name}' -d '${_domains}'" } else { $plugin_args = "--cert-name '${cert_name}'" } } } $hook_args = ['pre', 'post', 'deploy'].map | String $type | { $commands = getvar("${type}_hook_commands") if (!empty($commands)) { $hook_file = "${config_dir}/renewal-hooks-puppet/${title_nowc}-${type}.sh" letsencrypt::hook { "${title}-${type}": type => $type, hook_file => $hook_file, commands => $commands, before => Exec["letsencrypt certonly ${title}"], } "--${type}-hook \"${hook_file}\"" } else { undef } } # certbot uses --cert-name to generate the file path $live_path_certname = regsubst($cert_name, '^\*\.', '') $live_path = "${config_dir}/live/${live_path_certname}/cert.pem" $_command = flatten([ - $letsencrypt_command, - $default_args, - $plugin_args, - $hook_args, - $additional_args, + $letsencrypt_command, + $default_args, + $plugin_args, + $hook_args, + $additional_args, ]).filter | $arg | { $arg =~ NotUndef and $arg != [] } $command = join($_command, ' ') - $execution_environment = [ "VENV_PATH=${letsencrypt::venv_path}", ] + $environment + $execution_environment = ["VENV_PATH=${letsencrypt::venv_path}",] + $environment $verify_domains = join(unique($domains), '\' \'') if $ensure == 'present' { $exec_ensure = { 'unless' => "/usr/local/sbin/letsencrypt-domain-validation ${live_path} '${verify_domains}'" } } else { $exec_ensure = { 'onlyif' => "/usr/local/sbin/letsencrypt-domain-validation ${live_path} '${verify_domains}'" } } exec { "letsencrypt certonly ${title}": command => $command, * => $exec_ensure, path => $facts['path'], environment => $execution_environment, provider => 'shell', require => [ Class['letsencrypt'], File['/usr/local/sbin/letsencrypt-domain-validation'], ], } if $manage_cron { $maincommand = join(["${letsencrypt_command} --keep-until-expiring"] + $_command[1,-1], ' ') $cron_script_ensure = $ensure ? { 'present' => 'file', default => 'absent' } $cron_ensure = $ensure if $suppress_cron_output { $croncommand = "${maincommand} > /dev/null 2>&1" } else { $croncommand = $maincommand } if $cron_before_command { $renewcommand = "(${cron_before_command}) && ${croncommand}" } else { $renewcommand = $croncommand } if $cron_success_command { $cron_cmd = "${renewcommand} && (${cron_success_command})" } else { $cron_cmd = $renewcommand } file { "${letsencrypt::cron_scripts_path}/renew-${title}.sh": ensure => $cron_script_ensure, mode => '0755', owner => 'root', group => $letsencrypt::cron_owner_group, content => template('letsencrypt/renew-script.sh.erb'), } cron { "letsencrypt renew cron ${title}": ensure => $cron_ensure, command => "\"${letsencrypt::cron_scripts_path}/renew-${title}.sh\"", user => root, hour => $cron_hour, minute => $cron_minute, monthday => $cron_monthday, } } } diff --git a/manifests/config.pp b/manifests/config.pp index 9b8b17b..1ad1a0b 100644 --- a/manifests/config.pp +++ b/manifests/config.pp @@ -1,59 +1,58 @@ # @summary Configures the Let's Encrypt client. # # @api private # class letsencrypt::config ( $config_dir = $letsencrypt::config_dir, $config_file = $letsencrypt::config_file, $config = $letsencrypt::config, $email = $letsencrypt::email, $unsafe_registration = $letsencrypt::unsafe_registration, $agree_tos = $letsencrypt::agree_tos, ) { - assert_private() unless $agree_tos { fail("You must agree to the Let's Encrypt Terms of Service! See: https://letsencrypt.org/repository for more information." ) } file { $config_dir: ensure => directory } file { $letsencrypt::cron_scripts_path: ensure => directory, purge => true, } if $email { - $_config = merge($config, {'email' => $email}) + $_config = merge($config, { 'email' => $email }) } else { $_config = $config } unless 'email' in $_config { if $unsafe_registration { warning('No email address specified for the letsencrypt class! Registering unsafely!') ini_setting { "${config_file} register-unsafely-without-email true": ensure => present, path => $config_file, section => '', setting => 'register-unsafely-without-email', value => true, require => File[$config_dir], } } else { fail("Please specify an email address to register with Let's Encrypt using the \$email parameter on the letsencrypt class") } } $_config.each |$key,$value| { ini_setting { "${config_file} ${key} ${value}": ensure => present, path => $config_file, section => '', setting => $key, value => $value, require => File[$config_dir], } } } diff --git a/manifests/hook.pp b/manifests/hook.pp index d7117df..aa2ee2b 100644 --- a/manifests/hook.pp +++ b/manifests/hook.pp @@ -1,34 +1,32 @@ # @summary Creates hook scripts. # # This type is used by letsencrypt::renew and letsencrypt::certonly to create hook scripts. # # @param type Hook type. # @param hook_file Path to deploy hook script. # @param commands Bash commands to execute when the hook is run by certbot. # define letsencrypt::hook ( Enum['pre', 'post', 'deploy'] $type, String[1] $hook_file, # hook.sh.epp will validate this Variant[String[1],Array[String[1]]] $commands, ) { - $validate_env = $type ? { 'deploy' => true, default => false, } file { $hook_file: ensure => file, owner => 'root', group => 'root', mode => '0755', content => epp('letsencrypt/hook.sh.epp', { - commands => $commands, - validate_env => $validate_env, + commands => $commands, + validate_env => $validate_env, }), # Defined in letsencrypt::config require => File['letsencrypt-renewal-hooks-puppet'], } - } diff --git a/manifests/init.pp b/manifests/init.pp index 5bb1b90..d72707b 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -1,132 +1,131 @@ # @summary Install and configure Certbot, the LetsEncrypt client # # Install and configure Certbot, the LetsEncrypt client # # @example # class { 'letsencrypt' : # email => 'letsregister@example.com', # config => { # 'server' => 'https://acme-staging-v02.api.letsencrypt.org/directory', # }, # } # # @param email # The email address to use to register with Let's Encrypt. This takes # precedence over an 'email' setting defined in $config. # @param path The path to the letsencrypt installation. # @param venv_path virtualenv path for vcs-installed Certbot # @param environment An optional array of environment variables (in addition to VENV_PATH) # @param repo A Git URL to install the Let's encrypt client from. # @param version The Git ref (tag, sha, branch) to check out when installing the client with the `vcs` method. # @param package_name Name of package and command to use when installing the client with the `package` method. # @param package_ensure The value passed to `ensure` when installing the client with the `package` method. # @param package_command Path or name for letsencrypt executable when installing the client with the `package` method. # @param config_file The path to the configuration file for the letsencrypt cli. # @param config A hash representation of the letsencrypt configuration file. # @param cron_scripts_path The path for renewal scripts called by cron # @param cron_owner_group Group owner of cron renew scripts. # @param manage_config A feature flag to toggle the management of the letsencrypt configuration file. # @param manage_install A feature flag to toggle the management of the letsencrypt client installation. # @param manage_dependencies A feature flag to toggle the management of the letsencrypt dependencies. # @param configure_epel A feature flag to include the 'epel' class and depend on it for package installation. # @param install_method Method to install the letsencrypt client, either package or vcs. # @param agree_tos A flag to agree to the Let's Encrypt Terms of Service. # @param unsafe_registration A flag to allow using the 'register-unsafely-without-email' flag. # @param config_dir The path to the configuration directory. # @param key_size Size for the RSA public key # @param renew_pre_hook_commands Array of commands to run in a shell before obtaining/renewing any certificates. # @param renew_post_hook_commands Array of commands to run in a shell after attempting to obtain/renew certificates. # @param renew_deploy_hook_commands # Array of commands to run in a shell once for each successfully issued/renewed # certificate. Two environmental variables are supplied by certbot: # - $RENEWED_LINEAGE: Points to the live directory with the cert files and key. # Example: /etc/letsencrypt/live/example.com # - $RENEWED_DOMAINS: A space-delimited list of renewed certificate domains. # Example: "example.com www.example.com" # @param renew_additional_args Array of additional command line arguments to pass to 'certbot renew'. # @param renew_cron_ensure Intended state of the cron resource running certbot renew. # @param renew_cron_hour # Optional string, integer or array of hour(s) the renewal command should run. # E.g. '[0,12]' to execute at midnight and midday. # hour. # @param renew_cron_minute # Optional string, integer or array of minute(s) the renewal command should # run. E.g. 0 or '00' or [0,30]. # @param renew_cron_monthday # Optional string, integer or array of monthday(s) the renewal command should # run. E.g. '2-30/2' to run on even days. # class letsencrypt ( Boolean $configure_epel, Optional[String] $email = undef, String $path = '/opt/letsencrypt', $venv_path = '/opt/letsencrypt/.venv', Array $environment = [], String $repo = 'https://github.com/certbot/certbot.git', String $version = 'v0.39.0', String $package_name = 'certbot', $package_ensure = 'installed', String $package_command = 'certbot', Stdlib::Unixpath $config_dir = '/etc/letsencrypt', String $config_file = "${config_dir}/cli.ini", - Hash $config = {'server' => 'https://acme-v02.api.letsencrypt.org/directory'}, + Hash $config = { 'server' => 'https://acme-v02.api.letsencrypt.org/directory' }, String $cron_scripts_path = "${facts['puppet_vardir']}/letsencrypt", String $cron_owner_group = 'root', Boolean $manage_config = true, Boolean $manage_install = true, Boolean $manage_dependencies = true, Enum['package', 'vcs'] $install_method = 'package', Boolean $agree_tos = true, Boolean $unsafe_registration = false, Integer[2048] $key_size = 4096, # $renew_* should only be used in letsencrypt::renew (blame rspec) $renew_pre_hook_commands = [], $renew_post_hook_commands = [], $renew_deploy_hook_commands = [], $renew_additional_args = [], $renew_cron_ensure = 'absent', $renew_cron_hour = fqdn_rand(24), $renew_cron_minute = fqdn_rand(60, fqdn_rand_string(10)), $renew_cron_monthday = '*', ) { - if $manage_install { contain letsencrypt::install # lint:ignore:relative_classname_inclusion Class['letsencrypt::install'] ~> Exec['initialize letsencrypt'] Class['letsencrypt::install'] -> Class['letsencrypt::renew'] } $command = $install_method ? { 'package' => $package_command, 'vcs' => "${venv_path}/bin/letsencrypt", } $command_init = $install_method ? { 'package' => $package_command, 'vcs' => "${path}/letsencrypt-auto", } if $manage_config { contain letsencrypt::config # lint:ignore:relative_classname_inclusion Class['letsencrypt::config'] -> Exec['initialize letsencrypt'] } contain letsencrypt::renew # TODO: do we need this command when installing from package? exec { 'initialize letsencrypt': command => "${command_init} -h", path => $facts['path'], - environment => concat([ "VENV_PATH=${venv_path}" ], $environment), + environment => concat(["VENV_PATH=${venv_path}"], $environment), refreshonly => true, } # Used in letsencrypt::certonly Exec["letsencrypt certonly ${title}"] file { '/usr/local/sbin/letsencrypt-domain-validation': ensure => file, owner => 'root', group => 'root', mode => '0500', source => "puppet:///modules/${module_name}/domain-validation.sh", } } diff --git a/manifests/install.pp b/manifests/install.pp index ed7304d..0ba3313 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,49 +1,48 @@ # @summary Installs the Let's Encrypt client. # # @param manage_install A feature flag to toggle the management of the letsencrypt client installation. # @param manage_dependencies A feature flag to toggle the management of the letsencrypt dependencies. # @param configure_epel A feature flag to include the 'epel' class and depend on it for package installation. # @param install_method Method to install the letsencrypt client # @param path The path to the letsencrypt installation. # @param repo A Git URL to install the Let's encrypt client from. # @param version The Git ref (tag, sha, branch) to check out when installing the client with the `vcs` method. # @param package_ensure The value passed to `ensure` when installing the client with the `package` method. # @param package_name Name of package to use when installing the client with the `package` method. # class letsencrypt::install ( Boolean $manage_install = $letsencrypt::manage_install, Boolean $manage_dependencies = $letsencrypt::manage_dependencies, Boolean $configure_epel = $letsencrypt::configure_epel, Enum['package', 'vcs'] $install_method = $letsencrypt::install_method, String $package_name = $letsencrypt::package_name, String $package_ensure = $letsencrypt::package_ensure, String $path = $letsencrypt::path, String $repo = $letsencrypt::repo, String $version = $letsencrypt::version, ) { - if $install_method == 'vcs' { if $manage_dependencies { $dependencies = ['python', 'git'] ensure_packages($dependencies) Package[$dependencies] -> Vcsrepo[$path] } vcsrepo { $path: ensure => present, provider => git, source => $repo, revision => $version, } } else { package { 'letsencrypt': ensure => $package_ensure, name => $package_name, } if $configure_epel { include epel Class['epel'] -> Package['letsencrypt'] } } } diff --git a/manifests/plugin/dns_rfc2136.pp b/manifests/plugin/dns_rfc2136.pp index ed52432..ebe5fd3 100644 --- a/manifests/plugin/dns_rfc2136.pp +++ b/manifests/plugin/dns_rfc2136.pp @@ -1,53 +1,52 @@ # @summary Installs and configures the dns-rfc2136 plugin # # This class installs and configures the Let's Encrypt dns-rfc2136 plugin. # https://certbot-dns-rfc2136.readthedocs.io # # @param server Target DNS server. # @param key_name TSIG key name. # @param key_secret TSIG key secret. # @param key_algorithm TSIG key algorithm. # @param port Target DNS port. # @param propagation_seconds Number of seconds to wait for the DNS server to propagate the DNS-01 challenge. # @param manage_package Manage the plugin package. # @param package_name The name of the package to install when $manage_package is true. # @param config_dir The path to the configuration directory. # class letsencrypt::plugin::dns_rfc2136 ( Stdlib::Host $server, String[1] $key_name, String[1] $key_secret, String[1] $package_name, String[1] $key_algorithm = 'HMAC-SHA512', Stdlib::Port $port = 53, Integer $propagation_seconds = 10, Stdlib::Absolutepath $config_dir = $letsencrypt::config_dir, Boolean $manage_package = true, ) { require letsencrypt if $manage_package { package { $package_name: ensure => installed, } } $ini_vars = { dns_rfc2136_server => $server, dns_rfc2136_port => $port, dns_rfc2136_name => $key_name, dns_rfc2136_secret => $key_secret, dns_rfc2136_algorithm => $key_algorithm, } file { "${config_dir}/dns-rfc2136.ini": ensure => file, owner => 'root', group => 'root', mode => '0400', content => epp('letsencrypt/ini.epp', { - vars => { '' => $ini_vars }, + vars => { '' => $ini_vars }, }), } - } diff --git a/manifests/renew.pp b/manifests/renew.pp index 33b546b..350b9ff 100644 --- a/manifests/renew.pp +++ b/manifests/renew.pp @@ -1,90 +1,87 @@ - # @summary Configures renewal of Let's Encrypt certificates using Certbot # # Configures renewal of Let's Encrypt certificates using the certbot renew command. # # Note: Hooks set here will run before/after/for ALL certificates, including # any not managed by Puppet. If you want to create hooks for specific # certificates only, create them using letsencrypt::certonly. # # @param pre_hook_commands Array of commands to run in a shell before obtaining/renewing any certificates. # @param post_hook_commands Array of commands to run in a shell after attempting to obtain/renew certificates. # @param deploy_hook_commands # Array of commands to run in a shell once for each successfully issued/renewed # certificate. Two environmental variables are supplied by certbot: # - $RENEWED_LINEAGE: Points to the live directory with the cert files and key. # Example: /etc/letsencrypt/live/example.com # - $RENEWED_DOMAINS: A space-delimited list of renewed certificate domains. # Example: "example.com www.example.com" # @param additional_args Array of additional command line arguments to pass to 'certbot renew'. # @param cron_ensure Intended state of the cron resource running certbot renew # @param cron_hour # Optional string, integer or array of hour(s) the renewal command should run. # E.g. '[0,12]' to execute at midnight and midday. Default: fqdn-seeded random hour. # @param cron_minute # Optional string, integer or array of minute(s) the renewal command should # run. E.g. 0 or '00' or [0,30]. Default: fqdn-seeded random minute. # @param cron_monthday # Optional string, integer or array of monthday(s) the renewal command should # run. E.g. '2-30/2' to run on even days. Default: Every day. # class letsencrypt::renew ( Variant[String[1], Array[String[1]]] $pre_hook_commands = $letsencrypt::renew_pre_hook_commands, Variant[String[1], Array[String[1]]] $post_hook_commands = $letsencrypt::renew_post_hook_commands, Variant[String[1], Array[String[1]]] $deploy_hook_commands = $letsencrypt::renew_deploy_hook_commands, Array[String[1]] $additional_args = $letsencrypt::renew_additional_args, Enum['present', 'absent'] $cron_ensure = $letsencrypt::renew_cron_ensure, Letsencrypt::Cron::Hour $cron_hour = $letsencrypt::renew_cron_hour, Letsencrypt::Cron::Minute $cron_minute = $letsencrypt::renew_cron_minute, Letsencrypt::Cron::Monthday $cron_monthday = $letsencrypt::renew_cron_monthday, ) { - # Directory used for Puppet-managed renewal hooks. Make sure old unmanaged # hooks in this directory are purged. Leave custom hooks in the default # renewal-hooks directory alone. file { 'letsencrypt-renewal-hooks-puppet': ensure => directory, path => "${letsencrypt::config_dir}/renewal-hooks-puppet", owner => 'root', group => 'root', mode => '0755', recurse => true, purge => true, } $default_args = 'renew -q' $hook_args = ['pre', 'post', 'deploy'].map | String $type | { $commands = getvar("${type}_hook_commands") if (!empty($commands)) { $hook_file = "${letsencrypt::config_dir}/renewal-hooks-puppet/renew-${type}.sh" letsencrypt::hook { "renew-${type}": type => $type, hook_file => $hook_file, commands => $commands, } "--${type}-hook \"${hook_file}\"" } else { undef } } $_command = flatten([ - $letsencrypt::command, - $default_args, - $hook_args, - $additional_args, + $letsencrypt::command, + $default_args, + $hook_args, + $additional_args, ]).filter | $arg | { $arg =~ NotUndef and $arg != [] } $command = join($_command, ' ') cron { 'letsencrypt-renew': ensure => $cron_ensure, command => $command, user => 'root', hour => $cron_hour, minute => $cron_minute, monthday => $cron_monthday, } - }