-
Notifications
You must be signed in to change notification settings - Fork 0
Demo config from my production server using Nauthilus as HTTP API server
Christian Rößner edited this page Jan 14, 2025
·
1 revision
Here is a full working example from my production system that demonstrates the use cases:
---
# pfxhttp config file
server:
listen:
- kind: "socket_map"
name: "maps"
type: "unix"
address: "/run/pfxhttp/map.sock"
mode: "0666"
- kind: "policy_service"
name: "policies"
type: "unix"
address: "/run/pfxhttp/policy.sock"
mode: "0666"
logging:
level: debug
http_client:
max_connections_per_host: 10
max_idle_connections: 4
max_idle_connections_per_host: 1
idle_connection_timeout: 10
tls:
enabled: true
socket_maps:
relay_domains:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: relay_domains"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
relay_recipient_maps:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: relay_recipient_maps"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
virtual_alias_maps:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: virtual_alias_maps"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
transport:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: transport"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
quota_flag:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: quota_flag"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
enforce_tls_encryption_in:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: enforce_tls_encryption_in"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
enforce_tls_encryption_out:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: enforce_tls_encryption_out"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
account_status:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: account_status"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
prepend_id:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: prepend_id"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
login_maps:
target: https://login.authserv.me/api/v1/custom/pfx
custom_headers:
- "Authorization: Basic TOKEN"
- "X-Pfx-Name: login_maps"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "result"
error_field: "error"
policy_services:
policies:
target: https://login.authserv.me/api/v1/custom/pfxpolicy
custom_headers:
- "Authorization: Basic TOKEN"
payload: "{{ .Key }}"
status_code: 200
value_field: "result"
error_field: "error"Here is the Nauthilus pfx endpoint:
local nauthilus_util = require("nauthilus_util")
dynamic_loader("nauthilus_http_request")
local nauthilus_http_request = require("nauthilus_http_request")
dynamic_loader("nauthilus_ldap")
local nauthilus_ldap = require("nauthilus_ldap")
dynamic_loader("nauthilus_gll_json")
local json = require("json")
dynamic_loader("nauthilus_redis")
local nauthilus_redis = require("nauthilus_redis")
dynamic_loader("nauthilus_prometheus")
local nauthilus_prometheus = require("nauthilus_prometheus")
local N = "pfx"
local unpack = table.unpack or unpack
local logs = {}
local REDIS_KEY_EXPIRE = 60
local LDAP_BASE = {
default_base = "dc=acme,dc=com",
relay_domains = "ou=mailDomains,dc=acme,dc=com",
}
local LDAP_FILTER = {
relay_domains = [[
(&
(objectClass=rnsMSPostfixGroup)
(rnsMSVirtDomain=%s)
)]],
relay_recipient_maps = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSRecipientAddress=%s)
)]],
virtual_alias_maps = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSRecipientAddress=%s)
)]],
transport = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSRecipientAddress=%s)
)]],
quota_flag = [[
(&
(objectClass=rnsMSDovecotAccount)
(rnsMSRecipientAddress=%s)
)]],
enforce_tls_encryption_in = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSRequireTls=TRUE)
(rnsMSRecipientAddress=%s)
)]],
enforce_tls_encryption_out = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSRequireTls=TRUE)
(rnsMSRecipientAddress=%s)
)]],
account_status = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSEnablePostfix=FALSE)
(|
(rnsMSRecipientAddress=%s)
(uniqueIdentifier=%s)
)
)]],
prepend_id = [[
(&
(objectClass=rnsMSPostfixAccount)
(rnsMSRecipientAddress=%s)
)]],
login_maps = [[
(&
(objectClass=rnsMSPostfixAccount)
(|
(rnsMSRecipientAddress=%s)
(uniqueIdentifier=%s)
)
)]],
}
local LDAP_ATTRIBUTE = {
relay_domains = { "rnsMSRejectMsg" },
relay_recipient_maps = { "rnsMSRecipientAddress" },
virtual_alias_maps = { "rnsMSDeliverToAddress" },
transport = { "rnsMSPostfixTransport" },
quota_flag = { "rnsMSOverQuota" },
enforce_tls_encryption_in = { "rnsMSRequireTls" },
enforce_tls_encryption_out = { "rnsMSRequireTls" },
account_status = { "rnsMSEnablePostfix" },
prepend_id = { "rnsMSAbuseId" },
login_maps = { "rnsMSRecipientAddress", "uniqueIdentifier" },
}
local LDAP_SCOPE = {
BASE = "base",
SUB = "sub",
}
local function convert_to_string(value)
if nauthilus_util.is_table(value) then
return table.concat(value, ",")
end
return value
end
local function convert_to_table(value)
if nauthilus_util.is_string(value) then
local result = {}
for word in string.gmatch(value, "([^,]+)") do
table.insert(result, word)
end
if nauthilus_util.table_length(result) > 1 then
return result
end
end
return value
end
local function get_redis_connection()
local custom_pool = "default"
local custom_pool_name = os.getenv("CUSTOM_REDIS_POOL_NAME")
if custom_pool_name ~= nil and custom_pool_name ~= "" then
local err_redis_client
custom_pool, err_redis_client = nauthilus_redis.get_redis_connection(custom_pool_name)
nauthilus_util.if_error_raise(err_redis_client)
end
return custom_pool
end
local function cache_load(key, name)
local redis_key = "ntc:" .. name .. ":" .. key
local result, err = nauthilus_redis.redis_get(get_redis_connection(), redis_key)
nauthilus_util.if_error_raise(err)
return convert_to_table(result)
end
local function cache_save(key, name, value, expire)
local redis_key = "ntc:" .. name .. ":" .. key
local _, err = nauthilus_redis.redis_set(get_redis_connection(), redis_key, convert_to_string(value), expire)
nauthilus_util.if_error_raise(err)
end
local function flatten_ldap_filter(multi_line_string)
local single_line_string = multi_line_string:gsub("%s+", "")
return single_line_string
end
local function prepare_filter(format_filter, key)
local filter = flatten_ldap_filter(format_filter)
local count = select(2, string.gsub(filter, "%%s", ""))
local replacements = {}
for i = 1, count do
replacements[i] = key
end
local result = string.format(filter, unpack(replacements))
return result
end
local function ldap_search(name, key, session, logging, scope)
local filter = prepare_filter(LDAP_FILTER[name], key)
local timer = nauthilus_prometheus.start_histogram_timer(N .. "_duration_seconds", {
search = name,
})
local ldap_result, ldap_err = nauthilus_ldap.ldap_search({
session = session,
basedn = LDAP_BASE[name] or LDAP_BASE.default_base,
filter = filter,
attributes = LDAP_ATTRIBUTE[name],
scope = scope,
})
nauthilus_prometheus.stop_timer(timer)
if ldap_result and nauthilus_util.is_table(ldap_result) then
return ldap_result
elseif ldap_err then
nauthilus_util.print_result(logging, logs, "LDAP search result: " .. ldap_err)
end
return nil
end
local function process_ldap_result(ldap_result, name)
local result = {}
for key, value in pairs(ldap_result) do
for _, attr_key in ipairs(LDAP_ATTRIBUTE[name]) do
if key == attr_key then
if nauthilus_util.is_table(value) then
for _, item in ipairs(value) do
table.insert(result, item)
end
break
else
table.insert(result, tostring(value))
end
end
end
end
return result
end
-- Results with more than one value
local function handle_list(name, key, session, logging, scope)
local ldap_result = ldap_search(name, key, session, logging, scope)
if ldap_result then
local result = process_ldap_result(ldap_result, name)
if nauthilus_util.table_length(result) > 0 then
return result
end
end
return nil
end
-- Results with exactly one value
local function handle_single_value(name, key, session, logging, scope)
local ldap_result = ldap_search(name, key, session, logging, scope)
if ldap_result then
local result = process_ldap_result(ldap_result, name)
if nauthilus_util.table_length(result) == 1 then
return result[1]
end
end
return nil
end
local function handle_name(name, key, session, logging)
if name == "relay_domains" then
return handle_single_value(name, key, session, logging, LDAP_SCOPE.BASE)
elseif name == "relay_recipient_maps" then
return handle_list(name, key, session, logging, LDAP_SCOPE.SUB)
elseif name == "virtual_alias_maps" then
return handle_list(name, key, session, logging, LDAP_SCOPE.SUB)
elseif name == "transport" then
return handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
elseif name == "quota_flag" then
local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
if result == nil or string.lower(result) == "false" then
return "DUNNO"
else
return "552 5.2.2 Requested mail action aborted: exceeded storage allocation"
end
elseif name == "enforce_tls_encryption_in" then
local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
if result and string.lower(result) == "true" then
return "enforce_tls_encryption"
end
elseif name == "enforce_tls_encryption_out" then
local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
if result and string.lower(result) == "true" then
return "smtp-enforced-tls"
end
elseif name == "account_status" then
local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
if result then
return "REJECT Account suspended"
end
elseif name == "prepend_id" then
return string.format("REPEND X-Abuse-Id: %s", handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB))
elseif name == "login_maps" then
return handle_list(name, key, session, logging, LDAP_SCOPE.SUB)
end
return nil
end
function nauthilus_run_hook(logging, session)
logs.level = "info"
logs.caller = N .. ".lua"
logs.session = session
local header = nauthilus_http_request.get_http_request_header("Content-Type")
local body = nauthilus_http_request.get_http_request_body()
if nauthilus_util.table_length(header) == 0 or header[1] ~= "application/json" then
nauthilus_util.print_result(logging, logs, "HTTP request header: Wrong 'Content-Type'")
return logs
end
header = nauthilus_http_request.get_http_request_header("X-Pfx-Name")
local pfx_name = ""
if nauthilus_util.table_length(header) == 0 then
nauthilus_util.print_result(logging, logs, "HTTP request header: Missing required header X-Pfx-Name")
return logs
end
pfx_name = header[1]
local body_table, err_jdec = json.decode(body)
nauthilus_util.if_error_raise(err_jdec)
if not nauthilus_util.is_table(body_table) then
nauthilus_util.print_result(logging, logs, "HTTP request body: Result is not a table")
return logs
end
local pfx_key = ""
for k, v in pairs(body_table) do
if k == "key" then
if nauthilus_util.is_string(v) then
pfx_key = v
end
end
end
if pfx_name == "" or pfx_key == "" then
nauthilus_util.print_result(logging, logs, "Either pfx name or key is missing")
return logs
end
local cache_value
if pfx_name ~= "quota_flag" then
cache_value = cache_load(pfx_key, pfx_name)
end
if cache_value then
logs.result = cache_value
logs.cached = true
else
local response = handle_name(pfx_name, pfx_key, session, logging)
if response then
if pfx_name ~= "quota_flag" then
cache_save(pfx_key, pfx_name, response, REDIS_KEY_EXPIRE)
end
logs.cached = false
logs.result = response
end
end
nauthilus_util.print_result(logging, logs)
return logs
endAnd finally a template for policies:
local nauthilus_util = require("nauthilus_util")
dynamic_loader("nauthilus_http_request")
local nauthilus_http_request = require("nauthilus_http_request")
dynamic_loader("nauthilus_gll_json")
local json = require("json")
local N = "pfxpolicy"
local unpack = table.unpack or unpack
local logs = {}
function nauthilus_run_hook(logging, session)
logs.level = "info"
logs.caller = N .. ".lua"
logs.session = session
local header = nauthilus_http_request.get_http_request_header("Content-Type")
local body = nauthilus_http_request.get_http_request_body()
if nauthilus_util.table_length(header) == 0 or header[1] ~= "application/json" then
nauthilus_util.print_result(logging, logs, "HTTP request header: Wrong 'Content-Type'")
return logs
end
local body_table, err_jdec = json.decode(body)
nauthilus_util.if_error_raise(err_jdec)
local postfix_request = ""
for k,v in pairs(body_table) do
local space = ", "
if postfix_request == "" then
space = ""
end
postfix_request = postfix_request .. space .. k .. "=" .. v
end
logs.postfix_request = postfix_request
logs.result = "DUNNO"
nauthilus_util.print_result(logging, logs)
return logs
end