Skip to content

Commit

Permalink
Fixes #23211 - Add PuppetCa TokenWhitelisting provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Julian Todt committed Jul 2, 2018
1 parent e325683 commit 2ea6d64
Show file tree
Hide file tree
Showing 21 changed files with 517 additions and 4 deletions.
7 changes: 7 additions & 0 deletions bundler.d/puppetca_token_whitelisting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
group :puppetca_token_whitelisting do
if RUBY_VERSION < '2.1'
gem 'jwt', '~> 1.5.6'
else
gem 'jwt'
end
end
1 change: 1 addition & 0 deletions config/settings.d/puppetca.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# valid providers:
# - puppetca_hostname_whitelisting (verify CSRs based on a hostname whitelist)
# - puppetca_token_whitelisting (verify CSRs based on a token whitelist)
#:use_provider: puppetca_hostname_whitelisting
#:ssldir: /var/lib/puppet/ssl
#:puppetca_use_sudo: true
Expand Down
10 changes: 10 additions & 0 deletions config/settings.d/puppetca_token_whitelisting.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
#
# Configuration of the PuppetCA token_whitelisting provider
#

#:sign_all: false
#:token_ttl: 360
#:tokens_file: /var/lib/foreman-proxy/tokens.yml
# Which certificate to use when encrypting tokens (nil to use the SSL certificate)
#:certificate: nil
1 change: 1 addition & 0 deletions lib/smart_proxy_main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module Proxy
require 'dhcp_libvirt/dhcp_libvirt'
require 'puppetca/puppetca'
require 'puppetca_hostname_whitelisting/puppetca_hostname_whitelisting'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting'
require 'puppet_proxy/puppet'
require 'puppet_proxy_customrun/puppet_proxy_customrun'
require 'puppet_proxy_legacy/puppet_proxy_legacy'
Expand Down
16 changes: 15 additions & 1 deletion modules/puppetca/puppetca_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,27 @@ class Api < ::Sinatra::Base
post "/autosign/:certname" do
content_type :json
certname = params[:certname]
token_ttl = params[:token_ttl]
begin
autosigner.autosign(certname)
autosigner.autosign(certname, token_ttl)
rescue => e
log_halt 406, "Failed to enable autosign for #{certname}: #{e}"
end
end

post "/validate" do
content_type :json
unless autosigner.respond_to?(:validate_csr)
log_halt 501, "Provider only supports trivial autosigning"
end
begin
request.body.rewind
autosigner.validate_csr(request.body.read) ? 200 : 404
rescue => e
log_halt 406, "Failed to validate CSR: #{e}"
end
end

delete "/autosign/:certname" do
content_type :json
certname = params[:certname]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def disable certname

# add certname to puppet autosign file
# parameter is certname to use
def autosign certname
def autosign certname, ttl
FileUtils.touch(autosign_file) unless File.exist?(autosign_file)

open(autosign_file, File::RDWR) do |autosign|
Expand Down
14 changes: 14 additions & 0 deletions modules/puppetca_token_whitelisting/plugin_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module ::Proxy::PuppetCa::TokenWhitelisting
class PluginConfiguration
def load_classes
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_autosigner'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_csr'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_token_storage'
end

def load_dependency_injection_wirings(container_instance, settings)
container_instance.dependency :autosigner, lambda { ::Proxy::PuppetCa::TokenWhitelisting::Autosigner.new }
end
end
end

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'puppetca_token_whitelisting/plugin_configuration'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_plugin'
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
require 'jwt'
require 'openssl'

module ::Proxy::PuppetCa::TokenWhitelisting
class Autosigner
include ::Proxy::Log
include ::Proxy::Util

JWT_ALGORITHM = 'RS512'

def tokens_file
Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.tokens_file
end

def sign_all
Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.sign_all
end

def smartproxy_cert
@certificate ||= OpenSSL::PKey::RSA.new File.read cert_file
end

def storage
Proxy::PuppetCa::TokenWhitelisting::TokenStorage.new tokens_file
end

def token_ttl
Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.token_ttl
end

def cert_file
return Proxy::SETTINGS.ssl_private_key.to_s if Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.certificate.nil?

file = Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.certificate
unless File.exist?(file)
File.write file, OpenSSL::PKey::RSA.generate(2048)
end
file
end

# Invalidate a token based on the certname
def disable certname
storage.lock do
tokens = storage.read
tokens.delete_if do |token|
begin
decoded = JWT.decode(token, smartproxy_cert.public_key, true, algorithm: JWT_ALGORITHM)
decoded.first['certname'] == certname
rescue JWT::ExpiredSignature
true
end
end
storage.unsave_write tokens
end
end

# Create a new token for a certname
def autosign certname, ttl
ttl = ttl.to_i > 0 ? ttl.to_i : token_ttl
payload = { certname: certname, exp: Time.now.to_i + ttl * 60 }
token = JWT.encode payload, smartproxy_cert, JWT_ALGORITHM
storage.add token
{ generated_token: token }.to_json
end

# List the hosts that are currently valid
def autosign_list
storage.read.collect do |token|
begin
decoded = JWT.decode(token, smartproxy_cert.public_key, true, algorithm: JWT_ALGORITHM)
decoded.first['certname']
rescue JWT::ExpiredSignature
nil
end
end.compact
end

# Check whether a csr is valid and should be signed
# by checking its token if it exists
def validate_csr csr
if csr.nil?
logger.warn "Request did not include a CSR."
return false
end
if sign_all
logger.warn "Signing CSR without token verification."
return true
end
begin
req = Proxy::PuppetCa::TokenWhitelisting::CSR.new csr
token = req.challenge_password
rescue
logger.warn "Invalid CSR"
return false
end
if token.nil?
logger.warn "CSR did not include a token."
return false
end
validate_token token
end

def validate_token token
# token didnt expire?
begin
JWT.decode(token, smartproxy_cert.public_key, true, algorithm: JWT_ALGORITHM)
rescue JWT::ExpiredSignature
logger.warn "Token already expired."
return false
rescue JWT::DecodeError
logger.warn "Failed to decode token."
return false
end
# token in our list?
unless storage.read.include? token
logger.warn "Certname not valid."
return false
end
storage.remove token
return true
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Proxy::PuppetCa::TokenWhitelisting
class CSR
attr_reader :csr

def initialize(raw_csr)
@csr = OpenSSL::X509::Request.new(raw_csr)
end

def challenge_password
attribute = custom_attributes.detect do |attr|
['challengePassword', '1.2.840.113549.1.9.7'].include?(attr[:oid])
end
attribute ? attribute[:value] : nil
end

def custom_attributes
@csr.attributes.map do |attr|
{
oid: attr.oid,
value: attr.value.value.first.value
}
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module ::Proxy::PuppetCa::TokenWhitelisting
class Plugin < ::Proxy::Provider
plugin :puppetca_token_whitelisting, ::Proxy::VERSION

requires :puppetca, ::Proxy::VERSION
default_settings :sign_all => false, :tokens_file => '/var/lib/foreman-proxy/tokens.yml', :token_ttl => 360, :certificate => nil

load_classes ::Proxy::PuppetCa::TokenWhitelisting::PluginConfiguration
load_dependency_injection_wirings ::Proxy::PuppetCa::TokenWhitelisting::PluginConfiguration
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module ::Proxy::PuppetCa::TokenWhitelisting
class TokenStorage
include ::Proxy::Log
include ::Proxy::Util

def initialize tokens_file
@tokens_file = tokens_file
ensure_file
end

def ensure_file
return if File.exist?(@tokens_file)
FileUtils.mkdir_p File.dirname(@tokens_file)
FileUtils.touch @tokens_file
write []
end

def read
if RUBY_VERSION < '2.1'
YAML.load_file @tokens_file
else
YAML.safe_load File.read @tokens_file
end
end

def write content
lock do
unsave_write content
end
end

def unsave_write content
File.write @tokens_file, content.to_yaml
end

def lock &block
File.open(@tokens_file, "r+") do |f|
begin
f.flock File::LOCK_EX
yield
ensure
f.flock File::LOCK_UN
end
end
end

def add entry
write read.push entry
end

def remove entry
write read.delete_if { |data| data == entry }
end
end
end
26 changes: 26 additions & 0 deletions test/fixtures/puppetca/csr_example.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEdTCCAl0CAQAwGzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTCCAiIwDQYJ
KoZIhvcNAQEBBQADggIPADCCAgoCggIBAK+eSI5LYrBUROs02A9DYPqEpnN1YokD
MSb47NQW0A4+o7B7h/B3HFN2Moo/US/zNLDvHGuVDZIaIpudtNBTBSX0MQPtF8tQ
Lm7WA6wGwqpX3eR3OLXdWR/T4wIvFQ1gvCS/snuW8YennSeAl1Yijtr2EHiPFJ/i
Dy67vGhvXrqVDl/svIf13uw9zcxZ34VfE8zYg5WZ/thlhjbue/KKJdn+riNIYNK4
gwEjFNER9U38UPZUXMNJCEAXEJ7GKXQUKMXXtybp5jihPxTjbxFKilaIEJNM35ej
9Ra0OREGc0Cc0beaW9+n9ZFBxsfM/NV0nzOos4tmSEyAq54Nbd80ropYivrmSRQf
/yDPijvmC4frL1nhSJaf17rmfw27urbPcObbpuQFXCIlNFJd/CwpPds6ikZ9otDx
yTzPveLbQUCh3CzReODC7jopi7vchPH3cZVvuaE90REKL++3xHqOzRpq5tZ1xc2j
3agiQmTthnzABx6cj/q2ab4YAwpaSIAZFoHtzD8tsyD1WTGkL9jzBkCULd7ZcQdR
AL0PwGLYI2hbCsQV6i9noVZZ19+hHEjjk06lG5SKK+H8eTIybWr9IPU/yZowNPtd
et6samT8ltBLAxqTFffrdsTUTQAoN+ykgsML/N3LUuPACg1G6ge0MQVVvBIfCNiP
SjfPJmthHHc1AgMBAAGgFTATBgkqhkiG9w0BCQcxBhMEMTIzNDANBgkqhkiG9w0B
AQsFAAOCAgEAfTtWZAP8z9pxR6esLkoCfhhXaYzjnzJ5/4r+x/VPpJQEzI37CScG
Dma+UzVkIddCVc5oFtzLtVZtGTaygW2QyR7wu+an0qQBs+MVzkjPPnMLerDUR89c
Nk5DlDMaKFYV3JJpg2G2YdmOR0SEF0640Aw0/Ftx41iTSLSFopDMcTPRbZ9zm2AN
uer2TMIWpio6k6OyEJHkyifQOG1kgI+amVgk21kVRm5qUZV01QduLSxuN8KQKDYT
dmE31BpSQdVYzvrRPV558+NiWSrRheQtLfCl4BUZsZjfgh7OXSiy/yCZ6Co7FeDX
WbVtlIeaFukt5fPD4VKBQVIP296ZiB4BFIDLUcq87DaWbjbO1owS/B89MbAM6Fjy
FHCE3x0nUev3LYrKrJGouHYXiyUNGQk1ilI60y1xc3+B3ErjzpszJB1uBRk1fsem
Np60VUVqNPD7GzxlE/acgEa2Xij1vl+g4yIjWGVv5Fokb4fO6K+n8iSEYKG4+nMr
+vjP7bTcW4GymYPH38TtQfzhXvFMODL8ehiy2xjndEfLMitrbP1vjiLrCZD6gIk3
alxj7nHJMXF83Rqg/7OhERMVmUGE+tDiRD95bMK/eYq4sj49zke6puf5r0MeW6Fv
jfuZ0r0kJmJF/r2FZEKuScl0uS4/RWUvgUdUFwpZ3i8KzJWJ6NDm7eY=
-----END CERTIFICATE REQUEST-----
27 changes: 27 additions & 0 deletions test/fixtures/puppetca/rsa_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAoPDdXIhv26Ugu04WNoVEUI8FcO/8acp04RkjtjkbUFtYWFlZ
IgP29cA+dcfQEqzDCCbeVyB259yLjNmD48zn1sB139un7sXkay61SP9Q/XhIF2wE
LMSYhpFRE1QXEJo/XZizyf/PXMy+XY8gXBzZb4/X5Qm0KFYc5zmPEW1dfFxswkZA
eOJsX2mD28FKvWDaWLBxTP6lftNMCQogKdQAcV/oNXTr/RqUd8MChW2YDevOzFw3
s1vVpc4L5gXdU7cblcvUVAqVmnXRL+SHvzY14jOHDtbb1kAbHAD32rrOJWz/tM0i
r1vwo45JN8lFbFHrCKxPO1uvylb0lh8Y9v/VDwIDAQABAoIBAE/jwL5yK7SIX5sa
xAQEW0jx9e1983pAOYyTghqudlSJQ213zLsJ5MlQyLCGo07h215sjOoK/3tRCyS7
xLCLwnnjx5971fy81wotgWfe9UEdOAwIXnoqjNwfvnMtxtVC/Dll17lp9nFGYSjo
J1QSLg0nBjB3hKqRNH/DHrWz6DgWj0xltKYAim3bbIb+y1Mtb0CmqdfCpawomOtv
R3rlbSTjdy7RfTlAfyzNfG96fLifSJFWz0Kh/xrnlHLvYRh08ZWTS1H7arbSH2AI
8PnbXoFvL6CjT9ZTzOM0t7ltcPEpqaE5vtrB1u6MzGAUqKgLL7Sdq63lF02O7Wzy
VJCoAgECgYEA1cbMy4KdSuqBmC4BvZWdID/dDihPq076n+rJ1gCH5yMOzGaAan6C
0S0Nc95JlWsodxynWKp0Y0Gwd6YQStuppq5G/Ei5Moei2Pe3nHlfSuM1vhZAbE2d
Met7xbJ5bLWpMkf5A84e41iKLH+O3z0z9uMyHYWOdzdVCSzUZthsmsECgYEAwLqH
tSUxQ/e9cJX1CyQbdwDziW5t93U0Al0z9ZkITDeN68nZwBSAt/My40EyNGhPj9X8
skN46yDsUrofJxJd5SFlzQRvOLAzdbtF/32E4wawmIegSL3VuHCgLgxjq+vjci7T
DL7MKcbAyr9Vbo0jnxkQjll5c7rGyGkCqkoqc88CgYEA1GMRXnNjAGYh4OZ8djVp
iPvKDGHCXUk3OKAUbNfULbAn6K1BkgwkvdsLzX0gECIJbuV54V5gCajewNCsySKZ
1264Obeqv2gAQ51Av2XaZdV8tGD5GmC19z2kl5KsjnOhKMJWxRAIogh6JqGIeYQd
14B7btcc5paDln0CfTcyF0ECgYBO8dk25UY53hf2LybR6ndplrUrXVWkIJkuQrlO
5+GTPspehC8y0/Mp5m+40PLcrsGwXGoHJ0Y3oiPLezvZiFd3zOdjRGa+pMZfeleg
4Ox3Bj1+LUgFo+UQiOnEEwUc3iIrmmyO8vS0RBqish8vipbFy8GRXas3MZHNc95I
nNqo1wKBgGT4lfLGkg2dwInsDnsmq90c1WbZpH5kijnce46VJNxLt6XpnTB/eDoQ
ac9dg5d08ul/PPPEk/aM0oDlwTDSqVBtEjeB4IUlj400W5fFOfKQ9b5PBFTbKVOC
8ca2jZTCnk+ESLPe+LfSdAjqD6XkSSXviovOFIxUjASC80EwfeVo
-----END RSA PRIVATE KEY-----
3 changes: 3 additions & 0 deletions test/fixtures/puppetca/storage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
- foo.example.com
- test.bar.example.com
3 changes: 3 additions & 0 deletions test/fixtures/puppetca/tokens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
- eyJhbGciOiJSUzUxMiJ9.eyJjZXJ0bmFtZSI6ImZvby5leGFtcGxlLmNvbSJ9.AYjLD3Oa7LG5y7IVxmQ0yDS3Lac7Jfk4qcOtnE56_ddzo46lsU-KDp3mdNMIPQVxyy-GAFuD-l9ed2vD4WKyqmLGQT7rPIdN9km9GjBGeCmlearPBZfCoIkRICFkys7Odmig-khshgZjPOL_mBrWPulsUOwXVmC0s5c5rrXRCg-HYvsDw1cI5Al-h4-nx9sI2VoVVXtAJ06GM5x10x0apDLU9FGT4sMQdkJNjwYaiDrTEELcuakJ1lGbpAAaiDZM6rFvuDIpC-6BIrEA6NGCUqSFcVnwpg3Foe6VhXs-whQlaKUfbslCHB4M1UnHRXmSFqPn3aCyyiVv4rQy9i4Cpg
- eyJhbGciOiJSUzUxMiJ9.eyJjZXJ0bmFtZSI6InRlc3QuYmFyLmV4YW1wbGUuY29tIn0.RrRmzCrzlp7Lgm1BDChv5zK8WyDIgYaVU0NTpoVS7xxhq3LFJAeJ6JrkhR69DJM9V5X1jAM9R0E5VeH4AkheCQLb-8UKwx2wmGZLWNXZq4fc40R_5NsHGKh9EiCHPXkwl4rz8AlJQmODKtBRe-J4xZ1jsc3qZZJA6L41L5aOr1N-dREQk8Jc6H9_mBXxawUEINZiAxH3F0zulz8vd-2b28BQRBhL-p2YaJtnRYJnxVd9n-GcxBJPnxsSN-dWj1Ttfr-P5iFPcqA1GV2-hfCjLiN8FdImnsJxuOIUhyiEhX6O9O6GQsv6S8vTnrBdxmRYAoe2x2fFRimtHAycGhNmIA
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_should_add_autosign_entry
content = []
begin
## Execute
@autosigner.autosign 'foobar.example.com'
@autosigner.autosign 'foobar.example.com', 0
## Read output
content = @file.read.split("\n")
ensure
Expand All @@ -44,7 +44,7 @@ def test_should_not_duplicate_autosign_entry
before_content = @file.read
@file.seek(0)
## Execute
@autosigner.autosign 'foo.example.com'
@autosigner.autosign 'foo.example.com', 0
## Read output
after_content = @file.read
ensure
Expand Down
Loading

0 comments on commit 2ea6d64

Please sign in to comment.