Browse Source

feat: Add Vault KV v2 support for DKIM key management

Co-authored-by: v <v@rspamd.com>
pull/5654/head
Cursor Agent 5 days ago
parent
commit
9e9e020b9b
  1. 1
      ChangeLog
  2. 21
      lualib/lua_dkim_tools.lua
  3. 83
      lualib/rspamadm/vault.lua

1
ChangeLog

@ -6,6 +6,7 @@
* [Feature] Redis TLS: Configurable TLS connections in Redis backend
* [Feature] Map helpers alignment: Enforce 64-byte alignment to prevent unaligned memory access
* [Feature] Enhanced CLI for secretbox with additional security test coverage
* [Feature] Vault: Add support for HashiCorp Vault KV version 2 for DKIM key management
* [Fix] MIME encoding: Major overhauls and multiple fixes for MIME encoding logic
* [Fix] MIME encoding: Improved handling and decoding of UTF-8 in MIME headers
* [Fix] Learning system: Numerous fixes to learn checks and autolearn flag handling

21
lualib/lua_dkim_tools.lua

@ -617,8 +617,22 @@ exports.sign_using_vault = function(N, task, settings, selector, sign_func, err_
local http = require "rspamd_http"
local ucl = require "ucl"
local vault_path = settings.vault_path or 'dkim'
local vault_kv_version = settings.vault_kv_version or 1
-- For KV v2, we need to add 'data' to the path for read operations
if vault_kv_version == 2 then
local mount_point = vault_path:match('^([^/]+)')
local subpath = vault_path:match('^[^/]+/?(.*)')
if subpath and subpath ~= '' then
vault_path = mount_point .. '/data/' .. subpath
else
vault_path = mount_point .. '/data'
end
end
local full_url = string.format('%s/v1/%s/%s',
settings.vault_url, settings.vault_path or 'dkim', selector.domain)
settings.vault_url, vault_path, selector.domain)
local upstream_list = lua_util.http_upstreams_by_url(rspamd_config:get_mempool(), settings.vault_url)
local function vault_callback(err, code, body, _)
@ -638,7 +652,10 @@ exports.sign_using_vault = function(N, task, settings, selector, sign_func, err_
err_func(task, string.format('vault reply for %s (data=%s) is invalid, no data',
full_url, body))
else
local elts = obj.data.selectors or {}
-- For KV v2, data is nested under obj.data.data
-- For KV v1, data is under obj.data
local vault_data = vault_kv_version == 2 and obj.data.data or obj.data
local elts = vault_data and vault_data.selectors or {}
local errs = {}
local nvalid = 0

83
lualib/rspamadm/vault.lua

@ -49,6 +49,11 @@ parser:option "-o --output"
yaml = "yaml",
}
:default "ucl"
parser:option "-k --kv-version"
:description "Vault KV store version (1 or 2)"
:argname("<version>")
:convert(tonumber)
:default "1"
parser:command "list ls l"
:description "List elements in the vault"
@ -123,11 +128,47 @@ local function highlight(str, color)
end
local function vault_url(opts, path)
local vault_path = opts.path
-- For KV v2, we need to add 'data' to the path for read/write operations
if opts.kv_version == 2 then
-- Split the path to inject 'data' after the mount point
-- e.g., 'secret/dkim' becomes 'secret/data/dkim'
local mount_point = vault_path:match('^([^/]+)')
local subpath = vault_path:match('^[^/]+/?(.*)')
if subpath and subpath ~= '' then
vault_path = mount_point .. '/data/' .. subpath
else
vault_path = mount_point .. '/data'
end
end
if path then
return string.format('%s/v1/%s/%s', opts.addr, vault_path, path)
end
return string.format('%s/v1/%s', opts.addr, vault_path)
end
local function vault_url_metadata(opts, path)
-- For KV v2 metadata operations (like list)
local vault_path = opts.path
if opts.kv_version == 2 then
local mount_point = vault_path:match('^([^/]+)')
local subpath = vault_path:match('^[^/]+/?(.*)')
if subpath and subpath ~= '' then
vault_path = mount_point .. '/metadata/' .. subpath
else
vault_path = mount_point .. '/metadata'
end
end
if path then
return string.format('%s/v1/%s/%s', opts.addr, opts.path, path)
return string.format('%s/v1/%s/%s', opts.addr, vault_path, path)
end
return string.format('%s/v1/%s', opts.addr, opts.path)
return string.format('%s/v1/%s', opts.addr, vault_path)
end
local function is_http_error(err, data)
@ -198,7 +239,10 @@ local function show_handler(opts, domain)
os.exit(1)
else
maybe_print_vault_data(opts, data.content, function(obj)
return obj.data.selectors
-- For KV v2, data is nested under obj.data.data
-- For KV v1, data is under obj.data
local vault_data = opts.kv_version == 2 and obj.data.data or obj.data
return vault_data.selectors
end)
end
end
@ -227,7 +271,8 @@ local function delete_handler(opts, domain)
end
local function list_handler(opts)
local uri = vault_url(opts)
-- For KV v2, list operations use the metadata endpoint
local uri = opts.kv_version == 2 and vault_url_metadata(opts) or vault_url(opts)
local err, data = rspamd_http.request {
config = rspamd_config,
ev_base = rspamadm_ev_base,
@ -259,7 +304,7 @@ local function create_and_push_key(opts, domain, existing)
local uri = vault_url(opts, domain)
local sk, pk = genkey(opts)
local res = {
local payload = {
selectors = {
[1] = {
selector = opts.selector,
@ -274,13 +319,16 @@ local function create_and_push_key(opts, domain, existing)
}
for _, sel in ipairs(existing) do
res.selectors[#res.selectors + 1] = sel
payload.selectors[#payload.selectors + 1] = sel
end
if opts.expire then
res.selectors[1].valid_end = os.time() + opts.expire * 3600 * 24
payload.selectors[1].valid_end = os.time() + opts.expire * 3600 * 24
end
-- For KV v2, wrap the payload in a 'data' object
local res = opts.kv_version == 2 and { data = payload } or payload
local err, data = rspamd_http.request {
config = rspamd_config,
ev_base = rspamadm_ev_base,
@ -344,7 +392,10 @@ local function newkey_handler(opts, domain)
os.exit(1)
end
local elts = rep.data.selectors
-- For KV v2, data is nested under rep.data.data
-- For KV v1, data is under rep.data
local vault_data = opts.kv_version == 2 and rep.data.data or rep.data
local elts = vault_data and vault_data.selectors or nil
if not elts then
create_and_push_key(opts, domain, {})
@ -365,7 +416,7 @@ end
local function roll_handler(opts, domain)
local uri = vault_url(opts, domain)
local res = {
local payload = {
selectors = {}
}
@ -392,7 +443,10 @@ local function roll_handler(opts, domain)
os.exit(1)
end
local elts = rep.data.selectors
-- For KV v2, data is nested under rep.data.data
-- For KV v1, data is under rep.data
local vault_data = opts.kv_version == 2 and rep.data.data or rep.data
local elts = vault_data and vault_data.selectors or nil
if not elts then
printf("No keys to roll for domain %s", domain)
@ -479,14 +533,17 @@ local function roll_handler(opts, domain)
nelt.valid_end = os.time() + opts.expire * 3600 * 24
end
table.insert(res.selectors, nelt)
table.insert(payload.selectors, nelt)
end
for _, k in ipairs(keys) do
table.insert(res.selectors, k)
table.insert(payload.selectors, k)
end
end
end
-- For KV v2, wrap the payload in a 'data' object
local res = opts.kv_version == 2 and { data = payload } or payload
-- We can now store res in the vault
err, data = rspamd_http.request {
config = rspamd_config,
@ -509,7 +566,7 @@ local function roll_handler(opts, domain)
maybe_print_vault_data(opts, data.content)
os.exit(1)
else
for _, key in ipairs(res.selectors) do
for _, key in ipairs(payload.selectors) do
if not key.valid_end or key.valid_end > os.time() + opts.ttl * 3600 * 24 then
maybe_printf(opts, 'rolled key for: %s, new selector: %s', domain, key.selector)
maybe_printf(opts, 'please place the corresponding public key as following:')

Loading…
Cancel
Save