I recently ran into a known bug with the puppet certificate generate command that made it useless to me for creating user certificates.
So I had to do the CSR dance from Ruby myself to work around it, it’s quite simple actually but as with all things in OpenSSL it’s weird and wonderful.
Since the Puppet Agent is written in Ruby and it can do this it means there’s a HTTP API somewhere, these are documented reasonably well – see /puppet-ca/v1/certificate_request/ and /puppet-ca/v1/certificate/. Not covered is how to make the CSRs and such.
First I have a little helper to make the HTTP client:
def ca_path; "/home/rip/.puppetlabs/etc/puppet/ssl/certs/ca.pem";end def cert_path; "/home/rip/.puppetlabs/etc/puppet/ssl/certs/rip.pem";end def key_path; "/home/rip/.puppetlabs/etc/puppet/ssl/private_keys/rip.pem";end def csr_path; "/home/rip/.puppetlabs/etc/puppet/ssl/certificate_requests/rip.pem";end def has_cert?; File.exist?(cert_path);end def has_ca?; File.exist?(ca_path);end def already_requested?;!has_cert? && File.exist?(key_path);end def http http = Net::HTTP.new(@ca, 8140) http.use_ssl = true if has_ca? http.ca_file = ca_path http.verify_mode = OpenSSL::SSL::VERIFY_PEER else http.verify_mode = OpenSSL::SSL::VERIFY_NONE end http end |
This is a HTTPS client that uses full verification of the remote host if we have a CA. There’s a small chicken and egg where you have to ask the CA for it’s own certificate where it’s a unverified connection. If this is a problem you need to arrange to put the CA on the machine in a safe manner.
Lets fetch the CA:
def fetch_ca return true if has_ca? req = Net::HTTP::Get.new("/puppet-ca/v1/certificate/ca", "Content-Type" => "text/plain") resp, _ = http.request(req) if resp.code == "200" File.open(ca_path, "w", Ob0644) {|f| f.write(resp.body)} puts("Saved CA certificate to %s" % ca_path) else abort("Failed to fetch CA from %s: %s: %s" % [@ca, resp.code, resp.message]) end has_ca? end |
At this point we have the CA and saved it, future requests will be verified against this CA. If you put the CA there using some other means this will do nothing.
Now we need to start making our CSR, first we have to make a private key, this is a 4096 bit key saved in pem format:
def write_key key = OpenSSL::PKey::RSA.new(4096) File.open(key_path, "w", Ob0640) {|f| f.write(key.to_pem)} key end |
And the CSR needs to be made using this key, Puppet CSRs are quite simple with few fields filled in, can’t see why you couldn’t fill in more fields and of course it now supports extensions, I didn’t add any of those here, just a OU:
def write_csr(key) csr = OpenSSL::X509::Request.new csr.version = 0 csr.public_key = key.public_key csr.subject = OpenSSL::X509::Name.new( [ ["CN", @certname, OpenSSL::ASN1::UTF8STRING], ["OU", "my org", OpenSSL::ASN1::UTF8STRING] ] ) csr.sign(key, OpenSSL::Digest::SHA1.new) File.open(csr_path, "w", Ob0644) {|f| f.write(csr.to_pem)} csr.to_pem end |
Let’s combine these to make the key and CSR and send the request to the Puppet CA, this request is verified using the CA:
def request_cert req = Net::HTTP::Put.new("/puppet-ca/v1/certificate_request/%s?environment=production" % @certname, "Content-Type" => "text/plain") req.body = write_csr(write_key) resp, _ = http.request(req) if resp.code == "200" puts("Requested certificate %s from %s" % [@certname, @ca]) else abort("Failed to request certificate from %s: %s: %s: %s" % [@ca, resp.code, resp.message, resp.body]) end end |
You’ll now have to sign the cert on your Puppet CA as normal, or use autosign, nothing new here.
And finally you can attempt to fetch the cert, this method is designed to return false if the cert is not yet ready on the master – ie. not signed yet.
def attempt_fetch_cert return true if has_cert? req = Net::HTTP::Get.new("/puppet-ca/v1/certificate/%s" % @certname, "Content-Type" => "text/plain") resp, _ = http.request(req) if resp.code == "200" File.open(cert_path, "w", Ob0644) {|f| f.write(resp.body)} puts("Saved certificate to %s" % cert_path) end has_cert? end |
Pulling this all together you have some code to make keys, CSR etc, cache the CA and request a cert is signed, it will then do a wait for cert like Puppet does till things are signed.
def main abort("Already have a certificate '%s', cannot continue" % @certname) if has_cert? make_ssl_dirs fetch_ca if already_requested? puts("Certificate %s has already been requested, attempting to retrieve it" % @certname) else puts("Requesting certificate for '%s'" % @certname) request_cert end puts("Waiting up to 120 seconds for it to be signed") puts 12.times do |time| print "Attempting to download certificate %s: %d / 12\r" % [@certname, time] break if attempt_fetch_cert sleep 10 end abort("Could not fetch the certificate after 120 seconds") unless has_cert? puts("Certificate %s has been stored in %s" % [@certname, ssl_dir]) end |