From be44510909c7f97137b4c4a7003ffbfae800ac46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Tarti=C3=A8re?= Date: Tue, 17 Feb 2026 12:00:21 -1000 Subject: [PATCH] Rework the way certificates renewal is managed Some linux distributions do not ship cron by default anymore. The module unconditionally manage cron entries, resulting in catalog application failures on these operating systems until cron is installed. To avoid this hard dependency, allow to choose how certificates renewal is te be handled with the new `dehydrated::renewal_provider` parameter which replate the `dehydrated::cron_integration` parameter. If you previously set `cron_integration => true`, you must remove this parameter and replace it with `renewal_provider => 'cron'`. If you prefer to rely on systemd to renew certificates, you can set `renewal_provider` accordingly. We also introduce another new parameter `renewal_interval` which allows to select how ofter certificate renewals are attempted. Because the world is moving toward short-lived certificates, the default internal is `daily`, but can be set to `weekly` to match the previous behavior. Also note that the default value `never` is suppored and allows to switch from one provider to another without leftovers from the legacy provider. For this a two-step upgrade is required: 1. Keep `renewal_provider` unchanged and set `renewal_interval => 'never'`, apply a catalog; 2. Change `renewal_provider` to the new provider and set `renewal_interval` to the expected value. --- .fixtures.yml | 1 + REFERENCE.md | 20 ++++++++++++---- manifests/cron.pp | 45 ++++++++++++++++++++--------------- manifests/init.pp | 14 ++++++++--- manifests/systemd.pp | 14 +++++++++++ spec/classes/cron_spec.rb | 27 +++++++++++++++++++++ spec/classes/systemd_spec.rb | 26 ++++++++++++++++++++ templates/systemd.service.epp | 10 ++++++++ templates/systemd.timer.epp | 11 +++++++++ 9 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 manifests/systemd.pp create mode 100644 spec/classes/cron_spec.rb create mode 100644 spec/classes/systemd_spec.rb create mode 100644 templates/systemd.service.epp create mode 100644 templates/systemd.timer.epp diff --git a/.fixtures.yml b/.fixtures.yml index 3a637f1..80616ed 100644 --- a/.fixtures.yml +++ b/.fixtures.yml @@ -5,4 +5,5 @@ fixtures: concat: https://github.com/puppetlabs/puppetlabs-concat.git cron_core: https://github.com/puppetlabs/puppetlabs-cron_core.git stdlib: https://github.com/puppetlabs/puppetlabs-stdlib.git + systemd: https://github.com/voxpupuli/puppet-systemd.git vcsrepo: https://github.com/puppetlabs/puppetlabs-vcsrepo.git diff --git a/REFERENCE.md b/REFERENCE.md index 24dbd0d..4f0ce4a 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -19,6 +19,7 @@ * `dehydrated::domains`: Manage the domains.txt file * `dehydrated::package`: Manage the dehydrated package * `dehydrated::repo`: Manage the dehydrated code +* `dehydrated::systemd`: Manage a system timer to refresh certificates * `dehydrated::user`: Manage the dehydrated user ### Defined types @@ -63,7 +64,8 @@ The following parameters are available in the `dehydrated` class: * [`repo_revision`](#-dehydrated--repo_revision) * [`dependencies`](#-dehydrated--dependencies) * [`apache_integration`](#-dehydrated--apache_integration) -* [`cron_integration`](#-dehydrated--cron_integration) +* [`renewal_provider`](#-dehydrated--renewal_provider) +* [`renewal_interval`](#-dehydrated--renewal_interval) * [`dehydrated_user`](#-dehydrated--dehydrated_user) * [`dehydrated_group`](#-dehydrated--dehydrated_group) * [`ip_version`](#-dehydrated--ip_version) @@ -175,13 +177,21 @@ Setup apache to serve the generated challenges. Default value: `false` -##### `cron_integration` +##### `renewal_provider` -Data type: `Boolean` +Data type: `Enum['cron','systemd','none']` -Setup cron to automatically renew certificates. +Which provider should trigger certificat renewal attempts. -Default value: `false` +Default value: `'none'` + +##### `renewal_interval` + +Data type: `Enum['never','daily','weekly']` + +How long to wait between certificate renewal attempts. + +Default value: `'daily'` ##### `dehydrated_user` diff --git a/manifests/cron.pp b/manifests/cron.pp index 3543516..890ef09 100644 --- a/manifests/cron.pp +++ b/manifests/cron.pp @@ -4,19 +4,24 @@ class dehydrated::cron { assert_private() - if $dehydrated::cron_integration { - $ensure = 'present' - } else { - $ensure = 'absent' - } + $supported_renewal_intervals = [ + 'daily', + 'weekly', + ] case $facts['os']['family'] { 'Debian', 'RedHat': { - cron { 'weekly_letsencrypt': - ensure => absent, + cron { 'daily_dehydrated': + ensure => bool2str($dehydrated::renewal_interval == 'daily', 'present', 'absent'), + command => "${dehydrated::bin} --accept-terms --cron --keep-going", + user => $dehydrated::user, + weekday => '*', + hour => 3, + minute => 30, } + cron { 'weekly_dehydrated': - ensure => $ensure, + ensure => bool2str($dehydrated::renewal_interval == 'weekly', 'present', 'absent'), command => "${dehydrated::bin} --accept-terms --cron --keep-going", user => $dehydrated::user, weekday => 0, @@ -25,17 +30,19 @@ } } 'FreeBSD': { - file_line { 'weekly_dehydrated_enable': - ensure => $ensure, - path => '/etc/periodic.conf', - line => 'weekly_dehydrated_enable="YES"', - match => '^weekly_(letsencrypt|dehydrated)_enable=', - } - file_line { 'weekly_dehydrated_user': - ensure => $ensure, - path => '/etc/periodic.conf', - line => "weekly_dehydrated_user=\"${dehydrated::user}\"", - match => '^weekly_(letsencrypt|dehydrated)_user=', + $supported_renewal_intervals.each |$interval| { + file_line { "${interval}_dehydrated_enable": + ensure => bool2str($interval == $dehydrated::renewal_interval, 'present', 'absent'), + path => '/etc/periodic.conf', + line => "${interval}_dehydrated_enable=\"YES\"", + match => "^${interval}_dehydrated_enable=", + } + file_line { "${interval}_dehydrated_user": + ensure => bool2str($interval == $dehydrated::renewal_interval, 'present', 'absent'), + path => '/etc/periodic.conf', + line => "${interval}_dehydrated_user=\"${dehydrated::user}\"", + match => "^${interval}_dehydrated_user=", + } } } default: { diff --git a/manifests/init.pp b/manifests/init.pp index b53bf4f..b2d7333 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -10,7 +10,8 @@ # @param repo_revision Revision to fetch from the repository providing dehydrated. # @param dependencies Extra dependencies needed to run dehydrated. # @param apache_integration Setup apache to serve the generated challenges. -# @param cron_integration Setup cron to automatically renew certificates. +# @param renewal_provider Which provider should trigger certificat renewal attempts. +# @param renewal_interval How long to wait between certificate renewal attempts. # @param dehydrated_user Which user should dehydrated run as? This will be implicitly enforced when running as root. # @param dehydrated_group Which group should dehydrated run as? This will be implicitly enforced when running as root. # @param ip_version Resolve names to addresses of IP version only. (curl) @@ -63,7 +64,9 @@ Array[String] $dependencies = [], Boolean $apache_integration = false, - Boolean $cron_integration = false, + + Enum['cron','systemd','none'] $renewal_provider = 'none', + Enum['never','daily','weekly'] $renewal_interval = 'daily', Optional[String[1]] $dehydrated_user = undef, Optional[String[1]] $dehydrated_group = undef, @@ -135,5 +138,10 @@ include dehydrated::apache } - include dehydrated::cron + case $renewal_provider { + 'cron': { include dehydrated::cron } + 'systemd': { include dehydrated::systemd } + 'none': {} + default: { fail("Unsupported renewal_provider ${renewal_provider}") } + } } diff --git a/manifests/systemd.pp b/manifests/systemd.pp new file mode 100644 index 0000000..73f0b9b --- /dev/null +++ b/manifests/systemd.pp @@ -0,0 +1,14 @@ +# @summary Manage a system timer to refresh certificates +# +# @api private +class dehydrated::systemd { + assert_private() + + systemd::timer { 'dehydrated.timer': + ensure => bool2str($dehydrated::renewal_interval != 'never', 'present', 'absent'), + active => true, + enable => true, + timer_content => epp('dehydrated/systemd.timer.epp'), + service_content => epp('dehydrated/systemd.service.epp'), + } +} diff --git a/spec/classes/cron_spec.rb b/spec/classes/cron_spec.rb new file mode 100644 index 0000000..14558a5 --- /dev/null +++ b/spec/classes/cron_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'dehydrated::cron' do + let(:pre_condition) do + <<~PP + class { 'dehydrated': + contact_email => 'dummy@example.com', + renewal_provider => 'cron', + } + PP + end + + on_supported_os.each do |os, facts| + context "on #{os}" do + let(:facts) { facts } + + it { is_expected.to compile.with_all_deps } + + if facts[:os]['family'] == 'Debian' + it { is_expected.to contain_cron('daily_dehydrated') } + it { is_expected.to contain_cron('weekly_dehydrated') } + end + end + end +end diff --git a/spec/classes/systemd_spec.rb b/spec/classes/systemd_spec.rb new file mode 100644 index 0000000..715f35a --- /dev/null +++ b/spec/classes/systemd_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'dehydrated::systemd' do + let(:pre_condition) do + <<~PP + class { 'dehydrated': + contact_email => 'dummy@example.com', + renewal_provider => 'systemd', + } + PP + end + + on_supported_os.each do |os, facts| + next unless facts[:os]['family'] == 'Debian' + + context "on #{os}" do + let(:facts) { facts } + + it { is_expected.to compile.with_all_deps } + + it { is_expected.to contain_systemd__timer('dehydrated.timer') } + end + end +end diff --git a/templates/systemd.service.epp b/templates/systemd.service.epp new file mode 100644 index 0000000..2762ff3 --- /dev/null +++ b/templates/systemd.service.epp @@ -0,0 +1,10 @@ +# Managed by Puppet, DO NOT EDIT + +[Unit] +Description=Renew dehydrated certificates about to expire + +[Service] +Type=oneshot +ExecStart=<%= $dehydrated::bin %> --accept-terms --cron --keep-going +User=<%= $dehydrated::user %> +Group=<%= $dehydrated::user %> diff --git a/templates/systemd.timer.epp b/templates/systemd.timer.epp new file mode 100644 index 0000000..882b7b6 --- /dev/null +++ b/templates/systemd.timer.epp @@ -0,0 +1,11 @@ +# Managed by Puppet, DO NOT EDIT + +[Unit] +Description=Trigger dehydrated certificates renewal + +[Timer] +OnCalendar=<%= $dehydrated::renewal_interval %> +Persistent=true + +[Install] +WantedBy=timers.target