From d775467efd2153d01779d3d19b6183168ba0ba3b Mon Sep 17 00:00:00 2001 From: "Qirui(Keery) Nie" Date: Mon, 23 Oct 2023 17:17:53 +0800 Subject: [PATCH] fix(aws-lambda): aws lambda service cache by service related fields (#11805) Cache the aws lambda service by composing a cache key using the service related fields, so that service object can be reused between plugins and vault refresh can take effect when key/secret is rotated * fix(aws-lambda): aws lambda service cache by service related fields * tests(aws-lambda): add test for checking service cache refresh when vault rotates * style(*): lint Fix KAG-2832 --- .../kong/aws_lambda_service_cache.yml | 3 + kong/plugins/aws-lambda/handler.lua | 35 ++++++- .../27-aws-lambda/99-access_spec.lua | 93 +++++++++++++++++++ .../custom_vaults/kong/vaults/random/init.lua | 13 +++ .../kong/vaults/random/schema.lua | 19 ++++ 5 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/kong/aws_lambda_service_cache.yml create mode 100644 spec/fixtures/custom_vaults/kong/vaults/random/init.lua create mode 100644 spec/fixtures/custom_vaults/kong/vaults/random/schema.lua diff --git a/changelog/unreleased/kong/aws_lambda_service_cache.yml b/changelog/unreleased/kong/aws_lambda_service_cache.yml new file mode 100644 index 000000000000..48c421b041aa --- /dev/null +++ b/changelog/unreleased/kong/aws_lambda_service_cache.yml @@ -0,0 +1,3 @@ +message: Cache the AWS lambda service by those lambda service related fields +type: bugfix +scope: Plugin diff --git a/kong/plugins/aws-lambda/handler.lua b/kong/plugins/aws-lambda/handler.lua index a2a6c597288e..2e1b78002d03 100644 --- a/kong/plugins/aws-lambda/handler.lua +++ b/kong/plugins/aws-lambda/handler.lua @@ -1,9 +1,12 @@ -- Copyright (C) Kong Inc. -local fmt = string.format local ngx_var = ngx.var local ngx_now = ngx.now local ngx_update_time = ngx.update_time +local md5_bin = ngx.md5_bin +local fmt = string.format +local buffer = require "string.buffer" +local lrucache = require "resty.lrucache" local kong = kong local meta = require "kong.meta" @@ -22,7 +25,7 @@ local AWS_REGION do AWS_REGION = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") end local AWS -local LAMBDA_SERVICE_CACHE = setmetatable({}, { __mode = "k" }) +local LAMBDA_SERVICE_CACHE local function get_now() @@ -32,11 +35,34 @@ end local function initialize() + LAMBDA_SERVICE_CACHE = lrucache.new(1000) AWS_GLOBAL_CONFIG = aws_config.global AWS = aws() initialize = nil end +local build_cache_key do + -- Use AWS Service related config fields to build cache key + -- so that service object can be reused between plugins and + -- vault refresh can take effect when key/secret is rotated + local SERVICE_RELATED_FIELD = { "timeout", "keepalive", "aws_key", "aws_secret", + "aws_assume_role_arn", "aws_role_session_name", + "aws_region", "host", "port", "disable_https", + "proxy_url", "aws_imds_protocol_version" } + + build_cache_key = function (conf) + local cache_key_buffer = buffer.new(100):reset() + for _, field in ipairs(SERVICE_RELATED_FIELD) do + local v = conf[field] + if v then + cache_key_buffer:putf("%s=%s;", field, v) + end + end + + return md5_bin(cache_key_buffer:get()) + end +end + local AWSLambdaHandler = { PRIORITY = 750, @@ -62,7 +88,8 @@ function AWSLambdaHandler:access(conf) local scheme = conf.disable_https and "http" or "https" local endpoint = fmt("%s://%s", scheme, host) - local lambda_service = LAMBDA_SERVICE_CACHE[conf] + local cache_key = build_cache_key(conf) + local lambda_service = LAMBDA_SERVICE_CACHE:get(cache_key) if not lambda_service then local credentials = AWS.config.credentials -- Override credential config according to plugin config @@ -132,7 +159,7 @@ function AWSLambdaHandler:access(conf) http_proxy = conf.proxy_url, https_proxy = conf.proxy_url, }) - LAMBDA_SERVICE_CACHE[conf] = lambda_service + LAMBDA_SERVICE_CACHE:set(cache_key, lambda_service) end local upstream_body_json = build_request_payload(conf) diff --git a/spec/03-plugins/27-aws-lambda/99-access_spec.lua b/spec/03-plugins/27-aws-lambda/99-access_spec.lua index b5c5db6668dd..dc9ec8205ebc 100644 --- a/spec/03-plugins/27-aws-lambda/99-access_spec.lua +++ b/spec/03-plugins/27-aws-lambda/99-access_spec.lua @@ -7,6 +7,7 @@ local fixtures = require "spec.fixtures.aws-lambda" local TEST_CONF = helpers.test_conf local server_tokens = meta._SERVER_TOKENS local null = ngx.null +local fmt = string.format @@ -1182,4 +1183,96 @@ for _, strategy in helpers.each_strategy() do end) end) end) + + describe("Plugin: AWS Lambda with #vault [#" .. strategy .. "]", function () + local proxy_client + local admin_client + + local ttl_time = 1 + + lazy_setup(function () + helpers.setenv("KONG_VAULT_ROTATION_INTERVAL", "1") + + local bp = helpers.get_db_utils(strategy, { + "routes", + "services", + "plugins", + "vaults", + }, { "aws-lambda" }, { "random" }) + + local route1 = bp.routes:insert { + hosts = { "lambda-vault.com" }, + } + + bp.plugins:insert { + name = "aws-lambda", + route = { id = route1.id }, + config = { + port = 10001, + aws_key = fmt("{vault://random/aws_key?ttl=%s&resurrect_ttl=0}", ttl_time), + aws_secret = "aws_secret", + aws_region = "us-east-1", + function_name = "functionEcho", + }, + } + + assert(helpers.start_kong({ + database = strategy, + prefix = helpers.test_conf.prefix, + nginx_conf = "spec/fixtures/custom_nginx.template", + vaults = "random", + plugins = "bundled", + log_level = "error", + }, nil, nil, fixtures)) + end) + + lazy_teardown(function() + helpers.unsetenv("KONG_VAULT_ROTATION_INTERVAL") + + helpers.stop_kong() + end) + + before_each(function() + proxy_client = helpers.proxy_client() + admin_client = helpers.admin_client() + end) + + after_each(function () + proxy_client:close() + admin_client:close() + end) + + it("lambda service should use latest reference value after Vault ttl", function () + local res = assert(proxy_client:send { + method = "GET", + path = "/get?key1=some_value1&key2=some_value2&key3=some_value3", + headers = { + ["Host"] = "lambda-vault.com" + } + }) + assert.res_status(200, res) + local body = assert.response(res).has.jsonbody() + local authorization_header = body.headers.authorization + local first_aws_key = string.match(authorization_header, "Credential=(.+)/") + + assert.eventually(function() + proxy_client:close() + proxy_client = helpers.proxy_client() + + local res = assert(proxy_client:send { + method = "GET", + path = "/get?key1=some_value1&key2=some_value2&key3=some_value3", + headers = { + ["Host"] = "lambda-vault.com" + } + }) + assert.res_status(200, res) + local body = assert.response(res).has.jsonbody() + local authorization_header = body.headers.authorization + local second_aws_key = string.match(authorization_header, "Credential=(.+)/") + + return first_aws_key ~= second_aws_key + end).ignore_exceptions(true).with_timeout(ttl_time * 2).is_truthy() + end) + end) end diff --git a/spec/fixtures/custom_vaults/kong/vaults/random/init.lua b/spec/fixtures/custom_vaults/kong/vaults/random/init.lua new file mode 100644 index 000000000000..617754ebe6e9 --- /dev/null +++ b/spec/fixtures/custom_vaults/kong/vaults/random/init.lua @@ -0,0 +1,13 @@ +local utils = require "kong.tools.utils" + +local function get(conf, resource, version) + -- Return a random string every time + kong.log.err("get() called") + return utils.random_string() +end + + +return { + VERSION = "1.0.0", + get = get, +} diff --git a/spec/fixtures/custom_vaults/kong/vaults/random/schema.lua b/spec/fixtures/custom_vaults/kong/vaults/random/schema.lua new file mode 100644 index 000000000000..c48b47ce2061 --- /dev/null +++ b/spec/fixtures/custom_vaults/kong/vaults/random/schema.lua @@ -0,0 +1,19 @@ +local typedefs = require "kong.db.schema.typedefs" + +return { + name = "random", + fields = { + { + config = { + type = "record", + fields = { + { prefix = { type = "string" } }, + { suffix = { type = "string" } }, + { ttl = typedefs.ttl }, + { neg_ttl = typedefs.ttl }, + { resurrect_ttl = typedefs.ttl }, + }, + }, + }, + }, +}