Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
98a6475
feat(basic-auth): Add option to use {vault://} in username and passwo…
lordgreg Oct 13, 2025
e2064f6
feat(hmac-auth): Add option to use {vault://} in username and secret …
lordgreg Oct 13, 2025
5ef5005
feat(jwt): Add option to use {vault://} in secret field
lordgreg Oct 13, 2025
7df0b8a
feat(oauth2): Add option to use {vault://} in client_id, client_secre…
lordgreg Oct 13, 2025
7bd8bb3
feat(request-transformer): Add option to use {vault://} in fields
lordgreg Oct 13, 2025
a852681
feat(response-transformer): Add option to use {vault://} in fields
lordgreg Oct 13, 2025
c05fb9b
docs(changelog): Add changelog for vault template support
lordgreg Oct 13, 2025
6d52709
feat(vault): Add tests for vault integration in basic-auth plugin
lordgreg Oct 16, 2025
59e6de3
feat(vault): Add tests for vault integration in response-transformer …
lordgreg Oct 16, 2025
89a263c
feat(vault): Add tests for vault integration in JWT plugin
lordgreg Oct 16, 2025
f3185d8
feat(vault): Add tests for vault integration in hmac-auth plugin
lordgreg Oct 16, 2025
9f0e84c
feat(vault): Add tests for vault integration in request-transformer p…
lordgreg Oct 16, 2025
2c83103
feat(vault): Add tests for vault integration in oauth2 plugin
lordgreg Oct 16, 2025
8be60d5
fix(changelog): Fix filename for changelog
lordgreg Oct 16, 2025
33e9a90
fix(changelog): Fix typo in changelog file
lordgreg Oct 16, 2025
eebdbf1
Merge branch 'master' into master
lordgreg Oct 24, 2025
a2a110f
Merge branch 'master' into master
lordgreg Oct 28, 2025
6bdaab0
fix(test): Update linting error
lordgreg Nov 4, 2025
36828d1
Merge branch 'master' of https://github.com/lordgreg/kong
lordgreg Nov 4, 2025
cd8a648
Merge branch 'master' into master
lordgreg Nov 28, 2025
f6ae29d
Revert "feat(response-transformer): Add option to use {vault://} in f…
lordgreg Jan 14, 2026
b8e188f
Revert "feat(request-transformer): Add option to use {vault://} in fi…
lordgreg Jan 14, 2026
8aa9286
feat(vault): Remove referenceable from hash_secret in oauth2 dao
lordgreg Jan 14, 2026
c190c95
fix(test): Correct filename for vault spec
lordgreg Jan 14, 2026
ad160aa
fix(test): remove vault specs
lordgreg Jan 15, 2026
b671a5e
tests(jwt): update vault tests to match test semantics
lordgreg Jan 15, 2026
17c9ac2
tests(basic-auth): update vault tests to match test semantics
lordgreg Jan 15, 2026
f89c5bb
Merge branch 'master' into master
lordgreg Feb 3, 2026
0877600
fix(test): Standardize vault-referenced tests
lordgreg Feb 3, 2026
cb41634
Merge branch 'master' of https://github.com/lordgreg/kong
lordgreg Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: Added an option to use {vault://} in specific fields of plugins
type: feature
scope: Plugin
4 changes: 2 additions & 2 deletions kong/plugins/basic-auth/daos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ return {
{ id = typedefs.uuid },
{ created_at = typedefs.auto_timestamp_s },
{ consumer = { type = "foreign", reference = "consumers", required = true, on_delete = "cascade" }, },
{ username = { type = "string", required = true, unique = true }, },
{ password = { type = "string", required = true, encrypted = true }, }, -- encrypted = true is a Kong Enterprise Exclusive feature, it does nothing in Kong CE
{ username = { type = "string", required = true, unique = true, referenceable = true }, },
{ password = { type = "string", required = true, encrypted = true, referenceable = true }, }, -- encrypted = true is a Kong Enterprise Exclusive feature, it does nothing in Kong CE
{ tags = typedefs.tags },
},
transformations = {
Expand Down
4 changes: 2 additions & 2 deletions kong/plugins/hmac-auth/daos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ return {
{ id = typedefs.uuid },
{ created_at = typedefs.auto_timestamp_s },
{ consumer = { type = "foreign", reference = "consumers", required = true, on_delete = "cascade", }, },
{ username = { type = "string", required = true, unique = true }, },
{ secret = { type = "string", auto = true }, },
{ username = { type = "string", required = true, unique = true, referenceable = true }, },
{ secret = { type = "string", auto = true, referenceable = true }, },
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking the HMAC credential secret field as referenceable means this value will be resolved via kong.vault.get on the data plane. If the vault reference cannot be resolved (for example, a missing or mis-typed environment variable), resolve_reference in kong.db.schema.init replaces it with an empty string, so hmac-auth will happily verify signatures using a known empty key, allowing an attacker who guesses the username to forge valid HMAC signatures. This field should fail closed on vault resolution errors (e.g., reject the credential or authentication) instead of silently falling back to an empty secret.

Suggested change
{ secret = { type = "string", auto = true, referenceable = true }, },
{ secret = { type = "string", auto = true }, },

Copilot uses AI. Check for mistakes.
{ tags = typedefs.tags },
},
},
Expand Down
2 changes: 1 addition & 1 deletion kong/plugins/jwt/daos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ return {
{ created_at = typedefs.auto_timestamp_s },
{ consumer = { type = "foreign", reference = "consumers", required = true, on_delete = "cascade", }, },
{ key = { type = "string", required = false, unique = true, auto = true }, },
{ secret = { type = "string", auto = true }, },
{ secret = { type = "string", auto = true, referenceable = true }, },
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the JWT credential secret field referenceable causes it to be dereferenced via kong.vault.get at select time, and on failure resolve_reference replaces the value with an empty string. Because the JWT plugin treats any non-nil jwt_secret.secret as a valid key and passes it directly into jwt:verify_signature, an unresolved vault reference would downgrade the shared secret to an empty string, enabling trivial forgery of JWTs for that key if the vault reference is misconfigured. Vault resolution failures for this field should be treated as fatal (e.g., deny authentication or disable the credential) rather than defaulting to an empty secret.

Suggested change
{ secret = { type = "string", auto = true, referenceable = true }, },
{ secret = { type = "string", auto = true }, },

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this as a valid point. The fields from consumer should be referenceable using the vault semantics. If its not referenceable (empty), then the user hasn't follow the guidelines. This is exactly the same as if user would add EMPTY secret directly into consumer part. Please advise @raoxiaoyan

{ rsa_public_key = { type = "string" }, },
{ algorithm = {
type = "string",
Expand Down
6 changes: 3 additions & 3 deletions kong/plugins/oauth2/daos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ local oauth2_credentials = {
{ created_at = typedefs.auto_timestamp_s },
{ consumer = { type = "foreign", reference = "consumers", required = true, on_delete = "cascade", }, },
{ name = { type = "string", required = true }, },
{ client_id = { type = "string", required = false, unique = true, auto = true }, },
{ client_secret = { type = "string", required = false, auto = true, encrypted = true }, }, -- encrypted = true is a Kong Enterprise Exclusive feature. It does nothing in Kong CE
{ hash_secret = { type = "boolean", required = true, default = false }, },
{ client_id = { type = "string", required = false, unique = true, auto = true, referenceable = true }, },
{ client_secret = { type = "string", required = false, auto = true, encrypted = true, referenceable = true }, }, -- encrypted = true is a Kong Enterprise Exclusive feature. It does nothing in Kong CE
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By marking oauth2_credentials.client_secret as referenceable, the plugin will resolve secrets from vault at runtime, but if the vault reference cannot be resolved resolve_reference in kong.db.schema.init substitutes an empty string. In the non-hashed branch (hash_secret = false), the OAuth2 plugin then authenticates confidential clients by simple equality client.client_secret == client_secret, so a misconfigured or missing vault secret would reduce the client secret to an empty string and allow any caller presenting an empty secret to be accepted. For this field, vault resolution errors should cause authentication to fail (or the credential/plugin to be rejected) instead of silently falling back to an empty secret.

Suggested change
{ client_secret = { type = "string", required = false, auto = true, encrypted = true, referenceable = true }, }, -- encrypted = true is a Kong Enterprise Exclusive feature. It does nothing in Kong CE
{ client_secret = { type = "string", required = false, auto = true, encrypted = true }, }, -- encrypted = true is a Kong Enterprise Exclusive feature. It does nothing in Kong CE

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see this as an error, almost same as with jwt. Please advise @raoxiaoyan .

{ hash_secret = { type = "boolean", required = true, default = false, referenceable = true }, },
Comment thread
lordgreg marked this conversation as resolved.
Outdated
{ redirect_uris = {
type = "array",
required = false,
Expand Down
4 changes: 4 additions & 0 deletions kong/plugins/request-transformer/schema.lua
Comment thread
lordgreg marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ local strings_array = {
type = "array",
default = {},
required = true,
referenceable = true,
elements = { type = "string" },
}

Expand All @@ -58,6 +59,7 @@ local headers_array = {
type = "array",
default = {},
required = true,
referenceable = true,
elements = { type = "string", custom_validator = validate_headers },
}

Expand All @@ -76,6 +78,7 @@ local colon_strings_array = {
type = "array",
default = {},
required = true,
referenceable = true,
elements = { type = "string", custom_validator = check_for_value }
}

Expand All @@ -102,6 +105,7 @@ local colon_headers_array = {
type = "array",
default = {},
required = true,
referenceable = true,
elements = { type = "string", match = "^[^:]+:.*$", custom_validator = validate_colon_headers },
}

Expand Down
4 changes: 4 additions & 0 deletions kong/plugins/response-transformer/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local string_array = {
type = "array",
default = {},
required = true,
referenceable = true,
Comment thread
lordgreg marked this conversation as resolved.
Outdated
elements = { type = "string" },
}

Expand All @@ -33,6 +34,7 @@ local colon_string_array = {
type = "array",
default = {},
required = true,
referenceable = true,
elements = { type = "string", match = "^[^:]+:.*$" },
}

Expand All @@ -53,6 +55,7 @@ local colon_string_record = {
{ json_types = { description = "List of JSON type names. Specify the types of the JSON values returned when appending\nJSON properties. Each string element can be one of: boolean, number, or string.", type = "array",
default = {},
required = true,
referenceable = true,
elements = {
type = "string",
one_of = { "boolean", "number", "string" }
Expand All @@ -66,6 +69,7 @@ local colon_headers_array = {
type = "array",
default = {},
required = true,
referenceable = true,
elements = { type = "string", match = "^[^:]+:.*$", custom_validator = validate_colon_headers },
}

Expand Down
201 changes: 201 additions & 0 deletions spec/03-plugins/10-basic-auth/06-vault_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
local helpers = require("spec.helpers")
Comment thread
lordgreg marked this conversation as resolved.
Outdated
local conf_loader = require("kong.conf_loader")
Comment thread
lordgreg marked this conversation as resolved.
Outdated

describe("basic-auth: (vault integration)", function()
local get

before_each(function()
local conf = assert(conf_loader(nil, {
vaults = "bundled",
}))

local kong_global = require("kong.global")
Comment thread
lordgreg marked this conversation as resolved.
Outdated
_G.kong = kong_global.new()
kong_global.init_pdk(kong, conf)

get = _G.kong.vault.get
end)

describe("vault reference resolution", function()
it("should handle all variations of variable name", function()
local env_name = "MY_VAR_NAME"
local env_value = "complex_value_789"

finally(function()
helpers.unsetenv(env_name)
end)

helpers.setenv(env_name, env_value)

assert.equal(env_value, get("{vault://env/MY_VAR_NAME}"))
assert.equal(env_value, get("{vault://env/MY-VAR-NAME}"))
assert.equal(env_value, get("{vault://env/my_var_name}"))
assert.equal(env_value, get("{vault://env/my-var-name}"))
assert.equal(env_value, get("{vault://env/My_Var_Name}"))
assert.equal(env_value, get("{vault://env/My-Var-Name}"))
end)

Comment thread
lordgreg marked this conversation as resolved.
Outdated
it("should handle vault reference with different environment variable name", function()
local env_name = "BASIC_AUTH_SECRET"
local env_value = "another_secret_456"

finally(function()
helpers.unsetenv(env_name)
end)

helpers.setenv(env_name, env_value)

local res, err = get("{vault://env/basic_auth_secret}")
assert.is_nil(err)
assert.equal(env_value, res)
end)

it("should handle vault reference with JSON secret", function()
local env_name = "TEST_JSON_SECRETS"
local env_value = '{"username": "json_user", "password": "db_secret_789"}'

finally(function()
helpers.unsetenv(env_name)
end)

helpers.setenv(env_name, env_value)

local res, err = get("{vault://env/test_json_secrets/password}")
assert.is_nil(err)
assert.equal("db_secret_789", res)
end)

it("should fail gracefully when environment variable does not exist", function()
helpers.unsetenv("NON_EXISTENT_VAR")

local res, err = get("{vault://env/non_existent_var}")
assert.matches("could not get value from external vault", err)
assert.is_nil(res)
end)

it("should handle vault reference with prefix", function()
local env_name = "TEST_PASSWORD"
local env_value = "prefixed_secret"

finally(function()
helpers.unsetenv(env_name)
end)

helpers.setenv(env_name, env_value)

local res, err = get("{vault://env/password?prefix=test_}")
assert.is_nil(err)
assert.equal(env_value, res)
end)

it("should work with empty environment variable value", function()
local env_name = "EMPTY_PASSWORD"

finally(function()
helpers.unsetenv(env_name)
end)

helpers.setenv(env_name, "")

local res, err = get("{vault://env/empty_password}")
assert.is_nil(err)
assert.equal("", res)
end)
end)

describe("username field vault references", function()
it("should handle both username and password as vault references", function()
finally(function()
helpers.unsetenv("AUTH_USERNAME")
helpers.unsetenv("AUTH_PASSWORD")
end)

helpers.setenv("AUTH_USERNAME", "vault_user_both")
helpers.setenv("AUTH_PASSWORD", "vault_pass_both")

local username_res, username_err = get("{vault://env/auth_username}")
local password_res, password_err = get("{vault://env/auth_password}")

assert.is_nil(username_err)
assert.is_nil(password_err)
assert.equal("vault_user_both", username_res)
assert.equal("vault_pass_both", password_res)
end)
end)

describe("edge cases and validation", function()
it("should handle malformed vault references gracefully", function()
-- Test various malformed vault references
local malformed_refs = {
"{vault://invalid/format",
"vault://env/missing_braces}",
"{vault://env/}",
"{vault://env}",
"{vault://}",
"{vault://env/valid_name?invalid_query=",
Comment thread
lordgreg marked this conversation as resolved.
Outdated
}

for _, ref in ipairs(malformed_refs) do
local res, err = get(ref)
-- Should either return nil with error, or return the original string unchanged
if res then
assert.equal(ref, res, "Malformed reference should be returned unchanged: " .. ref)
else
assert.is_string(err, "Should have error message for malformed reference: " .. ref)
end
end
end)

it("should preserve non-vault values unchanged", function()
local regular_value = "regular_password"

local res, err = get(regular_value)
if res then
assert.equal(regular_value, res)
else
assert.is_nil(err)
end
end)

Comment thread
lordgreg marked this conversation as resolved.
Outdated
it("should work with special characters in environment variable names", function()
local env_name = "SPECIAL_CHARS_(1337@)"
local env_value = "special_value"

finally(function()
helpers.unsetenv(env_name)
end)

helpers.setenv(env_name, env_value)

assert.equal(env_value, get("{vault://env/SPECIAL_CHARS_(1337@)}"))
assert.equal(env_value, get("{vault://env/SPECIAL-CHARS_(1337@)}"))
assert.equal(env_value, get("{vault://env/special-chars_(1337@)}"))
assert.equal(env_value, get("{vault://env/special_chars_(1337@)}"))
end)
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent test structure: This test "should work with special characters in environment variable names" is unique to basic-auth and doesn't appear in the edge cases sections of other plugin vault spec files. For consistency, either this test should be removed or the test approach should be harmonized across all plugin vault spec files.

Suggested change
it("should work with special characters in environment variable names", function()
local env_name = "SPECIAL_CHARS_(1337@)"
local env_value = "special_value"
finally(function()
helpers.unsetenv(env_name)
end)
helpers.setenv(env_name, env_value)
assert.equal(env_value, get("{vault://env/SPECIAL_CHARS_(1337@)}"))
assert.equal(env_value, get("{vault://env/SPECIAL-CHARS_(1337@)}"))
assert.equal(env_value, get("{vault://env/special-chars_(1337@)}"))
assert.equal(env_value, get("{vault://env/special_chars_(1337@)}"))
end)

Copilot uses AI. Check for mistakes.
end)

describe("integration with basic-auth plugin", function()
it("should demonstrate vault usage in basic-auth context", function()
local password_env = "BASIC_AUTH_PASSWORD"
local username_env = "BASIC_AUTH_USERNAME"

finally(function()
helpers.unsetenv(password_env)
helpers.unsetenv(username_env)
end)

helpers.setenv(password_env, "secure_password_123")
helpers.setenv(username_env, "secure_username")

-- Simulate how basic-auth would resolve vault references
local resolved_password, pass_err = get("{vault://env/basic_auth_password}")
local resolved_username, user_err = get("{vault://env/basic_auth_username}")

assert.is_nil(pass_err)
assert.is_nil(user_err)
assert.equal("secure_password_123", resolved_password)
assert.equal("secure_username", resolved_username)
end)
end)
end)

Loading