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, _)
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
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"
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)
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
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,
local uri = vault_url(opts, domain)
local sk, pk = genkey(opts)
- local res = {
+ local payload = {
selectors = {
[1] = {
selector = opts.selector,
}
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,
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, {})
local function roll_handler(opts, domain)
local uri = vault_url(opts, domain)
- local res = {
+ local payload = {
selectors = {}
}
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)
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,
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:')