continue work on ssl/tls fallback and management

This commit is contained in:
florian 2023-12-16 16:24:44 +01:00
parent 8efcd2b8a1
commit 62449f84c0
No known key found for this signature in database
GPG key ID: 93EE47CC3D061500
9 changed files with 373 additions and 315 deletions

View file

@ -13,7 +13,7 @@ function plugin:initialize(id, ctx)
local current_phase = ngx.get_phase()
for _, check_phase in ipairs {
"set",
"ssl_certificate"
"ssl_certificate",
"access",
"content",
"header_filter",

View file

@ -680,6 +680,7 @@ utils.get_phases = function()
"init_worker",
"set",
"access",
"ssl_certificate",
"header",
"log",
"preread",
@ -692,6 +693,7 @@ utils.is_cosocket_available = function()
local phases = {
"timer",
"access",
"ssl_certificate",
"preread",
}
local current_phase = ngx.get_phase()

View file

@ -23,7 +23,9 @@ server {
# include LUA files
include {{ NGINX_PREFIX }}set-lua.conf;
include {{ NGINX_PREFIX }}ssl-certificate-lua.conf;
include {{ NGINX_PREFIX }}access-lua.conf;
include {{ NGINX_PREFIX }}header-lua.conf;
include {{ NGINX_PREFIX }}log-lua.conf;
# include config files

View file

@ -6,6 +6,7 @@ ssl_certificate_by_lua_block {
local cdatastore = require "bunkerweb.datastore"
local cclusterstore = require "bunkerweb.clusterstore"
local cjson = require "cjson"
local ssl = require "ngx.ssl"
-- Don't process internal requests
local logger = clogger:new("SSL-CERTIFICATE")
@ -39,8 +40,6 @@ ssl_certificate_by_lua_block {
-- Call ssl_certificate() methods
logger:log(ngx.INFO, "calling ssl_certificate() methods of plugins ...")
local status = nil
local redirect = nil
for i, plugin_id in ipairs(order.ssl_certificate) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
@ -63,32 +62,33 @@ ssl_certificate_by_lua_block {
logger:log(ngx.ERR, plugin_id .. ":ssl_certificate() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":ssl_certificate() call successful : " .. ret.msg)
if ret.cert then
if ret.status then
logger:log(ngx.INFO, plugin_id .. " is setting certificate/key : " .. ret.msg)
local ok, err = ssl.set_cert(ret.status[1])
if not ok then
logger:log(ngx.ERR, "error while setting certificate : " .. err)
else
local ok, err = ssl.set_priv_key(ret.status[2])
if not ok then
logger:log(ngx.ERR, "error while setting private key : " .. err)
else
return true
end
end
end
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method access() is not defined")
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method ssl_certificate() is not defined")
end
end
end
logger:log(ngx.INFO, "called access() methods of plugins")
logger:log(ngx.INFO, "called ssl_certificate() methods of plugins")
-- Save ctx
ngx.ctx = ctx
logger:log(ngx.INFO, "access phase ended")
-- Return status if needed
if status then
return ngx.exit(status)
end
-- Redirect if needed
if redirect then
return ngx.redirect(redirect)
end
logger:log(ngx.INFO, "ssl_certificate phase ended")
return true
}

View file

@ -0,0 +1,132 @@
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local ssl = require "ngx.ssl"
local customcert = class("customcert", plugin)
function customcert:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "customcert", ctx)
end
function customcert:init()
local ok, err = true, "success"
if utils.has_variable("USE_CUSTOM_SSL", "yes") then
local multisite, err = utils.get_variable("MULTISITE")
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars, err = utils.get_multiple_variables({"USE_CUSTOM_SSL"})
if not vars then
return self:ret(false, "can't get USE_CUSTOM_SSL variables : " .. err)
end
if vars["global"]["USE_CUSTOM_SSL"] == "yes" then
local check, data = self:read_files()
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
for server_name, multisite_vars in pairs(vars) do
if multisite_vars["USE_CUSTOM_SSL"] == "yes" then
local check, data = self:read_files(server_name)
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
end
else
local check, data = self:read_files()
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
else
err = "custom ssl is not used"
end
return self:ret(ok, err)
end
function customcert:ssl_certificate()
if self.variables["USE_CUSTOM_SSL"] == "yes" then
local global_data, err = self.datastore:get("plugin_customcert_global", true)
if not global_data and err ~= "not found" then
return self:ret(false, "error while getting plugin_customcert_global from datastore : " .. err)
end
local site_data, err = self.datastore:get("plugin_customcert_" .. self.ctx.bw.server_name, true)
if not site_data and err ~= "not found" then
return self:ret(false, "error while getting plugin_customcert_" .. self.ctx.bw.server_name .. " from datastore : " .. err)
end
if not global_data and not site_data then
return self:ret(false, "both global and site cert are not present in datastore")
end
return self:ret(true, "certificate/key data found", site_data or global_data)
end
return self:ret(true, "custom certificate is not used")
end
function customcert:read_files(server_name)
local files = {
"/var/cache/bunkerweb/customcert/" .. (server_name or "") .. "/cert.pem",
"/var/cache/bunkerweb/customcert/" .. (server_name or "") .. "/key.pem"
}
local data = {}
for i, file in ipairs(files) do
local f, err = io.open(file, "r")
if not f then
return false, file .. " = " .. err
end
table.insert(data, f:read("*a"))
f:close()
end
return true, data
end
function customcert:load_data(data, server_name)
-- Load certificate
local cert_chain, err = ssl.parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key, err = ssl.parse_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end
local cache_key = "plugin_customcert_" .. (server_name or "global")
local ok, err = self.datastore:set(cache_key, {cert_chain, priv_key}, nil, true)
if not ok then
return false, "error while setting data into datastore : " .. err
end
return true
end
return customcert

View file

@ -1,297 +0,0 @@
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local customcert = class("customcert", plugin)
function customcert:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "customcert", ctx)
end
function customcert:init_worker()
if utils.has_variable("USE_CUSTOM_SSL", "yes") then
end
end
function customcert:ssl_certificate()
if self.variables["USE_CUSTOM_SSL"] == "yes" then
return true, ""
end
return true, "customcert is not used"
end
return customcert
function bunkernet:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "bunkernet", ctx)
-- Get BunkerNet ID and save info
if ngx.get_phase() ~= "init" and self:is_needed() then
local id, err = self.datastore:get("plugin_bunkernet_id", true)
if id then
self.bunkernet_id = id
self.version = (self.ctx and self.ctx.bw.version) or utils.get_version()
self.integration = (self.ctx and self.ctx.bw.integration) or utils.get_integration()
else
self.logger:log(ngx.ERR, "can't get BunkerNet ID from datastore : " .. err)
end
end
end
function bunkernet:is_needed()
-- Loading case
if self.is_loading then
return false
end
-- Request phases (no default)
if self.is_request and (self.ctx.bw.server_name ~= "_") then
return self.variables["USE_BUNKERNET"] == "yes"
end
-- Other cases : at least one service uses it
local is_needed, err = utils.has_variable("USE_BUNKERNET", "yes")
if is_needed == nil then
self.logger:log(ngx.ERR, "can't check USE_BUNKERNET variable : " .. err)
end
return is_needed
end
function bunkernet:init_worker()
-- Check if needed
if not self:is_needed() then
return self:ret(true, "no service uses BunkerNet, skipping init_worker")
end
-- Check id
if not self.bunkernet_id then
return self:ret(false, "missing instance ID")
end
-- Send ping request
local ok, err, status, _ = self:ping()
if not ok then
return self:ret(false, "error while sending request to API : " .. err)
end
if status ~= 200 then
return self:ret(
false,
"received status " .. tostring(status) .. " from API using instance ID " .. self.bunkernet_id
)
end
self.logger:log(ngx.NOTICE, "connectivity with API using instance ID " .. self.bunkernet_id .. " is successful")
return self:ret(true, "connectivity with API using instance ID " .. self.bunkernet_id .. " is successful")
end
function bunkernet:init()
-- Check if needed
if not self:is_needed() then
return self:ret(true, "no service uses BunkerNet, skipping init")
end
-- Check if instance ID is present
local f, err = io.open("/var/cache/bunkerweb/bunkernet/instance.id", "r")
if not f then
return self:ret(false, "can't read instance id : " .. err)
end
-- Retrieve instance ID
local id = f:read("*all"):gsub("[\r\n]", "")
f:close()
-- Store ID in datastore
local ok, err = self.datastore:set("plugin_bunkernet_id", id, nil, true)
if not ok then
return self:ret(false, "can't save instance ID to the datastore : " .. err)
end
-- Load databases
local ret = true
local i = 0
local db = {
ip = {},
}
local f, err = io.open("/var/cache/bunkerweb/bunkernet/ip.list", "r")
if not f then
ret = false
else
for line in f:lines() do
if (utils.is_ipv4(line) or utils.is_ipv6(line)) and utils.ip_is_global(line) then
table.insert(db.ip, line)
i = i + 1
end
end
end
if not ret then
return self:ret(false, "error while reading database : " .. err)
end
f:close()
local ok, err = self.datastore:set("plugin_bunkernet_db", db, nil, true)
if not ok then
return self:ret(false, "can't store bunkernet database into datastore : " .. err)
end
return self:ret(true, "successfully loaded " .. tostring(i) .. " bad IPs using instance ID " .. id)
end
function bunkernet:access()
-- Check if needed
if not self:is_needed() then
return self:ret(true, "service doesn't use BunkerNet, skipping access")
end
-- Check id
if not self.bunkernet_id then
return self:ret(false, "missing instance ID")
end
-- Check if IP is global
if not self.ctx.bw.ip_is_global then
return self:ret(true, "IP is not global")
end
-- Check if whitelisted
if self.ctx.bw.is_whitelisted == "yes" then
return self:ret(true, "client is whitelisted")
end
-- Extract DB
local db, err = self.datastore:get("plugin_bunkernet_db", true)
if db then
-- Check if is IP is present
if #db.ip > 0 then
-- luacheck: ignore 421
local present, err = utils.is_ip_in_networks(self.ctx.bw.remote_addr, db.ip)
if present == nil then
return self:ret(false, "can't check if ip is in db : " .. err)
end
if present then
return self:ret(true, "ip is in db", utils.get_deny_status(self.ctx))
end
end
else
return self:ret(false, "can't get bunkernet db " .. err)
end
return self:ret(true, "not in db")
end
function bunkernet:log(bypass_checks)
if not bypass_checks then
-- Check if needed
if not self:is_needed() then
return self:ret(true, "service doesn't use BunkerNet, skipping log")
end
-- Check id
if not self.bunkernet_id then
return self:ret(false, "missing instance ID")
end
end
-- Check if IP has been blocked
local reason = utils.get_reason(self.ctx)
if not reason then
return self:ret(true, "ip is not blocked")
end
if reason == "bunkernet" then
return self:ret(true, "skipping report because the reason is bunkernet")
end
-- Check if IP is global
if not self.ctx.bw.ip_is_global then
return self:ret(true, "IP is not global")
end
-- TODO : check if IP has been reported recently
-- luacheck: ignore 212 431
local function report_callback(premature, obj, ip, reason, method, url, headers)
local ok, err, status, _ = obj:report(ip, reason, method, url, headers)
if status == 429 then
obj.logger:log(ngx.WARN, "bunkernet API is rate limiting us")
elseif not ok then
obj.logger:log(ngx.ERR, "can't report IP : " .. err)
else
obj.logger:log(ngx.NOTICE, "successfully reported IP " .. ip .. " (reason : " .. reason .. ")")
end
end
local hdr, err = ngx.timer.at(
0,
report_callback,
self,
self.ctx.bw.remote_addr,
reason,
self.ctx.bw.request_method,
self.ctx.bw.request_uri,
ngx.req.get_headers()
)
if not hdr then
return self:ret(false, "can't create report timer : " .. err)
end
return self:ret(true, "created report timer")
end
function bunkernet:log_default()
-- Check if needed
if not self:is_needed() then
return self:ret(true, "no service uses BunkerNet, skipping log_default")
end
-- Check id
if not self.bunkernet_id then
return self:ret(false, "missing instance ID")
end
-- Check if default server is disabled
local check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false)
if check == nil then
return self:ret(false, "error while getting variable DISABLE_DEFAULT_SERVER : " .. err)
end
if check ~= "yes" then
return self:ret(true, "default server is not disabled")
end
-- Call log method
return self:log(true)
end
function bunkernet:log_stream()
return self:log()
end
function bunkernet:request(method, url, data)
local httpc, err = http.new()
if not httpc then
return false, "can't instantiate http object : " .. err
end
local all_data = {
id = self.bunkernet_id,
version = self.version,
integration = self.integration,
}
if data then
for k, v in pairs(data) do
all_data[k] = v
end
end
local res, err = httpc:request_uri(self.variables["BUNKERNET_SERVER"] .. url, {
method = method,
body = cjson.encode(all_data),
headers = {
["Content-Type"] = "application/json",
["User-Agent"] = "BunkerWeb/" .. self.version,
},
})
httpc:close()
if not res then
return false, "error while sending request : " .. err
end
if res.status ~= 200 then
return false, "status code != 200", res.status, nil
end
local ok, ret = pcall(cjson.decode, res.body)
if not ok then
return false, "error while decoding json : " .. ret
end
return true, "success", res.status, ret
end
function bunkernet:ping()
return self:request("GET", "/ping", {})
end
function bunkernet:report(ip, reason, method, url, headers)
local data = {
ip = ip,
reason = reason,
method = method,
url = url,
headers = headers,
}
return self:request("POST", "/report", data)
end
return bunkernet

View file

@ -1,6 +1,8 @@
local cjson = require "cjson"
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local ssl = require "ngx.ssl"
local letsencrypt = class("letsencrypt", plugin)
@ -9,6 +11,107 @@ function letsencrypt:initialize(ctx)
plugin.initialize(self, "letsencrypt", ctx)
end
function letsencrypt:init()
local ok, err = true, "success"
if utils.has_variable("AUTO_LETS_ENCRYPT", "yes") then
local multisite, err = utils.get_variable("MULTISITE")
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars, err = utils.get_multiple_variables({"AUTO_LETS_ENCRYPT"})
if not vars then
return self:ret(false, "can't get AUTO_LETS_ENCRYPT variables : " .. err)
end
for server_name, multisite_vars in pairs(vars) do
if multisite_vars["AUTO_LETS_ENCRYPT"] == "yes" then
local check, data = self:read_files(server_name)
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
end
else
local server_name, err = utils.get_variable("SERVER_NAME")
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local check, data = self:read_files(server_name:gmatch("%S+")[1])
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
else
err = "let's encrypt is not used"
end
return self:ret(ok, err)
end
function letsencrypt:ssl_certificate()
if self.variables["AUTO_LETS_ENCRYPT"] == "yes" then
local data, err = self.datastore:get("plugin_letsencrypt_" .. self.ctx.bw.server_name, true)
if not data then
return self:ret(false, "error while getting plugin_letsencrypt_" .. self.ctx.bw.server_name .. " from datastore : " .. err)
end
return self:ret(true, "certificate/key data found", data)
end
return self:ret(true, "let's encrypt is not used")
end
function letsencrypt:read_files(server_name)
local files = {
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name .. "/privkey.pem"
}
local data = {}
for i, file in ipairs(files) do
local f, err = io.open(file, "r")
if not f then
return false, file .. " = " .. err
end
table.insert(data, f:read("*a"))
f:close()
end
return true, data
end
function letsencrypt:load_data(data, server_name)
-- Load certificate
local cert_chain, err = ssl.parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key, err = ssl.parse_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end
local cache_key = "plugin_letsencrypt_" .. (server_name or "global")
local ok, err = self.datastore:set(cache_key, {cert_chain, priv_key}, nil, true)
if not ok then
return false, "error while setting data into datastore : " .. err
end
return true
end
function letsencrypt:access()
if string.sub(self.ctx.bw.uri, 1, string.len("/.well-known/acme-challenge/")) == "/.well-known/acme-challenge/" then
self.logger:log(ngx.NOTICE, "got a visit from Let's Encrypt, let's whitelist it")

View file

@ -0,0 +1,114 @@
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local ssl = require "ngx.ssl"
local selfsigned = class("selfsigned", plugin)
function selfsigned:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "selfsigned", ctx)
end
function selfsigned:init()
local ok, err = true, "success"
if utils.has_variable("GENERATE_SELF_SIGNED_SSL", "yes") then
local multisite, err = utils.get_variable("MULTISITE")
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars, err = utils.get_multiple_variables({"GENERATE_SELF_SIGNED_SSL"})
if not vars then
return self:ret(false, "can't get GENERATE_SELF_SIGNED_SSL variables : " .. err)
end
for server_name, multisite_vars in pairs(vars) do
if multisite_vars["GENERATE_SELF_SIGNED_SSL"] == "yes" then
local check, data = self:read_files(server_name)
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
end
else
local server_name, err = utils.get_variable("SERVER_NAME")
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local check, data = self:read_files(server_name:gmatch("%S+")[1])
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. err)
ok = false
err = "error reading files"
else
local check, err = self:load_data(data)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
ok = false
err = "error loading data"
end
end
end
else
err = "self signed is not used"
end
return self:ret(ok, err)
end
function selfsigned:ssl_certificate()
if self.variables["GENERATE_SELF_SIGNED_SSL"] == "yes" then
local data, err = self.datastore:get("plugin_selfsigned_" .. self.ctx.bw.server_name, true)
if not data then
return self:ret(false, "error while getting plugin_selfsigned_" .. self.ctx.bw.server_name .. " from datastore : " .. err)
end
return self:ret(true, "certificate/key data found", data)
end
return self:ret(true, "selfsigned is not used")
end
function selfsigned:read_files(server_name)
local files = {
"/var/cache/bunkerweb/selfsigned/" .. server_name .. "/cert.pem",
"/var/cache/bunkerweb/selfsigned/" .. server_name .. "/key.pem"
}
local data = {}
for i, file in ipairs(files) do
local f, err = io.open(file, "r")
if not f then
return false, file .. " = " .. err
end
table.insert(data, f:read("*a"))
f:close()
end
return true, data
end
function selfsigned:load_data(data, server_name)
-- Load certificate
local cert_chain, err = ssl.parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key, err = ssl.parse_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end
local cache_key = "plugin_selfsigned_" .. (server_name or "global")
local ok, err = self.datastore:set(cache_key, {cert_chain, priv_key}, nil, true)
if not ok then
return false, "error while setting data into datastore : " .. err
end
return true
end
return selfsigned

View file

@ -101,6 +101,8 @@ class Templator:
for root_conf in (
"server.conf",
"access-lua.conf",
"ssl-certificate-lua.conf",
"header-lua.conf",
"init-lua.conf",
"log-lua.conf",
"set-lua.conf",