From 728cff00c00bdf8c0d3e86995a3498ed027ad4ad Mon Sep 17 00:00:00 2001
From: Fabien COMBERNOUS <fabien.combernous@adullact.org>
Date: Fri, 18 Nov 2022 23:55:58 +0100
Subject: [PATCH] Being able to generate CRL for all CA

---
 REFERENCE.md                       | 53 ++++++++++++++++++
 manifests/ca/gencrl.pp             | 60 ++++++++++++++++++++
 manifests/ca/gencrls.pp            | 22 ++++++++
 manifests/goose.pp                 |  4 +-
 manifests/init.pp                  | 89 +++++++++---------------------
 manifests/params.pp                | 15 +++++
 spec/acceptance/cfssl_spec.rb      |  9 ++-
 spec/classes/ca/gencrls_spec.rb    | 13 +++++
 spec/classes/params_spec.rb        | 13 +++++
 templates/cfssl-gencrl.service.epp |  8 ++-
 templates/cfssl-gencrl.sh.epp      | 24 --------
 templates/cfssl-gencrl.timer.epp   |  4 +-
 templates/cfssl.service.epp        |  2 +-
 13 files changed, 220 insertions(+), 96 deletions(-)
 create mode 100644 manifests/ca/gencrl.pp
 create mode 100644 manifests/ca/gencrls.pp
 create mode 100644 manifests/params.pp
 create mode 100644 spec/classes/ca/gencrls_spec.rb
 create mode 100644 spec/classes/params_spec.rb
 delete mode 100644 templates/cfssl-gencrl.sh.epp

diff --git a/REFERENCE.md b/REFERENCE.md
index d137c74..73a133d 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -9,8 +9,10 @@
 #### Public Classes
 
 * [`cfssl`](#cfssl): Install and configure CFSSL, serve process and CRL generation.
+* [`cfssl::ca::gencrls`](#cfsslcagencrls): Creates services to generate CRL for a list of CA
 * [`cfssl::ca::intermediates`](#cfsslcaintermediates): Creates `cfssl::ca::intermediate` defined types.
 * [`cfssl::ca::root`](#cfsslcaroot): Init a selfsigned root authority
+* [`cfssl::params`](#cfsslparams): A short summary of the purpose of this class
 
 #### Private Classes
 
@@ -18,8 +20,16 @@
 
 ### Defined types
 
+#### Public Defined types
+
 * [`cfssl::ca::intermediate`](#cfsslcaintermediate): Creates an intermediate authority signed by root authority
 
+#### Private Defined types
+
+* `cfssl::ca::gencrl`: Creates a service to generate CRL for a CA
+
+Creates a service to generate CRL for a CA
+
 ### Data types
 
 * [`Cfssl::Authkey`](#cfsslauthkey): Struct representing authentication key used by CFSSL serve during sign requests
@@ -274,6 +284,37 @@ The Certificate authority served with CFSSL serve
 
 Default value: ``undef``
 
+### <a name="cfsslcagencrls"></a>`cfssl::ca::gencrls`
+
+Creates services to generate CRL for a list of CA
+
+#### Examples
+
+##### 
+
+```puppet
+class { 'cfssl::ca::gencrls':
+  authorities => [
+    'MYEXEMPLE ROOT CA',
+    'MYEXEMPLE INTERMDIATE CA',
+  ],
+}
+```
+
+#### Parameters
+
+The following parameters are available in the `cfssl::ca::gencrls` class:
+
+* [`authorities`](#authorities)
+
+##### <a name="authorities"></a>`authorities`
+
+Data type: `Array[String]`
+
+A list of authority CN's, already defined.
+
+Default value: `[]`
+
 ### <a name="cfsslcaintermediates"></a>`cfssl::ca::intermediates`
 
 Creates `cfssl::ca::intermediate` defined types.
@@ -369,6 +410,18 @@ Cryptographic algorithm used for creating key pairs.
 
 Default value: `{ algo => 'rsa', size => 2048 }`
 
+### <a name="cfsslparams"></a>`cfssl::params`
+
+A description of what this class does
+
+#### Examples
+
+##### 
+
+```puppet
+include cfssl::params
+```
+
 ## Defined types
 
 ### <a name="cfsslcaintermediate"></a>`cfssl::ca::intermediate`
diff --git a/manifests/ca/gencrl.pp b/manifests/ca/gencrl.pp
new file mode 100644
index 0000000..cdafa92
--- /dev/null
+++ b/manifests/ca/gencrl.pp
@@ -0,0 +1,60 @@
+# @summary Creates a service to generate CRL for a CA
+#
+#  Creates a service to generate CRL for a CA
+#
+# @example
+#   cfssl::ca::gencrl { 'MYEXEMPLE INTERMDIATE CA': }
+#
+# @api private
+#
+# @param db_config_json Name of JSON config file for database usaed by CFSSL
+# @param systemd_unitdir Directory where systemd units are created
+define cfssl::ca::gencrl (
+  String $db_config_json = $cfssl::params::db_config_json,
+  Stdlib::Absolutepath $systemd_unitdir = $cfssl::params::systemd_unitdir,
+) {
+  assert_private()
+
+  require cfssl::ca::root
+
+  $_ca = regsubst($name, '\s', '', 'G')
+
+  if $cfssl::crl_manage {
+    ensure_packages('coreutils', { ensure => 'present' })
+
+    file { "${systemd_unitdir}/cfssl-gencrl-${_ca}.service":
+      ensure  => file,
+      mode    => '0644',
+      owner   => 0,
+      group   => 0,
+      content => epp('cfssl/cfssl-gencrl.service.epp', { 'ca' => $_ca }),
+    }
+    ~> service { "cfssl-gencrl-${_ca}.service":
+      ensure   => 'running',
+      enable   => true,
+      provider => 'systemd',
+    }
+
+    file { "${systemd_unitdir}/cfssl-gencrl-${_ca}.timer":
+      ensure  => file,
+      mode    => '0644',
+      owner   => 0,
+      group   => 0,
+      content => epp('cfssl/cfssl-gencrl.timer.epp', { 'ca' => $_ca }),
+    }
+    ~> service { "cfssl-gencrl-${_ca}.timer":
+      ensure   => 'running',
+      enable   => true,
+      provider => 'systemd',
+    }
+  } else {
+    service { ["cfssl-gencrl-${_ca}.service" ,"cfssl-gencrl-${_ca}.timer"]:
+      ensure   => 'stopped',
+      enable   => false,
+      provider => 'systemd',
+    }
+    -> file { ["${systemd_unitdir}/cfssl-gencrl-${_ca}.service" ,"${systemd_unitdir}/cfssl-gencrl-${_ca}.timer"]:
+      ensure  => absent,
+    }
+  }
+}
diff --git a/manifests/ca/gencrls.pp b/manifests/ca/gencrls.pp
new file mode 100644
index 0000000..88d29ee
--- /dev/null
+++ b/manifests/ca/gencrls.pp
@@ -0,0 +1,22 @@
+# @summary  Creates services to generate CRL for a list of CA
+#
+# Creates services to generate CRL for a list of CA
+#
+# @example
+#   class { 'cfssl::ca::gencrls':
+#     authorities => [
+#       'MYEXEMPLE ROOT CA',
+#       'MYEXEMPLE INTERMDIATE CA',
+#     ],
+#   }
+#
+# @param authorities A list of authority CN's, already defined.
+#
+class cfssl::ca::gencrls (
+  Array[String] $authorities = [],
+) {
+  $authorities.each | String[1] $_authority | {
+    cfssl::ca::gencrl { $_authority:
+    }
+  }
+}
diff --git a/manifests/goose.pp b/manifests/goose.pp
index 363e106..133ec78 100644
--- a/manifests/goose.pp
+++ b/manifests/goose.pp
@@ -10,12 +10,12 @@
 class cfssl::goose {
   assert_private()
   # stuffs for goose : a database migration tool used by CFSSL
-  package { 'golang-1.16':
+  package { $cfssl::params::go_package:
     ensure => present,
   }
   -> file { '/usr/local/bin/go':
     ensure => link,
-    target => '/usr/lib/go-1.16/bin/go',
+    target => $cfssl::params::go_targetlink,
   }
   -> exec { 'install goose':
     command     => '/usr/local/bin/go get bitbucket.org/liamstask/goose/cmd/goose',
diff --git a/manifests/init.pp b/manifests/init.pp
index 5c3f525..4970f61 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -73,17 +73,11 @@ class cfssl (
   Integer $crl_expiry = 604800,
   String[1] $crl_gentimer = '*:00:00',
   Optional[String[1]] $serve_ca = undef,
-) {
+) inherits cfssl::params {
   include cfssl::goose
   include postgresql::server
 
-  $_binaries = ['cfssljson','cfssl-certinfo']
-  $_systemd_unit_file = '/etc/systemd/system/cfssl.service'
-  $_goose_cfssldbmigrate_path = "/home/${sysuser}/goose-cfssldbmigrate"
-  $_serve_config_json = 'serve-config.json'
-  $_db_config_json = 'db-config.json'
-  $_systemd_unitdir = '/etc/systemd/system'
-  $_crlunits = ['cfssl-gencrl.service', 'cfssl-gencrl.timer']
+  $_goose_cfssldbmigrate_path = "/home/${cfssl::sysuser}/goose-cfssldbmigrate"
 
   group { $sysgroup :
     ensure => present,
@@ -110,7 +104,7 @@ class cfssl (
     require => User[$sysuser],
   }
 
-  $_binaries.each | String $_bin | {
+  $cfssl::params::binaries.each | String $_bin | {
     $_archiveurn = "v${version}/${_bin}_${version}_linux_amd64"
 
     archive { "${binpath}/${_bin}" :
@@ -141,14 +135,14 @@ class cfssl (
     password => postgresql::postgresql_password($dbuser, $dbpassword),
   }
 
-  file { "${confdir}/${_db_config_json}":
+  file { "${confdir}/${cfssl::params::db_config_json}":
     ensure  => file,
     mode    => '0600',
     owner   => $sysuser,
     group   => $sysgroup,
     content => epp('cfssl/db-config.json.epp'),
   }
-  file { "${confdir}/${_serve_config_json}":
+  file { "${confdir}/${cfssl::params::serve_config_json}":
     ensure  => file,
     mode    => '0600',
     owner   => $sysuser,
@@ -175,13 +169,13 @@ class cfssl (
     command     => "/home/${sysuser}/go/bin/goose --env puppetmigrate -path ${_goose_cfssldbmigrate_path}/certdb/pg up",
     user        => $sysuser,
     environment => ["HOME=/home/${sysuser}/"],
+    onlyif      => "/home/${sysuser}/go/bin/goose --env puppetmigrate -path ${_goose_cfssldbmigrate_path}/certdb/pg status | grep -q 'Pending'",
     require     => [
       User[$sysuser],
       Vcsrepo[$_goose_cfssldbmigrate_path],
       Postgresql::Server::Db[$dbname],
+      Class[cfssl::goose],
     ],
-    subscribe   => Vcsrepo[$_goose_cfssldbmigrate_path],
-    refreshonly => true,
   }
 
   class { 'cfssl::ca::root':
@@ -201,7 +195,7 @@ class cfssl (
   }
 
   if $serve_ca {
-    file { $_systemd_unit_file:
+    file { $cfssl::params::systemd_unit_file:
       ensure  => file,
       mode    => '0644',
       owner   => 0,
@@ -215,59 +209,13 @@ class cfssl (
         Archive["${binpath}/cfssl"],
         Postgresql::Server::Db[$dbname],
         Exec['goose pg up'],
-        File["${confdir}/${_serve_config_json}"],
-        File["${confdir}/${_db_config_json}"],
+        File["${confdir}/${cfssl::params::serve_config_json}"],
+        File["${confdir}/${cfssl::params::db_config_json}"],
         Class['cfssl::ca::root'],
       ],
       subscribe => Archive["${binpath}/cfssl"],
       provider  => 'systemd',
     }
-
-    if $cfssl::crl_manage {
-      ensure_packages(['jq','coreutils'], { ensure => 'present' })
-
-      file { "${cfssl::binpath}/cfssl-gencrl.sh":
-        ensure  => file,
-        mode    => '0755',
-        owner   => 0,
-        group   => 0,
-        content => epp('cfssl/cfssl-gencrl.sh.epp'),
-      }
-
-      $_crlunits.each | String $_crlunit | {
-        file { "${_systemd_unitdir}/${_crlunit}":
-          ensure  => file,
-          mode    => '0644',
-          owner   => 0,
-          group   => 0,
-          content => epp("cfssl/${$_crlunit}.epp"),
-        }
-        ~> service { $_crlunit:
-          ensure    => 'running',
-          enable    => true,
-          require   => [
-            File["${cfssl::binpath}/cfssl-gencrl.sh"],
-            Service['cfssl'],
-          ],
-          subscribe => Service['cfssl'],
-          provider  => 'systemd',
-        }
-      }
-    } else {
-      $_crlunits.each | String $_crlunit | {
-        service { $_crlunit:
-          ensure   => 'stopped',
-          enable   => false,
-          provider => 'systemd',
-        }
-        -> file { "${_systemd_unitdir}/${_crlunit}":
-          ensure  => absent,
-        }
-      }
-      file { "${cfssl::binpath}/cfssl-gencrl.sh":
-        ensure  => absent,
-      }
-    }
   } else {
     service { 'cfssl':
       ensure    => 'stopped',
@@ -276,12 +224,25 @@ class cfssl (
         Archive["${binpath}/cfssl"],
         Postgresql::Server::Db[$dbname],
         Exec['goose pg up'],
-        File["${confdir}/${_serve_config_json}"],
-        File["${confdir}/${_db_config_json}"],
+        File["${confdir}/${cfssl::params::serve_config_json}"],
+        File["${confdir}/${cfssl::params::db_config_json}"],
         Class['cfssl::ca::root'],
       ],
       subscribe => Archive["${binpath}/cfssl"],
       provider  => 'systemd',
     }
   }
+
+  if $cfssl::crl_manage {
+    $_ca_list = [$rootca_manifest['cn'], $intermediatesca.keys, $serve_ca].flatten.unique
+    class { 'cfssl::ca::gencrls':
+      authorities => $_ca_list,
+      require     => [
+        Archive["${binpath}/cfssl"],
+        Postgresql::Server::Db[$dbname],
+        Exec['goose pg up'],
+        Class['cfssl::ca::root'],
+      ],
+    }
+  }
 }
diff --git a/manifests/params.pp b/manifests/params.pp
new file mode 100644
index 0000000..bcfe87a
--- /dev/null
+++ b/manifests/params.pp
@@ -0,0 +1,15 @@
+# @summary A short summary of the purpose of this class
+#
+# A description of what this class does
+#
+# @example
+#   include cfssl::params
+class cfssl::params {
+  $binaries = ['cfssljson','cfssl-certinfo']
+  $serve_config_json = 'serve-config.json'
+  $db_config_json = 'db-config.json'
+  $systemd_unitdir = '/etc/systemd/system'
+  $systemd_unit_file = "${systemd_unitdir}/cfssl.service"
+  $go_package = 'golang-1.16'
+  $go_targetlink = '/usr/lib/go-1.16/bin/go'
+}
diff --git a/spec/acceptance/cfssl_spec.rb b/spec/acceptance/cfssl_spec.rb
index 2f05707..b89aa7e 100644
--- a/spec/acceptance/cfssl_spec.rb
+++ b/spec/acceptance/cfssl_spec.rb
@@ -62,7 +62,7 @@ describe 'cfssl' do
       its(:stdout) { is_expected.to match %r{CA:TRUE} }
     end
 
-    describe command('openssl crl -in /var/cfssl/crl.pem -text -noout') do
+    describe command('openssl crl -in /var/cfssl/crl-MYEXEMPLEROOTCA.pem -text -noout') do
       its(:stdout) { is_expected.to match %r{Certificate Revocation List } }
       its(:stdout) { is_expected.to match %r{Issuer: C = FR, L = MONTPELLIER, O = MYEXEMPLE ORG, CN = MYEXEMPLE ROOT CA} }
       its(:stdout) { is_expected.to match %r{No Revoked Certificates} }
@@ -118,7 +118,12 @@ describe 'cfssl' do
       its(:stdout) { is_expected.to match %r{CA:TRUE, pathlen:1} }
     end
 
-    describe command('openssl crl -in /var/cfssl/crl.pem -text -noout') do
+    describe command('openssl crl -in /var/cfssl/crl-MYEXEMPLEROOTCA.pem -text -noout') do
+      its(:stdout) { is_expected.to match %r{Certificate Revocation List } }
+      its(:stdout) { is_expected.to match %r{Issuer: C = FR, L = MONTPELLIER, O = MYEXEMPLE ORG, CN = MYEXEMPLE ROOT CA} }
+      its(:stdout) { is_expected.to match %r{No Revoked Certificates} }
+    end
+    describe command('openssl crl -in /var/cfssl/crl-MYEXEMPLEINTERMDIATECA.pem -text -noout') do
       its(:stdout) { is_expected.to match %r{Certificate Revocation List } }
       its(:stdout) { is_expected.to match %r{Issuer: C = FR, L = MONTPELLIER, O = MYEXEMPLE ORG, CN = MYEXEMPLE INTERMDIATE CA} }
       its(:stdout) { is_expected.to match %r{No Revoked Certificates} }
diff --git a/spec/classes/ca/gencrls_spec.rb b/spec/classes/ca/gencrls_spec.rb
new file mode 100644
index 0000000..5be0882
--- /dev/null
+++ b/spec/classes/ca/gencrls_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'cfssl::ca::gencrls' do
+  on_supported_os.each do |os, os_facts|
+    context "on #{os}" do
+      let(:facts) { os_facts }
+
+      it { is_expected.to compile }
+    end
+  end
+end
diff --git a/spec/classes/params_spec.rb b/spec/classes/params_spec.rb
new file mode 100644
index 0000000..2381fb9
--- /dev/null
+++ b/spec/classes/params_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'cfssl::params' do
+  on_supported_os.each do |os, os_facts|
+    context "on #{os}" do
+      let(:facts) { os_facts }
+
+      it { is_expected.to compile }
+    end
+  end
+end
diff --git a/templates/cfssl-gencrl.service.epp b/templates/cfssl-gencrl.service.epp
index 232b1a0..44cb09d 100644
--- a/templates/cfssl-gencrl.service.epp
+++ b/templates/cfssl-gencrl.service.epp
@@ -1,9 +1,13 @@
+<%- | String  $ca
+| -%>
 [Unit]
-Description=CloudFlare's PKI CRL generator
+Description=CFSSL CRL generator for CA <%= $ca %>
 Requires=network-online.target
 After=cfssl.service
 
 [Service]
 Type=oneshot
 RemainAfterExit=yes
-ExecStart=<%= $cfssl::binpath %>/cfssl-gencrl.sh
+Environment=CRL=<%= $cfssl::crldir %>/crl-<%= $ca %>.pem
+ExecStart=sh -c 'echo "-----BEGIN X509 CRL-----" > $CRL ; cfssl crl -db-config <%= $cfssl::confdir %>/<%= $cfssl::params::db_config_json %> -ca <%= $cfssl::confdir %>/ca/<%= $ca %>.pem -ca-key <%= $cfssl::confdir %>/ca/<%= $ca %>-key.pem | fold -w 64 >> $CRL ; echo "-----END X509 CRL-----" >> $CRL'
+
diff --git a/templates/cfssl-gencrl.sh.epp b/templates/cfssl-gencrl.sh.epp
deleted file mode 100644
index 980a73a..0000000
--- a/templates/cfssl-gencrl.sh.epp
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-
-TMPFILE="/tmp/crl${$}.pem"
-
-echo "-----BEGIN X509 CRL-----" > $TMPFILE
-curl -s -d '{}' -H "Content-Type: application/json" -X GET <%= $cfssl::binding_ip %>:<%= $cfssl::port %>/api/v1/cfssl/crl?expiry=<%= $cfssl::crl_expiry %>s | jq -r '.result' | fold -w 64 >> $TMPFILE
-echo "-----END X509 CRL-----" >> $TMPFILE
-
-if [ -s $TMPFILE ]
-then
-  if openssl crl -in $TMPFILE -text -noout | grep -qP '^Certificate Revocation List '
-  then
-    # the temp file looks as expected, we move it to crldir
-    cp $TMPFILE <%= $cfssl::crldir %>/crl.pem
-  else
-    echo "gencrl : missing header 'Certificate Revocation List'"
-    exit 1
-  fi
-else
-  echo "gencrl : empty file"
-  exit 1
-fi
-
-
diff --git a/templates/cfssl-gencrl.timer.epp b/templates/cfssl-gencrl.timer.epp
index 61c8970..70d7415 100644
--- a/templates/cfssl-gencrl.timer.epp
+++ b/templates/cfssl-gencrl.timer.epp
@@ -1,5 +1,7 @@
+<%- | String  $ca
+| -%>
 [Unit]
-Description=CloudFlare's PKI CRL generator
+Description=CFSSL CRL generator for CA <%= $ca %>
 
 [Timer]
 OnCalendar=*-*-* <%= $cfssl::crl_gentimer %>
diff --git a/templates/cfssl.service.epp b/templates/cfssl.service.epp
index f49b637..17924be 100644
--- a/templates/cfssl.service.epp
+++ b/templates/cfssl.service.epp
@@ -6,7 +6,7 @@ After=network-online.target
 [Service]
 User=<%= $cfssl::sysuser %>
 Group=<%= $cfssl::sysgroup %>
-ExecStart=<%= $cfssl::binpath %>/cfssl -log_dir <%= $cfssl::logdir %> serve -address <%= $cfssl::binding_ip %> -ca <%= $cfssl::confdir %>/ca/<%= regsubst($cfssl::serve_ca, '\s', '', 'G') %>.pem -ca-key <%= $cfssl::confdir %>/ca/<%= regsubst($cfssl::serve_ca, '\s', '', 'G')  %>-key.pem -port <%= $cfssl::port %> -db-config <%= $cfssl::confdir %>/db-config.json -config <%= $cfssl::confdir %>/serve-config.json -loglevel <%= $cfssl::log_level %>
+ExecStart=<%= $cfssl::binpath %>/cfssl -log_dir <%= $cfssl::logdir %> serve -address <%= $cfssl::binding_ip %> -ca <%= $cfssl::confdir %>/ca/<%= regsubst($cfssl::serve_ca, '\s', '', 'G') %>.pem -ca-key <%= $cfssl::confdir %>/ca/<%= regsubst($cfssl::serve_ca, '\s', '', 'G')  %>-key.pem -port <%= $cfssl::port %> -db-config <%= $cfssl::confdir %>/<%= $cfssl::params::db_config_json %> -config <%= $cfssl::confdir %>/<%= $cfssl::params::serve_config_json %> -loglevel <%= $cfssl::log_level %>
 Restart=always
 PrivateTmp=yes
 ProtectSystem=full
-- 
GitLab