Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 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
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.
{ 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.
{ hash_secret = { type = "boolean", required = true, default = false, referenceable = true }, },
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a Boolean type. Don't need to set this field as a referenceable.

Copy link
Author

Choose a reason for hiding this comment

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

Removed the referenceable from hash_secret.

{ redirect_uris = {
type = "array",
required = false,
Expand Down
4 changes: 4 additions & 0 deletions kong/plugins/request-transformer/schema.lua
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do these fields need to be set as a referenceable field?

Copy link
Author

Choose a reason for hiding this comment

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

Removed. I thought maybe there is a use-case to pass the headers using ENVAR but looking at it at the moment, there is no actual requirement.

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

Copy link
Author

Choose a reason for hiding this comment

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

Removed. I thought maybe there is a use-case to pass the headers using ENVAR but looking at it at the moment, there is no actual requirement.

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")
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 require style: This file uses parentheses in require statements (e.g., require("spec.helpers")), while all other vault spec files use the style without parentheses (e.g., require "spec.helpers"). For consistency across the test suite, this should match the style used in the other vault spec files.

Copilot uses AI. Check for mistakes.
local conf_loader = require("kong.conf_loader")
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 require style: This file uses parentheses in require statements (e.g., require("kong.conf_loader")), while all other vault spec files use the style without parentheses. For consistency across the test suite, this should match the style used in the other vault spec files.

Copilot uses AI. Check for mistakes.

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")
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 require style: This file uses parentheses in require statements (e.g., require("kong.global")), while all other vault spec files use the style without parentheses. For consistency across the test suite, this should match the style used in the other vault spec files.

Copilot uses AI. Check for mistakes.
_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 on lines +20 to +37
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 handle all variations of variable name" is unique to basic-auth and doesn't appear in other plugin vault spec files. While the test demonstrates environment variable name normalization, it's testing vault behavior rather than basic-auth specific functionality. For consistency across the test suite, either this test should be removed or added to all other plugin vault spec files if it's testing important behavior.

Suggested change
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)

Copilot uses AI. Check for mistakes.
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=",
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.

Unnecessary malformed vault reference test: The malformed reference {vault://env/valid_name?invalid_query= is included in this test but not in any other plugin's vault spec file. All other plugins test the same set of malformed references without this one. Either this should be removed for consistency, or if it's testing an important case, it should be added to all other plugin vault spec files.

Suggested change
"{vault://env/valid_name?invalid_query=",

Copilot uses AI. Check for mistakes.
}

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 on lines +149 to +159
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 preserve non-vault values unchanged" is unique to basic-auth and doesn't appear in other plugin vault spec files. For consistency across the test suite, either this test should be removed or added to all other plugin vault spec files if it's testing an important behavior.

Suggested change
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)

Copilot uses AI. Check for mistakes.
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)
Comment on lines +159 to +174
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