Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(plugins/acme): username/password is a valid authentication method #13496

Merged
merged 2 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ jobs:
ports:
- 9411:9411

redis-auth:
fffonion marked this conversation as resolved.
Show resolved Hide resolved
image: redis/redis-stack-server
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
gruceo marked this conversation as resolved.
Show resolved Hide resolved
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6381:6379
env:
REDIS_ARGS: "--requirepass passdefault"

steps:
- name: Bump max open files
run: |
Expand Down
3 changes: 3 additions & 0 deletions changelog/unreleased/kong/fix-acme-username-password-auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: "**ACME**: Fixed an issue where username and password were not accepted as valid authentication methods."
type: bugfix
scope: Plugin
Copy link
Contributor

Choose a reason for hiding this comment

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

we probably need an addtional changelog for the bumped dependency, as I
heard from @AndyZhang0707

Copy link
Contributor Author

@gruceo gruceo Aug 15, 2024

Choose a reason for hiding this comment

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

Done in 004627a

15 changes: 15 additions & 0 deletions kong/clustering/compat/checkers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ local compatible_checkers = {
function (config_table, dp_version, log_suffix)
local has_update
for _, plugin in ipairs(config_table.plugins or {}) do
if plugin.name == 'acme' then
local config = plugin.config
if config.storage_config.redis.username ~= nil then
Copy link
Contributor Author

@gruceo gruceo Aug 15, 2024

Choose a reason for hiding this comment

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

Thanks @fffonion. The test failure in spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua:340 was actually eye-opening. It revealed that removing the new username/password fields will cause issues to older DPs. Therefore I'm going with the safest option which is to not do anything (just log the warning instead). I'd like some input from @nowNick here as well.

This is the updated log warning in the CP side when we create acme plugin and have DP 3.6.1:

kong-cp  | 2024/08/15 22:21:38 [warn] 1811#0: *1294 [lua] checkers.lua:20: log_warn_message(): [clustering] Kong Gateway v3.8.0 configures acme plugin with redis username which is incompatible with dataplane version 3.6.1 and will not work in this release.. [id: e79130ba-32cb-456d-a541-9a159d072142, host: 0c0d4012bade, ip: 172.18.0.8, version: 3.6.1], client: 172.18.0.8, server: kong_cluster_listener, request: "GET /v1/outlet?node_id=e79130ba-32cb-456d-a541-9a159d072142&node_hostname=0c0d4012bade&node_version=3.6.1 HTTP/1.1", host: "kong-cp:8005"
kong-cp  | 2024/08/15 22:21:38 [warn] 1811#0: *1294 [lua] checkers.lua:20: log_warn_message(): [clustering] Kong Gateway v3.8.0 configures acme plugin with redis password which is incompatible with dataplane version 3.6.1 and will not work in this release. Please use redis.auth config instead.. [id: e79130ba-32cb-456d-a541-9a159d072142, host: 0c0d4012bade, ip: 172.18.0.8, version: 3.6.1], client: 172.18.0.8, server: kong_cluster_listener, request: "GET /v1/outlet?node_id=e79130ba-32cb-456d-a541-9a159d072142&node_hostname=0c0d4012bade&node_version=3.6.1 HTTP/1.1", host: "kong-cp:8005"

Copy link
Contributor

Choose a reason for hiding this comment

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

If we do not remove it via

config.storage_config.redis.username = nil
config.storage_config.redis.password = nil

The config will be refused by DP. A log warning may be not enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't get refused. Added a test to prove it: 3ff5eb8

Copy link
Member

Choose a reason for hiding this comment

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

This is interesting. How can the older schema validate the storage_config if it has unknown fields in it? This might be a sign of something else being wrong. E.g. sub-record validation does not work or something?

Copy link
Member

Choose a reason for hiding this comment

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

I think the test here is probably wrong that send @gruceo to wrong direction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bungle I think it's because some of the storage_config fields like username and password are actually supported in some older versions, it's just that the acme plugin didn't use it. I added this extra test: 8f1cb44

As you can see, username and password are supported until 3.6.1 but not in 3.5.0. In a 3.8.x CP pushing unkwnon fields to a 3.5.0 DP, the config reverts to older schema here: https://github.com/Kong/kong/blob/master/kong/clustering/compat/checkers.lua#L141-L162

kong-cp  | 2024/08/19 10:57:34 [warn] 536#0: *1296 [lua] checkers.lua:20: log_warn_message(): [clustering] Kong Gateway v3.8.0 configures acme plugin with redis username which is incompatible with dataplane version 3.5.0 and will not work in this release. [id: df7ec872-23ac-4180-8f5b-1b810efadfcb, host: 0bfb8a368e8d, ip: 172.18.0.7, version: 3.5.0], client: 172.18.0.7, server: kong_cluster_listener, request: "GET /v1/outlet?node_id=df7ec872-23ac-4180-8f5b-1b810efadfcb&node_hostname=0bfb8a368e8d&node_version=3.5.0 HTTP/1.1", host: "kong-cp:8005"
kong-cp  | 2024/08/19 10:57:34 [warn] 536#0: *1296 [lua] checkers.lua:20: log_warn_message(): [clustering] Kong Gateway v3.8.0 configures acme plugin with redis password which is incompatible with dataplane version 3.5.0 and will not work in this release. Please use redis.auth config instead. [id: df7ec872-23ac-4180-8f5b-1b810efadfcb, host: 0bfb8a368e8d, ip: 172.18.0.7, version: 3.5.0], client: 172.18.0.7, server: kong_cluster_listener, request: "GET /v1/outlet?node_id=df7ec872-23ac-4180-8f5b-1b810efadfcb&node_hostname=0bfb8a368e8d&node_version=3.5.0 HTTP/1.1", host: "kong-cp:8005"
kong-cp  | 2024/08/19 10:57:34 [warn] 536#0: *1296 [lua] checkers.lua:20: log_warn_message(): [clustering] Kong Gateway v3.8.0 adapts acme plugin redis configuration to older version which is incompatible with dataplane version 3.5.0 and will revert to older schema. [id: df7ec872-23ac-4180-8f5b-1b810efadfcb, host: 0bfb8a368e8d, ip: 172.18.0.7, version: 3.5.0], client: 172.18.0.7, server: kong_cluster_listener, request: "GET /v1/outlet?node_id=df7ec872-23ac-4180-8f5b-1b810efadfcb&node_hostname=0bfb8a368e8d&node_version=3.5.0 HTTP/1.1", host: "kong-cp:8005"

cc @nowNick

log_warn_message('configures ' .. plugin.name .. ' plugin with redis username',
'not work in this release',
dp_version, log_suffix)
end

if config.storage_config.redis.password ~= nil then
log_warn_message('configures ' .. plugin.name .. ' plugin with redis password',
'not work in this release. Please use redis.auth config instead',
dp_version, log_suffix)
end
end

if plugin.name == 'aws-lambda' then
local config = plugin.config
if config.aws_sts_endpoint_url ~= nil then
Expand Down
2 changes: 2 additions & 0 deletions kong/plugins/acme/storage/config_adapters/redis.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ local function redis_config_adapter(conf)
ssl = conf.ssl,
ssl_verify = conf.ssl_verify,
ssl_server_name = conf.server_name,
username = conf.username,
fffonion marked this conversation as resolved.
Show resolved Hide resolved
password = conf.password,

namespace = conf.extra_options.namespace,
scan_count = conf.extra_options.scan_count,
Expand Down
15 changes: 14 additions & 1 deletion scripts/dependency_services/docker-compose-test-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ services:
ports:
- 127.0.0.1::9411
command: --logging.level.zipkin2=DEBUG

redis-auth:
image: redis/redis-stack-server
ports:
- 127.0.0.1::6381
environment:
- REDIS_ARGS=--requirepass passdefault
volumes:
- redis-auth-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--pass", "passdefault", "ping"]
interval: 5s
timeout: 10s
retries: 10
volumes:
postgres-data:
redis-data:
redis-auth-data:
116 changes: 115 additions & 1 deletion spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,121 @@ describe("CP/DP config compat transformations #" .. strategy, function()

describe("compatibility tests for redis standarization", function()
describe("acme plugin", function()
it("translates standardized redis config to older acme structure", function()
it("translates 3.8.x standardized redis config to older (3.5.0) acme structure", function()
-- [[ 3.8.x ]] --
local acme = admin.plugins:insert {
name = "acme",
enabled = true,
config = {
account_email = "[email protected]",
storage = "redis",
storage_config = {
-- [[ new structure redis
redis = {
host = "localhost",
port = 57198,
username = "test",
password = "secret",
database = 2,
timeout = 1100,
ssl = true,
ssl_verify = true,
server_name = "example.test",
extra_options = {
namespace = "test_namespace",
scan_count = 13
}
}
-- ]]
}
}
}

local expected_acme_prior_38 = cycle_aware_deep_copy(acme)
expected_acme_prior_38.config.storage_config.redis = {
host = "localhost",
port = 57198,
-- username and password are not supported in 3.5.0
--username = "test",
--password = "secret",
auth = "secret",
database = 2,
ssl = true,
ssl_verify = true,
ssl_server_name = "example.test",
namespace = "test_namespace",
scan_count = 13,
-- below fields are also not supported in 3.5.0
--timeout = 1100,
--server_name = "example.test",
--extra_options = {
-- namespace = "test_namespace",
-- scan_count = 13
--}
}
do_assert(uuid(), "3.5.0", expected_acme_prior_38)

-- cleanup
admin.plugins:remove({ id = acme.id })
end)

it("translates 3.8.x standardized redis config to older (3.6.1) acme structure", function()
-- [[ 3.8.x ]] --
local acme = admin.plugins:insert {
name = "acme",
enabled = true,
config = {
account_email = "[email protected]",
storage = "redis",
storage_config = {
-- [[ new structure redis
redis = {
host = "localhost",
port = 57198,
username = "test",
password = "secret",
database = 2,
timeout = 1100,
ssl = true,
ssl_verify = true,
server_name = "example.test",
extra_options = {
namespace = "test_namespace",
scan_count = 13
}
}
-- ]]
}
}
}

local expected_acme_prior_38 = cycle_aware_deep_copy(acme)
expected_acme_prior_38.config.storage_config.redis = {
host = "localhost",
port = 57198,
username = "test",
auth = "secret",
password = "secret",
database = 2,
ssl = true,
ssl_verify = true,
ssl_server_name = "example.test",
namespace = "test_namespace",
scan_count = 13,
timeout = 1100,
server_name = "example.test",
extra_options = {
namespace = "test_namespace",
scan_count = 13
}
}
do_assert(uuid(), "3.6.1", expected_acme_prior_38)

-- cleanup
admin.plugins:remove({ id = acme.id })
end)

it("translates 3.6.x standardized redis config to older (3.5.0) acme structure", function()
-- [[ 3.6.x ]] --
local acme = admin.plugins:insert {
name = "acme",
Expand Down
130 changes: 130 additions & 0 deletions spec/03-plugins/29-acme/05-redis_storage_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -555,4 +555,134 @@ describe("Plugin: acme (storage.redis)", function()
end)
end)

describe("redis authentication", function()
describe("happy path", function()

it("should successfully connect to Redis with Auth using username/password", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
username = "default",
password = "passdefault",
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.is_nil(err)
local value, err = storage:get("foo")
assert.is_nil(err)
assert.equal("bar", value)
end)

it("should successfully connect to Redis with Auth using legacy auth", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
auth = "passdefault",
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.is_nil(err)
local value, err = storage:get("foo")
assert.is_nil(err)
assert.equal("bar", value)
end)

it("should successfully connect to Redis with Auth using just password", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
password = "passdefault",
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.is_nil(err)
local value, err = storage:get("foo")
assert.is_nil(err)
assert.equal("bar", value)
end)
end)

describe("unhappy path", function()
it("should not connect to Redis with Auth using just username", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
username = "default",
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.equal("can't select database NOAUTH Authentication required.", err)
end)

it("should not connect to Redis with Auth using wrong username", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
username = "wrongusername",
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.equal("can't select database NOAUTH Authentication required.", err)
end)

it("should not connect to Redis with Auth using wrong password and no username", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
password = "wrongpassword"
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.equal("authentication failed WRONGPASS invalid username-password pair or user is disabled.", err)
end)

it("should not connect to Redis with Auth using wrong password and correct username", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
username = "default",
password = "wrongpassword"
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.equal("authentication failed WRONGPASS invalid username-password pair or user is disabled.", err)
end)

it("should not connect to Redis with Auth using correct password and wrong username", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_auth_port,
database = 0,
username = "kong",
password = "passdefault"
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)
local err = storage:set("foo", "bar", 10)
assert.equal("authentication failed WRONGPASS invalid username-password pair or user is disabled.", err)
end)
end)
end)
end)
2 changes: 2 additions & 0 deletions spec/helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ local OTELCOL_FILE_EXPORTER_PATH = os.getenv("KONG_SPEC_TEST_OTELCOL_FILE_EXPORT
local REDIS_HOST = os.getenv("KONG_SPEC_TEST_REDIS_HOST") or "localhost"
local REDIS_PORT = tonumber(os.getenv("KONG_SPEC_TEST_REDIS_PORT") or 6379)
local REDIS_SSL_PORT = tonumber(os.getenv("KONG_SPEC_TEST_REDIS_SSL_PORT") or 6380)
local REDIS_AUTH_PORT = tonumber(os.getenv("KONG_SPEC_TEST_REDIS_AUTH_PORT") or 6381)
fffonion marked this conversation as resolved.
Show resolved Hide resolved
local REDIS_SSL_SNI = os.getenv("KONG_SPEC_TEST_REDIS_SSL_SNI") or "test-redis.example.com"
local TEST_COVERAGE_MODE = os.getenv("KONG_COVERAGE")
local TEST_COVERAGE_TIMEOUT = 30
Expand Down Expand Up @@ -4340,6 +4341,7 @@ end
redis_port = REDIS_PORT,
redis_ssl_port = REDIS_SSL_PORT,
redis_ssl_sni = REDIS_SSL_SNI,
redis_auth_port = REDIS_AUTH_PORT,

blackhole_host = BLACKHOLE_HOST,

Expand Down
Loading