Skip to content

Commit

Permalink
fix broken CA certificates by including the required CA extensions
Browse files Browse the repository at this point in the history
Previously several applications/libraries would complain that the CA
is invalid due to the missing CA:TRUE attribute.

It is *assumed* that the previous behaviour was intentional, because
creating a CA cert with certutil expects user input, which is not
possible when running this command with Puppet.

This commit uses openssl to create CA certificates, which involves
more steps, but reliably produces a fully functional CA.
  • Loading branch information
fraenki committed Nov 16, 2020
1 parent 998d04c commit c5a1327
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 85 deletions.
1 change: 1 addition & 0 deletions .fixtures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ fixtures:
"concat": "git://github.com/puppetlabs/puppetlabs-concat.git"
"facts": "https://github.com/puppetlabs/puppetlabs-facts.git"
"inifile": "git://github.com/puppetlabs/puppetlabs-inifile.git"
"openssl": "https://github.com/camptocamp/puppet-openssl.git"
"provision": "https://github.com/puppetlabs/provision.git"
"puppet_agent": "https://github.com/puppetlabs/puppetlabs-puppet_agent.git"
"stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git"
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
This release fixes a major bug when using self-signed certificates. In previous
releases the internal CA certificate was created without the required
extensions. As a result, using LDAPS could lead to various SSL errors. Note
that only *new* CA certificates will benefit from this bugfix. The README
contains instructions to purge the existing SSL certificates.

### Added
* Add new dependency: camptocamp/openssl

### Changed
* Use camptocamp/openssl to generate CA certificates

### Fixed
* Fix broken CA certificates by including the required CA extensions
* Fix missing newline in cert bundle

## [2.1.0] - 2020-11-07
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [Modifying existing LDIF data](#modifying-existing-ldif-data)
- [Adding new LDIF data](#adding-new-ldif-data)
- [Adding baseline LDIF data](#adding-baseline-ldif-data)
- [Recreate SSL certs](#recreate-ssl-certs)
1. [Reference](#reference)
1. [Limitations](#limitations)
- [Supported versions](#supported-versions)
Expand Down Expand Up @@ -358,6 +359,48 @@ ds_389::instance { 'example':

Note that while you can declare these via the `ds_389::add` define, puppet's resource load ordering may potentially result in it attempting to add the ldif before a configuration change that it requires.

### Recreate SSL certs

Currently some manual steps are required to regenerate the SSL certificates. A new Bolt task would be nice, PRs welcome. :)

As always, create a backup before attempting this procedure.

Run the following shell commands as root to remove the existing certificates:

```shell
export LDAP_INSTANCE="my-instance-name"

test -d /etc/dirsrv/slapd-${LDAP_INSTANCE} || exit 1

systemctl stop dirsrv@${LDAP_INSTANCE}

dd if=/dev/random count=1024 | sha256sum | awk '{print $1}' > /tmp/noisefile-${LDAP_INSTANCE}
cut -d: -f2 /etc/dirsrv/slapd-${LDAP_INSTANCE}/pin.txt > /tmp/passfile-${LDAP_INSTANCE}

rm -f /etc/dirsrv/slapd-${LDAP_INSTANCE}/${LDAP_INSTANCE}CA.cnf \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/${LDAP_INSTANCE}CA-Key.pem \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/${LDAP_INSTANCE}CA.p12 \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/${LDAP_INSTANCE}CA.pem \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/${LDAP_INSTANCE}Cert-Key.pem \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/${LDAP_INSTANCE}Cert.pem \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/ssl_config.done \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/ssl.done \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/ssl_enable.done \
/etc/dirsrv/slapd-${LDAP_INSTANCE}/ssl.ldif

certutil -D -n "${LDAP_INSTANCE}Cert" -d /etc/dirsrv/slapd-${LDAP_INSTANCE}
certutil -D -n "${LDAP_INSTANCE}CA" -d /etc/dirsrv/slapd-${LDAP_INSTANCE}
```

Next edit `/etc/dirsrv/slapd-${LDAP_INSTANCE}/dse.ldif` and remove the following entries including their attributes:

```
cn=AES,cn=encrypted attribute keys,cn=database_name,cn=ldbm database,cn=plugins,cn=config
cn=3DES,cn=encrypted attribute keys,cn=database_name,cn=ldbm database,cn=plugins,cn=config
```

Afterwards run Puppet to regenerate both the CA and the server certificates.

## Reference

Classes and parameters are documented in [REFERENCE.md](REFERENCE.md).
Expand Down
143 changes: 105 additions & 38 deletions manifests/instance.pp
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@
$sans = undef
}

# Certificate attributes and filenames.
$ca_key = "${instance_path}/${server_id}CA-Key.pem"
$ca_conf = "${instance_path}/${server_id}CA.cnf"
$ca_cert = "${instance_path}/${server_id}CA.pem"
$ca_p12 = "${instance_path}/${server_id}CA.p12"
$ca_nickname = "${server_id}CA"
$ssl_cert_name = "${server_id}Cert"

# Create noise file.
$temp_noise_file = "/tmp/noisefile-${server_id}"
$temp_pass_file = "/tmp/passfile-${server_id}"
Expand All @@ -254,55 +262,129 @@
notify => Exec["Generate password file: ${server_id}"],
}

# Create pwd file.
# Create password file.
exec { "Generate password file: ${server_id}":
command => "echo ${root_dn_pass} > ${temp_pass_file}",
path => $ds_389::path,
refreshonly => true,
notify => Exec["Create cert DB: ${server_id}"],
}

# Create cert db.
exec { "Create cert DB: ${server_id}":
# Create nss db.
-> exec { "Create cert DB: ${server_id}":
command => "certutil -N -d ${instance_path} -f ${temp_pass_file}",
path => $ds_389::path,
refreshonly => true,
notify => Exec["Generate key pair: ${server_id}"],
notify => Ssl_pkey["Generate CA private key: ${server_id}"],
}

# Generate the private key for the CA.
-> ssl_pkey { "Generate CA private key: ${server_id}":
ensure => 'present',
name => $ca_key,
size => 4096,
}

# Fix permissions of CA private key.
-> file { "Fix permissions of CA private key: ${server_id}":
ensure => 'present',
name => $ca_key,
mode => '0640',
owner => $user,
group => $group,
}

# Create the OpenSSL config template for the CA cert.
-> file { "Create CA config: ${server_id}":
ensure => 'present',
name => $ca_conf,
content => epp('ds_389/openssl_ca.cnf.epp',{
dc => $facts['networking']['fqdn'],
cn => $ca_nickname,
}),
}

# Generate key pair.
exec { "Generate key pair: ${server_id}":
command => "certutil -G -d ${instance_path} -g 4096 -z ${temp_noise_file} -f ${temp_pass_file}",
# Create the CA certificate.
-> x509_cert { "Create CA cert: ${server_id}":
ensure => 'present',
name => $ca_cert,
template => $ca_conf,
private_key => $ca_key,
days => 3650,
req_ext => false,
}

# Export CA cert to pkcs12, which is required for import into nss db.
# TODO: openssl::export::pkcs12 cannot be used, because it does not support
# a password file (yet).
-> exec { "Prepare CA cert for import (pkcs12): ${server_id}":
cwd => $instance_path,
command => "openssl pkcs12 -export -in ${ca_cert} -inkey ${ca_key} -out ${ca_p12} -password file:${temp_pass_file}",
path => $ds_389::path,
refreshonly => true,
notify => Exec["Make ca cert and add to database: ${server_id}"],
subscribe => [
X509_cert["Create CA cert: ${server_id}"],
],
}

# Make certs and add to database.
exec { "Make ca cert and add to database: ${server_id}":
# Import CA cert+key into nss db.
-> exec { "Import CA cert: ${server_id}":
cwd => $instance_path,
command => "certutil -S -n \"${server_id}CA\" -s \"cn=${server_id}CA,dc=${server_host}\" -x -t \"CT,,\" -v 120 -d ${instance_path} -k rsa -z ${temp_noise_file} -f ${temp_pass_file} ; sleep 2", # lint:ignore:140chars
command => "pk12util -i ${ca_p12} -d sql:${instance_path} -k ${temp_pass_file} -w ${temp_pass_file}",
path => $ds_389::path,
refreshonly => true,
subscribe => [
X509_cert["Create CA cert: ${server_id}"],
],
notify => [
Exec["Make server cert and add to database: ${server_id}"],
Exec["Clean up temp files: ${server_id}"],
Exec["Add trust for CA: ${server_id}"],
],
}

exec { "Add trust for CA: ${server_id}":
command => "certutil -M -n \"${server_id}CA\" -t CT,, -d ${instance_path}",
# Change nickname to make it clear that this is the CA cert.
-> exec { "Fix name of imported CA: ${server_id}":
cwd => $instance_path,
command => "certutil --rename -n \"${ca_nickname} - ${facts['networking']['fqdn']}\" --new-n \"${ca_nickname}\" -d sql:${instance_path}", # lint:ignore:140chars
path => $ds_389::path,
refreshonly => true,
subscribe => [
X509_cert["Create CA cert: ${server_id}"],
],
}

# Configure trust attributes.
-> exec { "Add trust for CA: ${server_id}":
command => "certutil -M -n \"${ca_nickname}\" -t CT,C,C -d ${instance_path} -f ${temp_pass_file}",
path => $ds_389::path,
unless => "certutil -L -d ${instance_path} | grep \"${ca_nickname}\" | grep \"CTu,Cu,Cu\"",
subscribe => [
X509_cert["Create CA cert: ${server_id}"],
],
notify => Exec["Export CA cert: ${server_id}"],
}

# Export ca cert.
-> exec { "Export CA cert: ${server_id}":
cwd => $instance_path,
command => "certutil -d ${instance_path} -L -n \"${ca_nickname}\" -a > ${ca_cert}",
path => $ds_389::path,
unless => "certutil -L -d ${instance_path} | grep \"${server_id}CA\" | grep \"CT\"",
notify => Exec["Export CA cert: ${server_id}"],
creates => $ca_cert,
}

# Make server cert and add to database.
$ssl_cert_name = "${server_id}Cert"
# Copy ca cert to openldap.
-> file { "${ds_389::cacerts_path}/${server_id}CA.pem":
ensure => file,
source => $ca_cert,
require => Exec["Export CA cert: ${server_id}"],
notify => Exec["Rehash cacertdir: ${server_id}"],
}

# Create server cert and add to database.
exec { "Make server cert and add to database: ${server_id}":
cwd => $instance_path,
command => "certutil -S -n \"${ssl_cert_name}\" -m 101 -s \"cn=${server_host}\" -c \"${server_id}CA\" -t \"u,u,u\" -v 120 -d ${instance_path} -k rsa -z ${temp_noise_file} -f ${temp_pass_file} ${sans} ; sleep 2", # lint:ignore:140chars
command => "certutil -S -n \"${ssl_cert_name}\" -m 101 -s \"cn=${server_host}\" -c \"${ca_nickname}\" -t \"u,u,u\" -v 120 -d ${instance_path} -k rsa -z ${temp_noise_file} -f ${temp_pass_file} ${sans} && sleep 2", # lint:ignore:140chars
path => $ds_389::path,
refreshonly => true,
notify => [
Expand All @@ -312,38 +394,23 @@
],
}

exec { "Add trust for server cert: ${server_id}":
# Configure trust attributes.
-> exec { "Add trust for server cert: ${server_id}":
command => "certutil -M -n \"${ssl_cert_name}\" -t u,u,u -d ${instance_path}",
path => $ds_389::path,
unless => "certutil -L -d ${instance_path} | grep \"${ssl_cert_name}\" | grep \"u,u,u\"",
notify => Exec["Export server cert: ${server_id}"],
}

# Set perms on database directory.
exec { "Set permissions on database directory: ${server_id}":
-> exec { "Set permissions on database directory: ${server_id}":
command => "chown ${user}:${group} ${instance_path}",
path => $ds_389::path,
refreshonly => true,
}

# Export ca cert.
exec { "Export CA cert: ${server_id}":
cwd => $instance_path,
command => "certutil -d ${instance_path} -L -n \"${server_id}CA\" -a > ${server_id}CA.pem",
path => $ds_389::path,
creates => "${instance_path}/${server_id}CA.pem",
}

# Copy ca cert to openldap.
file { "${ds_389::cacerts_path}/${server_id}CA.pem":
ensure => file,
source => "${instance_path}/${server_id}CA.pem",
require => Exec["Export CA cert: ${server_id}"],
notify => Exec["Rehash cacertdir: ${server_id}"],
}

# Remove temp files (pwd and noise).
exec { "Clean up temp files: ${server_id}":
# Remove temp files (passwd and noise).
-> exec { "Clean up temp files: ${server_id}":
command => "rm -f ${temp_noise_file} ${temp_pass_file}",
path => $ds_389::path,
refreshonly => true,
Expand Down
4 changes: 4 additions & 0 deletions metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"name": "puppetlabs-inifile",
"version_requirement": ">=3.0.0 <5.0.0"
},
{
"name": "camptocamp-openssl",
"version_requirement": ">=1.14.0 <2.0.0"
},
{
"name": "puppetlabs-stdlib",
"version_requirement": ">=4.25.0 <7.0.0"
Expand Down
46 changes: 41 additions & 5 deletions spec/classes/ds389_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
require 'spec_helper'

describe 'ds_389' do
# content blocks
let(:openssl_ca_cnf) do
'[ req ]
default_bits = 4096
default_md = sha256
distinguished_name = req_distinguished_name
prompt = no
x509_extensions = v3_ca
[ req_distinguished_name ]
DC = foo.example.com
CN = fooCA
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints = CA:true
'
end

on_supported_os(facterversion: '2.4').each do |os, os_facts|
context "on #{os}" do
let(:facts) { os_facts }
let(:facts) do
os_facts.merge(
networking: { fqdn: 'foo.example.com' },
)
end

context 'without any parameters' do
it { is_expected.to compile }
Expand Down Expand Up @@ -210,18 +234,30 @@
it { is_expected.to contain_ds_389__instance('foo') }
it { is_expected.to contain_ds_389__service('foo') }
it { is_expected.to contain_ds_389__ssl('foo') }
it { is_expected.to contain_exec('Add trust for CA: foo') }
it { is_expected.to contain_exec('Add trust for server cert: foo') }
it { is_expected.to contain_exec('Clean up temp files: foo') }
it { is_expected.to contain_exec('Create cert DB: foo') }
it { is_expected.to contain_exec('Export CA cert: foo') }
it { is_expected.to contain_exec('Export server cert: foo') }
it { is_expected.to contain_exec('Generate key pair: foo') }
it { is_expected.to contain_ssl_pkey('Generate CA private key: foo') }
it { is_expected.to contain_exec('Generate noise file: foo') }
it { is_expected.to contain_exec('Generate password file: foo') }
it { is_expected.to contain_exec('Import ssl ldif: foo') }
it { is_expected.to contain_exec('Make ca cert and add to database: foo') }

it {
is_expected.to contain_file('Create CA config: foo').with(
ensure: 'present',
content: openssl_ca_cnf,
)
}
it { is_expected.to contain_x509_cert('Create CA cert: foo') }
it { is_expected.to contain_exec('Prepare CA cert for import (pkcs12): foo') }
it { is_expected.to contain_exec('Import CA cert: foo') }
it { is_expected.to contain_exec('Fix name of imported CA: foo') }
it { is_expected.to contain_exec('Add trust for CA: foo') }

it { is_expected.to contain_exec('Make server cert and add to database: foo') }
it { is_expected.to contain_exec('Add trust for server cert: foo') }

it { is_expected.to contain_exec('Rehash cacertdir: foo') }
it { is_expected.to contain_exec('Restart foo to enable SSL') }
it { is_expected.to contain_exec('Set permissions on database directory: foo') }
Expand Down
Loading

0 comments on commit c5a1327

Please sign in to comment.