Skip to content
This repository has been archived by the owner on Apr 27, 2021. It is now read-only.

Add lua endpoint to handle certificates in dynamic configuration mode #56

Closed
wants to merge 10 commits into from

Conversation

hnrytrn
Copy link

@hnrytrn hnrytrn commented Jun 4, 2018

What this PR does / why we need it:

This PR is a part of adding dynamic certificate serving functionality (https://github.com/Shopify/edgescale/issues/515). This adds a Lua endpoint that stores certificates and keys of a host in a shared dictionary and allows Lua directives to retrieve them.

@Shopify/edgescale

@hnrytrn hnrytrn requested review from andrewloux and ElvinEfendi June 4, 2018 18:38
@hnrytrn hnrytrn force-pushed the master branch 3 times, most recently from 5618b93 to cc61952 Compare June 5, 2018 13:58
@@ -196,6 +196,8 @@ func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool,
"lua_shared_dict balancer_ewma 1M",
"lua_shared_dict balancer_ewma_last_touched_at 1M",
"lua_shared_dict sticky_sessions 1M",
"lua_shared_dict cert_data 10M",
"lua_shared_dict key_data 10M",

Choose a reason for hiding this comment

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

why different dictionaries?

Choose a reason for hiding this comment

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

Why not use configuration_data for keeping certificates? My initial plan was to keep every config piece in there. Any reason to not use single dictionary for all?

Copy link
Author

Choose a reason for hiding this comment

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

Sure, I just thought it was cleaner to have different dictionaries, but I can update it to just use the configuration_data dictionary

end
local success, err = configuration_data:set(cert.HostName .. cert_ext, cert.FullChainCert)
if not success then
ngx.log(ngx.ERR, "certificate dynamic-configuration: error setting certificate: " .. tostring(err))

Choose a reason for hiding this comment

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

Could we dump cert.Hostname here as well? Or is that composite in the err that would be thrown here?

@@ -1,6 +1,11 @@
local json = require("cjson")

-- this is the Lua representation of Configuration struct in internal/ingress/types.go
local configuration_data = ngx.shared.configuration_data

Choose a reason for hiding this comment

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

We're attempting to make configuration_data's structure mirror this: https://github.com/kubernetes/ingress-nginx/blob/master/internal/ingress/types.go#L52

I feel like that breaks in this PR. Maybe we want a servers key within configuration_data?

On the same vein; in addition, in your corresponding controller PR - we already have an ingress.Server type, which we could use instead of your new Certificate type in your configureCerts method. What do you think?

E.g.

servers := make([]*ingress.Server, len(pcfg.Servers))

for i, server := range pcfg.Servers {
  luaServer := &ingress.Server{
      ...
   }

Copy link
Author

Choose a reason for hiding this comment

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

Good point, I agree. I will update the PRs.

Copy link
Author

Choose a reason for hiding this comment

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

I talked to Elvin who wrote this comment and he said that this didn't have to mirror the Configuration struct in internal/ingress/types.go perfectly. I reverted the commit that used servers as a key, so we don't have to JSON decode the servers data and iterate through each server for every request.

@@ -58,7 +110,7 @@ function _M.call()
ngx.log(ngx.ERR, "dynamic-configuration: error updating configuration: " .. tostring(err))
ngx.status = ngx.HTTP_BAD_REQUEST
return
end
end

Choose a reason for hiding this comment

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

redundant whitespace in the end of line

function _M.call()
if ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "GET" then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.print("Only POST and GET requests are allowed!")
return
end

if ngx.var.request_uri == "/configuration/servers" then
handle_server_request();
return;

Choose a reason for hiding this comment

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

no need for ;

return
end

-- Handler for the /configuration/servers endpoint

Choose a reason for hiding this comment

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

do you think it is hard for someone to figure out that this function is handler for that requests?

@@ -27,13 +29,63 @@ local function fetch_request_body()
return body
end

-- Returns the certificate for a given host
function _M.get_cert(host_name)

Choose a reason for hiding this comment

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

the convention is to not separate host and name, use hostname instead

@hnrytrn hnrytrn force-pushed the master branch 2 times, most recently from 39fb157 to 07d6fc2 Compare June 12, 2018 15:33
@hnrytrn hnrytrn force-pushed the master branch 3 times, most recently from abda1c4 to c21fcb4 Compare June 27, 2018 18:24
if not success then
ngx.log(ngx.ERR, "certificate dynamic-configuration: error setting certificate: "
.. tostring(err), cert.hostname)
ngx.status = ngx.HTTP_BAD_REQUEST

Choose a reason for hiding this comment

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

what is this a bad request? should it not be internal server error (status 500)?

ngx.log(ngx.ERR, "certificate dynamic-configuration: error setting certificate: "
.. tostring(err), cert.hostname)
ngx.status = ngx.HTTP_BAD_REQUEST
return

Choose a reason for hiding this comment

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

You are short circuiting when we fail to store one cert and never try the rest. I suggest we go through the whole list and try to store all of them and collect the errors. In the end check if there was any error and if there was any then return internal server error and log

end

ngx.status = ngx.HTTP_CREATED
return

Choose a reason for hiding this comment

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

is this necessary?


-- Update certificates and private keys for each host
for _, cert in pairs(certs) do
if cert.hostname and cert.sslCert.pemCertKey then
Copy link

@ElvinEfendi ElvinEfendi Jul 27, 2018

Choose a reason for hiding this comment

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

let's also log a warning when this does not hold, this will help with potential debugging in future

return
end

local cert_str = fetch_request_body()

Choose a reason for hiding this comment

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

what about raw_certs?

local ok, certs = pcall(json.decode, raw_certs)
if not ok then
ngx.log(ngx.ERR, "could not parse certificate: " .. tostring(certs))
return

Choose a reason for hiding this comment

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

we need to set status here

return
end

if not certs then

Choose a reason for hiding this comment

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

this is not needed


local err_buf = {}
-- Update certificates and private keys for each host
for _, cert in pairs(certs) do

Choose a reason for hiding this comment

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

for an array it's recommended to use ipairs.

pairs for when it is a dictionary

return configuration_data:get(hostname)
end

local function handle_cert_request()
Copy link

@ElvinEfendi ElvinEfendi Jul 31, 2018

Choose a reason for hiding this comment

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

since this is handling requests to /configuration/servers, let's call it handle_servers, and rename the variables accordingly

"error setting certificate for %s: %s\n", cert.hostname, tostring(err))
end
else
ngx.log(ngx.WARN, "certificate dynamic-configuration: hostname and pemCertKey are not present")

Choose a reason for hiding this comment

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

no need for log prefixes.

--

hostname OR pemCertKey

-- Update certificates and private keys for each host
for _, cert in pairs(certs) do
if cert.hostname and cert.sslCert.pemCertKey then
local success, err = configuration_data:set(cert.hostname, cert.sslCert.pemCertKey)

Choose a reason for hiding this comment

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

let's introduce a new certificates_data dict to store certs.

if cert.hostname and cert.sslCert.pemCertKey then
local success, err = configuration_data:set(cert.hostname, cert.sslCert.pemCertKey)
if not success then
err_buf[#err_buf + 1] = string.format("certificate dynamic-configuration: " ..

Choose a reason for hiding this comment

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

Let's treat the array as array. You can use table.insert() function to add a new element to err_buf array.

@@ -27,13 +29,67 @@ local function fetch_request_body()
return body
end

-- Returns the certificate and key for a given host

Choose a reason for hiding this comment

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

does not add much value given the function name

@@ -27,13 +29,67 @@ local function fetch_request_body()
return body
end

-- Returns the certificate and key for a given host
function _M.get_cert_key(hostname)

Choose a reason for hiding this comment

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

get_pem_cert_key to be consistent with controller and https://github.com/Shopify/ingress/pull/60/files

return configuration_data:get(hostname)
end

local function handle_cert_request()

Choose a reason for hiding this comment

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

let's export this when TEST variable is set and write unit test for it

@hnrytrn hnrytrn force-pushed the master branch 2 times, most recently from 84f121b to f9aae61 Compare August 1, 2018 15:17
-- Update certificates and private keys for each host
for _, server in ipairs(servers) do
if server.hostname and server.sslCert.pemCertKey then
local success, err = certificate_data:set(server.hostname, server.sslCert.pemCertKey)

Choose a reason for hiding this comment

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

When the dictionary is full Nginx will remove least used item to spare some space for the new one and try to fit the new item. In that case success will still be true. But IMO this is unacceptable for what we are trying to do. That would mean we delete the certificate of app1 so that we can make app2 work.

I think we should use safe_set (https://github.com/openresty/lua-nginx-module#ngxshareddictsafe_set) and when the error message is "no memory" immediately short-circuit from the loop and return 502, Internal Server Error and log the message.

-- this is the Lua representation of Configuration struct in internal/ingress/types.go
local configuration_data = ngx.shared.configuration_data
local certificate_data = ngx.shared.certificate_data

Choose a reason for hiding this comment

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

you need to declare this dictionary in nginx.tmpl. Also what size do you think of configuring for it? Given you configure it to be 60MB, how many certificates can we fit?

Copy link
Author

Choose a reason for hiding this comment

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

Assuming the certificate is generated with a 2048 bit RSA key/cert pair, the PEM encoded certificate and private key would ~3.2kB. So if we have a dictionary of 60mB we would be able to store 18 750 certificates. With that, I think setting it to 16MB would be good enough (5000 certificates). What do you think?

Choose a reason for hiding this comment

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

Later we should add this info into docs.

HTTP_CREATED = 201,
HTTP_INTERNAL_SERVER_ERROR = 500,
ERR = nil,
req = {

Choose a reason for hiding this comment

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

would not just mocking ngx.req be enough?

_G.ngx.req = {
  ...
}

Also please make sure you unmock it after your test finishes otherwise you're leaking a state.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, will unmock after.

Unless I'm missing something just mocking ngx.req wouldn't be enough. The tests require access to ngx.status, which wouldn't be accessible unless ngx was mocked. By mocking ngx I had to add the other mocked functions and variables that are used by configuration.lua.

@ElvinEfendi
Copy link

The current tests cases look good! Once #56 (comment) is addressed we will need a test for that too.

ngx.req.get_body_data = function() return mock_servers end

assert.has_no.errors(configuration.handle_servers)
assert.same(ngx.status, ngx.HTTP_CREATED)

Choose a reason for hiding this comment

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

you should also assert that the data is stored in the shared dictionary - that's the most important outcome of calling handle_servers

sslCert = {
pemCertKey = "pemCertKey"
}
}

Choose a reason for hiding this comment

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

can you add more than one host to make sure the error collection logic works?

pemCertKey = "pemCertKey"
}
}
})

Choose a reason for hiding this comment

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

can you have more than one server and return "no memory" on only i.e second of them and assert that the loop gets broken and function does not try to store the next one

@hnrytrn
Copy link
Author

hnrytrn commented Aug 2, 2018

Moved PR upstream kubernetes#2889

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants