Skip to content

Commit 9ddce5e

Browse files
committed
Add an optional proxy for signing AWS Elasticsearch requests.
1 parent a201220 commit 9ddce5e

File tree

11 files changed

+323
-6
lines changed

11 files changed

+323
-6
lines changed

config/default.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ elasticsearch:
199199
breaker:
200200
fielddata:
201201
limit: 60%
202+
aws_signing_proxy:
203+
host: 127.0.0.1
204+
port: 14017
202205
analytics:
203206
adapter: elasticsearch
204207
timezone: UTC

src/api-umbrella/cli/read_config.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,9 @@ local function set_computed_config()
356356
},
357357
["_service_general_db_enabled?"] = array_includes(config["services"], "general_db"),
358358
["_service_log_db_enabled?"] = array_includes(config["services"], "log_db"),
359+
["_service_elasticsearch_aws_signing_proxy_enabled?"] = array_includes(config["services"], "elasticsearch_aws_signing_proxy"),
359360
["_service_router_enabled?"] = array_includes(config["services"], "router"),
360361
["_service_web_enabled?"] = array_includes(config["services"], "web"),
361-
["_service_nginx_reloader_enabled?"] = (array_includes(config["services"], "router") and config["nginx"]["_reloader_frequency"]),
362362
router = {
363363
trusted_proxies = trusted_proxies,
364364
},

src/api-umbrella/cli/setup.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local deep_merge_overwrite_arrays = require "api-umbrella.utils.deep_merge_overwrite_arrays"
12
local dir = require "pl.dir"
23
local file = require "pl.file"
34
local invert_table = require "api-umbrella.utils.invert_table"
@@ -214,6 +215,9 @@ local function activate_services()
214215
if config["_service_log_db_enabled?"] then
215216
active_services["elasticsearch"] = 1
216217
end
218+
if config["_service_elasticsearch_aws_signing_proxy_enabled?"] then
219+
active_services["elasticsearch-aws-signing-proxy"] = 1
220+
end
217221
if config["_service_router_enabled?"] then
218222
active_services["geoip-auto-updater"] = 1
219223
active_services["mora"] = 1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
inspect = require "inspect"
2+
config = require "api-umbrella.proxy.models.file_config"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local username = config["elasticsearch"]["aws_signing_proxy"]["username"]
2+
if not username then
3+
ngx.say("elasticsearch.aws_signing_proxy.username must be configured in /etc/api-umbrella/api-umbrella.yml")
4+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
5+
end
6+
7+
local password = config["elasticsearch"]["aws_signing_proxy"]["password"]
8+
if not password then
9+
ngx.say("elasticsearch.aws_signing_proxy.password must be configured in /etc/api-umbrella/api-umbrella.yml")
10+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
11+
end
12+
13+
local aws_region = config["elasticsearch"]["aws_signing_proxy"]["aws_region"]
14+
if not aws_region then
15+
ngx.say("elasticsearch.aws_signing_proxy.aws_region must be configured in /etc/api-umbrella/api-umbrella.yml")
16+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
17+
end
18+
19+
local aws_access_key_id = config["elasticsearch"]["aws_signing_proxy"]["aws_access_key_id"]
20+
if not aws_access_key_id then
21+
ngx.say("elasticsearch.aws_signing_proxy.aws_access_key_id must be configured in /etc/api-umbrella/api-umbrella.yml")
22+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
23+
end
24+
25+
local aws_secret_access_key = config["elasticsearch"]["aws_signing_proxy"]["aws_secret_access_key"]
26+
if not aws_secret_access_key then
27+
ngx.say("elasticsearch.aws_signing_proxy.aws_access_key_id must be configured in /etc/api-umbrella/api-umbrella.yml")
28+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
29+
end
30+
31+
local remote_username = ngx.var.remote_user
32+
local remote_password = ngx.var.remote_passwd
33+
if not ngx.var.remote_user or not remote_password then
34+
ngx.header["WWW-Authenticate"] = 'Basic realm="Restricted"'
35+
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
36+
end
37+
38+
if remote_username ~= username or remote_password ~= password then
39+
return ngx.exit(ngx.HTTP_FORBIDDEN)
40+
end
41+
42+
local host = config["elasticsearch"]["aws_signing_proxy"]["aws_host"]
43+
ngx.req.set_header("Host", host)
44+
45+
local signing = require "api-umbrella.utils.aws_signing_v4"
46+
signing.sign_request(aws_region, aws_access_key_id, aws_secret_access_key)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
local nettle_hmac = require "resty.nettle.hmac"
2+
local resty_sha256 = require "resty.sha256"
3+
local to_hex = require("resty.string").to_hex
4+
5+
local escape_uri = ngx.escape_uri
6+
local gsub = ngx.re.gsub
7+
8+
local AWS_SERVICE = "es"
9+
local UNSIGNED_HEADERS = {
10+
authorization = 1,
11+
expect = 1,
12+
}
13+
14+
local _M = {}
15+
16+
local function hmac(secret_key, value)
17+
assert(secret_key)
18+
assert(value)
19+
20+
local hmac_sha256 = nettle_hmac.sha256.new(secret_key)
21+
hmac_sha256:update(value)
22+
local binary = hmac_sha256:digest()
23+
24+
return binary
25+
end
26+
27+
local function sha256_hexdigest(value)
28+
local sha256 = resty_sha256:new()
29+
sha256:update(value or "")
30+
return to_hex(sha256:final())
31+
end
32+
33+
local function canonical_header_name(name)
34+
return string.lower(name)
35+
end
36+
37+
local function canonical_header_value(value)
38+
return gsub(value, [[\s+]], " ", "jo")
39+
end
40+
41+
local function escape_uri_component(value)
42+
if(value == true) then
43+
return ""
44+
else
45+
return escape_uri(value or "")
46+
end
47+
end
48+
49+
local function get_headers()
50+
local headers = {}
51+
52+
local raw_headers = ngx.req.get_headers()
53+
for name, value in pairs(raw_headers) do
54+
if type(value) == "table" then
55+
for multi_name, multi_value in pairs(value) do
56+
table.insert(headers, {
57+
name = canonical_header_name(multi_name),
58+
value = canonical_header_value(multi_value),
59+
})
60+
end
61+
else
62+
table.insert(headers, {
63+
name = canonical_header_name(name),
64+
value = canonical_header_value(value),
65+
})
66+
end
67+
end
68+
69+
return headers
70+
end
71+
72+
local function get_canonical_headers(headers)
73+
local canonical = {}
74+
for _, header in ipairs(headers) do
75+
if not UNSIGNED_HEADERS[header.name] then
76+
table.insert(canonical, header.name .. ":" .. header.value)
77+
end
78+
end
79+
80+
table.sort(canonical)
81+
return table.concat(canonical, "\n")
82+
end
83+
84+
local function get_signed_headers(headers)
85+
local signed = {}
86+
for _, header in ipairs(headers) do
87+
if not UNSIGNED_HEADERS[header.name] then
88+
table.insert(signed, header.name)
89+
end
90+
end
91+
92+
table.sort(signed)
93+
return table.concat(signed, ";")
94+
end
95+
96+
local function get_canonical_query_string()
97+
local canonical = {}
98+
local args = ngx.req.get_uri_args()
99+
for name, value in pairs(args) do
100+
if type(value) == "table" then
101+
for multi_name, multi_value in pairs(value) do
102+
table.insert(canonical, escape_uri_component(multi_name) .. "=" .. escape_uri_component(multi_value))
103+
end
104+
else
105+
table.insert(canonical, escape_uri_component(name) .. "=" .. escape_uri_component(value))
106+
end
107+
end
108+
109+
table.sort(canonical)
110+
return table.concat(canonical, "&")
111+
end
112+
113+
local function get_canonical_request(headers, signed_headers, content_sha256)
114+
return table.concat({
115+
ngx.var.request_method,
116+
gsub(escape_uri(ngx.var.uri), [[%2F]], "/", "ijo"),
117+
get_canonical_query_string(),
118+
get_canonical_headers(headers) .. "\n",
119+
signed_headers,
120+
content_sha256,
121+
}, "\n")
122+
end
123+
124+
local function get_credential_scope(aws_region, date)
125+
return table.concat({
126+
date,
127+
aws_region,
128+
AWS_SERVICE,
129+
"aws4_request",
130+
}, "/")
131+
end
132+
133+
local function get_string_to_sign(datetime, credential_scope, canonical_request)
134+
return table.concat({
135+
"AWS4-HMAC-SHA256",
136+
datetime,
137+
credential_scope,
138+
sha256_hexdigest(canonical_request),
139+
}, "\n")
140+
end
141+
142+
local function get_signature(aws_region, aws_secret_access_key, date, string_to_sign)
143+
local k_date = hmac("AWS4" .. aws_secret_access_key, date)
144+
local k_region = hmac(k_date, aws_region)
145+
local k_service = hmac(k_region, AWS_SERVICE)
146+
local k_credentials = hmac(k_service, "aws4_request")
147+
return to_hex(hmac(k_credentials, string_to_sign))
148+
end
149+
150+
local function get_authorization(aws_access_key_id, credential_scope, signed_headers, signature)
151+
return table.concat({
152+
"AWS4-HMAC-SHA256 Credential=" .. aws_access_key_id .. "/" .. credential_scope,
153+
"SignedHeaders=" .. signed_headers,
154+
"Signature=" .. signature,
155+
}, ", ")
156+
end
157+
158+
function _M.sign_request(aws_region, aws_access_key_id, aws_secret_access_key)
159+
local datetime = os.date("!%Y%m%dT%H%M%SZ", ngx.now())
160+
local date = string.sub(datetime, 1, 8)
161+
ngx.req.set_header("X-Amz-Date", os.date("!%Y%m%dT%H%M%SZ", ngx.now()))
162+
163+
ngx.req.read_body()
164+
local body = ngx.req.get_body_data()
165+
local content_sha256 = sha256_hexdigest(body)
166+
ngx.req.set_header("X-Amz-Content-Sha256", content_sha256)
167+
168+
local headers = get_headers()
169+
local signed_headers = get_signed_headers(headers)
170+
local credential_scope = get_credential_scope(aws_region, date)
171+
172+
local canonical_request = get_canonical_request(headers, signed_headers, content_sha256)
173+
local string_to_sign = get_string_to_sign(datetime, credential_scope, canonical_request)
174+
local signature = get_signature(aws_region, aws_secret_access_key, date, string_to_sign)
175+
local authorization = get_authorization(aws_access_key_id, credential_scope, signed_headers, signature)
176+
ngx.req.set_header("Authorization", authorization)
177+
end
178+
179+
return _M
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
worker_processes 1;
2+
error_log stderr notice;
3+
daemon off;
4+
pid {{run_dir}}/elasticsearch-aws-signing-proxy.pid;
5+
6+
{{#user}}
7+
user {{user}} {{group}};
8+
{{/user}}
9+
10+
events {
11+
worker_connections {{nginx.worker_connections}};
12+
}
13+
14+
env API_UMBRELLA_SRC_ROOT;
15+
env API_UMBRELLA_RUNTIME_CONFIG;
16+
17+
pcre_jit on;
18+
19+
http {
20+
access_log {{log_dir}}/elasticsearch-aws-signing-proxy/{{nginx.access_log_filename}} combined;
21+
22+
client_body_temp_path {{tmp_dir}}/elasticsearch-aws-signing-proxy-client_body_temp;
23+
proxy_temp_path {{tmp_dir}}/elasticsearch-aws-signing-proxy-proxy_temp;
24+
fastcgi_temp_path {{tmp_dir}}/elasticsearch-aws-signing-proxy-fastcgi_temp;
25+
uwsgi_temp_path {{tmp_dir}}/elasticsearch-aws-signing-proxy-uwsgi_temp;
26+
scgi_temp_path {{tmp_dir}}/elasticsearch-aws-signing-proxy-scgi_temp;
27+
server_tokens off;
28+
29+
# FIXME: Detect path/make configurable.
30+
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
31+
lua_ssl_verify_depth 5;
32+
33+
lua_package_path '{{_src_root_dir}}/src/api-umbrella/elasticsearch-aws-signing-proxy/?.lua;{{_package_path}}';
34+
lua_package_cpath '{{_package_cpath}}';
35+
lua_check_client_abort on;
36+
if_modified_since off;
37+
38+
lua_shared_dict locks {{nginx.shared_dicts.locks.size}};
39+
40+
{{#dns_resolver._nameservers_nginx}}
41+
# FIXME: Make ipv6 configurable.
42+
resolver {{dns_resolver._nameservers_nginx}} ipv6=off;
43+
resolver_timeout 12s;
44+
{{/dns_resolver._nameservers_nginx}}
45+
46+
include ./mime.conf;
47+
include ./realip.conf;
48+
49+
client_max_body_size 10m;
50+
client_body_buffer_size 10m;
51+
52+
init_by_lua_file '{{_src_root_dir}}/src/api-umbrella/elasticsearch-aws-signing-proxy/init.lua';
53+
54+
server {
55+
listen {{elasticsearch.aws_signing_proxy.host}}:{{elasticsearch.aws_signing_proxy.port}};
56+
57+
{{#_development_env?}}
58+
lua_code_cache off;
59+
{{/_development_env?}}
60+
61+
location / {
62+
access_by_lua_file '{{_src_root_dir}}/src/api-umbrella/elasticsearch-aws-signing-proxy/proxy.lua';
63+
64+
proxy_buffering off;
65+
set $backend_upstream "https://{{elasticsearch.aws_signing_proxy.aws_host}}:443";
66+
proxy_pass $backend_upstream;
67+
}
68+
}
69+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
API_UMBRELLA_RUNTIME_CONFIG={{_api_umbrella_config_runtime_file}}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
exec ../rc.log "$@"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
3+
# Redirect stderr to stdout
4+
exec 2>&1
5+
6+
if [ "${1}" = "start" ]; then
7+
echo "starting ${2}..."
8+
9+
run_args=("-e" "rc.env" "-c" "{{_src_root_dir}}")
10+
exec runtool "${run_args[@]}" nginx -p "{{_src_root_dir}}/" -c "{{etc_dir}}/nginx/elasticsearch-aws-signing-proxy.conf"
11+
fi
12+
13+
exit 0

test/support/models/log_item.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#require "elasticsearch/persistence/model"
2-
31
class LogItem
42
include ActiveAttr::Model
53

@@ -71,7 +69,7 @@ def self.clean_indices!
7169
opts[:search_type] = "scan"
7270
end
7371
result = self.client.search(opts)
74-
while true
72+
loop do
7573
hits = result["hits"]["hits"]
7674
break if hits.empty?
7775
hits.each do |hit|
@@ -102,8 +100,8 @@ def serializable_hash
102100

103101
def save
104102
index_time = self.request_at
105-
if(index_time.kind_of?(Fixnum))
106-
index_time = Time.at(index_time / 1000.0)
103+
if(index_time.kind_of?(Integer))
104+
index_time = Time.at(index_time / 1000.0).utc
107105
end
108106

109107
index_name = "api-umbrella-logs-write-#{index_time.utc.strftime("%Y-%m")}"

0 commit comments

Comments
 (0)