From: Cursor Agent Date: Fri, 3 Oct 2025 09:52:51 +0000 (+0000) Subject: feat: Add Vault KV v2 support for DKIM key management X-Git-Tag: 3.13.2~8^2~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9e9e020b9b6b8125053b51ffec1f6a4404b54069;p=thirdparty%2Frspamd.git feat: Add Vault KV v2 support for DKIM key management Co-authored-by: v --- diff --git a/ChangeLog b/ChangeLog index 937f160b65..4afca9f993 100644 --- a/ChangeLog +++ b/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 diff --git a/lualib/lua_dkim_tools.lua b/lualib/lua_dkim_tools.lua index 37c3b163b8..fe13c0c502 100644 --- a/lualib/lua_dkim_tools.lua +++ b/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 diff --git a/lualib/rspamadm/vault.lua b/lualib/rspamadm/vault.lua index 840e504e05..8933944efc 100644 --- a/lualib/rspamadm/vault.lua +++ b/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("") + :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:')