From fc1e257615b0119c7a20a6458d155d9f12870bd0 Mon Sep 17 00:00:00 2001 From: DanielRailean Date: Thu, 29 Feb 2024 15:27:59 +0100 Subject: [PATCH] feat: better errors, simpler code. (#39) * fixed commit * added schema config params. * Update Readme.md * Update sigv4.lua * Update sigv4.lua --- Readme.md | 28 ++++++- ... kong-aws-request-signing-1.0.5-3.rockspec | 2 +- kong/plugins/aws-request-signing/handler.lua | 52 ++++++++----- kong/plugins/aws-request-signing/schema.lua | 10 +++ kong/plugins/aws-request-signing/sigv4.lua | 74 +++++++++---------- 5 files changed, 104 insertions(+), 62 deletions(-) rename kong-aws-request-signing-1.0.4-3.rockspec => kong-aws-request-signing-1.0.5-3.rockspec (97%) diff --git a/Readme.md b/Readme.md index 2152386..fa56819 100644 --- a/Readme.md +++ b/Readme.md @@ -50,9 +50,19 @@ default = false required = false sign_query - Controls if the signature will be sent in the header or in the query. By default, header is used, if enabled will sign the query. -type = boolean +type = "boolean" required = true default = false + +preserve_auth_header - Controls if the bearer token will be passed to the upstream +type = "boolean" +required = true +default = true + +preserve_auth_header_key - The header key where the bearer token will be saved and passed to the upstream. works only if 'preserve_auth_header' parameter above is set to true. +type = "string" +required = true +default = "x-authorization" ``` ## Using multiple Lambdas with the same Kong Service @@ -74,7 +84,7 @@ There are two things necessary to make a custom plugin work in Kong: The easiest way to install the plugin is using `luarocks`. ```sh -luarocks install https://github.com/LEGO/kong-aws-request-signing/raw/main/rocks/kong-aws-request-signing-1.0.4-3.all.rock +luarocks install https://github.com/LEGO/kong-aws-request-signing/raw/main/rocks/kong-aws-request-signing-1.0.5-3.all.rock ``` You can substitute `1.0.0-3` in the command above with any other version you want to install. @@ -99,6 +109,20 @@ plugins: pluginName: aws-request-signing ``` +## Signing requests containing a body + +In case of requests contanining a body, the plugin is highly reliant on the nginx configuration, because it neets to access the body to sign it. +The behaviour is controlled by the following Kong configuration parameters: + +```text +nginx_http_client_max_body_size +nginx_http_client_body_buffer_size +``` + +[Kong docs reference.](https://docs.konghq.com/gateway/latest/reference/configuration/#nginx_http_client_body_buffer_size) + +The default value for max body size is `0`, which means unlimited, so consider setting the `nginx_http_client_body_buffer_size` as high as you consider reasonable, as requests containing a bigger body, will fail. + ## AWS Setup required 1. You have a [Lambda function](https://eu-west-1.console.aws.amazon.com/lambda/home?region=eu-west-1#) deployed with `Function URL` enabled and Auth type : `AWS_IAM` or you have an S3 bucket with public access disabled. diff --git a/kong-aws-request-signing-1.0.4-3.rockspec b/kong-aws-request-signing-1.0.5-3.rockspec similarity index 97% rename from kong-aws-request-signing-1.0.4-3.rockspec rename to kong-aws-request-signing-1.0.5-3.rockspec index fae3050..fde8c43 100644 --- a/kong-aws-request-signing-1.0.4-3.rockspec +++ b/kong-aws-request-signing-1.0.5-3.rockspec @@ -1,6 +1,6 @@ local plugin_name = "aws-request-signing" local package_name = "kong-" .. plugin_name -local package_version = "1.0.4" +local package_version = "1.0.5" local rockspec_revision = "3" local github_account_name = "LEGO" diff --git a/kong/plugins/aws-request-signing/handler.lua b/kong/plugins/aws-request-signing/handler.lua index f708dd0..f62ab95 100644 --- a/kong/plugins/aws-request-signing/handler.lua +++ b/kong/plugins/aws-request-signing/handler.lua @@ -6,12 +6,6 @@ local error = error local type = type local json = require "cjson" - -local get_raw_body = kong.request.get_raw_body - -local set_headers = kong.service.request.set_headers -local set_raw_query = kong.service.request.set_raw_query - local IAM_CREDENTIALS_CACHE_KEY_PATTERN = "plugin.aws-request-signing.iam_role_temp_creds.%s" local AWSLambdaSTS = {} @@ -52,6 +46,7 @@ if _TEST then end local function get_iam_credentials(sts_conf, refresh, return_sts_error) + local generic_error = "Error fetching STS credentials. Enable 'return_sts_error' in config for details." local iam_role_cred_cache_key = string.format(IAM_CREDENTIALS_CACHE_KEY_PATTERN, sts_conf.RoleArn) if refresh then @@ -73,7 +68,7 @@ local function get_iam_credentials(sts_conf, refresh, return_sts_error) local resError = json.decode(errJson) return kong.response.exit(resError.sts_status, { message = resError.message, stsResponse = resError.sts_body }) else - return kong.response.exit(401, {message = 'Error fetching STS credentials!'}) + return kong.response.exit(401, {message = generic_error}) end end @@ -93,7 +88,7 @@ local function get_iam_credentials(sts_conf, refresh, return_sts_error) local resError = json.decode(errJson) return kong.response.exit(resError.sts_status, { message = resError.message, stsResponse = resError.sts_body }) else - return kong.response.exit(401, {message = 'Error fetching STS credentials!'}) + return kong.response.exit(401, {message = generic_error}) end end kong.log.debug("expiring key , invalidated iam_cache and fetched fresh credentials!") @@ -112,7 +107,13 @@ function AWSLambdaSTS:access(conf) if service == nil then kong.log.err("Unable to retrieve bound service!") - return kong.response.exit(500, { message = "Internal error 1!" }) + return kong.response.exit(500, { message = "The plugin must be bound to a service!" }) + end + + if conf.preserve_auth_header then + kong.service.request.set_headers({ + [conf.preserve_auth_header_key] = request_headers.authorization + }) end if conf.override_target_protocol then @@ -139,18 +140,30 @@ function AWSLambdaSTS:access(conf) -- we only send those two headers for signing local upstream_headers = { host = final_host, - ["x-authorization"] = request_headers.authorization + -- those will be nill thus we only pass the host on requests without body + ["content-length"] = request_headers["content-length"], + ["content-type"] = request_headers["content-type"] } -- removing the authorization, we either do not need it or we set it again later. kong.service.request.clear_header("authorization") - local opts = { + -- might fail if too big. is controlled by the folowing nginx params: + -- nginx_http_client_max_body_size + -- nginx_http_client_body_buffer_size + local req_body, get_body_err = kong.request.get_raw_body() + + if get_body_err or req_body == nil then + kong.log.err(get_body_err) + return kong.response.exit(400, { message = "Request body exceeds size limit and cannot be used by plugins." }) + end + + local sigv4_opts = { region = conf.aws_region, service = conf.aws_service, method = kong.request.get_method(), headers = upstream_headers, - body = get_raw_body(), + body = req_body, path = ngx.var.upstream_uri, host = final_host, port = service.port, @@ -161,20 +174,21 @@ function AWSLambdaSTS:access(conf) sign_query = conf.sign_query } - local request, err = sigv4(opts) - if err then - return error(err) + local signed_request, sigv4_err = sigv4(sigv4_opts) + if sigv4_err then + kong.log.err(sigv4_err) + return error(sigv4_err) end - if not request then + if not signed_request then return kong.response.exit(500, { message = "Unable to SIGV4 the request!" }) end - set_headers(request.headers) - set_raw_query(request.query) + kong.service.request.set_headers(signed_request.headers) + kong.service.request.set_raw_query(signed_request.query) end AWSLambdaSTS.PRIORITY = 110 -AWSLambdaSTS.VERSION = "1.0.4" +AWSLambdaSTS.VERSION = "1.0.5" return AWSLambdaSTS diff --git a/kong/plugins/aws-request-signing/schema.lua b/kong/plugins/aws-request-signing/schema.lua index 4e3426a..030d1bd 100644 --- a/kong/plugins/aws-request-signing/schema.lua +++ b/kong/plugins/aws-request-signing/schema.lua @@ -56,6 +56,16 @@ return { required = true, default = false, } }, + { preserve_auth_header = { + type = "boolean", + required = true, + default = true, + } }, + { preserve_auth_header_key = { + type = "string", + required = true, + default = "x-authorization", + } } } }, } diff --git a/kong/plugins/aws-request-signing/sigv4.lua b/kong/plugins/aws-request-signing/sigv4.lua index f9d0ed9..54ccf3c 100644 --- a/kong/plugins/aws-request-signing/sigv4.lua +++ b/kong/plugins/aws-request-signing/sigv4.lua @@ -91,27 +91,19 @@ local function canonicalise_query_string(query) end local function get_canonical_headers(headers) - local canonical_headers, signed_headers do - -- We structure this code in a way so that we only have to sort once. - canonical_headers, signed_headers = {}, {} - local i = 0 - for name, value in pairs(headers) do - if value then -- ignore headers with 'false', they are used to override defaults - i = i + 1 - local name_lower = name:lower() - signed_headers[i] = name_lower - canonical_headers[name_lower] = pl_string.strip(value) - end - end - table.sort(signed_headers) - for j=1, i do - local name = signed_headers[j] - local value = canonical_headers[name] - canonical_headers[j] = name .. ":" .. value .. "\n" - end - signed_headers = table.concat(signed_headers, ";", 1, i) - canonical_headers = table.concat(canonical_headers, nil, 1, i) + local signed_headers_arr = {} + local canonical_headers = "" + + -- sorting all header names after inserting in an array + for header_key in pairs(headers) do table.insert(signed_headers_arr, header_key:lower()) end + table.sort(signed_headers_arr) + + -- going over the sorted array and adding the header and header values to the canonical headers + local signed_headers = table.concat(signed_headers_arr, ";", 1) + for _, header_key in pairs(signed_headers_arr) do + canonical_headers = canonical_headers .. header_key .. ":" .. pl_string.strip(headers[header_key]) .. "\n" end + return { canonical_headers = canonical_headers, signed_headers = signed_headers @@ -136,7 +128,7 @@ local function prepare_awsv4_request(opts) local secret_key = opts.secret_key local request_headers = opts.headers or {} - local request_payload = opts.body + local request_body = opts.body local request_query = opts.query local timestamp = ngx.time() @@ -146,6 +138,9 @@ local function prepare_awsv4_request(opts) local canonical_uri = canonicalise_path(opts.path, service) local credential_scope = date .. "/" .. region .. "/" .. service .. "/aws4_request" + + local bodyHash = to_hex(hash(request_body)) + -- If the "standard" port is not in use, the port should be added to the Host header local host_header do if port == 443 or port == 80 then @@ -154,31 +149,35 @@ local function prepare_awsv4_request(opts) host_header = string.format("%s:%d", host, port) end end + request_headers["host"] = host_header + request_headers["x-amz-content-sha256"] = bodyHash + + local expiresInSeconds = 300 if not opts.sign_query then request_headers["x-amz-date"] = request_date request_headers["x-amz-security-token"] = opts.session_token if service == "s3" then - request_headers["x-amz-expires"] = "300" - request_headers["x-amz-content-sha256"] = "UNSIGNED-PAYLOAD" + request_headers["x-amz-expires"] = expiresInSeconds .. "" end end - local transformed_headers = get_canonical_headers(request_headers) + local canonical_headers = get_canonical_headers(request_headers) if opts.sign_query then - local expires = "" + local expires_query_param = "" if service == "s3" then - expires = "&X-Amz-Expires=300" + expires_query_param = "&X-Amz-Expires=" .. expiresInSeconds end - request_query = request_query .. "&X-Amz-Security-Token=" .. url_encode(opts.session_token) - .. expires + request_query = request_query + .. "&X-Amz-Security-Token=" .. url_encode(opts.session_token) + .. expires_query_param .. "&X-Amz-Date=" .. request_date .. "&X-Amz-Algorithm="..ALGORITHM .. "&X-Amz-Credential=" .. access_key .. "/" .. credential_scope - .. "&X-Amz-SignedHeaders=" .. transformed_headers.signed_headers + .. "&X-Amz-SignedHeaders=" .. canonical_headers.signed_headers end request_query = removeCharFromStart(request_query, "&") @@ -186,24 +185,18 @@ local function prepare_awsv4_request(opts) -- Task 1: Create a Canonical Request For Signature Version 4 -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html - local bodyHash = to_hex(hash(request_payload or "")) - if service == "s3" then - bodyHash = "UNSIGNED-PAYLOAD" - end - local canonical_request = request_method .. '\n' .. canonical_uri .. '\n' .. (canonical_querystring or "") .. '\n' .. - transformed_headers.canonical_headers .. '\n' .. - transformed_headers.signed_headers .. '\n' .. + canonical_headers.canonical_headers .. '\n' .. + canonical_headers.signed_headers .. '\n' .. bodyHash local hashed_canonical_request = to_hex(hash(canonical_request)) -- Task 2: Create a String to Sign for Signature Version 4 -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html - local string_to_sign = ALGORITHM .. '\n' .. request_date .. '\n' .. @@ -221,10 +214,11 @@ local function prepare_awsv4_request(opts) if opts.sign_query then request_query = request_query .. "&X-Amz-Signature=" .. signature else - request_headers["authorization"] = ALGORITHM + local auth_header = ALGORITHM .. " Credential=" .. access_key .. "/" .. credential_scope - .. ", SignedHeaders=" .. transformed_headers.signed_headers + .. ", SignedHeaders=" .. canonical_headers.signed_headers .. ", Signature=" .. signature + request_headers["authorization"] = auth_header end return { @@ -233,4 +227,4 @@ local function prepare_awsv4_request(opts) } end -return prepare_awsv4_request \ No newline at end of file +return prepare_awsv4_request