Skip to content

Commit

Permalink
Policy: MTLS upstream policy
Browse files Browse the repository at this point in the history
By default, at the moment if a user wants to use MTLS with the upstream
API, the only way is using `APICAST_PROXY_SSL_CERTIFICATE`, and this
certificate will be used in all services.

With this policy, that includes an Nginx patch, and a new Nginx-module,
different client certificates can be used by service, so one APICast
instance can host more than MTLS connections to different upstreams.

Signed-off-by: Eloy Coto <[email protected]>
  • Loading branch information
eloycoto committed Mar 21, 2020
1 parent 35046b5 commit 2f6b062
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ executors:
environment:
S2I_VERSION: "1.1.12-2a783420"
DOCKER_COMPOSE_VERSION: "1.16.1"
OPENRESTY_VERSION: "1.17.4.1-0-centos8"
OPENRESTY_VERSION: "1.17.5.1-0-centos8"

openresty:
working_directory: /opt/app-root/apicast
Expand Down
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/mtls/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require("mtls")
107 changes: 107 additions & 0 deletions gateway/src/apicast/policy/mtls/mtls.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
-- This policy enables MTLS with the upstream API endpoint

local ssl = require('ngx.ssl')
local open = io.open
local ffi = require "ffi"
local C = ffi.C
local base = require "resty.core.base"
local get_request = base.get_request


ffi.cdef([[
int ngx_http_apicast_ffi_set_proxy_cert_key(
ngx_http_request_t *r, void *cdata_chain, void *cdata_key);
]])


local policy = require('apicast.policy')
local _M = policy.new('mtls', "builtin")

local path_type = "path"

local new = _M.new

local function set_certs(cert, key)
local r = get_request()
if not r then
ngx.log(ngx.ERR, "No valid request")
return
end
-- @TODO fix here the integer result
local val = C.ngx_http_apicast_ffi_set_proxy_cert_key(r, cert, key)
if val ~= ngx.OK then
ngx.log(ngx.ERR, "Certificate cannot be set correctly")
end
end

local function read_file(path)
ngx.log(ngx.DEBUG, "reading path:", path)

local file = open(path, "rb")
if not file then
ngx.log(ngx.ERR, "Cannot read path: ", path)
return nil
end

local content = file:read("*a")
file:close()
return content
end

local function get_cert(value, value_type)
local data = value
if value_type == path_type then
data = read_file(value)
end
return data
end

local function read_certificate(value, value_type)
local data = get_cert(value, value_type)
if data == nil then
ngx.log(ngx.ERR, "Certificate value is invalid")
return
end

return ssl.parse_pem_cert(data)
end

local function read_certificate_key(value, value_type)

local data = get_cert(value, value_type)

if data == nil then
ngx.log(ngx.ERR, "Certificate value is invalid")
return
end

if data == nil then
ngx.log(ngx.ERR, "Certificate key value is invalid")
return
end

return ssl.parse_pem_priv_key(data)

end

function _M.new(config)
local self = new(config)
if config == nil then
config = {}
end

self.cert = read_certificate(
config.certificate,
config.certificate_type or path_type)
self.cert_key = read_certificate_key(
config.certificate_key,
config.certificate_key_type or path_type)
return self
end


function _M:balancer(context)
set_certs(self.cert, self.cert_key)
end

return _M
153 changes: 153 additions & 0 deletions t/apicast-policy-mtls.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use lib 't';
use Test::APIcast::Blackbox 'no_plan';

repeat_each(1);
run_tests();


__DATA__
=== TEST 1: MTLS policy with correct certificate
--- init eval
$Test::Nginx::Util::ENDPOINT_SSL_PORT = Test::APIcast::get_random_port();
--- user_files fixture=mutual_ssl.pl eval
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0))
}
}
--- configuration eval
<<EOF
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "https://test:$Test::Nginx::Util::ENDPOINT_SSL_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{ "name": "apicast.policy.apicast" },
{
"name": "apicast.policy.mtls",
"configuration": {
"certificate": "$ENV{TEST_NGINX_SERVER_ROOT}/html/client.crt",
"certificate_type": "path",
"certificate_key": "$ENV{TEST_NGINX_SERVER_ROOT}/html/client.key",
"certificate_key_type": "path"
}
}
]
}
}
]
}
EOF
--- upstream eval
<<EOF
listen $Test::Nginx::Util::ENDPOINT_SSL_PORT ssl;
ssl_certificate $ENV{TEST_NGINX_SERVER_ROOT}/html/server.crt;
ssl_certificate_key $ENV{TEST_NGINX_SERVER_ROOT}/html/server.key;
ssl_client_certificate $ENV{TEST_NGINX_SERVER_ROOT}/html/client.crt;
ssl_verify_client on;
location / {
echo 'ssl_client_s_dn: \$ssl_client_s_dn';
echo 'ssl_client_i_dn: \$ssl_client_i_dn';
}
EOF
--- request
GET /?user_key=value
--- response_body
ssl_client_s_dn: CN=localhost,OU=APIcast,O=3scale
ssl_client_i_dn: CN=localhost,OU=APIcast,O=3scale
--- error_code: 200
--- no_error_log
[error]
=== TEST 2: MTLS policy takes precedence over env variables
In this test we set the env variables to an invalid keys, to make sure that the
correct ones are used.
--- init eval
$Test::Nginx::Util::ENDPOINT_SSL_PORT = Test::APIcast::get_random_port();
--- user_files fixture=mutual_ssl.pl eval
--- env random_port eval
(
'APICAST_PROXY_HTTPS_CERTIFICATE' => "$Test::Nginx::Util::ServRoot/html/server.crt",
'APICAST_PROXY_HTTPS_CERTIFICATE_KEY' => "$Test::Nginx::Util::ServRoot/html/server.key",
)
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0))
}
}
--- configuration eval
<<EOF
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "https://test:$Test::Nginx::Util::ENDPOINT_SSL_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{ "name": "apicast.policy.apicast" },
{
"name": "apicast.policy.mtls",
"configuration": {
"certificate": "$ENV{TEST_NGINX_SERVER_ROOT}/html/client.crt",
"certificate_type": "path",
"certificate_key": "$ENV{TEST_NGINX_SERVER_ROOT}/html/client.key",
"certificate_key_type": "path"
}
}
]
}
}
]
}
EOF
--- upstream eval
<<EOF
listen $Test::Nginx::Util::ENDPOINT_SSL_PORT ssl;
ssl_certificate $ENV{TEST_NGINX_SERVER_ROOT}/html/server.crt;
ssl_certificate_key $ENV{TEST_NGINX_SERVER_ROOT}/html/server.key;
ssl_client_certificate $ENV{TEST_NGINX_SERVER_ROOT}/html/client.crt;
ssl_verify_client on;
location / {
echo 'ssl_client_s_dn: \$ssl_client_s_dn';
echo 'ssl_client_i_dn: \$ssl_client_i_dn';
}
EOF
--- request
GET /?user_key=value
--- response_body
ssl_client_s_dn: CN=localhost,OU=APIcast,O=3scale
ssl_client_i_dn: CN=localhost,OU=APIcast,O=3scale
--- error_code: 200
--- no_error_log
[error]
# @TODO
# 2 Services, one with MTLS another one with normal TLS that is not affected. (To validate CTX in http request

0 comments on commit 2f6b062

Please sign in to comment.