Skip to content

Commit 03a0337

Browse files
committed
Introduce authentication middleware
1 parent 6c0ee87 commit 03a0337

25 files changed

+235
-82
lines changed

lib/proxy/helpers.rb

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,17 @@ def log_halt(code = nil, exception_or_msg = nil, custom_msg = nil)
2828
halt code, message
2929
end
3030

31-
# read the HTTPS client certificate from the environment and extract its CN
32-
def https_cert_cn
33-
certificate_raw = request.env['SSL_CLIENT_CERT'].to_s
34-
log_halt 403, 'could not read client cert from environment' if certificate_raw.empty?
35-
36-
begin
37-
certificate = OpenSSL::X509::Certificate.new certificate_raw
38-
if certificate.subject && certificate.subject.to_s =~ /CN=([^\s\/,]+)/i
39-
$1
40-
else
41-
log_halt 403, 'could not read CN from the client certificate'
42-
end
43-
rescue OpenSSL::X509::CertificateError => e
44-
log_halt 403, "could not parse the client certificate\n\n#{e.message}"
45-
end
46-
end
47-
4831
# parses the body as json and returns a hash of the body
4932
# returns empty hash if there is a json parse error, the body is empty or is not a hash
5033
# request.env["CONTENT_TYPE"] must contain application/json in order for the json to be parsed
51-
def parse_json_body
34+
def parse_json_body(request)
5235
json_data = {}
5336
# if the user has explicitly set the content_type then there must be something worth decoding
5437
# we use a regex because it might contain something else like: application/json;charset=utf-8
5538
# by default the content type will probably be set to "application/x-www-form-urlencoded" unless the
5639
# user changed it. If the user doesn't specify the content type we just ignore the body since a form
5740
# will be parsed into the request.params object for us by sinatra
58-
if request.env["CONTENT_TYPE"] =~ /application\/json/
41+
if request.media_type == 'application/json'
5942
begin
6043
body_parameters = request.body.read
6144
json_data = JSON.parse(body_parameters)
@@ -77,33 +60,4 @@ def dns_resolv(*args)
7760
def resolv(*args)
7861
::Proxy::LoggingResolv.new(Resolv.new(*args))
7962
end
80-
81-
# reverse lookup an IP address while verifying it via forward resolv
82-
def remote_fqdn(forward_verify = true)
83-
ip = request.env['REMOTE_ADDR']
84-
log_halt 403, 'could not get remote address from environment' if ip.empty?
85-
86-
begin
87-
dns = resolv
88-
fqdn = dns.getname(ip)
89-
rescue Resolv::ResolvError => e
90-
log_halt 403, "unable to resolve hostname for ip address #{ip}\n\n#{e.message}"
91-
end
92-
93-
if forward_verify
94-
begin
95-
forward = dns.getaddresses(fqdn)
96-
rescue Resolv::ResolvError => e
97-
log_halt 403, "could not forward verify the remote hostname - #{fqdn} (#{ip})\n\n#{e.message}"
98-
end
99-
100-
if forward.include?(ip)
101-
fqdn
102-
else
103-
log_halt 403, "untrusted client has no matching forward DNS lookup - #{fqdn} (#{ip})"
104-
end
105-
else
106-
fqdn
107-
end
108-
end
10963
end
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
module Proxy
2+
module Middleware
3+
class Authorization
4+
include ::Proxy::Log
5+
6+
def initialize(app)
7+
@app = app
8+
end
9+
10+
def call(env)
11+
if https?(env)
12+
certificate_raw = https_client_cert_raw(env)
13+
return unauthorized if certificate_raw.empty?
14+
15+
if trusted_hosts?
16+
begin
17+
certificate = OpenSSL::X509::Certificate.new(certificate_raw)
18+
rescue OpenSSL::X509::CertificateError => e
19+
logger.warn("Could not parse the client certificate: #{e.message}")
20+
return unauthorized
21+
end
22+
23+
fqdn = get_cn_from_certificate(certificate)
24+
unless fqdn
25+
logger.warn('Could not read CN from the client certificate')
26+
return unauthorized
27+
end
28+
29+
return denied(fqdn) unless trusted_host?(fqdn)
30+
end
31+
else
32+
if trusted_hosts?
33+
return denied(fqdn) unless trusted_host?(remote_fqdn)
34+
end
35+
end
36+
37+
@app.call(env)
38+
end
39+
40+
private
41+
42+
def settings
43+
Proxy::SETTINGS
44+
end
45+
46+
def unauthorized
47+
[401, {}, ['Unauthorized']]
48+
end
49+
50+
def denied(fqdn)
51+
path = request.path_info # TODO
52+
logger.warn("Untrusted client #{fqdn} attempted to access #{path}. Check :trusted_hosts: in settings.yml")
53+
[403, {}, ['Denied']]
54+
end
55+
56+
def https?(env)
57+
['yes', 'on', 1].include?(env['HTTPS'].to_s)
58+
end
59+
60+
def https_client_cert_raw(env)
61+
env['SSL_CLIENT_CERT'].to_s
62+
end
63+
64+
def trusted_hosts?
65+
settings.trusted_hosts
66+
end
67+
68+
def trusted_host?(fqdn)
69+
logger.debug "verifying remote client #{fqdn} against trusted_hosts #{trusted_hosts}"
70+
trusted_hosts.include?(fqdn.downcase)
71+
end
72+
73+
# reverse lookup an IP address while verifying it via forward resolv
74+
def remote_fqdn
75+
ip = env['REMOTE_ADDR']
76+
log_halt 403, 'could not get remote address from environment' if ip.empty?
77+
78+
begin
79+
dns = resolv
80+
fqdn = dns.getname(ip)
81+
rescue Resolv::ResolvError => e
82+
log_halt 403, "unable to resolve hostname for ip address #{ip}\n\n#{e.message}"
83+
end
84+
85+
if settings.forward_verify
86+
begin
87+
forward = dns.getaddresses(fqdn)
88+
rescue Resolv::ResolvError => e
89+
log_halt 403, "could not forward verify the remote hostname - #{fqdn} (#{ip})\n\n#{e.message}"
90+
end
91+
92+
if forward.include?(ip)
93+
fqdn
94+
else
95+
log_halt 403, "untrusted client has no matching forward DNS lookup - #{fqdn} (#{ip})"
96+
end
97+
else
98+
fqdn
99+
end
100+
end
101+
102+
def get_cn_from_certificate(certificate)
103+
return unless certificate && certificate.subject
104+
105+
cn = certificate.subject.to_a.find { |oid| oid == 'CN' }
106+
return unless cn
107+
108+
cn[2]
109+
end
110+
end
111+
end
112+
end

lib/sinatra/authorization.rb

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,19 @@ def do_authorize_with_trusted_hosts
3030
# HTTP: test the reverse DNS entry of the remote IP
3131
trusted_hosts = Proxy::SETTINGS.trusted_hosts
3232
if trusted_hosts
33-
logger.debug "verifying remote client #{request.env['REMOTE_ADDR']} against trusted_hosts #{trusted_hosts}"
33+
fqdn = (https?(request) ? https_cert_cn(request) : remote_fqdn(Proxy::SETTINGS.forward_verify)).downcase
3434

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

42-
unless Proxy::SETTINGS.trusted_hosts.include?(fqdn)
37+
unless trusted_hosts.include?(fqdn)
4338
log_halt 403, "Untrusted client #{fqdn} attempted to access #{request.path_info}. Check :trusted_hosts: in settings.yml"
4439
end
4540
end
4641
end
4742

4843
def do_authorize_with_ssl_client
49-
if ['yes', 'on', '1'].include? request.env['HTTPS'].to_s
50-
if request.env['SSL_CLIENT_CERT'].to_s.empty?
44+
if https?(request)
45+
if https_client_cert_raw(request).empty?
5146
log_halt 403, "No client SSL certificate supplied"
5247
end
5348
else
@@ -60,6 +55,84 @@ def do_authorize_any
6055
do_authorize_with_trusted_hosts
6156
do_authorize_with_ssl_client
6257
end
58+
59+
private
60+
61+
def https?(request)
62+
['yes', 'on', 1].include?(request.env['HTTPS'].to_s)
63+
end
64+
65+
def https_client_cert_raw(request)
66+
request.env['SSL_CLIENT_CERT'].to_s
67+
end
68+
69+
# read the HTTPS client certificate from the environment and extract its CN
70+
def https_cert_cn(request)
71+
log_halt 403, 'No HTTPS environment' unless https?(request)
72+
73+
certificate_raw = https_client_cert_raw(request)
74+
certificate = parse_openssl_cert(certificate_raw)
75+
log_halt 403, 'could not read client cert from environment' unless certificate
76+
77+
cn = get_cn_from_certificate(certificate)
78+
log_halt 403, 'could not read CN from the client certificate' unless certificate
79+
80+
cn
81+
rescue OpenSSL::X509::CertificateError => e
82+
log_halt 403, "could not parse the client certificate\n\n#{e.message}"
83+
end
84+
85+
# reverse lookup an IP address while verifying it via forward resolv
86+
def remote_fqdn(forward_verify = true)
87+
ip = request.env['REMOTE_ADDR']
88+
log_halt 403, 'could not get remote address from environment' if ip.empty?
89+
90+
begin
91+
dns = resolv
92+
fqdn = dns.getname(ip)
93+
rescue Resolv::ResolvError => e
94+
log_halt 403, "unable to resolve hostname for ip address #{ip}\n\n#{e.message}"
95+
end
96+
97+
if forward_verify
98+
begin
99+
forward = dns.getaddresses(fqdn)
100+
rescue Resolv::ResolvError => e
101+
log_halt 403, "could not forward verify the remote hostname - #{fqdn} (#{ip})\n\n#{e.message}"
102+
end
103+
104+
if forward.include?(ip)
105+
fqdn
106+
else
107+
log_halt 403, "untrusted client has no matching forward DNS lookup - #{fqdn} (#{ip})"
108+
end
109+
else
110+
fqdn
111+
end
112+
end
113+
114+
def parse_openssl_cert(certificate_raw)
115+
return if certificate_raw.nil? || certificate_raw.empty?
116+
117+
OpenSSL::X509::Certificate.new(certificate_raw)
118+
end
119+
120+
def get_cn_from_certificate(certificate)
121+
return unless certificate && certificate.subject
122+
123+
cn = certificate.subject.to_a.find { |oid| oid == 'CN' }
124+
return unless cn
125+
126+
cn[2]
127+
end
128+
end
129+
130+
def authorize!
131+
include Helpers
132+
133+
before do
134+
do_authorize_with_any
135+
end
63136
end
64137

65138
def authorize_with_trusted_hosts

lib/smart_proxy_main.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require 'proxy/http_download'
1717
require 'proxy/helpers'
1818
require 'proxy/memory_store'
19+
require 'proxy/middleware/authorization'
1920
require 'proxy/plugin_validators'
2021
require 'proxy/pluggable'
2122
require 'proxy/plugins'

modules/bmc/bmc_api.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
module Proxy::BMC
55
class Api < ::Sinatra::Base
66
helpers ::Proxy::Helpers
7-
authorize_with_trusted_hosts
8-
authorize_with_ssl_client
97
# All GET requests will only read ipmi data, no changes
108
# All PUT requests will update information on the bmc device
119

@@ -449,7 +447,7 @@ def bmc_setup
449447
# 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
450448
# a json encode body with the parameters, all of these items will be merged together
451449
def body_parameters
452-
@body_parameters ||= parse_json_body.merge(params)
450+
@body_parameters ||= parse_json_body(request).merge(params)
453451
end
454452

455453
def auth

modules/dhcp/dhcp_api.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ class Proxy::DhcpApi < ::Sinatra::Base
22
extend Proxy::DHCP::DependencyInjection
33

44
helpers ::Proxy::Helpers
5-
authorize_with_trusted_hosts
6-
authorize_with_ssl_client
75
use Rack::MethodOverride
86

97
inject_attr :dhcp_provider, :server

modules/dhcp/http_config.ru

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'dhcp/dhcp_api'
22

33
map "/dhcp" do
4+
use Proxy::Middleware::Authorization
45
run Proxy::DhcpApi
56
end

modules/dns/dns_api.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ class Api < ::Sinatra::Base
66
inject_attr :dns_provider, :server
77

88
helpers ::Proxy::Helpers
9-
authorize_with_trusted_hosts
10-
authorize_with_ssl_client
119

1210
post "/?" do
1311
fqdn = params[:fqdn]

modules/dns/http_config.ru

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'dns/dns_api'
22

33
map "/dns" do
4+
use Proxy::Middleware::Authorization
45
run Proxy::Dns::Api
56
end

modules/facts/facts_api.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
class Proxy::FactsApi < Sinatra::Base
22
helpers ::Proxy::Helpers
3-
authorize_with_trusted_hosts
4-
authorize_with_ssl_client
53

64
get "/?" do
75
content_type :json

0 commit comments

Comments
 (0)