Skip to content
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

Write real Rack middleware for authorization #789

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
110 changes: 110 additions & 0 deletions lib/proxy/middleware/authorization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
module Proxy
module Middleware
class 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
elsif trusted_hosts?
return denied(fqdn) unless trusted_host?(remote_fqdn)
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(env)
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&.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&.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
1 change: 1 addition & 0 deletions lib/smart_proxy_main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require 'proxy/http_download'
require 'proxy/helpers'
require 'proxy/memory_store'
require 'proxy/middleware/authorization'
require 'proxy/plugin_validators'
require 'proxy/pluggable'
require 'proxy/plugins'
Expand Down
4 changes: 1 addition & 3 deletions modules/bmc/bmc_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
module Proxy::BMC
class Api < ::Sinatra::Base
helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client
# All GET requests will only read ipmi data, no changes
# All PUT requests will update information on the bmc device

Expand Down Expand Up @@ -449,7 +447,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
2 changes: 0 additions & 2 deletions modules/dhcp/dhcp_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ class Proxy::DhcpApi < ::Sinatra::Base
extend Proxy::DHCP::DependencyInjection

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

inject_attr :dhcp_provider, :server
Expand Down
1 change: 1 addition & 0 deletions modules/dhcp/http_config.ru
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'dhcp/dhcp_api'

map "/dhcp" do
use Proxy::Middleware::Authorization
run Proxy::DhcpApi
end
2 changes: 0 additions & 2 deletions modules/dns/dns_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ class Api < ::Sinatra::Base
inject_attr :dns_provider, :server

helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client

post "/?" do
fqdn = params[:fqdn]
Expand Down
1 change: 1 addition & 0 deletions modules/dns/http_config.ru
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'dns/dns_api'

map "/dns" do
use Proxy::Middleware::Authorization
run Proxy::Dns::Api
end
2 changes: 0 additions & 2 deletions modules/facts/facts_api.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
class Proxy::FactsApi < Sinatra::Base
helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client

get "/?" do
content_type :json
Expand Down
1 change: 1 addition & 0 deletions modules/facts/http_config.ru
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'facts/facts_api'

map "/facts" do
use Proxy::Middleware::Authorization
run Proxy::FactsApi
end
1 change: 1 addition & 0 deletions modules/logs/http_config.ru
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'logs/logs_api'

map "/logs" do
use Proxy::Middleware::Authorization
run Proxy::LogsApi
end
2 changes: 0 additions & 2 deletions modules/logs/logs_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

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

get "/" do
content_type :json
Expand Down
1 change: 1 addition & 0 deletions modules/puppet_proxy/http_config.ru
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'puppet_proxy/puppet_api'

map "/puppet" do
use Proxy::Middleware::Authorization
run Proxy::Puppet::Api
end
Loading