项目文件夹

文件
2026-07-13 12:32:21 +08:00

1746 行
57 KiB
Lua

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
-- 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 cjson = require("cjson.safe").new()
local yield = require("kong.tools.yield").yield
local get_updated_now_ms = require("kong.tools.time").get_updated_now_ms
local replace_dashes = require("kong.tools.string").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("/")
local BYT_V = byte("v")
local BYT_A = byte("a")
local BYT_U = byte("u")
local BYT_L = byte("l")
local BYT_T = byte("t")
local VAULT_QUERY_OPTS = { workspace = ngx.null }
---
-- 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, 2) == BYT_V
and byte(reference, 3) == BYT_A
and byte(reference, 4) == BYT_U
and byte(reference, 5) == BYT_L
and byte(reference, 6) == BYT_T
and byte(reference, 7) == COLON
and byte(reference, 8) == SLASH
and byte(reference, 9) == SLASH
and byte(reference, -1) == BRACE_END
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 == 0 then
return nil, fmt("reference url has invalid path [%s]", reference)
elseif count == 1 then
resource = unescape_uri(parts[1])
elseif parts.is_directory then
resource = unescape_uri(concat(parts, "/", 1, count))
else
resource = unescape_uri(concat(parts, "/", 1, count - 1))
key = unescape_uri(parts[count])
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 INIT_SECRETS = {}
local INIT_WORKER_SECRETS = {}
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, VAULT_QUERY_OPTS)
else
vault, err = vaults:select_by_prefix(prefix, VAULT_QUERY_OPTS)
end
if not vault then
if err then
return nil, fmt("could not find vault (%s): %s", 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
-- shared dict ttl controls when the secret
-- value will be refreshed by `rotate_secrets`
-- timer. If a secret whose remaining time is less
-- than `config.resurrect_ttl`(or DAO_MAX_TTL
-- if not configured), it could possibly
-- be updated in every cycle of `rotate_secrets`.
--
-- The shdict_ttl should be
-- `config.ttl` + `config.resurrect_ttl`
-- to make sure the secret value persists for
-- at least `config.ttl` seconds.
-- When `config.resurrect_ttl` is not set and
-- `config.ttl` is not set, shdict_ttl will be
-- DAO_MAX_TTL * 2; when `config.resurrect_ttl`
-- is not set but `config.ttl` is set, shdict_ttl
-- will be ttl + DAO_MAX_TTL
shdict_ttl = 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
-- @treturn boolean|nil whether to resurrect value in case vault errors or doesn't return value
-- @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, resurrect)
local value, err, ttl = invoke_strategy(strategy, config, parsed_reference)
if resurrect and value == nil then
local resurrected_value = SECRETS_CACHE:get(cache_key)
if resurrected_value then
return resurrected_value
else
return nil, fmt("could not get value from external vault (%s)", err)
end
end
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.
--
-- On init worker phase the resolving of the secrets is postponed to a timer,
-- and in this case the function returns `""` when it fails to find a value
-- in a cache. This is because of current limitations in platform that disallows
-- using cosockets/coroutines in that phase.
--
-- @local
-- @function get
-- @tparam string reference the reference key to lookup
-- @tparam[opt] 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
-- this can fail on init as the lmdb cannot be accessed and secondly,
-- because the data is not yet inserted into LMDB when using KONG_DECLARATIVE_CONFIG.
if get_phase() == "init" then
if not INIT_SECRETS[cache_key] then
INIT_SECRETS[reference] = true
INIT_SECRETS[#INIT_SECRETS + 1] = reference
end
return ""
end
return nil, err
end
value = SECRETS_CACHE:get(cache_key)
if value == NEGATIVELY_CACHED_VALUE then
return nil
end
if not value then
if cache_only then
return nil, "could not find cached value"
end
-- this can fail on init worker as there is no cosockets / coroutines available
if get_phase() == "init_worker" then
if not INIT_WORKER_SECRETS[cache_key] then
INIT_WORKER_SECRETS[cache_key] = true
INIT_WORKER_SECRETS[#INIT_WORKER_SECRETS + 1] = cache_key
end
return ""
end
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 err 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
if not collected[reference] then
collected[reference] = true
collected.n = collected.n + 1
references[collected.n] = reference
end
elseif type(reference) == "table" then -- array, set or map of references
for _, ref in pairs(reference) do
if not collected[ref] then
collected[ref] = true
collected.n = collected.n + 1
references[collected.n] = ref
end
end
end
end
return references
end
---
-- Function `get_sorted_references` recursively iterates over options and returns
-- all the references in an sorted array. The same reference is in array only once.
--
-- @local
-- @function get_sorted_references
-- @tparam table options the options to look for the references
-- @treturn table|nil an sorted array of collected references, return `nil` in case no references were found.
--
-- @usage
-- local references = get_sorted_references({
-- username = "john",
-- password = "doe",
-- ["$refs"] = {
-- username = "{vault://aws/database/username}",
-- password = "{vault://aws/database/password}",
-- }
-- })
local function get_sorted_references(options)
local references = get_references(options)
if isempty(references) then
return
end
sort(references)
return references
end
---
-- Function `rotate_reference` rotates a secret reference.
--
-- @local
-- @function rotate_reference
-- @tparam string reference the reference to rotate
-- @tparam function the caching strategy created with `get_caching_strategy` function
-- @treturn true|nil `true` after successfully rotating a secret, otherwise `nil`
-- @treturn string|nil a string describing an error if there was one
local function rotate_reference(reference, caching_strategy)
local strategy, err, config, new_cache_key, parsed_reference, config_hash = get_strategy(reference)
if not strategy then
return nil, fmt("could not parse reference %s (%s)", reference, err)
end
strategy = caching_strategy(strategy, config_hash)
local ok, err = get_from_vault(reference, strategy, config, new_cache_key, parsed_reference)
if not ok then
return nil, fmt("could not retrieve value for reference %s (%s)", reference, err)
end
return true
end
---
-- Function `rotate_references` rotates the references passed in as an array.
--
-- @local
-- @function rotate_references
-- @tparam table references an array of references to rotate
-- @treturn boolean `true` after it has finished rotation over all the references
local function rotate_references(references)
local phase = get_phase()
local caching_strategy = get_caching_strategy()
for _, reference in ipairs(references) do
yield(true, phase)
local ok, err = rotate_reference(reference, caching_strategy)
if not ok then
self.log.warn(err)
end
end
return true
end
---
-- Function `execute_callback` updates options and then executes the callback
--
-- @local
-- @function execute_callback
-- @tparam function callback the callback to execute
-- @tparam table the callback options to be passed to callback (after updating them)
-- @treturn any the callback return value
-- @treturn string|nil a string describing an error if there was one
local function execute_callback(callback, options)
update(options)
return callback(options)
end
---
-- Function `try` attempts to execute a provided callback function with the provided options.
--
-- If the callback function fails, the `try` function will attempt to resolve references and update
-- the values in the options table before re-attempting the callback function.
--
-- @local
-- @function try
-- @tparam function callback the callback function to execute that takes options table as its argument
-- @tparam table options the options table to provide to the callback function.
-- @treturn any the result of the callback function if it succeeds, otherwise `nil`
-- @treturn string|nil a string describing an error if there was one
--
-- @usage
-- local function connect(options)
-- return database_connect(options)
-- end
--
-- local connection, err = try(connect, {
-- username = "john",
-- password = "doe",
-- ["$refs"] = {
-- username = "{vault://aws/database/username}",
-- password = "{vault://aws/database/password}",
-- }
-- })
local function try(callback, options)
local references = get_sorted_references(options)
if not references then
-- We cannot retry, so let's just call the callback and return
return callback(options)
end
local name = "vault.try:" .. calculate_hash(concat(references, "."))
local old_updated_at = RETRY_LRU:get(name) or 0
-- Try to execute the callback with the current options
local res = execute_callback(callback, options)
if res then
return res -- If the callback succeeds, return the result
end
-- Check if options were updated while executing callback
local new_updated_at = RETRY_LRU:get(name) or 0
if old_updated_at ~= new_updated_at then
return execute_callback(callback, options)
end
-- Is it worth to have node level mutex instead?
-- If so, the RETRY_LRU also needs to be node level.
concurrency.with_coroutine_mutex({
name = name,
timeout = ROTATION_INTERVAL,
}, function()
-- Check if references were updated while waiting for a lock
new_updated_at = RETRY_LRU:get(name) or 0
if old_updated_at ~= new_updated_at then
return -- already updated
end
rotate_references(references)
RETRY_LRU:set(name, get_updated_now_ms())
end)
-- Call the callback the second time
-- (may be same options as before, but not worth to optimize)
return execute_callback(callback, options)
end
---
-- Function `rotate_secret` rotates a secret reference.
--
-- @local
-- @function rotate_secret
-- @tparam string old_cache_key old cache key
-- @tparam function the caching strategy created with `get_caching_strategy` function
-- @treturn true|nil `true` after successfully rotating a secret, otherwise `nil`
-- @treturn string|nil a string describing an error if there was one
local function rotate_secret(old_cache_key, caching_strategy)
local reference, err = parse_cache_key(old_cache_key)
if not reference then
-- invalid cache keys are removed (in general should never happen)
SECRETS_CACHE:delete(old_cache_key)
return nil, err
end
local strategy, err, config, new_cache_key, parsed_reference, config_hash = get_strategy(reference)
if not strategy then
-- invalid cache keys are removed (e.g. a vault entity could have been removed)
SECRETS_CACHE:delete(old_cache_key)
return nil, fmt("could not parse reference %s (%s)", reference, err)
end
if old_cache_key ~= new_cache_key then
-- config has changed, thus the old cache key can be removed
SECRETS_CACHE:delete(old_cache_key)
end
-- The ttl for this key, is the TTL + the resurrect time
-- If the TTL is still greater than the resurrect time
-- we don't have to rotate the secret, except it if it
-- negatively cached.
local resurrect
local ttl = SECRETS_CACHE:ttl(new_cache_key)
if ttl and SECRETS_CACHE:get(new_cache_key) ~= NEGATIVELY_CACHED_VALUE then
local resurrect_ttl = max(config.resurrect_ttl or DAO_MAX_TTL, SECRETS_CACHE_MIN_TTL)
-- the secret is still within ttl, no need to refresh
if ttl > resurrect_ttl then
return true
end
-- the secret is still within resurrect ttl time, so when we try to refresh the secret
-- we do not forciblly override it with a negative value, so that the cached value
-- can be resurrected
resurrect = ttl > SECRETS_CACHE_MIN_TTL
end
strategy = caching_strategy(strategy, config_hash)
-- try to refresh the secret, according to the remaining time the cached value may or may not be refreshed.
local ok, err = get_from_vault(reference, strategy, config, new_cache_key, parsed_reference, resurrect)
if not ok then
return nil, fmt("could not retrieve value for reference %s (%s)", reference, err)
end
return true
end
---
-- Function `rotate_secrets` rotates the secrets.
--
-- It iterates over all keys in the secrets and, if a key corresponds to a reference and the
-- ttl of the key is less than or equal to the resurrection period, it refreshes the value
-- associated with the reference.
--
-- @local
-- @function rotate_secrets
-- @tparam table secrets the secrets to rotate
-- @treturn boolean `true` after it has finished iterating over all keys in the secrets
local function rotate_secrets(secrets)
local phase = get_phase()
local caching_strategy = get_caching_strategy()
for _, cache_key in ipairs(secrets) do
yield(true, phase)
local ok, err = rotate_secret(cache_key, caching_strategy)
if not ok then
self.log.notice(err)
end
end
return true
end
---
-- Function `rotate_secrets_cache` rotates the secrets in the shared dictionary cache.
--
-- @local
-- @function rotate_secrets_cache
-- @treturn boolean `true` after it has finished iterating over all keys in the shared dictionary cache
local function rotate_secrets_cache()
return rotate_secrets(SECRETS_CACHE:get_keys(0))
end
---
-- Function `rotate_secrets_init_worker` rotates the secrets in init worker cache
--
-- On init worker the secret resolving is postponed to a timer because init worker
-- cannot cosockets / coroutines, and there is no other workaround currently.
--
-- @local
-- @function rotate_secrets_init_worker
-- @treturn boolean `true` after it has finished iterating over all keys in the init worker cache
local function rotate_secrets_init_worker()
local _, err, err2
if INIT_SECRETS then
_, err = rotate_references(INIT_SECRETS)
end
if INIT_WORKER_SECRETS then
_, err2 = rotate_secrets(INIT_WORKER_SECRETS)
end
if err or err2 then
return nil, err or err2
end
return true
end
---
-- A secrets rotation timer handler.
--
-- Uses a node-level mutex to prevent multiple threads/workers running it the same time.
--
-- @local
-- @function rotate_secrets_timer
-- @tparam boolean premature `true` if server is shutting down
-- @tparam[opt] boolean init `true` when this is a one of init_worker timer run
-- By default rotates the secrets in shared dictionary cache.
local function rotate_secrets_timer(premature, init)
if premature then
return true
end
local ok, err = concurrency.with_worker_mutex(ROTATION_MUTEX_OPTS, init and rotate_secrets_init_worker or rotate_secrets_cache)
if not ok and err ~= "timeout" then
self.log.err("rotating secrets failed (", err, ")")
end
if init then
INIT_SECRETS = nil
INIT_WORKER_SECRETS = nil
end
return true
end
---
-- Flushes LRU caches and forcibly rotates the secrets.
--
-- This is only ever executed on traditional nodes.
--
-- @local
-- @function handle_vault_crud_event
-- @tparam table data event data
local function handle_vault_crud_event(data)
local cache = self.core_cache
if cache then
local vaults = self.db.vaults
local old_entity = data.old_entity
local old_prefix
if old_entity then
old_prefix = old_entity.prefix
if old_prefix and old_prefix ~= ngx.null then
cache:invalidate(vaults:cache_key(old_prefix))
end
end
local entity = data.entity
if entity then
local prefix = entity.prefix
if prefix and prefix ~= ngx.null and prefix ~= old_prefix then
cache:invalidate(vaults:cache_key(prefix))
end
end
end
LRU:flush_all()
-- refresh all the secrets
local _, err = self.timer:named_at("secret-rotation-on-crud-event", 0, rotate_secrets_timer)
if err then
self.log.err("could not schedule timer to rotate vault secret references on crud event: ", err)
end
end
local function should_register_crud_event()
local conf = self.configuration
local not_dbless = conf.database ~= "off" -- postgres
local dp_with_rpc_sync = conf.role == "data_plane" and
conf.cluster_rpc_sync
return not_dbless or dp_with_rpc_sync
end
local initialized
---
-- Initializes vault.
--
-- Registers event handlers and starts a recurring secrets
-- rotation timer. It does nothing on control planes.
--
-- @local
-- @function init_worker
local function init_worker()
if initialized then
return
end
initialized = true
if should_register_crud_event() then
self.worker_events.register(handle_vault_crud_event, "crud", "vaults")
end
local _, err = self.timer:named_every("secret-rotation", ROTATION_INTERVAL, rotate_secrets_timer)
if err then
self.log.err("could not schedule timer to rotate vault secret references: ", err)
end
local _, err = self.timer:named_at("secret-rotation-on-init", 0, rotate_secrets_timer, true)
if err then
self.log.err("could not schedule timer to rotate vault secret references on init: ", err)
end
end
---
-- Called on `init` phase, and stores value in secrets cache.
--
-- @local
-- @function init_in_cache_from_value
-- @tparam string reference a vault reference.
-- @tparan value string value that is stored in secrets cache.
local function init_in_cache_from_value(reference, value)
local strategy, err, config, cache_key = get_strategy(reference)
if not strategy then
return nil, err
end
-- doesn't support vault returned ttl, but none of the vaults supports it,
-- and the support for vault returned ttl might be removed later.
local cache_value, shdict_ttl, lru_ttl = get_cache_value_and_ttl(value, config)
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
LRU:set(reference, cache_value, lru_ttl)
end
return true
end
---
-- Called on `init` phase, and used to warmup secrets cache.
--
-- @local
-- @function init_in_cache
-- @tparam string reference a vault reference.
-- @tparan table record a table that is a container for de-referenced value.
-- @tparam field string field name in a record to which to store the de-referenced value.
local function init_in_cache(reference, record, field)
local value, err = init_in_cache_from_value(reference, record[field])
if not value then
self.log.warn("error caching secret reference ", reference, ": ", err)
end
end
---
-- Called on `init` phase, and used to warmup secrets cache.
-- @local
-- @function init
local function init()
recurse_config_refs(self.configuration, init_in_cache)
end
local _VAULT = {} -- the public PDK interfaces
---
-- Flush vault LRU cache and start a timer to rotate secrets.
--
-- @local
-- @function kong.vault.flush
--
-- @usage
-- kong.vault.flush()
function _VAULT.flush()
LRU:flush_all()
-- refresh all the secrets
local _, err = self.timer:named_at("secret-rotation-on-flush", 0, rotate_secrets_timer)
if err then
self.log.err("could not schedule timer to rotate vault secret references: ", err)
end
end
---
-- Checks if the passed in reference looks like a reference.
-- Valid references start with '{vault://' and end with '}'.
--
-- If you need more thorough validation,
-- use `kong.vault.parse_reference`.
--
-- @function kong.vault.is_reference
-- @tparam string reference reference to check
-- @treturn boolean `true` is the passed in reference looks like a reference, otherwise `false`
--
-- @usage
-- kong.vault.is_reference("{vault://env/key}") -- true
-- kong.vault.is_reference("not a reference") -- false
function _VAULT.is_reference(reference)
return is_reference(reference)
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
-- }
-- ```
--
-- @function kong.vault.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`
--
-- @usage
-- local ref, err = kong.vault.parse_reference("{vault://env/cert/key?prefix=SSL_#1}") -- table
function _VAULT.parse_reference(reference)
return parse_reference(reference)
end
---
-- Resolves the passed in reference and returns the value of it.
--
-- @function kong.vault.get
-- @tparam string reference reference to resolve
-- @treturn string|nil resolved value of the reference
-- @treturn string|nil error message on failure, otherwise `nil`
--
-- @usage
-- local value, err = kong.vault.get("{vault://env/cert/key}")
function _VAULT.get(reference)
return get(reference)
end
---
-- Helper function for secret rotation based on TTLs. Currently experimental.
--
-- @function kong.vault.update
-- @tparam table options options containing secrets and references (this function modifies the input options)
-- @treturn table options with updated secret values
--
-- @usage
-- local options = kong.vault.update({
-- cert = "-----BEGIN CERTIFICATE-----...",
-- key = "-----BEGIN RSA PRIVATE KEY-----...",
-- cert_alt = "-----BEGIN CERTIFICATE-----...",
-- key_alt = "-----BEGIN EC PRIVATE KEY-----...",
-- ["$refs"] = {
-- cert = "{vault://aws/cert}",
-- key = "{vault://aws/key}",
-- cert_alt = "{vault://aws/cert-alt}",
-- key_alt = "{vault://aws/key-alt}",
-- }
-- })
--
-- -- or
--
-- local options = {
-- cert = "-----BEGIN CERTIFICATE-----...",
-- key = "-----BEGIN RSA PRIVATE KEY-----...",
-- cert_alt = "-----BEGIN CERTIFICATE-----...",
-- key_alt = "-----BEGIN EC PRIVATE KEY-----...",
-- ["$refs"] = {
-- cert = "{vault://aws/cert}",
-- key = "{vault://aws/key}",
-- cert_alt = "{vault://aws/cert-alt}",
-- key_alt = "{vault://aws/key-alt}",
-- }
-- }
-- kong.vault.update(options)
function _VAULT.update(options)
return update(options)
end
---
-- Helper function for automatic secret rotation. Currently experimental.
--
-- @function kong.vault.try
-- @tparam function callback callback function
-- @tparam table options options containing credentials and references
-- @treturn string|nil return value of the callback function
-- @treturn string|nil error message on failure, otherwise `nil`
--
-- @usage
-- local function connect(options)
-- return database_connect(options)
-- end
--
-- local connection, err = kong.vault.try(connect, {
-- username = "john",
-- password = "doe",
-- ["$refs"] = {
-- username = "{vault://aws/database-username}",
-- password = "{vault://aws/database-password}",
-- }
-- })
function _VAULT.try(callback, options)
return try(callback, options)
end
---
-- Initializes vault.
--
-- Registers event handlers (on non-dbless nodes) and starts a recurring secrets
-- rotation timer. Does nothing on control planes.
--
-- @local
-- @function kong.vault.init_worker
function _VAULT.init_worker()
init_worker()
end
---
-- Warmups vault caches from config.
--
-- @local
-- @function kong.vault.warmup
function _VAULT.warmup(input)
for k, v in pairs(input) do
local kt = type(k)
if kt == "table" then
_VAULT.warmup(k)
elseif kt == "string" and is_reference(k) then
get(k)
end
local vt = type(v)
if vt == "table" then
_VAULT.warmup(v)
elseif vt == "string" and is_reference(v) then
get(v)
end
end
end
if get_phase() == "init" then
init()
end
return _VAULT
end
return {
new = new,
is_reference = is_reference,
parse_reference = parse_reference,
}