-
Notifications
You must be signed in to change notification settings - Fork 222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #23211 - Add PuppetCa TokenWhitelisting provider #592
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
#!/usr/bin/env ruby | ||
# | ||
# This takes a base64 encoded CSR as input and uploads it to foreman-proxy for analysis. | ||
# It is used to verify if the CSR should be signed by PuppetCA. | ||
# | ||
|
||
require 'net/http' | ||
require 'net/https' | ||
require 'fileutils' | ||
require 'json' | ||
require 'yaml' | ||
require 'socket' | ||
|
||
def log(message) | ||
puts message | ||
`logger "[PuppetCA CSR Verify] #{message}"` | ||
end | ||
|
||
csr = STDIN.read | ||
|
||
if RbConfig::CONFIG['host_os'] =~ /freebsd|dragonfly/i | ||
settings_file = '/usr/local/etc/foreman-proxy/settings.yml' | ||
else | ||
settings_file = '/etc/foreman-proxy/settings.yml' | ||
end | ||
SETTINGS = YAML.load_file(settings_file) | ||
PUPPETCA = YAML.load_file(SETTINGS[:settings_directory] + '/puppetca.yml') | ||
unless PUPPETCA[:enabled] | ||
log('PuppetCa is not enabled!') | ||
exit 1 | ||
end | ||
protocol = PUPPETCA[:enabled] == true ? 'https' : PUPPETCA[:enabled] | ||
port = protocol == 'https' ? SETTINGS[:https_port] : SETTINGS[:http_port] | ||
fqdn = Socket.gethostbyname(Socket.gethostname).first | ||
# e.g. https://hostname.localdomain.com:8443/puppet/ca/autosign | ||
uri = URI.parse("#{protocol}://#{fqdn}:#{port}/puppet/ca/autosign") | ||
res = Net::HTTP.new(uri.host, uri.port) | ||
if protocol == 'https' | ||
res.use_ssl = true | ||
res.ca_file = SETTINGS[:ssl_ca_file] | ||
res.verify_mode = OpenSSL::SSL::VERIFY_PEER | ||
res.cert = OpenSSL::X509::Certificate.new(File.read(SETTINGS[:ssl_certificate])) | ||
res.key = OpenSSL::PKey::RSA.new(File.read(SETTINGS[:ssl_private_key]), nil) | ||
end | ||
res.open_timeout = SETTINGS[:timeout] | ||
res.read_timeout = SETTINGS[:timeout] | ||
req = Net::HTTP::Post.new(uri.request_uri) | ||
req.add_field('Accept', 'application/json,version=2' ) | ||
req.body = csr | ||
begin | ||
res.start do |http| | ||
response = http.request(req) | ||
exit 0 if response.code == '200' | ||
log("Called smart proxy for CSR verification, but received a #{response.code} http status.") | ||
exit 1 | ||
end | ||
rescue => e | ||
log("Failed to call smart proxy for CSR verification. " + e.to_s) | ||
exit 1 | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
modules/puppetca_token_whitelisting/plugin_configuration.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
2 changes: 2 additions & 0 deletions
2
modules/puppetca_token_whitelisting/puppetca_token_whitelisting.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
121 changes: 121 additions & 0 deletions
121
modules/puppetca_token_whitelisting/puppetca_token_whitelisting_autosigner.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
require 'jwt' | ||
require 'openssl' | ||
|
||
module ::Proxy::PuppetCa::TokenWhitelisting | ||
class Autosigner | ||
include ::Proxy::Log | ||
include ::Proxy::Util | ||
|
||
JWT_ALGORITHM = 'RS512' | ||
RSA_BITSIZE = '2048' | ||
|
||
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(RSA_BITSIZE) | ||
File.chmod 0600, file | ||
end | ||
file | ||
end | ||
|
||
# Invalidate a token based on the certname | ||
def disable certname | ||
storage.remove_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 | ||
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." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be info I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had that when we talked about the first SmartProxyPR #576 where witlessbird suggested warnings. |
||
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 |
25 changes: 25 additions & 0 deletions
25
modules/puppetca_token_whitelisting/puppetca_token_whitelisting_csr.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
11 changes: 11 additions & 0 deletions
11
modules/puppetca_token_whitelisting/puppetca_token_whitelisting_plugin.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
61 changes: 61 additions & 0 deletions
61
modules/puppetca_token_whitelisting/puppetca_token_whitelisting_token_storage.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
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 | ||
unsafe_write content | ||
end | ||
end | ||
|
||
def unsafe_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 | ||
|
||
def remove_if &block | ||
lock do | ||
unsafe_write read.delete_if { |token| yield(token) } | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering about the 404 code; will there be a need to distinguish between a proxy without this feature and one with? Probably not but it might be worth to consider 400 Bad Request for invalid CSRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well the 404 here only gets returned when the CSR is valid but the token is not. A proxy without this feature returns 501 (line 43) and an invalid CSR raises an Exception leading to a 406 (line 49). Does that work for you?