You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lua_dkim_tools.lua 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. --[[
  2. Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
  3. Copyright (c) 2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. local exports = {}
  15. local E = {}
  16. local lua_util = require "lua_util"
  17. local rspamd_util = require "rspamd_util"
  18. local logger = require "rspamd_logger"
  19. local fun = require "fun"
  20. local function check_violation(N, task, domain)
  21. -- Check for DKIM_REJECT
  22. local sym_check = 'R_DKIM_REJECT'
  23. if N == 'arc' then sym_check = 'ARC_REJECT' end
  24. if task:has_symbol(sym_check) then
  25. local sym = task:get_symbol(sym_check)
  26. logger.infox(task, 'skip signing for %s: violation %s found: %s',
  27. domain, sym_check, sym.options)
  28. return false
  29. end
  30. return true
  31. end
  32. local function insert_or_update_prop(N, task, p, prop, origin, data)
  33. if #p == 0 then
  34. local k = {}
  35. k[prop] = data
  36. table.insert(p, k)
  37. lua_util.debugm(N, task, 'add %s "%s" using %s', prop, data, origin)
  38. else
  39. for _, k in ipairs(p) do
  40. if not k[prop] then
  41. k[prop] = data
  42. lua_util.debugm(N, task, 'set %s to "%s" using %s', prop, data, origin)
  43. end
  44. end
  45. end
  46. end
  47. local function get_mempool_selectors(N, task)
  48. local p = {}
  49. local key_var = "dkim_key"
  50. local selector_var = "dkim_selector"
  51. if N == "arc" then
  52. key_var = "arc_key"
  53. selector_var = "arc_selector"
  54. end
  55. p.key = task:get_mempool():get_variable(key_var)
  56. p.selector = task:get_mempool():get_variable(selector_var)
  57. if (not p.key or not p.selector) then
  58. return false, {}
  59. end
  60. lua_util.debugm(N, task, 'override selector and key to %s:%s', p.key, p.selector)
  61. return true, p
  62. end
  63. local function parse_dkim_http_headers(N, task, settings)
  64. -- Configure headers
  65. local headers = {
  66. sign_header = settings.http_sign_header or "PerformDkimSign",
  67. sign_on_reject_header = settings.http_sign_on_reject_header_header or 'SignOnAuthFailed',
  68. domain_header = settings.http_domain_header or 'DkimDomain',
  69. selector_header = settings.http_selector_header or 'DkimSelector',
  70. key_header = settings.http_key_header or 'DkimPrivateKey'
  71. }
  72. if task:get_request_header(headers.sign_header) then
  73. local domain = task:get_request_header(headers.domain_header)
  74. local selector = task:get_request_header(headers.selector_header)
  75. local key = task:get_request_header(headers.key_header)
  76. if not (domain and selector and key) then
  77. logger.errx(task, 'missing required headers to sign email')
  78. return false,{}
  79. end
  80. -- Now check if we need to check the existing auth
  81. local hdr = task:get_request_header(headers.sign_on_reject_header)
  82. if not hdr or tostring(hdr) == '0' or tostring(hdr) == 'false' then
  83. if not check_violation(N, task, domain, selector) then
  84. return false, {}
  85. end
  86. end
  87. local p = {}
  88. local k = {
  89. domain = tostring(domain),
  90. rawkey = tostring(key),
  91. selector = tostring(selector),
  92. }
  93. table.insert(p, k)
  94. return true, p
  95. end
  96. lua_util.debugm(N, task, 'no sign header %s', headers.sign_header)
  97. return false,{}
  98. end
  99. local function prepare_dkim_signing(N, task, settings)
  100. local is_local, is_sign_networks
  101. if settings.use_http_headers then
  102. local res,tbl = parse_dkim_http_headers(N, task, settings)
  103. if not res then
  104. if not settings.allow_headers_fallback then
  105. return res,{}
  106. else
  107. lua_util.debugm(N, task, 'failed to read http headers, fallback to normal schema')
  108. end
  109. else
  110. return res,tbl
  111. end
  112. end
  113. local auser = task:get_user()
  114. local ip = task:get_from_ip()
  115. if ip and ip:is_local() then
  116. is_local = true
  117. end
  118. if settings.auth_only and auser then
  119. lua_util.debugm(N, task, 'user is authenticated')
  120. elseif (settings.sign_networks and settings.sign_networks:get_key(ip)) then
  121. is_sign_networks = true
  122. lua_util.debugm(N, task, 'mail is from address in sign_networks')
  123. elseif settings.sign_local and is_local then
  124. lua_util.debugm(N, task, 'mail is from local address')
  125. elseif settings.sign_inbound and not is_local and not auser then
  126. lua_util.debugm(N, task, 'mail was sent to us')
  127. else
  128. lua_util.debugm(N, task, 'ignoring unauthenticated mail')
  129. return false,{}
  130. end
  131. local efrom = task:get_from('smtp')
  132. if not settings.allow_envfrom_empty and
  133. #(((efrom or E)[1] or E).addr or '') == 0 then
  134. lua_util.debugm(N, task, 'empty envelope from not allowed')
  135. return false,{}
  136. end
  137. local hfrom = task:get_from('mime')
  138. if not settings.allow_hdrfrom_multiple and (hfrom or E)[2] then
  139. lua_util.debugm(N, task, 'multiple header from not allowed')
  140. return false,{}
  141. end
  142. local eto = task:get_recipients(0)
  143. local dkim_domain
  144. local hdom = ((hfrom or E)[1] or E).domain
  145. local edom = ((efrom or E)[1] or E).domain
  146. local tdom = ((eto or E)[1] or E).domain
  147. local udom = string.match(auser or '', '.*@(.*)')
  148. local function get_dkim_domain(dtype)
  149. if settings[dtype] == 'header' then
  150. return hdom
  151. elseif settings[dtype] == 'envelope' then
  152. return edom
  153. elseif settings[dtype] == 'auth' then
  154. return udom
  155. elseif settings[dtype] == 'recipient' then
  156. return tdom
  157. else
  158. return settings[dtype]:lower()
  159. end
  160. end
  161. if hdom then
  162. hdom = hdom:lower()
  163. end
  164. if edom then
  165. edom = edom:lower()
  166. end
  167. if udom then
  168. udom = udom:lower()
  169. end
  170. if tdom then
  171. tdom = tdom:lower()
  172. end
  173. if settings.signing_table and (settings.key_table or settings.use_vault) then
  174. -- OpenDKIM style
  175. if settings.sign_networks and not is_sign_networks then
  176. lua_util.debugm(N, task,
  177. 'signing_table: sign networks specified but IP is not from that network, skip signing')
  178. return false,{}
  179. end
  180. if not hfrom or not hfrom[1] or not hfrom[1].addr then
  181. lua_util.debugm(N, task,
  182. 'signing_table: cannot get data when no header from is presented')
  183. return false,{}
  184. end
  185. local sign_entry = settings.signing_table:get_key(hfrom[1].addr)
  186. if sign_entry then
  187. lua_util.debugm(N, task,
  188. 'signing_table: found entry for %s: %s', hfrom[1].addr, sign_entry)
  189. if sign_entry == '%' then
  190. sign_entry = hdom
  191. end
  192. if settings.key_table then
  193. -- Now search in key table
  194. local key_entry = settings.key_table:get_key(sign_entry)
  195. if key_entry then
  196. local parts = lua_util.str_split(key_entry, ':')
  197. if #parts == 2 then
  198. -- domain + key
  199. local selector = settings.selector
  200. if not selector then
  201. logger.errx(task, 'no selector defined for sign_entry %s, key_entry %s',
  202. sign_entry, key_entry)
  203. return false,{}
  204. end
  205. local res = {
  206. selector = selector,
  207. domain = parts[1]:gsub('%%', hdom)
  208. }
  209. local st = parts[2]:sub(1, 2)
  210. if st:sub(1, 1) == '/' or st == './' or st == '..' then
  211. res.key = parts[2]:gsub('%%', hdom)
  212. lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, key file=%s',
  213. hdom, selector, res.domain, res.key)
  214. else
  215. res.rawkey = parts[2] -- No sanity check here
  216. lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, raw key used',
  217. hdom, selector, res.domain)
  218. end
  219. return true,{res}
  220. elseif #parts == 3 then
  221. -- domain, selector, key
  222. local selector = parts[2]
  223. local res = {
  224. selector = selector,
  225. domain = parts[1]:gsub('%%', hdom)
  226. }
  227. local st = parts[3]:sub(1, 2)
  228. if st:sub(1, 1) == '/' or st == './' or st == '..' then
  229. res.key = parts[3]:gsub('%%', hdom)
  230. lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, key file=%s',
  231. hdom, selector, res.domain, res.key)
  232. else
  233. res.rawkey = parts[3] -- No sanity check here
  234. lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, raw key used',
  235. hdom, selector, res.domain)
  236. end
  237. return true,{res}
  238. else
  239. logger.errx(task, 'invalid key entry for sign entry %s: %s; when signing %s domain',
  240. sign_entry, key_entry, hdom)
  241. return false,{}
  242. end
  243. elseif settings.use_vault then
  244. -- Sign table is presented, the rest is covered by vault
  245. lua_util.debugm(N, task, 'check vault for %s, by sign entry %s, key entry is missing',
  246. hdom, sign_entry)
  247. return true, {
  248. domain = sign_entry,
  249. vault = true
  250. }
  251. else
  252. logger.errx(task, 'missing key entry for sign entry %s; when signing %s domain',
  253. sign_entry, hdom)
  254. return false,{}
  255. end
  256. else
  257. logger.errx(task, 'cannot get key entry for signing entry %s, when signing %s domain',
  258. sign_entry, hdom)
  259. return false,{}
  260. end
  261. else
  262. lua_util.debugm(N, task,
  263. 'signing_table: no entry for %s', hfrom[1].addr)
  264. return false,{}
  265. end
  266. else
  267. if settings.use_domain_sign_networks and is_sign_networks then
  268. dkim_domain = get_dkim_domain('use_domain_sign_networks')
  269. lua_util.debugm(N, task,
  270. 'sign_networks: use domain(%s) for signature: %s',
  271. settings.use_domain_sign_networks, dkim_domain)
  272. elseif settings.use_domain_sign_local and is_local then
  273. dkim_domain = get_dkim_domain('use_domain_sign_local')
  274. lua_util.debugm(N, task, 'local: use domain(%s) for signature: %s',
  275. settings.use_domain_sign_local, dkim_domain)
  276. elseif settings.use_domain_sign_inbound and not is_local and not auser then
  277. dkim_domain = get_dkim_domain('use_domain_sign_inbound')
  278. lua_util.debugm(N, task, 'inbound: use domain(%s) for signature: %s',
  279. settings.use_domain_sign_inbound, dkim_domain)
  280. elseif settings.use_domain_custom then
  281. if type(settings.use_domain_custom) == 'string' then
  282. -- Load custom function
  283. local loadstring = loadstring or load
  284. local ret, res_or_err = pcall(loadstring(settings.use_domain_custom))
  285. if ret then
  286. if type(res_or_err) == 'function' then
  287. settings.use_domain_custom = res_or_err
  288. dkim_domain = settings.use_domain_custom(task)
  289. lua_util.debugm(N, task, 'use custom domain for signing: %s',
  290. dkim_domain)
  291. else
  292. logger.errx(task, 'cannot load dkim domain custom script: invalid type: %s, expected function',
  293. type(res_or_err))
  294. settings.use_domain_custom = nil
  295. end
  296. else
  297. logger.errx(task, 'cannot load dkim domain custom script: %s', res_or_err)
  298. settings.use_domain_custom = nil
  299. end
  300. else
  301. dkim_domain = settings.use_domain_custom(task)
  302. lua_util.debugm(N, task, 'use custom domain for signing: %s',
  303. dkim_domain)
  304. end
  305. else
  306. dkim_domain = get_dkim_domain('use_domain')
  307. lua_util.debugm(N, task, 'use domain(%s) for signature: %s',
  308. settings.use_domain, dkim_domain)
  309. end
  310. end
  311. if not dkim_domain then
  312. lua_util.debugm(N, task, 'could not extract dkim domain')
  313. return false,{}
  314. end
  315. if settings.use_esld then
  316. dkim_domain = rspamd_util.get_tld(dkim_domain)
  317. if hdom then
  318. hdom = rspamd_util.get_tld(hdom)
  319. end
  320. if edom then
  321. edom = rspamd_util.get_tld(edom)
  322. end
  323. end
  324. lua_util.debugm(N, task, 'final DKIM domain: %s', dkim_domain)
  325. -- Sanity checks
  326. if edom and hdom and not settings.allow_hdrfrom_mismatch and hdom ~= edom then
  327. if settings.allow_hdrfrom_mismatch_local and is_local then
  328. lua_util.debugm(N, task, 'domain mismatch allowed for local IP: %1 != %2', hdom, edom)
  329. elseif settings.allow_hdrfrom_mismatch_sign_networks and is_sign_networks then
  330. lua_util.debugm(N, task, 'domain mismatch allowed for sign_networks: %1 != %2', hdom, edom)
  331. else
  332. lua_util.debugm(N, task, 'domain mismatch not allowed: %1 != %2', hdom, edom)
  333. return false,{}
  334. end
  335. end
  336. if auser and not settings.allow_username_mismatch then
  337. if not udom then
  338. lua_util.debugm(N, task, 'couldnt find domain in username')
  339. return false,{}
  340. end
  341. if settings.use_esld then
  342. udom = rspamd_util.get_tld(udom)
  343. end
  344. if udom ~= dkim_domain then
  345. lua_util.debugm(N, task, 'user domain mismatch')
  346. return false,{}
  347. end
  348. end
  349. local p = {}
  350. if settings.use_vault then
  351. if settings.vault_domains then
  352. if settings.vault_domains:get_key(dkim_domain) then
  353. return true, {
  354. domain = dkim_domain,
  355. vault = true,
  356. }
  357. else
  358. lua_util.debugm(N, task, 'domain %s is not designated for vault',
  359. dkim_domain)
  360. return false,{}
  361. end
  362. else
  363. -- TODO: try every domain in the vault
  364. return true, {
  365. domain = dkim_domain,
  366. vault = true,
  367. }
  368. end
  369. end
  370. if settings.domain[dkim_domain] then
  371. -- support old style selector/paths
  372. if settings.domain[dkim_domain].selector or
  373. settings.domain[dkim_domain].path then
  374. local k = {}
  375. k.selector = settings.domain[dkim_domain].selector
  376. k.key = settings.domain[dkim_domain].path
  377. table.insert(p, k)
  378. end
  379. for _, s in ipairs((settings.domain[dkim_domain].selectors or {})) do
  380. lua_util.debugm(N, task, 'adding selector: %1', s)
  381. local k = {}
  382. k.selector = s.selector
  383. k.key = s.path
  384. table.insert(p, k)
  385. end
  386. end
  387. if #p == 0 then
  388. local ret, k = get_mempool_selectors(N, task)
  389. if ret then
  390. table.insert(p, k)
  391. lua_util.debugm(N, task, 'using mempool selector %s with key %s',
  392. k.selector, k.key)
  393. end
  394. end
  395. if settings.selector_map then
  396. local data = settings.selector_map:get_key(dkim_domain)
  397. if data then
  398. insert_or_update_prop(N, task, p, 'selector', 'selector_map', data)
  399. else
  400. lua_util.debugm(N, task, 'no selector in map for %s', dkim_domain)
  401. end
  402. end
  403. if settings.path_map then
  404. local data = settings.path_map:get_key(dkim_domain)
  405. if data then
  406. insert_or_update_prop(N, task, p, 'key', 'path_map', data)
  407. else
  408. lua_util.debugm(N, task, 'no key in map for %s', dkim_domain)
  409. end
  410. end
  411. if #p == 0 and not settings.try_fallback then
  412. lua_util.debugm(N, task, 'dkim unconfigured and fallback disabled')
  413. return false,{}
  414. end
  415. if not settings.use_redis then
  416. insert_or_update_prop(N, task, p, 'key',
  417. 'default path', settings.path)
  418. end
  419. insert_or_update_prop(N, task, p, 'selector',
  420. 'default selector', settings.selector)
  421. if settings.check_violation then
  422. if not check_violation(N, task, p.domain) then
  423. return false,{}
  424. end
  425. end
  426. insert_or_update_prop(N, task, p, 'domain', 'dkim_domain',
  427. dkim_domain)
  428. return true,p
  429. end
  430. exports.prepare_dkim_signing = prepare_dkim_signing
  431. exports.sign_using_redis = function(N, task, settings, selectors, sign_func, err_func)
  432. local lua_redis = require "lua_redis"
  433. local function try_redis_key(selector, p)
  434. p.key = nil
  435. p.selector = selector
  436. local rk = string.format('%s.%s', p.selector, p.domain)
  437. local function redis_key_cb(err, data)
  438. if err then
  439. err_func(string.format("cannot make request to load DKIM key for %s: %s",
  440. rk, err))
  441. elseif type(data) ~= 'string' then
  442. lua_util.debugm(N, task, "missing DKIM key for %s", rk)
  443. else
  444. p.rawkey = data
  445. lua_util.debugm(N, task, 'found and parsed key for %s:%s in Redis',
  446. p.domain, p.selector)
  447. sign_func(task, p)
  448. end
  449. end
  450. local rret = lua_redis.redis_make_request(task,
  451. settings.redis_params, -- connect params
  452. rk, -- hash key
  453. false, -- is write
  454. redis_key_cb, --callback
  455. 'HGET', -- command
  456. {settings.key_prefix, rk} -- arguments
  457. )
  458. if not rret then
  459. err_func(task,
  460. string.format( "cannot make request to load DKIM key for %s", rk))
  461. end
  462. end
  463. for _, p in ipairs(selectors) do
  464. if settings.selector_prefix then
  465. logger.infox(task, "using selector prefix '%s' for domain '%s'",
  466. settings.selector_prefix, p.domain);
  467. local function redis_selector_cb(err, data)
  468. if err or type(data) ~= 'string' then
  469. err_func(task, string.format("cannot make request to load DKIM selector for domain %s: %s",
  470. p.domain, err))
  471. else
  472. try_redis_key(data, p)
  473. end
  474. end
  475. local rret = lua_redis.redis_make_request(task,
  476. settings.redis_params, -- connect params
  477. p.domain, -- hash key
  478. false, -- is write
  479. redis_selector_cb, --callback
  480. 'HGET', -- command
  481. {settings.selector_prefix, p.domain} -- arguments
  482. )
  483. if not rret then
  484. err_func(task, string.format("cannot make Redis request to load DKIM selector for domain %s",
  485. p.domain))
  486. end
  487. else
  488. try_redis_key(p.selector, p)
  489. end
  490. end
  491. end
  492. exports.sign_using_vault = function(N, task, settings, selectors, sign_func, err_func)
  493. local http = require "rspamd_http"
  494. local ucl = require "ucl"
  495. local full_url = string.format('%s/v1/%s/%s',
  496. settings.vault_url, settings.vault_path or 'dkim', selectors.domain)
  497. local function vault_callback(err, code, body, _)
  498. if code ~= 200 then
  499. err_func(task, string.format('cannot request data from the vault url: %s; %s (%s)',
  500. full_url, err, body))
  501. else
  502. local parser = ucl.parser()
  503. local res,parser_err = parser:parse_string(body)
  504. if not res then
  505. err_func(task, string.format('vault reply for %s (data=%s) cannot be parsed: %s',
  506. full_url, body, parser_err))
  507. else
  508. local obj = parser:get_object()
  509. if not obj or not obj.data then
  510. err_func(task, string.format('vault reply for %s (data=%s) is invalid, no data',
  511. full_url, body))
  512. else
  513. local elts = obj.data.selectors or {}
  514. -- Filter selectors by time/sanity
  515. local function is_selector_valid(p)
  516. if not p.key or not p.selector then
  517. return false
  518. end
  519. if p.valid_start then
  520. -- Check start time
  521. if rspamd_util.get_time() < tonumber(p.valid_start) then
  522. return false
  523. end
  524. end
  525. if p.valid_end then
  526. if rspamd_util.get_time() >= tonumber(p.valid_end) then
  527. return false
  528. end
  529. end
  530. return true
  531. end
  532. fun.each(function(p)
  533. local dkim_sign_data = {
  534. rawkey = p.key,
  535. selector = p.selector,
  536. domain = p.domain or selectors.domain
  537. }
  538. lua_util.debugm(N, task, 'found and parsed key for %s:%s in Vault',
  539. dkim_sign_data.domain, dkim_sign_data.selector)
  540. sign_func(task, dkim_sign_data)
  541. end, fun.filter(is_selector_valid, elts))
  542. end
  543. end
  544. end
  545. end
  546. local ret = http.request{
  547. task = task,
  548. url = full_url,
  549. callback = vault_callback,
  550. timeout = settings.http_timeout or 5.0,
  551. no_ssl_verify = settings.no_ssl_verify,
  552. keepalive = true,
  553. headers = {
  554. ['X-Vault-Token'] = settings.vault_token,
  555. },
  556. }
  557. if not ret then
  558. err_func(task, string.format("cannot make HTTP request to load DKIM data domain %s",
  559. selectors.domain))
  560. end
  561. end
  562. exports.validate_signing_settings = function(settings)
  563. return settings.use_redis or
  564. settings.path or
  565. settings.domain or
  566. settings.path_map or
  567. settings.selector_map or
  568. settings.use_http_headers or
  569. (settings.signing_table and settings.key_table) or
  570. (settings.use_vault and settings.vault_url and settings.vault_token)
  571. end
  572. exports.process_signing_settings = function(N, settings, opts)
  573. local lua_maps = require "lua_maps"
  574. for k,v in pairs(opts) do
  575. if k == 'sign_networks' then
  576. settings[k] = lua_maps.map_add(N, k, 'radix', 'DKIM signing networks')
  577. elseif k == 'path_map' then
  578. settings[k] = lua_maps.map_add(N, k, 'map', 'Paths to DKIM signing keys')
  579. elseif k == 'selector_map' then
  580. settings[k] = lua_maps.map_add(N, k, 'map', 'DKIM selectors')
  581. elseif k == 'signing_table' then
  582. settings[k] = lua_maps.map_add(N, k, 'glob', 'DKIM signing table')
  583. elseif k == 'key_table' then
  584. settings[k] = lua_maps.map_add(N, k, 'glob', 'DKIM keys table')
  585. elseif k == 'vault_domains' then
  586. settings[k] = lua_maps.map_add(N, k, 'glob', 'DKIM signing domains in vault')
  587. else
  588. settings[k] = v
  589. end
  590. end
  591. end
  592. return exports