Browse Source

[Project] Implement keys rotation in the vault

tags/1.9.3
Vsevolod Stakhov 5 years ago
parent
commit
9b36ecc93b
1 changed files with 180 additions and 0 deletions
  1. 180
    0
      lualib/rspamadm/vault.lua

+ 180
- 0
lualib/rspamadm/vault.lua View File

@@ -91,6 +91,20 @@ newkey:option "-x --expire"
:convert(tonumber)
newkey:flag "-r --rewrite"

local roll = parser:command "roll rollover"
:description "Perform keys rollover"
roll:argument "domain"
:description "Domain to roll key(s) for"
:args "+"
roll:option "-T --ttl"
:description "Validity period for old keys (days)"
:convert(tonumber)
:default "1"
roll:flag "-r --remove-expired"
:description "Remove expired keys"
roll:option "-x --expire"
:argname("<days>")
:convert(tonumber)

local function printf(fmt, ...)
if fmt then
@@ -252,7 +266,10 @@ local function create_and_push_key(opts, domain, existing)
selector = opts.selector,
domain = domain,
key = tostring(sk),
pubkey = tostring(pk),
alg = opts.algorithm,
bits = opts.bits or 0,
valid_start = os.time(),
}
}
}
@@ -346,6 +363,167 @@ local function newkey_handler(opts, domain)
end
end

local function roll_handler(opts, domain)
local uri = vault_url(opts, domain)
local res = {
selectors = {}
}

local err,data = rspamd_http.request{
config = rspamd_config,
ev_base = rspamadm_ev_base,
session = rspamadm_session,
resolver = rspamadm_dns_resolver,
url = uri,
method = 'get',
headers = {
['X-Vault-Token'] = opts.token
}
}

if is_http_error(err, data) or not data.content then
printf("No keys to roll for domain %s", domain)
os.exit(1)
else
local rep = parse_vault_reply(data.content)

if not rep or not rep.data then
printf('cannot parse reply for %s: %s', uri, data.content)
os.exit(1)
end

local elts = rep.data.selectors

if not elts then
printf("No keys to roll for domain %s", domain)
os.exit(1)
end

local nkeys = {} -- indexed by algorithm

local function insert_key(sel, add_expire)
if not nkeys[sel.alg] then
nkeys[sel.alg] = {}
end

if add_expire then
sel.valid_end = os.time() + opts.ttl * 3600 * 24
end

table.insert(nkeys[sel.alg], sel)
end

for _,sel in ipairs(elts) do
if sel.valid_end and sel.valid_end < os.time() then
if not opts.remove_expired then
insert_key(sel, false)
else
maybe_printf(opts, 'removed expired key for %s (selector %s, expire "%s"',
domain, sel.selector, os.date('%c', sel.valid_end))
end
else
insert_key(sel, true)
end
end

-- Now we need to ensure that all but one selectors have either expired or just a single key
for alg,keys in pairs(nkeys) do
table.sort(keys, function(k1, k2)
if k1.valid_end and k2.valid_end then
return k1.valid_end > k2.valid_end
elseif k1.valid_end then
return true
elseif k2.valid_end then
return false
end
return false
end)
-- Exclude the key with the highest expiration date and examine the rest
if not (#keys == 1 or fun.all(function(k)
return k.valid_end and k.valid_end < os.time()
end, fun.tail(keys))) then
printf('bad keys list for %s and %s algorithm', domain, alg)
fun.each(function(k)
if not k.valid_end then
printf('selector %s, algorithm %s has a key with no expire',
k.selector, k.alg)
elseif k.valid_end >= os.time() then
printf('selector %s, algorithm %s has a key that not yet expired: %s',
k.selector, k.alg, os.date('%c', k.valid_end))
end
end, fun.tail(keys))
os.exit(1)
end
-- OK to process
-- Insert keys for each algorithm in pairs <old_key(s)>, <new_key>
local sk,pk = genkey({algorithm = alg, bits = keys[1].bits})
local selector = string.format('%s-%s', alg,
os.date("%Y%m%d"))

if selector == keys[1].selector then
selector = selector .. '-1'
end
local nelt = {
selector = selector,
domain = domain,
key = tostring(sk),
pubkey = tostring(pk),
alg = alg,
bits = keys[1].bits,
valid_start = os.time(),
}

if opts.expire then
nelt.valid_end = os.time() + opts.expire * 3600 * 24
end

table.insert(res.selectors, nelt)
for _,k in ipairs(keys) do
table.insert(res.selectors, k)
end
end
end

-- We can now store res in the vault
err,data = rspamd_http.request{
config = rspamd_config,
ev_base = rspamadm_ev_base,
session = rspamadm_session,
resolver = rspamadm_dns_resolver,
url = uri,
method = 'put',
headers = {
['X-Vault-Token'] = opts.token
},
body = {
ucl.to_format(res, 'json-compact')
},
}

if is_http_error(err, data) then
printf('cannot put request to the vault (%s), HTTP error code %s', uri, data.code)
maybe_print_vault_data(opts, data.content)
os.exit(1)
else
for _,key in ipairs(res.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:')

if opts.silent then
printf('%s', key.pubkey)
else
print_dkim_txt_record(key.pubkey, key.selector, key.alg)
end

end
end

maybe_printf(opts, 'your old keys will be valid until %s',
os.date('%c', os.time() + opts.ttl * 3600 * 24))
end
end

local function handler(args)
local opts = parser:parse(args)

@@ -373,6 +551,8 @@ local function handler(args)
fun.each(function(d) show_handler(opts, d) end, opts.domain)
elseif command == 'newkey' then
fun.each(function(d) newkey_handler(opts, d) end, opts.domain)
elseif command == 'roll' then
fun.each(function(d) roll_handler(opts, d) end, opts.domain)
elseif command == 'delete' then
fun.each(function(d) delete_handler(opts, d) end, opts.domain)
else

Loading…
Cancel
Save