Skip to content

Commit

Permalink
Merge pull request #1491 from tkan145/THREESCALE-10156-ssl_verify_client
Browse files Browse the repository at this point in the history
[THREESCALE-10156] Configure ssl_verify_client per product
  • Loading branch information
tkan145 authored Oct 17, 2024
2 parents a30b29b + c7f735b commit 2e32a07
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Added the `APICAST_PROXY_BUFFER_SIZE` variable to allow configuration of the buffer size for handling response from the proxied servers. [PR #1473](https://github.com/3scale/APIcast/pull/1473), [THREESCALE-8410](https://issues.redhat.com/browse/THREESCALE-8410)

- Added the `APICAST_HTTPS_VERIFY_CLIENT` variable to allow configuration of the `ssl_verify_client` directive. [PR #1491](https://github.com/3scale/APIcast/pull/1491) [THREESCALE-10156](https://issues.redhat.com/browse/THREESCALE-10156)

## [3.15.0] 2024-04-04

### Fixed
Expand Down
9 changes: 9 additions & 0 deletions doc/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,15 @@ Path to a file with the X.509 certificate secret key in the PEM format.
Defines the maximum length of the client certificate chain.
If this parameter has `1` as its value, it is possible to include an additional certificate in the client certificate chain. For example, root certificate authority.

### `APICAST_HTTPS_VERIFY_CLIENT`

**Default:** `optional_no_ca`
**Values:**
- `off`: Do not request client certificates or perform client certificate verification.
- `optional_no_ca`: Requests the client certificate, but does not fail the request when the client certificate is not signed by a trusted CA certificate.

Enables verification of client certificates. You can verify client certificates TLS Client Certificate Validation policy.

### `all_proxy`, `ALL_PROXY`

**Default:** no value
Expand Down
2 changes: 1 addition & 1 deletion gateway/http.d/apicast.conf.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ server {
{{ "conf/server.key" | filesystem | first }}
{%- endif %};

ssl_verify_client optional_no_ca;
ssl_verify_client {{ env.APICAST_HTTPS_VERIFY_CLIENT | default: "optional_no_ca" }};
ssl_certificate_by_lua_block { require('apicast.executor'):ssl_certificate() }
ssl_verify_depth {{ env.APICAST_HTTPS_VERIFY_DEPTH | default: 1 }};
{%- endif %}
Expand Down
23 changes: 23 additions & 0 deletions gateway/src/apicast/policy/tls_validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,26 @@ This policy can validate TLS Client Certificate against a whitelist.
Whitelist expects PEM formatted CA or Client certificates.
It is not necessary to have the full certificate chain, just partial matches are allowed.
For example you can add to the whitelist just leaf client certificates without the whole bundle with a CA certificate.

## Configuration

For this policy to work, APIcast need to be setup to listen for TLS connection.

By default, during the TLS handshake, APIcast requests client certificates, but will not verify the certificate or terminate the request unless a TLS Validation Policy is in the chain. In most cases, the client not presenting a client certificate will not affect a service that does not have TLS Validation policy configured. The only exception is when a browser or front-end application uses the service. In this case, the browser will always prompt the user to choose a client certificate to send if they have any client certificates set up while accessing the service.

To work around this, set the environment variable `APICAST_HTTPS_VERIFY_CLIENT` to `off`. This instructs APIcast to request a client certificate only when the policy is in the chain.

NOTE: This policy is not compatible with `APICAST_PATH_ROUTING` or `APICAST_PATH_ROUTING_ONLY` when `APICAST_HTTPS_VERIFY_CLIENT` is set to `off`.

## Example

```
{
"name": "apicast.policy.tls_validation",
"configuration": {
"whitelist": [
{ "pem_certificate": ""-----BEGIN CERTIFICATE----- XXXXXX -----END CERTIFICATE-----"}
]
}
}
```
28 changes: 25 additions & 3 deletions gateway/src/apicast/policy/tls_validation/tls_validation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local policy = require('apicast.policy')
local _M = policy.new('tls_validation')
local X509_STORE = require('resty.openssl.x509.store')
local X509 = require('resty.openssl.x509')
local ngx_ssl = require "ngx.ssl"

local ipairs = ipairs
local tostring = tostring
Expand Down Expand Up @@ -45,19 +46,40 @@ function _M.new(config)
return self
end

function _M:ssl_certificate()
-- Request client certificate
--
-- We don't validate the certificate during the handshake, thus set `depth` to 0 (default is 1)
-- value here in order to save CPU cycles
--
-- TODO:
-- provide ca_certs: See https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/ssl.md#verify_client
-- handle verify_depth
--
return ngx_ssl.verify_client()
end

function _M:access()
local cert = X509.parse_pem_cert(ngx.var.ssl_client_raw_cert)
if not cert then
ngx.status = self.error_status
ngx.say("No required TLS certificate was sent")
return ngx.exit(ngx.status)
end

local store = self.x509_store

local ok, err = store:validate_cert(cert)
-- err is printed inside validate_cert method
-- so no need capture the err here
local ok, _ = store:validate_cert(cert)

if not ok then
ngx.status = self.error_status
ngx.say(err)
ngx.say("TLS certificate validation failed")
return ngx.exit(ngx.status)
end

return ok, err
return ok, nil
end

return _M
6 changes: 3 additions & 3 deletions gateway/src/resty/openssl/x509/store.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local base = require('resty.openssl.base')
local X509_STORE_CTX = require('resty.openssl.x509.store.ctx')
local ffi = require('ffi')
local ffi_gc = ffi.gc

ffi.cdef([[
// https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_new.html
Expand Down Expand Up @@ -45,7 +46,7 @@ local function X509_VERIFY_PARAM(flags)
-- https://www.openssl.org/docs/man1.1.0/crypto/X509_VERIFY_PARAM_get_depth.html#example
ffi_assert(C.X509_VERIFY_PARAM_set_flags(verify_param, flags))

return ffi.gc(verify_param, C.X509_VERIFY_PARAM_free)
return ffi_gc(verify_param, C.X509_VERIFY_PARAM_free)
end

local _M = {}
Expand Down Expand Up @@ -73,9 +74,8 @@ end

function _M.new()
local store = ffi_assert(C.X509_STORE_new())
ffi_gc(store, C.X509_STORE_free)

-- @TODO cleanup here
-- ffi_gc(store, C.X509_STORE_free)
-- enabling partial chains allows us to trust leaf certificates
local verify_param = X509_VERIFY_PARAM(X509_V_FLAG_PARTIAL_CHAIN)

Expand Down
8 changes: 5 additions & 3 deletions spec/policy/tls_validation/tls_validation_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('tls_validation policy', function()
policy:access()

assert.stub(ngx.exit).was_called_with(400)
assert.stub(ngx.say).was_called_with('unable to get local issuer certificate')
assert.stub(ngx.say).was_called_with([[TLS certificate validation failed]])
end)

it('rejects certificates that are not valid yet', function()
Expand All @@ -43,7 +43,8 @@ describe('tls_validation policy', function()

policy:access()

assert.stub(ngx.say).was_called_with('certificate is not yet valid')
assert.stub(ngx.exit).was_called_with(400)
assert.stub(ngx.say).was_called_with([[TLS certificate validation failed]])
end)

it('rejects certificates that are not longer valid', function()
Expand All @@ -53,7 +54,8 @@ describe('tls_validation policy', function()

policy:access()

assert.stub(ngx.say).was_called_with([[certificate has expired]])
assert.stub(ngx.exit).was_called_with(400)
assert.stub(ngx.say).was_called_with([[TLS certificate validation failed]])
end)

it('accepts whitelisted certificate', function()
Expand Down
156 changes: 154 additions & 2 deletions t/apicast-policy-tls_validation.t
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ proxy_pass https://$server_addr:$apicast_port/t;
proxy_set_header Host test;
log_by_lua_block { collectgarbage() }
--- response_body
unable to get local issuer certificate
TLS certificate validation failed
--- error_code: 400
--- no_error_log
[error]
Expand Down Expand Up @@ -154,7 +154,7 @@ proxy_pass https://$server_addr:$apicast_port/t;
proxy_set_header Host test;
log_by_lua_block { collectgarbage() }
--- response_body
Invalid certificate verification context
No required TLS certificate was sent
--- error_code: 400
--- no_error_log
[error]
Expand Down Expand Up @@ -201,3 +201,155 @@ GET /t HTTP/1.0
--- no_error_log
[error]
--- user_files fixture=CA/files.pl eval
=== TEST 6: TLS Client Certificate request client certificate when "APICAST_HTTPS_VERIFY_CLIENT: off"
and the policy is in the chain
--- configuration eval
use JSON qw(to_json);
use File::Slurp qw(read_file);
to_json({
services => [{
proxy => {
hosts => ['test'],
policy_chain => [
{ name => 'apicast.policy.tls_validation',
configuration => {
whitelist => [
{ pem_certificate => CORE::join('', read_file('t/fixtures/CA/intermediate-ca.crt')) }
]
}
},
{ name => 'apicast.policy.echo' },
]
}
}]
});
--- test env
proxy_ssl_verify on;
proxy_ssl_trusted_certificate $TEST_NGINX_SERVER_ROOT/html/ca.crt;
proxy_ssl_certificate $TEST_NGINX_SERVER_ROOT/html/client.crt;
proxy_ssl_certificate_key $TEST_NGINX_SERVER_ROOT/html/client.key;
proxy_pass https://$server_addr:$apicast_port/t;
proxy_set_header Host test;
log_by_lua_block { collectgarbage() }
--- response_body
GET /t HTTP/1.0
--- error_code: 200
--- no_error_log
[error]
--- user_files fixture=CA/files.pl eval
=== TEST 7: TLS Client Certificate request client certificate with path routing enabled
--- env eval
('APICAST_PATH_ROUTING' => '1')
--- configuration eval
use JSON qw(to_json);
use File::Slurp qw(read_file);
to_json({
services => [{
id => 2,
backend_version => 1,
proxy => {
hosts => ['test'],
policy_chain => [
{ name => 'apicast.policy.tls_validation',
configuration => {
whitelist => [
{ pem_certificate => CORE::join('', read_file('t/fixtures/CA/intermediate-ca.crt')) }
]
}
},
{ name => 'apicast.policy.echo' },
]
}
}, {
id => 3,
backend_version => 1,
proxy => {
hosts => ['test'],
policy_chain => [
{ name => 'apicast.policy.echo', configuration => { status => 404 }}
]
}
}]
});
--- test env
proxy_ssl_verify on;
proxy_ssl_trusted_certificate $TEST_NGINX_SERVER_ROOT/html/ca.crt;
proxy_ssl_certificate $TEST_NGINX_SERVER_ROOT/html/client.crt;
proxy_ssl_certificate_key $TEST_NGINX_SERVER_ROOT/html/client.key;
proxy_pass https://$server_addr:$apicast_port/t;
proxy_set_header Host test;
log_by_lua_block { collectgarbage() }
--- response_body
GET /t HTTP/1.0
--- error_code: 200
--- no_error_log
[error]
--- user_files fixture=CA/files.pl eval
=== TEST 8: TLS Client Certificate request client certificate with "APICAST_HTTPS_VERIFY_CLIENT: off"
and path routing enabled
When path routing is enabled, APIcast will not able to select the correct service and build the
corresponding policy chain during the TLS handshake. It will then fallback to the setting defined by
`ssl_client_verify` and with `APICAST_HTTPS_VERIFY_CLIENT` is set to `off`, no client certificate will
be requested.
--- env eval
(
'APICAST_PATH_ROUTING' => '1',
'APICAST_HTTPS_VERIFY_CLIENT' => 'off'
)
--- configuration eval
use JSON qw(to_json);
use File::Slurp qw(read_file);
to_json({
services => [{
id => 2,
backend_version => 1,
proxy => {
hosts => ['test'],
policy_chain => [
{ name => 'apicast.policy.tls_validation',
configuration => {
whitelist => [
{ pem_certificate => CORE::join('', read_file('t/fixtures/CA/intermediate-ca.crt')) }
]
}
},
{ name => 'apicast.policy.echo' },
]
}
}, {
id => 3,
backend_version => 1,
proxy => {
hosts => ['test'],
policy_chain => [
{ name => 'apicast.policy.echo', configuration => { status => 404 }}
]
}
}]
});
--- test env
proxy_ssl_verify on;
proxy_ssl_trusted_certificate $TEST_NGINX_SERVER_ROOT/html/ca.crt;
proxy_ssl_certificate $TEST_NGINX_SERVER_ROOT/html/client.crt;
proxy_ssl_certificate_key $TEST_NGINX_SERVER_ROOT/html/client.key;
proxy_pass https://$server_addr:$apicast_port/t;
proxy_set_header Host test;
log_by_lua_block { collectgarbage() }
--- response_body
No required TLS certificate was sent
--- error_code: 400
--- no_error_log
[error]
--- user_files fixture=CA/files.pl eval
Loading

0 comments on commit 2e32a07

Please sign in to comment.