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

[THREESCALE-10156] Configure ssl_verify_client per product #1491

Merged
merged 8 commits into from
Oct 17, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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`: Don't request client certificates and don't do client certificate verification.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `off`: Don't request client certificates and don't do client certificate verification.
- `off`: Do not request client certificates or perform client certificate verification.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggesting the removal of the contraction and use of "don't" twice in one line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

- `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. The client certificates can be verified with TLS Client Certificate Validation policy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Enables verification of client certificates. The client certificates can be verified with TLS Client Certificate Validation policy
Enables verification of client certificates. You can verify client certificates with TLS Client Certificate Validation policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Enables verification of client certificates. The client certificates can be verified with TLS Client Certificate Validation policy
Enables verification of client certificates. The client certificates can be verified with TLS Client Certificate Validation policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


### `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, client certificates are requested during the TLS handshake, however, APIcast 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 the service is used by a browser or front-end application, which will cause the browser to always prompt the end user to select a client certificate to send if they have ANY client certificates configured when browsing the service.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By default, client certificates are requested during the TLS handshake, however, APIcast 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 the service is used by a browser or front-end application, which will cause the browser to always prompt the end user to select a client certificate to send if they have ANY client certificates configured when browsing the service.
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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


To work around this, the environment variable `APICAST_HTTPS_VERIFY_CLIENT` can be set to `off` to instruct APIcast to request a client certificate ONLY when the policy is in the chain.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To work around this, the environment variable `APICAST_HTTPS_VERIFY_CLIENT` can be set to `off` to instruct APIcast to request a client certificate ONLY when the policy is in the chain.
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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


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 _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 @@
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()

Check warning on line 59 in gateway/src/apicast/policy/tls_validation/tls_validation.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/policy/tls_validation/tls_validation.lua#L59

Added line #L59 was not covered by tests
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)

Check warning on line 67 in gateway/src/apicast/policy/tls_validation/tls_validation.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/policy/tls_validation/tls_validation.lua#L65-L67

Added lines #L65 - L67 were not covered by tests
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
Loading