258 lines
6.6 KiB
Lua
258 lines
6.6 KiB
Lua
local kong_certificate = require "kong.runloop.certificate"
|
|
local client = require "kong.plugins.acme.client"
|
|
local ngx_ssl = require "ngx.ssl"
|
|
local kong_meta = require "kong.meta"
|
|
|
|
local ipairs = ipairs
|
|
local setmetatable = setmetatable
|
|
|
|
local string_sub = string.sub
|
|
local string_gsub = string.gsub
|
|
local string_find = string.find
|
|
local string_lower = string.lower
|
|
local table_insert = table.insert
|
|
local table_concat = table.concat
|
|
|
|
local ngx_timer_at = ngx.timer.at
|
|
local ngx_re_match = ngx.re.match
|
|
local server_name = ngx_ssl.server_name
|
|
|
|
local acme_challenge_path = [[^/\.well-known/acme-challenge/(.+)]]
|
|
|
|
-- cache for dummy cert kong generated (it's a table)
|
|
local default_cert_key
|
|
|
|
local ACMEHandler = {}
|
|
|
|
-- this has to be higher than auth plugins,
|
|
-- otherwise acme-challenges endpoints may be blocked by auth plugins
|
|
-- causing validation failures
|
|
ACMEHandler.VERSION = kong_meta.version
|
|
ACMEHandler.PRIORITY = 1705
|
|
|
|
local function build_domain_matcher(domains)
|
|
local domains_plain = {}
|
|
local domains_wildcard = {}
|
|
local domains_wildcard_count = 0
|
|
|
|
if domains == nil or domains == ngx.null then
|
|
return false
|
|
end
|
|
|
|
for _, d in ipairs(domains) do
|
|
if string_sub(d, 1, 1) == "*" then
|
|
d = string_gsub(string_sub(d, 2), "%.", "\\.")
|
|
table_insert(domains_wildcard, d)
|
|
domains_wildcard_count = domains_wildcard_count + 1
|
|
else
|
|
domains_plain[d] = true
|
|
end
|
|
end
|
|
|
|
local domains_pattern
|
|
if domains_wildcard_count > 0 then
|
|
domains_pattern = "(" .. table_concat(domains_wildcard, "|") .. ")$"
|
|
end
|
|
|
|
return setmetatable(domains_plain, {
|
|
__index = function(_, k)
|
|
if not domains_pattern then
|
|
return false
|
|
end
|
|
return ngx_re_match(k, domains_pattern, "jo")
|
|
end
|
|
})
|
|
end
|
|
|
|
-- cache the domains_matcher. ACME is a global plugin.
|
|
local domains_matcher
|
|
|
|
-- expose it for use in api.lua
|
|
ACMEHandler.build_domain_matcher = build_domain_matcher
|
|
|
|
|
|
local CONFIG
|
|
|
|
|
|
local function renew(premature)
|
|
if premature or not CONFIG then
|
|
return
|
|
end
|
|
client.renew_certificate(CONFIG)
|
|
end
|
|
|
|
|
|
ACMEHandler.renew = renew
|
|
|
|
|
|
function ACMEHandler:init_worker()
|
|
local worker_id = ngx.worker.id() or -1
|
|
kong.log.info("acme renew timer started on worker ", worker_id)
|
|
ngx.timer.every(86400, renew)
|
|
end
|
|
|
|
|
|
function ACMEHandler:configure(configs)
|
|
CONFIG = configs and configs[1] or nil
|
|
if CONFIG then
|
|
domains_matcher = build_domain_matcher(CONFIG.domains)
|
|
end
|
|
end
|
|
|
|
|
|
local function check_domains(conf, host)
|
|
if not conf.enable_ipv4_common_name and string_find(host, "^(%d+)%.(%d+)%.(%d+)%.(%d+)$") then
|
|
return false
|
|
end
|
|
|
|
if conf.allow_any_domain then
|
|
return true
|
|
end
|
|
|
|
-- create the cache at first usage
|
|
if domains_matcher == nil then
|
|
domains_matcher = build_domain_matcher(conf.domains)
|
|
end
|
|
|
|
return domains_matcher and domains_matcher[host]
|
|
end
|
|
|
|
function ACMEHandler:certificate(conf)
|
|
-- we can't check for Host header in this phase
|
|
local host, err = server_name()
|
|
if err then
|
|
kong.log.warn("failed to read SNI server name: ", err)
|
|
return
|
|
elseif not host then
|
|
kong.log.debug("ignoring because no SNI provided by client")
|
|
return
|
|
end
|
|
|
|
host = string_lower(host)
|
|
|
|
if not check_domains(conf, host) then
|
|
kong.log.debug("ignoring because domain is not in allowed-list: ", host)
|
|
return
|
|
end
|
|
|
|
local cert_and_key, err = kong_certificate.find_certificate(host)
|
|
if err then
|
|
kong.log.err("error find certificate for current request:", err)
|
|
return
|
|
end
|
|
|
|
if not default_cert_key then
|
|
-- hack: find_certificate() returns default cert and key if no sni defined
|
|
default_cert_key = kong_certificate.find_certificate()
|
|
end
|
|
|
|
-- note we compare the table address, this relies on the fact that Kong doesn't
|
|
-- copy the default cert table around
|
|
if cert_and_key ~= default_cert_key then
|
|
kong.log.debug("ignoring because non-default cert is served")
|
|
return
|
|
end
|
|
|
|
local certkey, err = client.load_certkey_cached(conf, host)
|
|
if err then
|
|
kong.log.warn("can't load cert and key from storage: ", err)
|
|
return
|
|
end
|
|
|
|
-- cert not found, get a new one and serve default cert for now
|
|
if not certkey then
|
|
if kong.configuration.role == "data_plane" and conf.storage == "kong" then
|
|
kong.log.err("creating new certificate through proxy side with ",
|
|
"\"kong\" storage in Hybrid mode is not supported; ",
|
|
"consider create certificate using Admin API or ",
|
|
"use other external storages")
|
|
return
|
|
end
|
|
|
|
ngx_timer_at(0, function()
|
|
local ok, err = client.update_certificate(conf, host, nil)
|
|
if err then
|
|
kong.log.err("failed to update certificate for host: ", host, " err:", err)
|
|
return
|
|
end
|
|
-- if not ok and err is nil, meaning the update is running by another worker
|
|
if ok then
|
|
err = client.store_renew_config(conf, host)
|
|
if err then
|
|
kong.log.err("failed to store renew config for host: ", host, " err:", err)
|
|
return
|
|
end
|
|
end
|
|
end)
|
|
return
|
|
end
|
|
|
|
-- this will only be run in dbless
|
|
kong.log.debug("set certificate for host: ", host)
|
|
local _, err
|
|
_, err = ngx_ssl.clear_certs()
|
|
if err then
|
|
kong.log.warn("failed to clear certs: ", err)
|
|
end
|
|
_, err = ngx_ssl.set_der_cert(certkey.cert)
|
|
if err then
|
|
kong.log.warn("failed to set cert: ", err)
|
|
end
|
|
_, err = ngx_ssl.set_der_priv_key(certkey.key)
|
|
if err then
|
|
kong.log.warn("failed to set key: ", err)
|
|
end
|
|
end
|
|
|
|
-- access phase is to terminate the http-01 challenge request if necessary
|
|
function ACMEHandler:access(conf)
|
|
|
|
local protocol = kong.client.get_protocol()
|
|
|
|
-- http-01 challenge only sends to http port
|
|
if protocol == 'http' then
|
|
local host = kong.request.get_host()
|
|
if not host then
|
|
return
|
|
end
|
|
|
|
if not check_domains(conf, host) then
|
|
-- We do not log here because it would flood the log
|
|
return
|
|
end
|
|
|
|
local captures, err =
|
|
ngx_re_match(kong.request.get_path(), acme_challenge_path, "jo")
|
|
if err then
|
|
kong.log(kong.WARN, "error matching acme-challenge uri: ", err)
|
|
return
|
|
end
|
|
|
|
if captures then
|
|
-- if this is just a sanity test, we always return 404 status
|
|
if captures[1] == "x" then
|
|
return kong.response.exit(404, "Not found\n")
|
|
end
|
|
|
|
-- TODO: race condition creating account?
|
|
local err = client.create_account(conf)
|
|
if err then
|
|
kong.log.err("failed to create account:", err)
|
|
return
|
|
end
|
|
|
|
local acme_client, err = client.new(conf)
|
|
if err then
|
|
kong.log.err("failed to create acme client:", err)
|
|
return
|
|
end
|
|
|
|
acme_client:serve_http_challenge()
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
|
|
return ACMEHandler
|