-
Notifications
You must be signed in to change notification settings - Fork 84
dynamic allowlisting on host and ip prefix #937
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # generated from domain_ip_allowlist.acl.erb | ||
| <% | ||
| require "base64" | ||
| require 'zlib' | ||
| require 'stringio' | ||
|
|
||
| if_p("ha_proxy.domain_ip_allowlist") do |lines| | ||
| uncompressed = '' | ||
| if lines.is_a?(Array) | ||
| uncompressed << "\# detected ACL entries provided as array in cleartext format\n" | ||
| lines.each do |line| | ||
| uncompressed << line << "\n" | ||
| end | ||
| else | ||
| gzplain = Base64.decode64(lines) | ||
| gz = Zlib::GzipReader.new(StringIO.new(gzplain)) | ||
| uncompressed = gz.read | ||
| end | ||
| %> | ||
| # BEGIN domain-ip allowlist | ||
| <%= uncompressed %> | ||
| # END domain-ip allowlist | ||
| <% | ||
| end | ||
| %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -289,6 +289,28 @@ end | |
| # to keep backward compatibility enable_additional_health_check_proxy if expect_proxy_cidrs is not empty. | ||
| enable_additional_health_check_proxy = p("ha_proxy.enable_additional_health_check_proxy", p("ha_proxy.expect_proxy_cidrs", []).size > 0) | ||
|
|
||
| domain_ip_allowlist_config = <<~TXTBLOCK | ||
|
|
||
| # Checks the request's host header and source IP against an ACL file. | ||
| # Each ACL entry has the format: domain|ip-version|ip-binary-prefix | ||
| # - A domain starting with "." matches all subdomains (e.g. ".example.com" matches "foo.example.com") | ||
| # - ip-binary-prefix is the IP address in binary; a shorter prefix matches a wider range (like CIDR) | ||
| # Examples: .bar.example.com|4|010101010101 (IPv4, matches /12 prefix) | ||
| # foo.bar.example.com|6|010101...01 (IPv6 exact host) | ||
| http-request set-var(req.ip_bin) src,base2 | ||
| http-request set-var(req.ip_proto) str(4) | ||
| http-request set-var(req.ip_proto) str(6) if { var(req.ip_bin),length gt 32 } | ||
|
Contributor
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. I would be surprised if there isn't a HAProxy native way of checking if |
||
| http-request set-var(req.domain) hdr(host),host_only,lower | ||
| http-request set-var-fmt(req.domain_key) %[var(req.domain)]|%[var(req.ip_proto)]|%[var(req.ip_bin)] | ||
|
Contributor
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. There's probably a way to inline the |
||
| http-request set-var(req.parent_domain) var(req.domain),regsub(^[^\.]+,) | ||
|
Contributor
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.
|
||
| http-request set-var-fmt(req.parent_domain_key) %[var(req.parent_domain)]|%[var(req.ip_proto)]|%[var(req.ip_bin)] | ||
| acl domain_ip_matched var(req.domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl | ||
| acl parent_domain_ip_matched var(req.parent_domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl | ||
|
Contributor
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. Oh, it's unfortunate that we have to match again with the parent domain, but I guess there's no way around it. |
||
| http-request set-var-fmt(txn.block_reason) "blocked by domain-ip allowlist, key=%[var(req.domain_key)]" if !domain_ip_matched !parent_domain_ip_matched | ||
| http-request deny status 403 content-type text/plain string "blocked by domain-ip allowlist" if !domain_ip_matched !parent_domain_ip_matched | ||
|
|
||
| TXTBLOCK | ||
|
|
||
| -%> | ||
|
|
||
| global | ||
|
|
@@ -475,6 +497,9 @@ frontend http-in | |
| acl blacklist src -f /var/vcap/jobs/haproxy/config/blacklist_cidrs.txt | ||
| tcp-request content reject if blacklist | ||
| <%- end -%> | ||
| <%- if_p("ha_proxy.enable_domain_ip_allowlist") do -%> | ||
| <%= format_indented_multiline_config(domain_ip_allowlist_config) %> | ||
| <%- end -%> | ||
| <%- if p("ha_proxy.block_all") -%> | ||
| tcp-request content reject | ||
| <%- end -%> | ||
|
|
@@ -606,6 +631,9 @@ frontend https-in | |
| acl blacklist src -f /var/vcap/jobs/haproxy/config/blacklist_cidrs.txt | ||
| tcp-request content reject if blacklist | ||
| <%- end -%> | ||
| <%- if_p("ha_proxy.enable_domain_ip_allowlist") do -%> | ||
| <%= format_indented_multiline_config(domain_ip_allowlist_config) %> | ||
| <%- end -%> | ||
| <%- if p("ha_proxy.block_all") -%> | ||
| tcp-request content reject | ||
| <%- end -%> | ||
|
|
@@ -790,6 +818,9 @@ frontend wss-in | |
| acl blacklist src -f /var/vcap/jobs/haproxy/config/blacklist_cidrs.txt | ||
| tcp-request content reject if blacklist | ||
| <%- end -%> | ||
| <%- if_p("ha_proxy.enable_domain_ip_allowlist") do -%> | ||
| <%= format_indented_multiline_config(domain_ip_allowlist_config) %> | ||
| <%- end -%> | ||
| <%- if p("ha_proxy.block_all") -%> | ||
| tcp-request content reject | ||
| <%- end -%> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'rspec' | ||
|
|
||
| describe 'config/haproxy.config domain-IP allowlist' do | ||
| let(:haproxy_conf) do | ||
| parse_haproxy_config(template.render({ 'ha_proxy' => properties })) | ||
| end | ||
|
|
||
| let(:frontend_http) { haproxy_conf['frontend http-in'] } | ||
| let(:frontend_https) { haproxy_conf['frontend https-in'] } | ||
| let(:frontend_wss) { haproxy_conf['frontend wss-in'] } | ||
|
|
||
| let(:default_properties) do | ||
| { | ||
| 'ssl_pem' => 'ssl pem contents', # required for https-in and wss-in frontends | ||
| 'enable_4443' => true # required for wss-in frontend | ||
| } | ||
| end | ||
|
|
||
| let(:properties) { default_properties } | ||
|
|
||
| context 'when ha_proxy.enable_domain_ip_allowlist is not set' do | ||
| it 'does not include domain-IP allowlist rules in http-in' do | ||
| expect(frontend_http).not_to include('set-var(req.ip_bin)') | ||
| expect(frontend_http).not_to include('domain_ip_allowlist.acl') | ||
| expect(frontend_http).not_to include('http-request deny status 403') | ||
| end | ||
|
|
||
| it 'does not include domain-IP allowlist rules in https-in' do | ||
| expect(frontend_https).not_to include('set-var(req.ip_bin)') | ||
| expect(frontend_https).not_to include('domain_ip_allowlist.acl') | ||
| end | ||
|
|
||
| it 'does not include domain-IP allowlist rules in wss-in' do | ||
| expect(frontend_wss).not_to include('set-var(req.ip_bin)') | ||
| expect(frontend_wss).not_to include('domain_ip_allowlist.acl') | ||
| end | ||
| end | ||
|
|
||
| context 'when ha_proxy.enable_domain_ip_allowlist is true' do | ||
| let(:properties) { default_properties.merge('enable_domain_ip_allowlist' => true) } | ||
|
|
||
| it 'converts source IP to binary in http-in' do | ||
| expect(frontend_http).to include('http-request set-var(req.ip_bin) src,base2') | ||
| end | ||
|
|
||
| it 'sets IP protocol version variable in http-in' do | ||
| expect(frontend_http).to include('http-request set-var(req.ip_proto) str(4)') | ||
| expect(frontend_http).to include('http-request set-var(req.ip_proto) str(6) if { var(req.ip_bin),length gt 32 }') | ||
| end | ||
|
|
||
| it 'extracts domain and parent domain from Host header in http-in' do | ||
| expect(frontend_http).to include('http-request set-var(req.domain) hdr(host),host_only,lower') | ||
| expect(frontend_http).to include('http-request set-var(req.parent_domain) var(req.domain),regsub(^[^\.]+,)') | ||
| end | ||
|
|
||
| it 'builds lookup keys for domain and parent domain in http-in' do | ||
| expect(frontend_http).to include('http-request set-var-fmt(req.domain_key) %[var(req.domain)]|%[var(req.ip_proto)]|%[var(req.ip_bin)]') | ||
| expect(frontend_http).to include('http-request set-var-fmt(req.parent_domain_key) %[var(req.parent_domain)]|%[var(req.ip_proto)]|%[var(req.ip_bin)]') | ||
| end | ||
|
|
||
| it 'defines ACLs against the allowlist file in http-in' do | ||
| expect(frontend_http).to include('acl domain_ip_matched var(req.domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl') | ||
| expect(frontend_http).to include('acl parent_domain_ip_matched var(req.parent_domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl') | ||
| end | ||
|
|
||
| it 'sets block_reason and denies with 403 when neither ACL matches in http-in' do | ||
| expect(frontend_http).to include('http-request set-var-fmt(txn.block_reason) "blocked by domain-ip allowlist, key=%[var(req.domain_key)]" if !domain_ip_matched !parent_domain_ip_matched') | ||
| expect(frontend_http).to include('http-request deny status 403 content-type text/plain string "blocked by domain-ip allowlist" if !domain_ip_matched !parent_domain_ip_matched') | ||
| end | ||
|
|
||
| it 'includes domain-IP allowlist rules in https-in' do | ||
| expect(frontend_https).to include('http-request set-var(req.ip_bin) src,base2') | ||
| expect(frontend_https).to include('acl domain_ip_matched var(req.domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl') | ||
| expect(frontend_https).to include('acl parent_domain_ip_matched var(req.parent_domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl') | ||
| expect(frontend_https).to include('http-request deny status 403 content-type text/plain string "blocked by domain-ip allowlist" if !domain_ip_matched !parent_domain_ip_matched') | ||
| end | ||
|
|
||
| it 'includes domain-IP allowlist rules in wss-in' do | ||
| expect(frontend_wss).to include('http-request set-var(req.ip_bin) src,base2') | ||
| expect(frontend_wss).to include('acl domain_ip_matched var(req.domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl') | ||
| expect(frontend_wss).to include('acl parent_domain_ip_matched var(req.parent_domain_key) -m beg -f opt@/var/vcap/jobs/haproxy/config/domain_ip_allowlist.acl') | ||
| expect(frontend_wss).to include('http-request deny status 403 content-type text/plain string "blocked by domain-ip allowlist" if !domain_ip_matched !parent_domain_ip_matched') | ||
| end | ||
| end | ||
| end |
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.
We usually use
.txtas file ending (instead of.acl), e.g. https://github.com/cloudfoundry/haproxy-boshrelease/blob/master/jobs/haproxy/templates/whitelist_cidrs.txt.erb