Skip to content

kong.vault.get() dedupes pending init-phase secrets by the wrong table key #14965

Description

@Adityaj0

Is there an existing issue for this?

  • I have searched the existing issues

Kong version

Reproduced by code inspection against master @ 391ee48.

Current Behavior

kong.vault.get() (kong/pdk/vault.lua) is documented to postpone secret resolution to a timer during the init phase, tracking pending references in a local INIT_SECRETS table so they can be resolved later by rotate_secrets_init_worker:

local strategy, err, config, cache_key, parsed_reference = get_strategy(reference)
if not strategy then
  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

get_strategy(reference) only returns a cache_key on its success path (every failure branch inside it returns return nil, err, i.e. only 2 values). Since this whole block only executes when strategy is nil (i.e. get_strategy failed), cache_key is always nil here. The guard if not INIT_SECRETS[cache_key] then is therefore always INIT_SECRETS[nil], which is always falsy — the guard is always true, even though the entry is stored under INIT_SECRETS[reference].

As a result, every kong.vault.get() call for the same unresolved reference during the init phase (the documented, expected case when using KONG_DECLARATIVE_CONFIG before LMDB is populated) appends a duplicate entry to INIT_SECRETS, instead of deduplicating by reference. In a declarative config where the same {vault://...} reference is reused across multiple entities/fields (e.g. a shared TLS cert/key reference), INIT_SECRETS grows with one entry per lookup instead of one entry per distinct reference.

INIT_SECRETS is later passed to rotate_references(INIT_SECRETS), so this causes redundant vault resolution work proportional to the number of lookups rather than the number of distinct references, and unbounded growth of the table for configs with many repeated references.

The equivalent branch for the init_worker phase a few lines below is correct, and uses the right key (valid there since it runs on get_strategy's success path):

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

Expected Behavior

INIT_SECRETS should contain one entry per distinct reference, matching the key it's actually stored under.

Steps To Reproduce

Code-path confirmation: get_strategy's only return nil, ... paths never include a 4th cache_key return value, so at the call site, cache_key is unconditionally nil whenever strategy is nil. This can be confirmed by reading kong/pdk/vault.lua lines ~680-704 (get_strategy) against lines ~884-895 (the init-phase branch of get).

At a system level: start Kong with KONG_DECLARATIVE_CONFIG referencing the same {vault://...} secret from multiple entities/fields, before the declarative config has been loaded into LMDB — INIT_SECRETS will accumulate one entry per lookup of that reference rather than a single deduplicated entry.

Anything else?

I have a fix ready and will open a pull request referencing this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions