Skip to content

Commit

Permalink
Policy: MTLS with trusted store and verify
Browse files Browse the repository at this point in the history
When using upstream_mtls policy, the policy only sends the client certificates
but does not verify the connection. Therefore, the policy name is not honouring
what it did.

A new x509_store will be created with this change, and it'll be used to verify
the SSL_context. This change will use some FFI functions that are part of
apicast-nginx-module[0]

Fix THREESCALE-7099
[0] https://github.com/3scale/apicast-nginx-module

Signed-off-by: Eloy Coto <[email protected]>
  • Loading branch information
eloycoto committed Jun 16, 2021
1 parent 3b25209 commit 6e9845a
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 3 deletions.
13 changes: 13 additions & 0 deletions gateway/src/apicast/policy/upstream_mtls/apicast-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
"embedded"
],
"default": "path"
},
"ca_certificates": {
"type": "array",
"title": "CA certificates",
"items": {
"type": "string",
"title": "PEM formatted certificate",
"description": "Certificate including the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----"
}
},
"verify": {
"type": "boolean",
"description": "Verify upstream connection"
}
},
"dependencies": {
Expand Down
64 changes: 64 additions & 0 deletions gateway/src/apicast/policy/upstream_mtls/upstream_mtls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ local data_url = require('resty.data_url')
local C = ffi.C
local get_request = base.get_request
local open = io.open
local pairs = pairs

local X509_STORE = require('resty.openssl.x509.store')
local X509 = require('resty.openssl.x509')

ffi.cdef([[
int ngx_http_apicast_ffi_set_proxy_cert_key(
ngx_http_request_t *r, void *cdata_chain, void *cdata_key);
int ngx_http_apicast_ffi_set_proxy_ca_cert(
ngx_http_request_t *r, void *cdata_ca);
int ngx_http_apicast_ffi_set_ssl_verify(
ngx_http_request_t *r, int verify, int verify_deph);
]])


Expand Down Expand Up @@ -82,6 +89,27 @@ local function read_certificate_key(value, value_type)

end

local function read_ca_certificates(ca_certificates)
local valid = false
local store = X509_STORE.new()
for _,certificate in pairs(ca_certificates) do
local cert, err = X509.parse_pem_cert(certificate)
if cert then
valid = true
store:add_cert(cert)
else
ngx.log(ngx.INFO, "cannot load certificate, err: ", err)
end
end

if valid then
return store.store
end

store = nil
return
end

function _M.new(config)
local self = new(config)
if config == nil then
Expand All @@ -94,6 +122,9 @@ function _M.new(config)
self.cert_key = read_certificate_key(
config.certificate_key,
config.certificate_key_type or path_type)
self.ca_store = read_ca_certificates(config.ca_certificates or {})
self.verify = config.verify

return self
end

Expand All @@ -114,10 +145,43 @@ function _M.set_certs(cert, key)
end
end

function _M.set_ca_cert(r, store)
local val = C.ngx_http_apicast_ffi_set_proxy_ca_cert(r, store)
if val == ngx.OK then
ngx.log(ngx.WARN, "Cannot set a valid trusted CA store")
return
end
end

-- All of this happens on balancer because this is subrequest inside APICAst
--to @upstream, so the request need to be the one that connects to the
--upstream0
function _M:balancer(context)
if self.cert and self.cert_key then
self.set_certs(self.cert, self.cert_key)
end

if not self.verify then
return
end

local r = get_request()
if not r then
ngx.log(ngx.WARN, "Invalid request")
return
end

if not self.ca_store then
ngx.log(ngx.WARN, "Set verify without including CA certificates")
return
end

self.set_ca_cert(r, self.ca_store)

local val = C.ngx_http_apicast_ffi_set_ssl_verify(r, ffi.new("int", 1), ffi.new("int", 1))
if val ~= ngx.OK then
ngx.log(ngx.WARN, "Cannot verify SSL upstream connection")
end
end

return _M
87 changes: 87 additions & 0 deletions spec/policy/upstream_mtls/upstream_mtls_spec.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
local upstream_mtls = require("apicast.policy.upstream_mtls")
local ssl = require('ngx.ssl')
local open = io.open

local function read_file(path)
local file = open(path, "rb")
if not file then return nil end
local content = file:read "*a" -- *a or *all reads the whole file
file:close()
return content
end

describe('Upstream MTLS policy', function()

local certificate_path = 't/fixtures/CA/root-ca.crt'
local certificate_key_path = 't/fixtures/CA/root-ca.key'

local certificate_content = read_file(certificate_path)
-- Set here the const to not use the pakcage ones, if not test will not fail
-- if changes0
local path_type = "path"
Expand Down Expand Up @@ -119,5 +129,82 @@ describe('Upstream MTLS policy', function()

end)

describe("CA_Certificates", function()


before_each(function()
stub.new(upstream_mtls, 'set_ca_cert', function() return true end)
end)

it("ca_store is nil if no certificates", function()
local config = {
certificate = "XXXX",
certificate_type = embedded_type,
certificate_key = "XXXX",
certificate_key_type = embedded_type,
ca_certificates = {},
}
local object = upstream_mtls.new(config)
assert.same(object.ca_store, nil)
end)

it("ca_store is nil if no valid certificates", function()
local config = {
certificate = "XXXX",
certificate_type = embedded_type,
certificate_key = "XXXX",
certificate_key_type = embedded_type,
ca_certificates = {"XXXX"},
}
local object = upstream_mtls.new(config)
assert.same(object.ca_store, nil)
end)

it("ca_store is cdata if valid ca certificate", function()
local config = {
certificate = "XXXX",
certificate_type = embedded_type,
certificate_key = "XXXX",
certificate_key_type = embedded_type,
ca_certificates = { certificate_content},
}
local object = upstream_mtls.new(config)
assert.same(type(object.ca_store), "cdata")
end)

it("CA certificate is not used if verify is not enabled", function()
local config = {
certificate = "XXXX",
certificate_type = embedded_type,
certificate_key = "XXXX",
certificate_key_type = embedded_type,
ca_certificates = { certificate_content},
verify = false
}
local object = upstream_mtls.new(config)
assert.same(type(object.ca_store), "cdata")

spy.on(object, "set_ca_cert")
object:balancer({})
assert.spy(object.set_ca_cert).was_not.called()
end)

it("CA certificate is used if verify is enabled", function()
local config = {
certificate = "XXXX",
certificate_type = embedded_type,
certificate_key = "XXXX",
certificate_key_type = embedded_type,
ca_certificates = { certificate_content},
verify = true
}
local object = upstream_mtls.new(config)
assert.same(type(object.ca_store), "cdata")

spy.on(object, "set_ca_cert")
object:balancer({})
assert.spy(object.set_ca_cert).was.called()
end)
end)
end)

Loading

0 comments on commit 6e9845a

Please sign in to comment.