From 14fc0f1c9100305e4c53920793e82a3672c89a92 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 23 Jul 2026 22:34:43 +0545 Subject: [PATCH] fix(secret): log an error when a secret reference cannot be resolved An unresolvable $secret:// or $env:// reference silently falls back to the literal reference string, which is then used verbatim as a password, key or token. On the cached path (used by plugin conf resolution) the underlying error was swallowed entirely, so a missing secret manager or an unset environment variable produced no diagnostic at all. Report the failure with the field name and the underlying error, and drop the now-duplicated log in the uncached branch. --- apisix/secret.lua | 16 ++-- docs/en/latest/terminology/secret.md | 2 + docs/zh/latest/terminology/secret.md | 2 + t/secret/central-secret-refs.t | 132 +++++++++++++++++++++++++++ t/secret/secret_lru.t | 2 + 5 files changed, 147 insertions(+), 7 deletions(-) diff --git a/apisix/secret.lua b/apisix/secret.lua index f25daa647c9d..143f2e37b215 100644 --- a/apisix/secret.lua +++ b/apisix/secret.lua @@ -225,12 +225,7 @@ local function fetch(uri, use_cache) end if not use_cache then - local val, err = fetch_by_uri(uri) - if err then - core.log.error("failed to fetch secret value: ", err) - return nil - end - return val + return fetch_by_uri(uri) end -- pass the secrets conf_version so the cache re-resolves when /secrets changes @@ -242,7 +237,14 @@ local function retrieve_refs(refs, use_cache) for k, v in pairs(refs) do local typ = type(v) if typ == "string" then - refs[k] = fetch(v, use_cache) or v + local val, err = fetch(v, use_cache) + if val == nil and _M.is_secret_ref(v) then + -- an unresolved reference is kept as-is, so it would otherwise + -- be used verbatim as a password, key or token + core.log.error("failed to resolve secret reference: ", v, + ", field: ", k, ", err: ", err or "no value") + end + refs[k] = val or v elseif typ == "table" then retrieve_refs(v, use_cache) end diff --git a/docs/en/latest/terminology/secret.md b/docs/en/latest/terminology/secret.md index ec713a661254..304856fb0f6e 100644 --- a/docs/en/latest/terminology/secret.md +++ b/docs/en/latest/terminology/secret.md @@ -62,6 +62,8 @@ When a configuration field uses a secret reference like `$secret://...` or `$env If a key-value pair `key: "$ENV://ABC"` is configured in APISIX and the value of `$ENV://ABC` is unassigned in the environment variable, `$ENV://ABC` will be interpreted as a string literal, instead of `nil`. +This applies to every secret reference: when a reference cannot be resolved (the secret manager is not configured, the lookup fails, or the environment variable is unset), the literal reference string is used as the field value and an error such as `failed to resolve secret reference: $secret://vault/1/foo/bar, field: password` is written to the error log. Check the error log if a plugin behaves as if its credential were wrong. + ::: ## Use environment variables to manage secrets diff --git a/docs/zh/latest/terminology/secret.md b/docs/zh/latest/terminology/secret.md index 9384a54b1745..bf06b3885524 100644 --- a/docs/zh/latest/terminology/secret.md +++ b/docs/zh/latest/terminology/secret.md @@ -62,6 +62,8 @@ APISIX 目前支持通过以下方式存储密钥: 如果某个配置项为:`key: "$ENV://ABC"`,当 APISIX Secret 中没有检索到 $ENV://ABC 对应的真实值,那么 key 的值将是 "$ENV://ABC" 而不是 `nil`。 +该行为适用于所有密钥引用:当引用无法解析时(密钥管理器未配置、查询失败或环境变量未设置),字段将保留引用字符串本身,同时错误日志中会记录类似 `failed to resolve secret reference: $secret://vault/1/foo/bar, field: password` 的错误。如果插件表现得像凭证不正确,请先检查错误日志。 + ::: ## 使用环境变量管理密钥 diff --git a/t/secret/central-secret-refs.t b/t/secret/central-secret-refs.t index 47f99675cf0d..0a0460c582c3 100644 --- a/t/secret/central-secret-refs.t +++ b/t/secret/central-secret-refs.t @@ -374,3 +374,135 @@ openid-connect has required field client_secret that must be a string. } --- response_body success + + + +=== TEST 13: unresolvable $secret:// ref keeps the literal and logs an error +--- config + location /t { + content_by_lua_block { + local secret = require("apisix.secret") + + local conf = { + client_secret = "$secret://vault/not-exist/foo/bar" + } + local resolved = secret.fetch_secrets(conf, true) + ngx.say("resolved: ", resolved.client_secret) + } + } +--- response_body +resolved: $secret://vault/not-exist/foo/bar +--- error_log +failed to resolve secret reference: $secret://vault/not-exist/foo/bar, field: client_secret + + + +=== TEST 14: unset $env:// ref keeps the literal and logs an error +--- config + location /t { + content_by_lua_block { + local secret = require("apisix.secret") + + local conf = { + nested = { + password = "$env://TEST_ENV_NOT_SET" + } + } + local resolved = secret.fetch_secrets(conf, true) + ngx.say("resolved: ", resolved.nested.password) + } + } +--- response_body +resolved: $env://TEST_ENV_NOT_SET +--- error_log +failed to resolve secret reference: $env://TEST_ENV_NOT_SET, field: password + + + +=== TEST 15: resolvable refs do not log a resolution error +--- config + location /t { + content_by_lua_block { + local secret = require("apisix.secret") + + local conf = { + host = "$env://TEST_HOST", + plain = "not-a-ref" + } + local resolved = secret.fetch_secrets(conf, true) + ngx.say("resolved: ", resolved.host) + } + } +--- response_body +resolved: test.example.com +--- no_error_log +failed to resolve secret reference + + + +=== TEST 16: unresolvable ref on the uncached path keeps the literal and logs +--- config + location /t { + content_by_lua_block { + local secret = require("apisix.secret") + + local conf = { + token = "$secret://vault/not-exist/foo/bar" + } + -- use_cache = false exercises the direct fetch_by_uri path + local resolved = secret.fetch_secrets(conf, false) + ngx.say("resolved: ", resolved.token) + } + } +--- response_body +resolved: $secret://vault/not-exist/foo/bar +--- error_log +failed to resolve secret reference: $secret://vault/not-exist/foo/bar, field: token, err: no secret conf + + + +=== TEST 17: route with a plugin referencing an unresolvable secret +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "proxy-rewrite": { + "headers": { + "set": { + "X-Secret-Token": "$env://SECRET_REF_UNRESOLVABLE" + } + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + ngx.say("success") + } + } +--- response_body +success + + + +=== TEST 18: request still succeeds, the literal is forwarded and the failure logged +--- request +GET /hello +--- error_code: 200 +--- error_log +failed to resolve secret reference: $env://SECRET_REF_UNRESOLVABLE, field: X-Secret-Token diff --git a/t/secret/secret_lru.t b/t/secret/secret_lru.t index 670bf03b5154..52a80dfc40d1 100644 --- a/t/secret/secret_lru.t +++ b/t/secret/secret_lru.t @@ -169,3 +169,5 @@ GET /t --- response_body value $secret://vault/lru/lru-key/jack/key +--- error_log +failed to resolve secret reference: $secret://vault/lru/lru-key/jack/key, field: key