/
vault.lua
1610 lines (1390 loc) · 51.8 KB
/
vault.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
-- Vault module
--
-- This module can be used to resolve, parse and verify vault references.
--
-- @module kong.vault
local require = require
local concurrency = require "kong.concurrency"
local constants = require "kong.constants"
local arguments = require "kong.api.arguments"
local lrucache = require "resty.lrucache"
local isempty = require "table.isempty"
local buffer = require "string.buffer"
local clone = require "table.clone"
local utils = require "kong.tools.utils"
local string_tools = require "kong.tools.string"
local cjson = require("cjson.safe").new()
local yield = utils.yield
local get_updated_now_ms = utils.get_updated_now_ms
local replace_dashes = string_tools.replace_dashes
local ngx = ngx
local get_phase = ngx.get_phase
local max = math.max
local min = math.min
local fmt = string.format
local sub = string.sub
local byte = string.byte
local type = type
local sort = table.sort
local pcall = pcall
local lower = string.lower
local pairs = pairs
local ipairs = ipairs
local concat = table.concat
local md5_bin = ngx.md5_bin
local tostring = tostring
local tonumber = tonumber
local decode_args = ngx.decode_args
local unescape_uri = ngx.unescape_uri
local parse_url = require("socket.url").parse
local parse_path = require("socket.url").parse_path
local encode_base64url = require("ngx.base64").encode_base64url
local decode_json = cjson.decode
local NEGATIVELY_CACHED_VALUE = "\0"
local ROTATION_INTERVAL = tonumber(os.getenv("KONG_VAULT_ROTATION_INTERVAL"), 10) or 60
local DAO_MAX_TTL = constants.DATABASE.DAO_MAX_TTL
local BRACE_START = byte("{")
local BRACE_END = byte("}")
local COLON = byte(":")
local SLASH = byte("/")
---
-- Checks if the passed in reference looks like a reference.
-- Valid references start with '{vault://' and end with '}'.
--
-- @local
-- @function is_reference
-- @tparam string reference reference to check
-- @treturn boolean `true` is the passed in reference looks like a reference, otherwise `false`
local function is_reference(reference)
return type(reference) == "string"
and byte(reference, 1) == BRACE_START
and byte(reference, -1) == BRACE_END
and byte(reference, 7) == COLON
and byte(reference, 8) == SLASH
and byte(reference, 9) == SLASH
and sub(reference, 2, 6) == "vault"
end
---
-- Parses and decodes the passed in reference and returns a table
-- containing its components.
--
-- Given a following resource:
-- ```lua
-- "{vault://env/cert/key?prefix=SSL_#1}"
-- ```
--
-- This function will return following table:
--
-- ```lua
-- {
-- name = "env", -- name of the Vault entity or Vault strategy
-- resource = "cert", -- resource where secret is stored
-- key = "key", -- key to lookup if the resource is secret object
-- config = { -- if there are any config options specified
-- prefix = "SSL_"
-- },
-- version = 1 -- if the version is specified
-- }
-- ```
--
-- @local
-- @function parse_reference
-- @tparam string reference reference to parse
-- @treturn table|nil a table containing each component of the reference, or `nil` on error
-- @treturn string|nil error message on failure, otherwise `nil`
local function parse_reference(reference)
if not is_reference(reference) then
return nil, fmt("not a reference [%s]", tostring(reference))
end
local url, err = parse_url(sub(reference, 2, -2))
if not url then
return nil, fmt("reference is not url (%s) [%s]", err, reference)
end
local name = url.host
if not name then
return nil, fmt("reference url is missing host [%s]", reference)
end
local path = url.path
if not path then
return nil, fmt("reference url is missing path [%s]", reference)
end
local resource = sub(path, 2)
if resource == "" then
return nil, fmt("reference url has empty path [%s]", reference)
end
local version = url.fragment
if version then
version = tonumber(version, 10)
if not version then
return nil, fmt("reference url has invalid version [%s]", reference)
end
end
local key
local parts = parse_path(resource)
local count = #parts
if count == 1 then
resource = unescape_uri(parts[1])
else
resource = unescape_uri(concat(parts, "/", 1, count - 1))
if parts[count] ~= "" then
key = unescape_uri(parts[count])
end
end
if resource == "" then
return nil, fmt("reference url has invalid path [%s]", reference)
end
local config
local query = url.query
if query and query ~= "" then
config = decode_args(query)
end
return {
name = url.host,
resource = resource,
key = key,
config = config,
version = version,
}
end
---
-- Create a instance of PDK Vault module
--
-- @local
-- @function new
-- @tparam table self a PDK instance
-- @treturn table a new instance of Vault
local function new(self)
-- Don't put this onto the top level of the file unless you're prepared for a surprise
local Schema = require "kong.db.schema"
local ROTATION_MUTEX_OPTS = {
name = "vault-rotation",
exptime = ROTATION_INTERVAL * 1.5, -- just in case the lock is not properly released
timeout = 0, -- we don't want to wait for release as we run a recurring timer
}
local LRU = lrucache.new(1000)
local RETRY_LRU = lrucache.new(1000)
local SECRETS_CACHE = ngx.shared.kong_secrets
local SECRETS_CACHE_MIN_TTL = ROTATION_INTERVAL * 2
local STRATEGIES = {}
local SCHEMAS = {}
local CONFIGS = {}
local BUNDLED_VAULTS = constants.BUNDLED_VAULTS
local VAULT_NAMES
do
local vaults = self and self.configuration and self.configuration.loaded_vaults
if vaults then
VAULT_NAMES = {}
for name in pairs(vaults) do
VAULT_NAMES[name] = true
end
else
VAULT_NAMES = BUNDLED_VAULTS and clone(BUNDLED_VAULTS) or {}
end
end
---
-- Calculates hash for a string.
--
-- @local
-- @function calculate_hash
-- @tparam string str a string to hash
-- @treturn string md5 hash as base64url encoded string
local function calculate_hash(str)
return encode_base64url(md5_bin(str))
end
---
-- Builds cache key from reference and configuration hash.
--
-- @local
-- @function build_cache_key
-- @tparam string reference the vault reference string
-- @tparam string config_hash the configuration hash
-- @treturn string the cache key for shared dictionary cache
local function build_cache_key(reference, config_hash)
return config_hash .. "." .. reference
end
---
-- Parses cache key back to a reference and a configuration hash.
--
-- @local
-- @function parse_cache_key
-- @tparam string cache_key the cache key used for shared dictionary cache
-- @treturn string|nil the vault reference string
-- @treturn string|nil a string describing an error if there was one
-- @treturn string the configuration hash
local function parse_cache_key(cache_key)
local buf = buffer.new():set(cache_key)
local config_hash = buf:get(22)
local divider = buf:get(1)
local reference = buf:get()
if divider ~= "." or not is_reference(reference) then
return nil, "invalid cache key (" .. cache_key .. ")"
end
return reference, nil, config_hash
end
---
-- This function extracts a key and returns its value from a JSON object.
--
-- It first decodes the JSON string into a Lua table, then checks for the presence and type of a specific key.
--
-- @local
-- @function extract_key_from_json_string
-- @tparam string json_string the JSON string to be parsed and decoded
-- @tparam string key the specific subfield to be searched for within the JSON object
-- @treturn string|nil the value associated with the specified key in the JSON object
-- @treturn string|nil a string describing an error if there was one
local function extract_key_from_json_string(json_string, key)
-- Note that this function will only find keys in flat maps.
-- Deeper nested structures are not supported.
local json, err = decode_json(json_string)
if type(json) ~= "table" then
return nil, fmt("unable to json decode value (%s): %s", json, err)
end
json_string = json[key]
if json_string == nil then
return nil, fmt("subfield %s not found in JSON secret", key)
elseif type(json_string) ~= "string" then
return nil, fmt("unexpected %s value in JSON secret for subfield %s", type(json_string), key)
end
return json_string
end
---
-- This function adjusts the 'time-to-live' (TTL) according to the configuration provided in 'vault_config'.
--
-- If the TTL is not a number or if it falls outside of the configured minimum or maximum TTL,
-- it will be adjusted accordingly. The adjustment happens on Vault strategy returned TTL values only.
--
-- @local
-- @function adjust_ttl
-- @tparam number|nil ttl The time-to-live value to be adjusted.
-- @tparam table|nil config the configuration table for the vault,
-- which may contain 'ttl', 'min_ttl', and 'max_ttl' fields.
-- @treturn number returns the adjusted TTL:
-- * if the initial TTL is not a number, it returns the 'ttl' field from the 'vault_config' table or 0 if it doesn't exist.
-- * if the initial TTL is greater than 'max_ttl' from 'vault_config', it returns 'max_ttl'.
-- * if the initial TTL is less than 'min_ttl' from 'vault_config', it returns 'min_ttl'.
-- * otherwise, it returns the given TTL.
local function adjust_ttl(ttl, config)
if type(ttl) ~= "number" then
return config and config.ttl or DAO_MAX_TTL
end
if ttl <= 0 then
-- for simplicity, we don't support never expiring keys
return DAO_MAX_TTL
end
local max_ttl = config and config.max_ttl
if max_ttl and max_ttl > 0 and ttl > max_ttl then
return max_ttl
end
local min_ttl = config and config.min_ttl
if min_ttl and ttl < min_ttl then
return min_ttl
end
return ttl
end
---
-- Decorates normal strategy with a caching strategy when rotating secrets.
--
-- With vault strategies we support JSON string responses, that means that
-- the vault can return n-number of related secrets, for example Postgres
-- username and password. The references could look like:
--
-- - {vault://my-vault/postgres/username}
-- - {vault://my-vault/postgres/password}
--
-- For LRU cache we use ´{vault://my-vault/postgres/username}` as a cache
-- key and for SHM we use `<config-hash>.{vault://my-vault/postgres/username}`
-- as a cache key. What we send to vault are:
--
-- 1. the config table
-- 2. the resource to lookup
-- 3. the version of secret
--
-- In the above references in both cases the `resource` is `postgres` and we
-- never send `/username` or `/password` to vault strategy. Thus the proper
-- cache key for vault strategy is: `<config-hash>.<resource>.<version>`.
-- This means that we can call the vault strategy just once, and not twice
-- to resolve both references. This also makes sure we get both secrets in
-- atomic way.
--
-- The caching strategy wraps the strategy so that call to it can be cached
-- when e.g. looping through secrets on rotation. Again that ensures atomicity,
-- and reduces calls to actual vault.
--
-- @local
-- @function get_caching_strategy
-- @treturn function returns a function that takes `strategy` and `config_hash`
-- as an argument, that returns a decorated strategy.
--
-- @usage
-- local caching_strategy = get_caching_strategy()
-- for _, reference in ipairs({ "{vault://my-vault/postgres/username}",
-- "{vault://my-vault/postgres/username}", })
-- do
-- local strategy, err, config, _, parsed_reference, config_hash = get_strategy(reference)
-- strategy = caching_strategy(strategy, config_hash)
-- local value, err, ttl = strategy.get(config, parsed_reference.resource, parsed_reference.version)
-- end
local function get_caching_strategy()
local cache = {}
return function(strategy, config_hash)
return {
get = function(config, resource, version)
local cache_key = fmt("%s.%s.%s", config_hash, resource or "", version or "")
local data = cache[cache_key]
if data then
return data[1], data[2], data[3]
end
local value, err, ttl = strategy.get(config, resource, version)
cache[cache_key] = {
value,
err,
ttl,
}
return value, err, ttl
end
}
end
end
---
-- Build schema aware configuration out of base configuration and the configuration overrides
-- (e.g. configuration parameters stored in a vault reference).
--
-- It infers and validates configuration fields, and only returns validated fields
-- in the returned config. It also calculates a deterministic configuration hash
-- that will can used to build shared dictionary's cache key.
--
-- @local
-- @function get_vault_config_and_hash
-- @tparam string name the name of vault strategy
-- @tparam table schema the scheme of vault strategy
-- @tparam table base_config the base configuration
-- @tparam table|nil config_overrides the configuration overrides
-- @treturn table validated and merged configuration from base configuration and config overrides
-- @treturn string calculated hash of the configuration
--
-- @usage
-- local config, hash = get_vault_config_and_hash("env", schema, { prefix = "DEFAULT_" },
-- { prefix = "MY_PREFIX_" })
local get_vault_config_and_hash do
local CONFIG_HASH_BUFFER = buffer.new(100)
get_vault_config_and_hash = function(name, schema, base_config, config_overrides)
CONFIG_HASH_BUFFER:reset():putf("%s;", name)
local config = {}
config_overrides = config_overrides or config
for k, f in schema:each_field() do
local v = config_overrides[k] or base_config[k]
v = arguments.infer_value(v, f)
if v ~= nil and schema:validate_field(f, v) then
config[k] = v
CONFIG_HASH_BUFFER:putf("%s=%s;", k, v)
end
end
return config, calculate_hash(CONFIG_HASH_BUFFER:get())
end
end
---
-- Fetches the strategy and schema for a given vault.
--
-- This function fetches the associated strategy and schema from the `STRATEGIES` and `SCHEMAS` tables,
-- respectively. If the strategy or schema isn't found in the tables, it attempts to initialize them
-- from the Lua modules.
--
-- @local
-- @function get_vault_strategy_and_schema
-- @tparam string name the name of the vault to fetch the strategy and schema for
-- @treturn table|nil the fetched or required strategy for the given vault
-- @treturn string|nil an error message, if an error occurred while fetching or requiring the strategy or schema
-- @treturn table|nil the vault strategy's configuration schema.
local function get_vault_strategy_and_schema(name)
local strategy = STRATEGIES[name]
local schema = SCHEMAS[name]
if strategy then
return strategy, nil, schema
end
local vaults = self and (self.db and self.db.vaults)
if vaults and vaults.strategies then
strategy = vaults.strategies[name]
if not strategy then
return nil, fmt("could not find vault (%s)", name)
end
schema = vaults.schema.subschemas[name]
if not schema then
return nil, fmt("could not find vault schema (%s): %s", name, strategy)
end
schema = Schema.new(schema.fields.config)
else
local ok
ok, strategy = pcall(require, fmt("kong.vaults.%s", name))
if not ok then
return nil, fmt("could not find vault (%s): %s", name, strategy)
end
local def
ok, def = pcall(require, fmt("kong.vaults.%s.schema", name))
if not ok then
return nil, fmt("could not find vault schema (%s): %s", name, def)
end
schema = Schema.new(require("kong.db.schema.entities.vaults"))
local err
ok, err = schema:new_subschema(name, def)
if not ok then
return nil, fmt("could not load vault sub-schema (%s): %s", name, err)
end
schema = schema.subschemas[name]
if not schema then
return nil, fmt("could not find vault sub-schema (%s)", name)
end
if type(strategy.init) == "function" then
strategy.init()
end
schema = Schema.new(schema.fields.config)
end
STRATEGIES[name] = strategy
SCHEMAS[name] = schema
return strategy, nil, schema
end
---
-- This function retrieves the base configuration for the default vault
-- using the vault strategy name.
--
-- The vault configuration is stored in Kong configuration from which this
-- function derives the default base configuration for the vault strategy.
--
-- @local
-- @function get_vault_name_and_config_by_name
-- @tparam string name The unique name of the vault strategy
-- @treturn string name of the vault strategy (same as the input string)
-- @treturn nil this never fails so it always returns `nil`
-- @treturn table|nil the vault strategy's base config derived from Kong configuration
--
-- @usage
-- local name, err, base_config = get_vault_name_and_config_by_name("env")
local function get_vault_name_and_config_by_name(name)
-- base config stays the same so we can cache it
local base_config = CONFIGS[name]
if not base_config then
base_config = {}
if self and self.configuration then
local configuration = self.configuration
local env_name = replace_dashes(name)
local _, err, schema = get_vault_strategy_and_schema(name)
if not schema then
return nil, err
end
for k, f in schema:each_field() do
-- n is the entry in the kong.configuration table, for example
-- KONG_VAULT_ENV_PREFIX will be found in kong.configuration
-- with a key "vault_env_prefix". Environment variables are
-- thus turned to lowercase and we just treat any "-" in them
-- as "_". For example if your custom vault was called "my-vault"
-- then you would configure it with KONG_VAULT_MY_VAULT_<setting>
-- or in kong.conf, where it would be called
-- "vault_my_vault_<setting>".
local n = lower(fmt("vault_%s_%s", env_name, replace_dashes(k)))
local v = configuration[n]
v = arguments.infer_value(v, f)
-- TODO: should we be more visible with validation errors?
-- In general it would be better to check the references
-- and not just a format when they are stored with admin
-- API, or in case of process secrets, when the kong is
-- started. So this is a note to remind future us.
-- Because current validations are less strict, it is fine
-- to ignore it here.
if v ~= nil and schema:validate_field(f, v) then
base_config[k] = v
elseif f.required and f.default ~= nil then
base_config[k] = f.default
end
end
CONFIGS[name] = base_config
end
end
return name, nil, base_config
end
---
-- This function retrieves a vault entity by its prefix from configuration
-- database, and returns the strategy name and the base configuration.
--
-- It either fetches the vault from a cache or directly from a configuration
-- database. The vault entity is expected to be found in a database (db) or
-- cache. If not found, an error message is returned.
--
-- @local
-- @function get_vault_name_and_config_by_prefix
-- @tparam string prefix the unique identifier of the vault entity to be retrieved
-- @treturn string|nil name of the vault strategy
-- @treturn string|nil a string describing an error if there was one
-- @treturn table|nil the vault entity config
--
-- @usage
-- local name, err, base_config = get_vault_name_and_config_by_prefix("my-vault")
local function get_vault_name_and_config_by_prefix(prefix)
if not (self and self.db) then
return nil, "unable to retrieve config from db"
end
-- find a vault - it can be either a named vault that needs to be loaded from the cache, or the
-- vault type accessed by name
local cache = self.core_cache
local vaults = self.db.vaults
local vault, err
if cache then
local vault_cache_key = vaults:cache_key(prefix)
vault, err = cache:get(vault_cache_key, nil, vaults.select_by_prefix, vaults, prefix)
else
vault, err = vaults:select_by_prefix(prefix)
end
if not vault then
if err then
self.log.notice("could not find vault (", prefix, "): ", err)
end
return nil, fmt("could not find vault (%s)", prefix)
end
return vault.name, nil, vault.config
end
---
-- Function `get_vault_name_and_base_config` retrieves name of the strategy
-- and its base configuration using name (for default vaults) or prefix for
-- database stored vault entities.
--
-- @local
-- @function get_vault_name_and_base_config
-- @tparam string name_or_prefix name of the vault strategy or prefix of the vault entity
-- @treturn string|nil name of the vault strategy
-- @treturn string|nil a string describing an error if there was one
-- @treturn table|nil the base configuration
--
-- @usage
-- local name, err, base_config = get_vault_name_and_base_config("env")
local function get_vault_name_and_base_config(name_or_prefix)
if VAULT_NAMES[name_or_prefix] then
return get_vault_name_and_config_by_name(name_or_prefix)
end
return get_vault_name_and_config_by_prefix(name_or_prefix)
end
---
-- Function `get_strategy` processes a reference to retrieve a strategy and configuration settings.
--
-- The function first parses the reference. Then, it gets the strategy, the schema, and the base configuration
-- settings for the vault based on the parsed reference. It checks the license type if required by the strategy.
-- Finally, it gets the configuration and the cache key of the reference.
--
-- @local
-- @function get_strategy
-- @tparam string reference the reference to be used to load strategy and its settings.
-- @tparam table|nil strategy the strategy used to fetch the secret
-- @treturn string|nil a string describing an error if there was one
-- @treturn table|nil the vault configuration for the reference
-- @treturn string|nil the cache key for shared dictionary for the reference
-- @treturn table|nil the parsed reference
--
-- @usage
-- local strategy, err, config, cache_key, parsed_reference = get_strategy(reference)
local function get_strategy(reference)
local parsed_reference, err = parse_reference(reference)
if not parsed_reference then
return nil, err
end
local name, err, base_config = get_vault_name_and_base_config(parsed_reference.name)
if not name then
return nil, err
end
local strategy, err, schema = get_vault_strategy_and_schema(name)
if not strategy then
return nil, err
end
if strategy.license_required and self.licensing and self.licensing:license_type() == "free" then
return nil, "vault " .. name .. " requires a license to be used"
end
local config, config_hash = get_vault_config_and_hash(name, schema, base_config, parsed_reference.config)
local cache_key = build_cache_key(reference, config_hash)
return strategy, nil, config, cache_key, parsed_reference, config_hash
end
---
-- Invokes a provided strategy to fetch a secret.
--
-- This function invokes a strategy provided to it to retrieve a secret from a vault.
-- The secret returned by the strategy must be a string containing a string value,
-- or JSON string containing the required key with a string value.
--
-- @local
-- @function invoke_strategy
-- @tparam table strategy the strategy used to fetch the secret
-- @tparam config the configuration required by the strategy
-- @tparam parsed_reference a table containing the resource name, the version of the secret
-- to be fetched, and optionally a key to search on returned JSON string
-- @treturn string|nil the value of the secret, or `nil`
-- @treturn string|nil a string describing an error if there was one
-- @treturn number|nil a ttl (time to live) of the fetched secret if there was one
--
-- @usage
-- local value, err, ttl = invoke_strategy(strategy, config, parsed_reference)
local function invoke_strategy(strategy, config, parsed_reference)
local value, err, ttl = strategy.get(config, parsed_reference.resource, parsed_reference.version)
if value == nil then
if err then
return nil, fmt("no value found (%s)", err)
end
return nil, "no value found"
elseif type(value) ~= "string" then
return nil, fmt("value returned from vault has invalid type (%s), string expected", type(value))
end
-- in vault reference, the secret can have multiple values, each stored under a key.
-- The vault returns a JSON string that contains an object which can be indexed by the key.
local key = parsed_reference.key
if key then
value, err = extract_key_from_json_string(value, key)
if not value then
return nil, fmt("could not get subfield value: %s", err)
end
end
return value, nil, ttl
end
---
-- Function `get_cache_value_and_ttl` returns a value for caching and its ttl
--
-- @local
-- @function get_cache_value_and_ttl
-- @tparam string value the vault returned value for a reference
-- @tparam table config the configuration settings to be used
-- @tparam[opt] number ttl the possible vault returned ttl
-- @treturn string value to be stored in shared dictionary
-- @treturn number shared dictionary ttl
-- @treturn number lru ttl
-- @usage local cache_value, shdict_ttl, lru_ttl = get_cache_value_and_ttl(value, config, ttl)
local function get_cache_value_and_ttl(value, config, ttl)
local cache_value, shdict_ttl, lru_ttl
if value then
cache_value = value
-- adjust ttl to the minimum and maximum values configured
ttl = adjust_ttl(ttl, config)
if config.resurrect_ttl then
lru_ttl = min(ttl + config.resurrect_ttl, DAO_MAX_TTL)
shdict_ttl = max(lru_ttl, SECRETS_CACHE_MIN_TTL)
else
lru_ttl = ttl
shdict_ttl = DAO_MAX_TTL
end
else
cache_value = NEGATIVELY_CACHED_VALUE
-- negatively cached values will be rotated on each rotation interval
shdict_ttl = max(config.neg_ttl or 0, SECRETS_CACHE_MIN_TTL)
end
return cache_value, shdict_ttl, lru_ttl
end
---
-- Function `get_from_vault` retrieves a value from the vault using the provided strategy.
--
-- The function first retrieves a value from the vault and its optionally returned ttl.
-- It then adjusts the ttl within configured bounds, stores the value in the SHDICT cache
-- with a ttl that includes a resurrection time, and stores the value in the LRU cache with
-- the adjusted ttl.
--
-- @local
-- @function get_from_vault
-- @tparam string reference the vault reference string
-- @tparam table strategy the strategy to be used to retrieve the value from the vault
-- @tparam table config the configuration settings to be used
-- @tparam string cache_key the cache key used for shared dictionary cache
-- @tparam table parsed_reference the parsed reference
-- @treturn string|nil the retrieved value from the vault, of `nil`
-- @treturn string|nil a string describing an error if there was one
-- @usage local value, err = get_from_vault(reference, strategy, config, cache_key, parsed_reference)
local function get_from_vault(reference, strategy, config, cache_key, parsed_reference)
local value, err, ttl = invoke_strategy(strategy, config, parsed_reference)
local cache_value, shdict_ttl, lru_ttl = get_cache_value_and_ttl(value, config, ttl)
local ok, cache_err = SECRETS_CACHE:safe_set(cache_key, cache_value, shdict_ttl)
if not ok then
return nil, cache_err
end
if cache_value == NEGATIVELY_CACHED_VALUE then
return nil, fmt("could not get value from external vault (%s)", err)
end
LRU:set(reference, cache_value, lru_ttl)
return cache_value
end
---
-- Function `get` retrieves a value from local (LRU), shared dictionary (SHDICT) cache.
--
-- If the value is not found in these caches and `cache_only` is not `truthy`,
-- it attempts to retrieve the value from a vault.
--
-- @local
-- @function get
-- @tparam string reference the reference key to lookup
-- @tparam boolean cache_only optional boolean flag (if set to `true`,
-- the function will not attempt to retrieve the value from the vault)
-- @treturn string the retrieved value corresponding to the provided reference,
-- or `nil` (when found negatively cached, or in case of an error)
-- @treturn string a string describing an error if there was one
--
-- @usage
-- local value, err = get(reference, cache_only)
local function get(reference, cache_only)
-- the LRU stale value is ignored
local value = LRU:get(reference)
if value then
return value
end
local strategy, err, config, cache_key, parsed_reference = get_strategy(reference)
if not strategy then
return nil, err
end
value = SECRETS_CACHE:get(cache_key)
if cache_only and not value then
return nil, "could not find cached value"
end
if value == NEGATIVELY_CACHED_VALUE then
return nil
end
if not value then
return get_from_vault(reference, strategy, config, cache_key, parsed_reference)
end
-- if we have something in the node-level cache, but not in the worker-level
-- cache, we should update the worker-level cache. Use the remaining TTL from the SHDICT
local lru_ttl = (SECRETS_CACHE:ttl(cache_key) or 0) - (config.resurrect_ttl or DAO_MAX_TTL)
-- only do that when the TTL is greater than 0.
if lru_ttl > 0 then
LRU:set(reference, value, lru_ttl)
end
return value
end
---
-- In place updates record's field from a cached reference.
--
-- @local
-- @function update_from_cache
-- @tparam string reference reference to look from the caches
-- @tparam table record record which field is updated from caches
-- @tparam string field name of the field
--
-- @usage
-- local record = { field = "old-value" }
-- update_from_cache("{vault://env/example}", record, "field" })
local function update_from_cache(reference, record, field)
local value, err = get(reference, true)
if not value then
self.log.warn("error updating secret reference ", reference, ": ", err)
end
record[field] = value or ""
end
---
-- Recurse over config and calls the callback for each found reference.
--
-- @local
-- @function recurse_config_refs
-- @tparam table config config table to recurse.
-- @tparam function callback callback to call on each reference.
-- @treturn table config that might have been updated, depending on callback.
local function recurse_config_refs(config, callback)
-- silently ignores other than tables
if type(config) ~= "table" then
return config
end
for key, value in pairs(config) do
if key ~= "$refs" and type(value) == "table" then
recurse_config_refs(value, callback)
end
end
local references = config["$refs"]
if type(references) ~= "table" or isempty(references) then
return config
end
for name, reference in pairs(references) do
if type(reference) == "string" then -- a string reference
callback(reference, config, name)
elseif type(reference) == "table" then -- array, set or map of references
for key, ref in pairs(reference) do
callback(ref, config[name], key)
end
end
end
return config
end
---
-- Function `update` recursively updates a configuration table.
--
-- This function recursively in-place updates a configuration table by
-- replacing reference fields with values fetched from a cache. The references
-- are specified in a `$refs` field.
--
-- If a reference cannot be fetched from the cache, the corresponding field is
-- set to nil and an warning is logged.
--
-- @local
-- @function update
-- @tparam table config a table representing the configuration to update (if `config`
-- is not a table, the function immediately returns it without any modifications)
-- @treturn table the config table (with possibly updated values).
--
-- @usage
-- local config = update(config)
-- OR
-- update(config)
local function update(config)
return recurse_config_refs(config, update_from_cache)
end
---
-- Function `get_references` recursively iterates over options and returns
-- all the references in an array. The same reference is in array only once.
--
-- @local
-- @function get_references
-- @tparam table options the options to look for the references
-- @tparam[opt] table references internal variable that is used for recursion
-- @tparam[opt] collected references internal variable that is used for recursion
-- @treturn table an array of collected references
--
-- @usage
-- local references = get_references({
-- username = "john",
-- password = "doe",
-- ["$refs"] = {
-- username = "{vault://aws/database/username}",
-- password = "{vault://aws/database/password}",
-- }
-- })
local function get_references(options, references, collected)
references = references or {}
collected = collected or { n = 0 }
if type(options) ~= "table" then
return references
end
for key, value in pairs(options) do
if key ~= "$refs" and type(value) == "table" then
get_references(value, references, collected)
end
end
local refs = options["$refs"]
if type(refs) ~= "table" or isempty(refs) then
return references
end
for _, reference in pairs(refs) do
if type(reference) == "string" then -- a string reference