commit 9b36ecc: [Project] Implement keys rotation in the vault

Vsevolod Stakhov vsevolod at highsecure.ru
Tue Apr 30 15:21:03 UTC 2019


Author: Vsevolod Stakhov
Date: 2019-04-30 16:20:32 +0100
URL: https://github.com/rspamd/rspamd/commit/9b36ecc93bd136a7ff52a67bcacb8adf60e6bf27 (HEAD -> master)

[Project] Implement keys rotation in the vault

---
 lualib/rspamadm/vault.lua | 180 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 180 insertions(+)

diff --git a/lualib/rspamadm/vault.lua b/lualib/rspamadm/vault.lua
index 9c01624fc..b11ba525e 100644
--- a/lualib/rspamadm/vault.lua
+++ b/lualib/rspamadm/vault.lua
@@ -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


More information about the Commits mailing list