Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ekohl committed Dec 15, 2020
1 parent 88fbc8e commit e8ce963
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 80 deletions.
50 changes: 2 additions & 48 deletions lib/proxy/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,17 @@ def log_halt(code = nil, exception_or_msg = nil, custom_msg = nil)
halt code, message
end

# read the HTTPS client certificate from the environment and extract its CN
def https_cert_cn
certificate_raw = request.env['SSL_CLIENT_CERT'].to_s
log_halt 403, 'could not read client cert from environment' if certificate_raw.empty?

begin
certificate = OpenSSL::X509::Certificate.new certificate_raw
if certificate.subject && certificate.subject.to_s =~ /CN=([^\s\/,]+)/i
$1
else
log_halt 403, 'could not read CN from the client certificate'
end
rescue OpenSSL::X509::CertificateError => e
log_halt 403, "could not parse the client certificate\n\n#{e.message}"
end
end

# parses the body as json and returns a hash of the body
# returns empty hash if there is a json parse error, the body is empty or is not a hash
# request.env["CONTENT_TYPE"] must contain application/json in order for the json to be parsed
def parse_json_body
def parse_json_body(request)
json_data = {}
# if the user has explicitly set the content_type then there must be something worth decoding
# we use a regex because it might contain something else like: application/json;charset=utf-8
# by default the content type will probably be set to "application/x-www-form-urlencoded" unless the
# user changed it. If the user doesn't specify the content type we just ignore the body since a form
# will be parsed into the request.params object for us by sinatra
if request.env["CONTENT_TYPE"] =~ /application\/json/
if request.media_type == 'application/json'
begin
body_parameters = request.body.read
json_data = JSON.parse(body_parameters)
Expand All @@ -77,33 +60,4 @@ def dns_resolv(*args)
def resolv(*args)
::Proxy::LoggingResolv.new(Resolv.new(*args))
end

# reverse lookup an IP address while verifying it via forward resolv
def remote_fqdn(forward_verify = true)
ip = request.env['REMOTE_ADDR']
log_halt 403, 'could not get remote address from environment' if ip.empty?

begin
dns = resolv
fqdn = dns.getname(ip)
rescue Resolv::ResolvError => e
log_halt 403, "unable to resolve hostname for ip address #{ip}\n\n#{e.message}"
end

if forward_verify
begin
forward = dns.getaddresses(fqdn)
rescue Resolv::ResolvError => e
log_halt 403, "could not forward verify the remote hostname - #{fqdn} (#{ip})\n\n#{e.message}"
end

if forward.include?(ip)
fqdn
else
log_halt 403, "untrusted client has no matching forward DNS lookup - #{fqdn} (#{ip})"
end
else
fqdn
end
end
end
113 changes: 113 additions & 0 deletions lib/proxy/middleware/authorization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
module Proxy
module Middleware
module Authorization
include ::Proxy::Log

def initialize(app)
@app = app
end

def call(env)
if https?(env)
certificate_raw = https_client_cert_raw(env)
return unauthorized if certificate_raw.empty?

if trusted_hosts?
begin
certificate = OpenSSL::X509::Certificate.new(certificate_raw)
rescue OpenSSL::X509::CertificateError => e
logger.warn("Could not parse the client certificate: #{e.message}")
return unauthorized
end

fqdn = get_cn_from_certificate(certificate)
unless fqdn
logger.warn('Could not read CN from the client certificate')
return unauthorized
end

return denied(fqdn) unless trusted_host?(fqdn)
end
else
if trusted_hosts?
return denied(fqdn) unless trusted_host?(remote_fqdn)
end
end


@app.call(env)
end

private

def settings
Proxy::SETTINGS
end

def unauthorized
[401, {}, 'Unauthorized']
end

def denied(fqdn)
path = request.path_info # TODO
logger.warn("Untrusted client #{fqdn} attempted to access #{path}. Check :trusted_hosts: in settings.yml")
[403, {}, 'Denied']
end

def https?(env)
['yes', 'on', 1].include?(env['HTTPS'].to_s)
end

def https_client_cert_raw(request)
env['SSL_CLIENT_CERT'].to_s
end

def trusted_hosts?
settings.trusted_hosts
end

def trusted_host?(fqdn)
logger.debug "verifying remote client #{fqdn} against trusted_hosts #{trusted_hosts}"
trusted_hosts.include?(fqdn.downcase)
end

# reverse lookup an IP address while verifying it via forward resolv
def remote_fqdn
ip = env['REMOTE_ADDR']
log_halt 403, 'could not get remote address from environment' if ip.empty?

begin
dns = resolv
fqdn = dns.getname(ip)
rescue Resolv::ResolvError => e
log_halt 403, "unable to resolve hostname for ip address #{ip}\n\n#{e.message}"
end

if settings.forward_verify
begin
forward = dns.getaddresses(fqdn)
rescue Resolv::ResolvError => e
log_halt 403, "could not forward verify the remote hostname - #{fqdn} (#{ip})\n\n#{e.message}"
end

if forward.include?(ip)
fqdn
else
log_halt 403, "untrusted client has no matching forward DNS lookup - #{fqdn} (#{ip})"
end
else
fqdn
end
end

def get_cn_from_certificate(certificate)
return unless certificate && certificate.subject

cn = certificate.subject.to_a.find { |oid| oid == 'CN' }
return unless cn

cn[2]
end
end
end
end
93 changes: 83 additions & 10 deletions lib/sinatra/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,19 @@ def do_authorize_with_trusted_hosts
# HTTP: test the reverse DNS entry of the remote IP
trusted_hosts = Proxy::SETTINGS.trusted_hosts
if trusted_hosts
logger.debug "verifying remote client #{request.env['REMOTE_ADDR']} against trusted_hosts #{trusted_hosts}"
fqdn = (https?(request) ? https_cert_cn(request) : remote_fqdn(Proxy::SETTINGS.forward_verify)).downcase

if ['yes', 'on', 1].include? request.env['HTTPS'].to_s
fqdn = https_cert_cn
else
fqdn = remote_fqdn(Proxy::SETTINGS.forward_verify)
end
fqdn = fqdn.downcase
logger.debug "verifying remote client #{fqdn} against trusted_hosts #{trusted_hosts}"

unless Proxy::SETTINGS.trusted_hosts.include?(fqdn)
unless trusted_hosts.include?(fqdn)
log_halt 403, "Untrusted client #{fqdn} attempted to access #{request.path_info}. Check :trusted_hosts: in settings.yml"
end
end
end

def do_authorize_with_ssl_client
if ['yes', 'on', '1'].include? request.env['HTTPS'].to_s
if request.env['SSL_CLIENT_CERT'].to_s.empty?
if https?(request)
if https_client_cert_raw(request).empty?
log_halt 403, "No client SSL certificate supplied"
end
else
Expand All @@ -60,6 +55,84 @@ def do_authorize_any
do_authorize_with_trusted_hosts
do_authorize_with_ssl_client
end

private

def https?(request)
['yes', 'on', 1].include?(request.env['HTTPS'].to_s)
end

def https_client_cert_raw(request)
request.env['SSL_CLIENT_CERT'].to_s
end

# read the HTTPS client certificate from the environment and extract its CN
def https_cert_cn(request)
log_halt 403, 'No HTTPS environment' unless https?(request)

certificate_raw = https_client_cert_raw(request)
certificate = parse_openssl_cert(certificate_raw)
log_halt 403, 'could not read client cert from environment' unless certificate

cn = get_cn_from_certificate(certificate)
log_halt 403, 'could not read CN from the client certificate' unless certificate

cn
rescue OpenSSL::X509::CertificateError => e
log_halt 403, "could not parse the client certificate\n\n#{e.message}"
end

# reverse lookup an IP address while verifying it via forward resolv
def remote_fqdn(forward_verify = true)
ip = request.env['REMOTE_ADDR']
log_halt 403, 'could not get remote address from environment' if ip.empty?

begin
dns = resolv
fqdn = dns.getname(ip)
rescue Resolv::ResolvError => e
log_halt 403, "unable to resolve hostname for ip address #{ip}\n\n#{e.message}"
end

if forward_verify
begin
forward = dns.getaddresses(fqdn)
rescue Resolv::ResolvError => e
log_halt 403, "could not forward verify the remote hostname - #{fqdn} (#{ip})\n\n#{e.message}"
end

if forward.include?(ip)
fqdn
else
log_halt 403, "untrusted client has no matching forward DNS lookup - #{fqdn} (#{ip})"
end
else
fqdn
end
end

def parse_openssl_cert(certificate_raw)
return if certificate_raw.nil? || certificate_raw.empty?

OpenSSL::X509::Certificate.new(certificate_raw)
end

def get_cn_from_certificate(certificate)
return unless certificate && certificate.subject

cn = certificate.subject.to_a.find { |oid| oid == 'CN' }
return unless cn

cn[2]
end
end

def authorize!
include Helpers

before do
do_authorize_with_any
end
end

def authorize_with_trusted_hosts
Expand Down
5 changes: 2 additions & 3 deletions modules/bmc/bmc_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
module Proxy::BMC
class Api < ::Sinatra::Base
helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any
# All GET requests will only read ipmi data, no changes
# All PUT requests will update information on the bmc device

Expand Down Expand Up @@ -436,7 +435,7 @@ def bmc_setup
# also if the user decides to do http://127.0.0.1/bmc/192.168.1.6/test?bmc_provider=freeipmi as well as pass in
# a json encode body with the parameters, all of these items will be merged together
def body_parameters
@body_parameters ||= parse_json_body.merge(params)
@body_parameters ||= parse_json_body(request).merge(params)
end

def auth
Expand Down
3 changes: 1 addition & 2 deletions modules/dhcp/dhcp_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ class Proxy::DhcpApi < ::Sinatra::Base
extend Proxy::DHCP::DependencyInjection

helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any
use Rack::MethodOverride

inject_attr :dhcp_provider, :server
Expand Down
3 changes: 1 addition & 2 deletions modules/dns/dns_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ class Api < ::Sinatra::Base
inject_attr :dns_provider, :server

helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

post "/?" do
fqdn = params[:fqdn]
Expand Down
3 changes: 1 addition & 2 deletions modules/facts/facts_api.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
class Proxy::FactsApi < Sinatra::Base
helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

get "/?" do
content_type :json
Expand Down
3 changes: 1 addition & 2 deletions modules/logs/logs_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

class Proxy::LogsApi < Sinatra::Base
helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

get "/" do
content_type :json
Expand Down
3 changes: 1 addition & 2 deletions modules/puppet_proxy/puppet_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ class Proxy::Puppet::Api < ::Sinatra::Base
extend Proxy::Puppet::DependencyInjection
helpers ::Proxy::Helpers

authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

inject_attr :class_retriever_impl, :class_retriever
inject_attr :environment_retriever_impl, :environment_retriever
Expand Down
3 changes: 1 addition & 2 deletions modules/puppetca/puppetca_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ class Api < ::Sinatra::Base
inject_attr :autosigner, :autosigner

helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

get "/?" do
content_type :json
Expand Down
3 changes: 1 addition & 2 deletions modules/realm/realm_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ class Api < Sinatra::Base
extend Proxy::Realm::DependencyInjection

helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

inject_attr :realm_provider_impl, :realm_provider

Expand Down
3 changes: 1 addition & 2 deletions modules/root/root_v2_api.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
class Proxy::RootV2Api < Sinatra::Base
helpers ::Proxy::Helpers

authorize_with_trusted_hosts
authorize_with_ssl_client
authorize_with_any

get "/features" do
enabled_plugins = ::Proxy::Plugins.instance.select do |plugin|
Expand Down
Loading

0 comments on commit e8ce963

Please sign in to comment.