diff --git a/manifests/ca/intermediate.pp b/manifests/ca/intermediate.pp new file mode 100644 index 0000000000000000000000000000000000000000..303e71c09f030efd4df75c8d7e4933d835755ace --- /dev/null +++ b/manifests/ca/intermediate.pp @@ -0,0 +1,63 @@ +# @summary A short summary of the purpose of this defined type. +# +# A description of what this defined type does +# +# @example +# cfssl::ca::intermediate { 'namevar': } +define cfssl::ca::intermediate ( + Hash $subject = { 'C' => 'FR', 'L' => 'MONTPELLIER', 'O' => 'EXEMPLE ORG', }, + String[1] $expiry = '26280h', + Cfssl::Ca::Key $key = { algo => 'rsa', size => 2048 }, +) { + require cfssl::ca::root + + $_rootca_name = regsubst($cfssl::ca::root::cn, '\s', '', 'G') + $_rootca_cert = "${cfssl::confdir}/ca/${_rootca_name}.pem" + $_rootca_privkey = "${cfssl::confdir}/ca/${_rootca_name}-key.pem" + + $_intermediatecn = $name + $_intermediateca_name = regsubst($name, '\s', '', 'G') + $_intermediateca_csr = { + cn => $_intermediatecn, + names => [$subject], + key => $key, + } + $_intermediateca_csr_json = to_json($_intermediateca_csr) + + $_root_to_intermediate_config = { + signing => { + 'default' => { + expiry => $expiry, + ca_constraint => { + is_ca => true, + max_path_len => 1, + }, + usages => [ + 'cert sign', + 'crl sign', + ], + }, + }, + } + $_root_to_intermediate_config_file = "${cfssl::confdir}/ca/root-to-intermediate-${_intermediateca_name}_config.json" + + file { $_root_to_intermediate_config_file: + ensure => file, + mode => '0600', + owner => $cfssl::sysuser, + group => $cfssl::sysgroup, + content => to_json_pretty($_root_to_intermediate_config), + } + -> exec { "genkey ${_intermediatecn}": + path => "/usr/bin:${cfssl::binpath}", + command => "echo '${_intermediateca_csr_json}' | cfssl genkey - | cfssljson -bare ${cfssl::confdir}/ca/${_intermediateca_name}", + creates => "${cfssl::confdir}/ca/${_intermediateca_name}-key.pem", + user => $cfssl::sysuser, + } + -> exec { "${cfssl::ca::root::cn} sign ${_intermediatecn} csr": + path => "/usr/bin:${cfssl::binpath}", + command => "cfssl sign -ca ${_rootca_cert} -ca-key ${_rootca_privkey} -config ${_root_to_intermediate_config_file} ${cfssl::confdir}/ca/${_intermediateca_name}.csr | cfssljson -bare ${cfssl::confdir}/ca/${_intermediateca_name}", + creates => "${cfssl::confdir}/ca/${_intermediateca_name}.pem", + user => $cfssl::sysuser, + } +} diff --git a/manifests/ca/intermediates.pp b/manifests/ca/intermediates.pp new file mode 100644 index 0000000000000000000000000000000000000000..4269d89f27088dd504ccc50e8df6643a061d06ee --- /dev/null +++ b/manifests/ca/intermediates.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::ca::intermediates +class cfssl::ca::intermediates ( + Hash $intermediates = {}, +) { + $intermediates.each | String[1] $_cn , Hash $_intermediate_manifest | { + cfssl::ca::intermediate { $_cn: + * => $_intermediate_manifest, + } + } +} diff --git a/manifests/ca/root.pp b/manifests/ca/root.pp index b323aa44a711f0aba864412a92c29517cd99b885..4bf94421f9046f5f519312fff721fcf52f3a93d3 100644 --- a/manifests/ca/root.pp +++ b/manifests/ca/root.pp @@ -6,11 +6,12 @@ # include cfssl::ca::root # class cfssl::ca::root ( - Hash $subject = { 'C' => 'FR', 'L' => 'MONTPELLIER', 'O' => 'EXEMPLE ORG', 'OU' => 'IT Dept' }, - String[1] $cn = 'EXEMPLE ROOT CA GEN1', + Hash $subject, + String[1] $cn, String[1] $expiry = '43800h', Cfssl::Ca::Key $key = { algo => 'rsa', size => 2048 }, ) { + $_rootca_name = regsubst($cn, '\s', '', 'G') $_rootca_csr = { cn => $cn, names => [$subject], @@ -21,8 +22,8 @@ class cfssl::ca::root ( exec { "initca ${cn}": path => "/usr/bin:${cfssl::binpath}", - command => "echo '${_rootca_csr_json}' | cfssl gencert -initca - | cfssljson -bare ${cfssl::confdir}/ca/${cfssl::serve_ca}", - creates => "${cfssl::confdir}/ca/${cfssl::serve_ca}-key.pem", + command => "echo '${_rootca_csr_json}' | cfssl gencert -initca - | cfssljson -bare ${cfssl::confdir}/ca/${_rootca_name}", + creates => "${cfssl::confdir}/ca/${_rootca_name}-key.pem", user => $cfssl::sysuser, } } diff --git a/manifests/init.pp b/manifests/init.pp index 506b18ff026ac51c7c5a63fda3644b9994dcdd82..bdc5dcb6418ffa9e2a9f705db141266eb3b7180c 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -8,7 +8,7 @@ # @param crl_expiry A value, in seconds, after which the CRL should expire from the moment of the request # class cfssl ( - Hash $rootca_manifest = {}, + Hash $rootca_manifest = { cn => 'EXEMPLE ROOT CA', subject => { 'C' => 'FR', 'L' => 'MONTPELLIER', 'O' => 'EXEMPLE ORG', } }, Stdlib::HTTPSUrl $downloadurl = 'https://github.com/cloudflare/cfssl/releases/download', String[1] $version = '1.6.3', String[1] $downloadchecksum = '16b42bfc592dc4d0ba1e51304f466cae7257edec13743384caf4106195ab6047', @@ -25,11 +25,12 @@ class cfssl ( Stdlib::Absolutepath $confdir = '/etc/cfssl', Stdlib::Absolutepath $binpath = '/usr/local/bin', Cfssl::Serveconfig $serve_config = { signing => { 'default' => { expiry => '1h', usages => ['client auth'] } } }, - String[1] $serve_ca = 'ROOT_ca', - Boolean $crl_manage = true, + Boolean $crl_manage = false, Stdlib::Absolutepath $crldir = '/var/cfssl', Integer $crl_expiry = 604800, String[1] $crl_gentimer = '*:00:00', + Hash $intermediatesca = {}, + Optional[String[1]] $serve_ca = undef, ) { include cfssl::goose include postgresql::server @@ -151,70 +152,94 @@ class cfssl ( ], } - file { $_systemd_unit_file: - ensure => file, - mode => '0644', - owner => 0, - group => 0, - content => epp('cfssl/cfssl.service.epp'), - } - ~> service { 'cfssl': - ensure => 'running', - enable => true, - require => [ - Archive["${binpath}/cfssl"], - Postgresql::Server::Db[$dbname], - Exec['goose pg up'], - File["${confdir}/${_serve_config_json}"], - File["${confdir}/${_db_config_json}"], - Class['cfssl::ca::root'], - ], - subscribe => Archive["${binpath}/cfssl"], - provider => 'systemd', + if $intermediatesca { + class { 'cfssl::ca::intermediates': + intermediates => $intermediatesca, + } } - if $cfssl::crl_manage { - ensure_packages(['jq','coreutils'], { ensure => 'present' }) - - file { "${cfssl::binpath}/cfssl-gencrl.sh": + if $serve_ca { + file { $_systemd_unit_file: ensure => file, - mode => '0755', + mode => '0644', owner => 0, group => 0, - content => epp('cfssl/cfssl-gencrl.sh.epp'), + content => epp('cfssl/cfssl.service.epp'), } + ~> service { 'cfssl': + ensure => 'running', + enable => true, + require => [ + Archive["${binpath}/cfssl"], + Postgresql::Server::Db[$dbname], + Exec['goose pg up'], + File["${confdir}/${_serve_config_json}"], + File["${confdir}/${_db_config_json}"], + Class['cfssl::ca::root'], + ], + subscribe => Archive["${binpath}/cfssl"], + provider => 'systemd', + } + + if $cfssl::crl_manage { + ensure_packages(['jq','coreutils'], { ensure => 'present' }) - $_crlunits.each | String $_crlunit | { - file { "${_systemd_unitdir}/${_crlunit}": + file { "${cfssl::binpath}/cfssl-gencrl.sh": ensure => file, - mode => '0644', + mode => '0755', owner => 0, group => 0, - content => epp("cfssl/${$_crlunit}.epp"), + content => epp('cfssl/cfssl-gencrl.sh.epp'), } - ~> service { $_crlunit: - ensure => 'running', - enable => true, - require => [ - File["${cfssl::binpath}/cfssl-gencrl.sh"], - Service['cfssl'], - ], - provider => 'systemd', + + $_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', + } else { + $_crlunits.each | String $_crlunit | { + service { $_crlunit: + ensure => 'stopped', + enable => false, + provider => 'systemd', + } + -> file { "${_systemd_unitdir}/${_crlunit}": + ensure => absent, + } } - -> file { "${_systemd_unitdir}/${_crlunit}": + file { "${cfssl::binpath}/cfssl-gencrl.sh": ensure => absent, } } - file { "${cfssl::binpath}/cfssl-gencrl.sh": - ensure => absent, + } else { + service { 'cfssl': + ensure => 'stopped', + enable => false, + require => [ + Archive["${binpath}/cfssl"], + Postgresql::Server::Db[$dbname], + Exec['goose pg up'], + File["${confdir}/${_serve_config_json}"], + File["${confdir}/${_db_config_json}"], + Class['cfssl::ca::root'], + ], + subscribe => Archive["${binpath}/cfssl"], + provider => 'systemd', } } } diff --git a/spec/acceptance/cfssl_spec.rb b/spec/acceptance/cfssl_spec.rb index fc6130374e1a105f65505a9684e42c5139d00c88..2f057072043da97cf914b0d3dae73eefd211fcf0 100644 --- a/spec/acceptance/cfssl_spec.rb +++ b/spec/acceptance/cfssl_spec.rb @@ -13,6 +13,39 @@ describe 'cfssl' do apply_manifest(pp, catch_changes: true) end + describe command('openssl x509 -in /etc/cfssl/ca/EXEMPLEROOTCA.pem -text -noout') do + # rubocop:disable RSpec/RepeatedDescription + its(:stdout) { is_expected.to match %r{Certificate:} } + its(:stdout) { is_expected.to match %r{Issuer: C = FR, L = MONTPELLIER, O = EXEMPLE ORG, CN = EXEMPLE ROOT CA} } + its(:stdout) { is_expected.to match %r{Subject: C = FR, L = MONTPELLIER, O = EXEMPLE ORG, CN = EXEMPLE ROOT CA} } + its(:stdout) { is_expected.to match %r{CA:TRUE} } + # rubocop:enable RSpec/RepeatedDescription + end + end + + context 'with served root ca' do + pp = %( + class { 'cfssl': + rootca_manifest => { + cn => 'MYEXEMPLE ROOT CA', + subject => { + 'C' => 'FR', + 'L' => 'MONTPELLIER', + 'O' => 'MYEXEMPLE ORG', + }, + }, + serve_ca => 'MYEXEMPLE ROOT CA', + crl_manage => true, + } + ) + + it 'applies without error' do + apply_manifest(pp, catch_failures: true) + end + it 'applies idempotently' do + apply_manifest(pp, catch_changes: true) + end + describe port(8080) do it { is_expected.to be_listening.on('127.0.0.1').with('tcp') } end @@ -20,11 +53,76 @@ describe 'cfssl' do describe command('curl -s -d "{}" -H "Content-Type: application/json" -X POST 127.0.0.1:8080/api/v1/cfssl/info') do its(:stdout) { is_expected.to match %r{BEGIN CERTIFICATE} } end + + describe command('openssl x509 -in /etc/cfssl/ca/MYEXEMPLEROOTCA.pem -text -noout') do + # rubocop:disable RSpec/RepeatedDescription + its(:stdout) { is_expected.to match %r{Certificate:} } + 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{Subject: C = FR, L = MONTPELLIER, O = MYEXEMPLE ORG, CN = MYEXEMPLE ROOT CA} } + its(:stdout) { is_expected.to match %r{CA:TRUE} } + end + describe command('openssl crl -in /var/cfssl/crl.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} } + # rubocop:enable RSpec/RepeatedDescription + end + end + + context 'with served intermediate ca' do + pp = %( + class { 'cfssl': + rootca_manifest => { + cn => 'MYEXEMPLE ROOT CA', + subject => { + 'C' => 'FR', + 'L' => 'MONTPELLIER', + 'O' => 'MYEXEMPLE ORG', + }, + }, + intermediatesca => { + 'MYEXEMPLE INTERMDIATE CA' => { + subject => { + 'C' => 'FR', + 'L' => 'MONTPELLIER', + 'O' => 'MYEXEMPLE ORG', + }, + }, + }, + serve_ca => 'MYEXEMPLE INTERMDIATE CA', + crl_manage => true, + } + ) + + it 'applies without error' do + apply_manifest(pp, catch_failures: true) + end + it 'applies idempotently' do + apply_manifest(pp, catch_changes: true) + end + + describe port(8080) do + it { is_expected.to be_listening.on('127.0.0.1').with('tcp') } + end + + describe command('curl -s -d "{}" -H "Content-Type: application/json" -X POST 127.0.0.1:8080/api/v1/cfssl/info') do + its(:stdout) { is_expected.to match %r{BEGIN CERTIFICATE} } + end + + describe command('openssl x509 -in /etc/cfssl/ca/MYEXEMPLEINTERMDIATECA.pem -text -noout') do # rubocop:disable RSpec/RepeatedDescription + its(:stdout) { is_expected.to match %r{Certificate:} } + 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{Subject: C = FR, L = MONTPELLIER, O = MYEXEMPLE ORG, CN = MYEXEMPLE INTERMDIATE CA} } + its(:stdout) { is_expected.to match %r{CA:TRUE, pathlen:1} } + end + + describe command('openssl crl -in /var/cfssl/crl.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 = EXEMPLE ORG, OU = IT Dept, CN = EXEMPLE ROOT CA GEN1} } + 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} } + # rubocop:enable RSpec/RepeatedDescription end end end diff --git a/spec/classes/ca/intermediates_spec.rb b/spec/classes/ca/intermediates_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5f1ab6c221003b224cdd1f24c4b6e04d3b37cdac --- /dev/null +++ b/spec/classes/ca/intermediates_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'cfssl::ca::intermediates' 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/defines/ca/intermediate_spec.rb b/spec/defines/ca/intermediate_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..934446ba37577b6c950362a69999e0f1706c0a97 --- /dev/null +++ b/spec/defines/ca/intermediate_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'cfssl::ca::intermediate' do + let(:title) { 'namevar' } + let(:pre_condition) do + <<-PRECOND + require postgresql::server + include cfssl + PRECOND + end + + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts.merge(service_provider: 'systemd') } + + it { is_expected.to compile } + end + end +end diff --git a/templates/ca-root-to-intermediate-config.json.epp b/templates/ca-root-to-intermediate-config.json.epp new file mode 100644 index 0000000000000000000000000000000000000000..3fb8c2ec301f4f70b61e8a7d53781f3ae78f978f --- /dev/null +++ b/templates/ca-root-to-intermediate-config.json.epp @@ -0,0 +1,15 @@ +{ + "signing": { + "default": { + "expiry": "70080h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 1, + }, + "usages": [ + "cert sign", + "crl sign", + ] + } + } +} diff --git a/templates/cfssl.service.epp b/templates/cfssl.service.epp index f568d8bd6141c6a061b661cb2f0081d5136bdd0e..f49b637c5606733e3560f81172b29a55deee870d 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/<%= $cfssl::serve_ca %>.pem -ca-key <%= $cfssl::confdir %>/ca/<%= $cfssl::serve_ca %>-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 %>/db-config.json -config <%= $cfssl::confdir %>/serve-config.json -loglevel <%= $cfssl::log_level %> Restart=always PrivateTmp=yes ProtectSystem=full