{"id":3473,"date":"2016-07-20T14:45:57","date_gmt":"2016-07-20T13:45:57","guid":{"rendered":"https:\/\/www.devco.net\/?p=3473"},"modified":"2016-07-20T14:45:57","modified_gmt":"2016-07-20T13:45:57","slug":"interacting-with-the-puppet-ca-from-ruby","status":"publish","type":"post","link":"https:\/\/www.devco.net\/archives\/2016\/07\/20\/interacting-with-the-puppet-ca-from-ruby.php","title":{"rendered":"Interacting with the Puppet CA from Ruby"},"content":{"rendered":"

I recently ran into a known bug<\/a> with the puppet certificate generate<\/em> command that made it useless to me for creating user certificates.<\/p>\n

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.<\/p>\n

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\/<\/a> and \/puppet-ca\/v1\/certificate\/<\/a>. Not covered is how to make the CSRs and such.<\/p>\n

First I have a little helper to make the HTTP client:<\/p>\n

\r\ndef ca_path; \"\/home\/rip\/.puppetlabs\/etc\/puppet\/ssl\/certs\/ca.pem\";end\r\ndef cert_path; \"\/home\/rip\/.puppetlabs\/etc\/puppet\/ssl\/certs\/rip.pem\";end\r\ndef key_path; \"\/home\/rip\/.puppetlabs\/etc\/puppet\/ssl\/private_keys\/rip.pem\";end\r\ndef csr_path; \"\/home\/rip\/.puppetlabs\/etc\/puppet\/ssl\/certificate_requests\/rip.pem\";end\r\ndef has_cert?; File.exist?(cert_path);end\r\ndef has_ca?; File.exist?(ca_path);end\r\ndef already_requested?;!has_cert? && File.exist?(key_path);end\r\n\r\ndef http\r\n  http = Net::HTTP.new(@ca, 8140)\r\n  http.use_ssl = true\r\n\r\n  if has_ca?\r\n    http.ca_file = ca_path\r\n    http.verify_mode = OpenSSL::SSL::VERIFY_PEER\r\n  else\r\n    http.verify_mode = OpenSSL::SSL::VERIFY_NONE\r\n  end\r\n\r\n  http\r\nend\r\n<\/pre>\n

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.<\/p>\n

Lets fetch the CA:<\/p>\n

\r\ndef fetch_ca\r\n  return true if has_ca?\r\n\r\n  req = Net::HTTP::Get.new(\"\/puppet-ca\/v1\/certificate\/ca\", \"Content-Type\" => \"text\/plain\")\r\n  resp, _ = http.request(req)\r\n\r\n  if resp.code == \"200\"\r\n    File.open(ca_path, \"w\", Ob0644) {|f| f.write(resp.body)}\r\n    puts(\"Saved CA certificate to %s\" % ca_path)\r\n  else\r\n    abort(\"Failed to fetch CA from %s: %s: %s\" % [@ca, resp.code, resp.message])\r\n  end\r\n\r\n  has_ca?\r\nend\r\n<\/pre>\n

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.<\/p>\n

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:<\/p>\n

\r\ndef write_key\r\n  key = OpenSSL::PKey::RSA.new(4096)\r\n  File.open(key_path, \"w\", Ob0640) {|f| f.write(key.to_pem)}\r\n  key\r\nend\r\n<\/pre>\n

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:<\/p>\n

\r\ndef write_csr(key)\r\n  csr = OpenSSL::X509::Request.new\r\n  csr.version = 0\r\n  csr.public_key = key.public_key\r\n  csr.subject = OpenSSL::X509::Name.new(\r\n    [\r\n      [\"CN\", @certname, OpenSSL::ASN1::UTF8STRING],\r\n      [\"OU\", \"my org\", OpenSSL::ASN1::UTF8STRING]\r\n    ]\r\n  )\r\n  csr.sign(key, OpenSSL::Digest::SHA1.new)\r\n\r\n  File.open(csr_path, \"w\", Ob0644) {|f| f.write(csr.to_pem)}\r\n\r\n  csr.to_pem\r\nend\r\n<\/pre>\n

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:<\/p>\n

\r\ndef request_cert\r\n  req = Net::HTTP::Put.new(\"\/puppet-ca\/v1\/certificate_request\/%s?environment=production\" % @certname, \"Content-Type\" => \"text\/plain\")\r\n  req.body = write_csr(write_key)\r\n  resp, _ = http.request(req)\r\n\r\n  if resp.code == \"200\"\r\n    puts(\"Requested certificate %s from %s\" % [@certname, @ca])\r\n  else\r\n    abort(\"Failed to request certificate from %s: %s: %s: %s\" % [@ca, resp.code, resp.message, resp.body])\r\n  end\r\nend\r\n<\/pre>\n

You’ll now have to sign the cert on your Puppet CA as normal, or use autosign, nothing new here.<\/p>\n

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.<\/p>\n

\r\ndef attempt_fetch_cert\r\n  return true if has_cert?\r\n\r\n  req = Net::HTTP::Get.new(\"\/puppet-ca\/v1\/certificate\/%s\" % @certname, \"Content-Type\" => \"text\/plain\")\r\n  resp, _ = http.request(req)\r\n\r\n  if resp.code == \"200\"\r\n    File.open(cert_path, \"w\", Ob0644) {|f| f.write(resp.body)}\r\n    puts(\"Saved certificate to %s\" % cert_path)\r\n  end\r\n\r\n  has_cert?\r\nend\r\n<\/pre>\n

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. <\/p>\n

\r\ndef main\r\n  abort(\"Already have a certificate '%s', cannot continue\" % @certname) if has_cert?\r\n\r\n  make_ssl_dirs\r\n  fetch_ca\r\n\r\n  if already_requested?\r\n    puts(\"Certificate %s has already been requested, attempting to retrieve it\" % @certname)\r\n  else\r\n    puts(\"Requesting certificate for '%s'\" % @certname)\r\n    request_cert\r\n  end\r\n\r\n  puts(\"Waiting up to 120 seconds for it to be signed\")\r\n  puts\r\n\r\n  12.times do |time|\r\n    print \"Attempting to download certificate %s: %d \/ 12\\r\" % [@certname, time]\r\n\r\n    break if attempt_fetch_cert\r\n\r\n    sleep 10\r\n  end\r\n\r\n  abort(\"Could not fetch the certificate after 120 seconds\") unless has_cert?\r\n\r\n  puts(\"Certificate %s has been stored in %s\" % [@certname, ssl_dir])\r\nend\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"

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 […]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","footnotes":""},"categories":[7],"tags":[21,13,124],"_links":{"self":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/3473"}],"collection":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/comments?post=3473"}],"version-history":[{"count":2,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/3473\/revisions"}],"predecessor-version":[{"id":3475,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/3473\/revisions\/3475"}],"wp:attachment":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/media?parent=3473"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/categories?post=3473"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/tags?post=3473"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}