Skip to content

Commit

Permalink
feat: better errors, simpler code. (#39)
Browse files Browse the repository at this point in the history
* fixed commit

* added schema config params.

* Update Readme.md

* Update sigv4.lua

* Update sigv4.lua
  • Loading branch information
DanielRailean authored Feb 29, 2024
1 parent 0a9c230 commit fc1e257
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 62 deletions.
28 changes: 26 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
52 changes: 33 additions & 19 deletions kong/plugins/aws-request-signing/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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!")
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
10 changes: 10 additions & 0 deletions kong/plugins/aws-request-signing/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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",
} }
}
},
}
Expand Down
74 changes: 34 additions & 40 deletions kong/plugins/aws-request-signing/sigv4.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -154,56 +149,54 @@ 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, "&")
local canonical_querystring = canonicalise_query_string(request_query)

-- 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' ..
Expand All @@ -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 {
Expand All @@ -233,4 +227,4 @@ local function prepare_awsv4_request(opts)
}
end

return prepare_awsv4_request
return prepare_awsv4_request

0 comments on commit fc1e257

Please sign in to comment.