diff --git a/kong-2.8.0-0.rockspec b/kong-2.8.0-0.rockspec index a66079ffe0f2..8134223ba8d1 100644 --- a/kong-2.8.0-0.rockspec +++ b/kong-2.8.0-0.rockspec @@ -405,6 +405,7 @@ build = { ["kong.plugins.aws-lambda.handler"] = "kong/plugins/aws-lambda/handler.lua", ["kong.plugins.aws-lambda.iam-ec2-credentials"] = "kong/plugins/aws-lambda/iam-ec2-credentials.lua", ["kong.plugins.aws-lambda.iam-ecs-credentials"] = "kong/plugins/aws-lambda/iam-ecs-credentials.lua", + ["kong.plugins.aws-lambda.iam-sts-credentials"] = "kong/plugins/aws-lambda/iam-sts-credentials.lua", ["kong.plugins.aws-lambda.schema"] = "kong/plugins/aws-lambda/schema.lua", ["kong.plugins.aws-lambda.v4"] = "kong/plugins/aws-lambda/v4.lua", ["kong.plugins.aws-lambda.request-util"] = "kong/plugins/aws-lambda/request-util.lua", diff --git a/kong/plugins/aws-lambda/handler.lua b/kong/plugins/aws-lambda/handler.lua index 2c0f7be75e1d..dd08ce1c1c6b 100644 --- a/kong/plugins/aws-lambda/handler.lua +++ b/kong/plugins/aws-lambda/handler.lua @@ -18,18 +18,39 @@ local AWS_REGION do end -local fetch_credentials do - local credential_sources = { - require "kong.plugins.aws-lambda.iam-ecs-credentials", - -- The EC2 one will always return `configured == true`, so must be the last! - require "kong.plugins.aws-lambda.iam-ec2-credentials", - } +local function fetch_aws_credentials(aws_conf) + local fetch_metadata_credentials do + local metadata_credentials_source = { + require "kong.plugins.aws-lambda.iam-ecs-credentials", + -- The EC2 one will always return `configured == true`, so must be the last! + require "kong.plugins.aws-lambda.iam-ec2-credentials", + } + + for _, credential_source in ipairs(metadata_credentials_source) do + if credential_source.configured then + fetch_metadata_credentials = credential_source.fetchCredentials + break + end + end + end + + if aws_conf.aws_assume_role_arn then + local metadata_credentials, err = fetch_metadata_credentials() - for _, credential_source in ipairs(credential_sources) do - if credential_source.configured then - fetch_credentials = credential_source.fetchCredentials - break + if err then + return nil, err end + + local aws_sts_cred_source = require "kong.plugins.aws-lambda.iam-sts-credentials" + return aws_sts_cred_source.fetch_assume_role_credentials(aws_conf.aws_region, + aws_conf.aws_assume_role_arn, + aws_conf.aws_role_session_name, + metadata_credentials.access_key, + metadata_credentials.secret_key, + metadata_credentials.session_token) + + else + return fetch_metadata_credentials() end end @@ -235,12 +256,19 @@ function AWSLambdaHandler:access(conf) query = conf.qualifier and "Qualifier=" .. conf.qualifier } + local aws_conf = { + aws_region = conf.aws_region, + aws_assume_role_arn = conf.aws_assume_role_arn, + aws_role_session_name = conf.aws_role_session_name, + } + if not conf.aws_key then -- no credentials provided, so try the IAM metadata service local iam_role_credentials = kong.cache:get( IAM_CREDENTIALS_CACHE_KEY, nil, - fetch_credentials + fetch_aws_credentials, + aws_conf ) if not iam_role_credentials then @@ -292,6 +320,10 @@ function AWSLambdaHandler:access(conf) local content = res.body + if res.status >= 400 then + return error(content) + end + -- setting the latency here is a bit tricky, but because we are not -- actually proxying, it will not be overwritten ctx.KONG_WAITING_TIME = get_now() - kong_wait_time_start diff --git a/kong/plugins/aws-lambda/iam-sts-credentials.lua b/kong/plugins/aws-lambda/iam-sts-credentials.lua new file mode 100644 index 000000000000..06cd5fca36b4 --- /dev/null +++ b/kong/plugins/aws-lambda/iam-sts-credentials.lua @@ -0,0 +1,107 @@ +local http = require "resty.http" +local json = require "cjson" +local aws_v4 = require "kong.plugins.aws-lambda.v4" +local utils = require "kong.tools.utils" +local ngx_now = ngx.now +local kong = kong + +local DEFAULT_SESSION_DURATION_SECONDS = 3600 +local DEFAULT_HTTP_CLINET_TIMEOUT = 60000 +local DEFAULT_ROLE_SESSION_NAME = "kong" + + +local function get_regional_sts_endpoint(aws_region) + if aws_region then + return 'sts.' .. aws_region .. '.amazonaws.com' + else + return 'sts.amazonaws.com' + end +end + + +local function fetch_assume_role_credentials(aws_region, assume_role_arn, + role_session_name, access_key, + secret_key, session_token) + if not assume_role_arn then + return nil, "Missing required parameter 'assume_role_arn' for fetching STS credentials" + end + + role_session_name = role_session_name or DEFAULT_ROLE_SESSION_NAME + + kong.log.debug('Trying to assume role [', assume_role_arn, ']') + + local sts_host = get_regional_sts_endpoint(aws_region) + + -- build the url and signature to assume role + local assume_role_request_headers = { + Accept = "application/json", + ["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8", + ["X-Amz-Security-Token"] = session_token, + Host = sts_host + } + + local assume_role_query_params = { + Action = "AssumeRole", + Version = "2011-06-15", + RoleArn = assume_role_arn, + DurationSeconds = DEFAULT_SESSION_DURATION_SECONDS, + RoleSessionName = role_session_name, + } + + local assume_role_sign_params = { + region = aws_region, + service = "sts", + access_key = access_key, + secret_key = secret_key, + method = "GET", + host = sts_host, + port = 443, + headers = assume_role_request_headers, + query = utils.encode_args(assume_role_query_params) + } + + local request, err + request, err = aws_v4(assume_role_sign_params) + + if err then + return nil, 'Unable to build signature to assume role [' + .. assume_role_arn .. '] - error :'.. tostring(err) + end + + -- Call STS to assume role + local client = http.new() + client:set_timeout(DEFAULT_HTTP_CLINET_TIMEOUT) + local res, err = client:request_uri(request.url, { + method = request.method, + headers = request.headers, + ssl_verify = false, + }) + + if err then + local err_s = 'Unable to assume role [' .. assume_role_arn .. ']' .. + ' due to: ' .. tostring(err) + return nil, err_s + end + + if res.status ~= 200 then + local err_s = 'Unable to assume role [' .. assume_role_arn .. '] due to:' .. + 'status [' .. res.status .. '] - ' .. + 'reason [' .. res.body .. ']' + return nil, err_s + end + + local credentials = json.decode(res.body).AssumeRoleResponse.AssumeRoleResult.Credentials + local result = { + access_key = credentials.AccessKeyId, + secret_key = credentials.SecretAccessKey, + session_token = credentials.SessionToken, + expiration = credentials.Expiration + } + + return result, nil, result.expiration - ngx_now() +end + + +return { + fetch_assume_role_credentials = fetch_assume_role_credentials, +} diff --git a/kong/plugins/aws-lambda/schema.lua b/kong/plugins/aws-lambda/schema.lua index 0dda5971d5ca..96829ca96d75 100644 --- a/kong/plugins/aws-lambda/schema.lua +++ b/kong/plugins/aws-lambda/schema.lua @@ -27,6 +27,15 @@ return { encrypted = true, -- Kong Enterprise-exclusive feature, does nothing in Kong CE referenceable = true, } }, + { aws_assume_role_arn = { + type = "string", + encrypted = true, -- Kong Enterprise-exclusive feature, does nothing in Kong CE + referenceable = true, + } }, + { aws_role_session_name = { + type = "string", + default = "kong", + } }, { aws_region = typedefs.host }, { function_name = { type = "string", diff --git a/spec/03-plugins/27-aws-lambda/07-iam-sts-credentials_spec.lua b/spec/03-plugins/27-aws-lambda/07-iam-sts-credentials_spec.lua new file mode 100644 index 000000000000..830f8d626d22 --- /dev/null +++ b/spec/03-plugins/27-aws-lambda/07-iam-sts-credentials_spec.lua @@ -0,0 +1,72 @@ +require "spec.helpers" + +describe("[AWS Lambda] iam-sts", function() + + local fetch_sts_assume_role, http_responses + + before_each(function() + package.loaded["kong.plugins.aws-lambda.iam-sts-credentials"] = nil + package.loaded["resty.http"] = nil + local http = require "resty.http" + -- mock the http module + http.new = function() + return { + set_timeout = function() end, + request_uri = function() + local body = http_responses[1] + table.remove(http_responses, 1) + return { + status = 200, + body = body, + } + end, + } + end + fetch_sts_assume_role = require("kong.plugins.aws-lambda.iam-sts-credentials").fetch_assume_role_credentials + end) + + after_each(function() + end) + + it("should fetch credentials from sts service", function() + http_responses = { + [[ +{ + "AssumeRoleResponse": { + "AssumeRoleResult": { + "SourceIdentity": "kong_session", + "AssumedRoleUser": { + "Arn": "arn:aws:iam::000000000001:role/temp-role", + "AssumedRoleId": "arn:aws:iam::000000000001:role/temp-role" + }, + "Credentials": { + "AccessKeyId": "the Access Key", + "SecretAccessKey": "the Big Secret", + "SessionToken": "the Token of Appreciation", + "Expiration": 1552424170 + }, + "PackedPolicySize": 1000 + }, + "ResponseMetadata": { + "RequestId": "c6104cbe-af31-11e0-8154-cbc7ccf896c7" + } + } +} +]] + } + + local aws_region = "ap-east-1" + local assume_role_arn = "arn:aws:iam::000000000001:role/temp-role" + local role_session_name = "kong_session" + local access_key = "test_access_key" + local secret_key = "test_secret_key" + local session_token = "test_session_token" + local iam_role_credentials, err = fetch_sts_assume_role(aws_region, assume_role_arn, role_session_name, access_key, secret_key, session_token) + + assert.is_nil(err) + assert.equal("the Access Key", iam_role_credentials.access_key) + assert.equal("the Big Secret", iam_role_credentials.secret_key) + assert.equal("the Token of Appreciation", iam_role_credentials.session_token) + assert.equal(1552424170, iam_role_credentials.expiration) + end) +end)