Everyone who has been using Puppet with a self-signed CA for over 5 years knows that dreaded time: the time when the CA and client certificates must be renewed.
This talk will present the ways to ease CA renewal, and present a new approach to renew Puppet client certificates in a secure and automated way.
8. 8/19www.camptocamp.com /
puppetlabs/certgen
■ Install from Puppet Forge
mod 'puppetlabs-certregen', '0.2.0'
■ Regenerate CA cert
$ sudo puppet certregen ca ca_serial 01–
■ Deploy new CA cert (before it expires!)
include certregen::client
12. 12/19www.camptocamp.com /
Cleaning certificats on CA
■ Required before new certificate
can be generated
■ Requires to tune the CA API in auth.conf
{
name: "Allow nodes to delete their own certificates",
match-request: {
path: "^/puppet-ca/v1/certificate(_status|_request)?/([^/]+)$"
type: regex
method: [delete]
},
Allow: "$2",
sort-order: 500
}
13. 13/19www.camptocamp.com /
Unique renewal tokens
■ Use hashed token incl. unchangeable trusted facts
■ Sample hashing function (compatible with Terraform's
base64sha256 builtin function)
■ Generate unique token per node in Puppet manifest:
Puppet::Parser::Functions.newfunction(:base64_sha256, :arity => 1, :type => :rvalue) do |args|
Digest::SHA256.base64digest(args[0])
end
# $psk is a secret parameter (e.g. from hiera)
# $certname comes from trusted facts
$token = base64_sha256("${psk}/${certname}")
14. 14/19www.camptocamp.com /
Adapt autosign script
#!/usr/bin/env ruby
require 'openssl'
request = STDIN.read
csr = OpenSSL::X509::Request.new(request)
# Don't you love OpenSSL's nested values?
challenge = csr.attributes.select { |a| a.oid == "challengePassword" }.first.value.value.first.value
# Puppetmaster logs include exit code
exit 2 if challenge.nil?
certname = ARGV[0]
hash = Digest::SHA256.base64digest("#{autosign_psk}/#{certname}")
if challenge == hash
exit 0
end
exit 1
15. 15/19www.camptocamp.com /
Throw in certificate extensions
def get_ext(csr, name)
Puppet::SSL::Oids.register_puppet_oids
# Some more OpenSSL nested values
exts = csr.attributes.select{ |a| a.oid == "extReq" }[0].value.value[0].value
# Turtles all the way down
val = exts.select { |e| e.value[0].short_name == name }[0].value[1].value
OpenSSL::ASN1.decode(val).value
end
pp_role = get_ext(csr, 'pp_role')
pp_environment = get_ext(csr, 'pp_environment')
hash = Digest::SHA256.base64digest("#{autosign_psk}/#{certname}/#{pp_role}/#{pp_environment}")
■ Lock token to specific trusted facts
16. 16/19www.camptocamp.com /
Couple with trusted facts provisioning
$role = $::trusted['extensions']['pp_role']
include sprintf(
'::roles_c2c::%s', regsubst($role, '/', '::', 'G')
)
■ Dynamic provisioning (no server code added)
■ Safe because linked to certificate
17. 17/19www.camptocamp.com /
Put it all together!
# csr_attributes.yaml
---
custom_attributes:
1.2.840.113549.1.9.7: '$
{token}'
# in common Puppet profile
puppet_certificate { $certname:
ensure => valid,
waitforcert => 60,
renewal_grace_period => 20,
clean => true,
}