Skip to content

Commit 7f5df74

Browse files
authored
feat: Support AWS S3 Express One Zone buckets (#229)
# What This change adds the `S3_SERVICE` configuration variable which will default to `s3` and may be one of `s3express` or `s3`. It also introduces the `virtual-v2` `S3_STYLE` argument option in support of the connectivity requirement of the S3 Express One Zone (directory) buckets. We are using this as a successor to `virtual` and believe it should work well in all AWS usages but want to be cautious as we make this change. Many thanks for @hveiga for driving the implementation of this feature in their original pull request. Setting this variable to s3express will change the "service" used to sign the requests with the V4 header to s3express. Currently the gateway works without this step, but it's advised in the documentation [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-security-best-practices.html). ## Other Changes We are moving the determination of the hostname used to query S3 into the docker entrypoint (or bootstrap script for non-docker installs). If `S3_STYLE` is set to `virtual` (this is the default and aws recommended scheme) then the hostname will be: ``` ${S3_BUCKET_NAME}.${S3_SERVER}:${S3_SERVER_PORT} ``` which will be used in these locations: * The `proxy_path` directive * The HTTP `Host` header sent to AWS * The `host` element of the canonical headers used in signing AWS signature V4 requests. Based on my reading here: https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html It looks like AWS recommends that the bucket be always prepended and other schemes exist only for backwards compatibility reasons. However, please comment on this discussion if you have concerns #231 Co-authored-by: @hveiga <[email protected]>"
1 parent 7f3064b commit 7f5df74

23 files changed

+362
-43
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ jobs:
5454
test-oss:
5555
runs-on: ubuntu-22.04
5656
needs: build-oss-for-test
57+
strategy:
58+
matrix:
59+
path_style: [virtual, virtual-v2]
5760
steps:
5861
- uses: actions/checkout@v4
5962
- name: Install dependencies
@@ -82,7 +85,7 @@ jobs:
8285
run: |
8386
docker load --input ${{ runner.temp }}/oss.tar
8487
- name: Run tests - stable njs version
85-
run: ./test.sh --type oss
88+
run: S3_STYLE=${{ matrix.path_style }} ./test.sh --type oss
8689

8790
build-latest-njs-for-test:
8891
runs-on: ubuntu-22.04

.gitignore

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,45 @@ test-settings.*
346346
s3-requests.http
347347
httpRequests/
348348

349-
.bin/
349+
.bin/
350+
351+
# Created by https://www.toptal.com/developers/gitignore/api/terraform
352+
# Edit at https://www.toptal.com/developers/gitignore?templates=terraform
353+
354+
### Terraform ###
355+
# Local .terraform directories
356+
**/.terraform/*
357+
358+
# .tfstate files
359+
*.tfstate
360+
*.tfstate.*
361+
362+
# Crash log files
363+
crash.log
364+
crash.*.log
365+
366+
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
367+
# password, private keys, and other secrets. These should not be part of version
368+
# control as they are data points which are potentially sensitive and subject
369+
# to change depending on the environment.
370+
*.tfvars
371+
*.tfvars.json
372+
373+
# Ignore override files as they are usually used to override resources locally and so
374+
# are not checked in
375+
override.tf
376+
override.tf.json
377+
*_override.tf
378+
*_override.tf.json
379+
380+
# Include override files you do wish to add to version control using negated pattern
381+
# !example_override.tf
382+
383+
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
384+
# example: *tfplan*
385+
386+
# Ignore CLI configuration files
387+
.terraformrc
388+
terraform.rc
389+
.tfplan
390+
# End of https://www.toptal.com/developers/gitignore/api/terraform

common/docker-entrypoint.d/00-check-for-required-env.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ set -e
2222

2323
failed=0
2424

25-
required=("S3_BUCKET_NAME" "S3_SERVER" "S3_SERVER_PORT" "S3_SERVER_PROTO"
25+
required=("S3_SERVICE" "S3_BUCKET_NAME" "S3_SERVER" "S3_SERVER_PORT" "S3_SERVER_PROTO"
2626
"S3_REGION" "S3_STYLE" "ALLOW_DIRECTORY_LIST" "AWS_SIGS_VERSION"
2727
"CORS_ENABLED")
2828

@@ -122,6 +122,7 @@ if [ $failed -gt 0 ]; then
122122
fi
123123

124124
echo "S3 Backend Environment"
125+
echo "Service: ${S3_SERVICE}"
125126
echo "Access Key ID: ${AWS_ACCESS_KEY_ID}"
126127
echo "Origin: ${S3_SERVER_PROTO}://${S3_BUCKET_NAME}.${S3_SERVER}:${S3_SERVER_PORT}"
127128
echo "Region: ${S3_REGION}"

common/docker-entrypoint.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,27 @@ if [ -z "${CORS_ALLOWED_ORIGIN+x}" ]; then
6868
export CORS_ALLOWED_ORIGIN="*"
6969
fi
7070

71+
# This is the primary logic to determine the s3 host used for the
72+
# upstream (the actual proxying action) as well as the `Host` header
73+
#
74+
# It is currently slightly more complex than necessary because we are transitioning
75+
# to a new logic which is defined by "virtual-v2". "virtual-v2" is the recommended setting
76+
# for all deployments.
77+
78+
# S3_UPSTREAM needs the port specified. The port must
79+
# correspond to https/http in the proxy_pass directive.
80+
if [ "${S3_STYLE}" == "virtual-v2" ]; then
81+
export S3_UPSTREAM="${S3_BUCKET_NAME}.${S3_SERVER}:${S3_SERVER_PORT}"
82+
export S3_HOST_HEADER="${S3_BUCKET_NAME}.${S3_SERVER}:${S3_SERVER_PORT}"
83+
elif [ "${S3_STYLE}" == "path" ]; then
84+
export S3_UPSTREAM="${S3_SERVER}:${S3_SERVER_PORT}"
85+
export S3_HOST_HEADER="${S3_SERVER}:${S3_SERVER_PORT}"
86+
else
87+
export S3_UPSTREAM="${S3_SERVER}:${S3_SERVER_PORT}"
88+
export S3_HOST_HEADER="${S3_BUCKET_NAME}.${S3_SERVER}"
89+
fi
90+
91+
7192
# Nothing is modified under this line
7293

7394
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then

common/etc/nginx/include/s3gateway.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ _requireEnvVars('S3_SERVER_PORT');
3939
_requireEnvVars('S3_REGION');
4040
_requireEnvVars('AWS_SIGS_VERSION');
4141
_requireEnvVars('S3_STYLE');
42+
_requireEnvVars('S3_SERVICE');
4243

4344

4445
/**
@@ -86,7 +87,7 @@ const INDEX_PAGE = "index.html";
8687
* Constant defining the service requests are being signed for.
8788
* @type {string}
8889
*/
89-
const SERVICE = 's3';
90+
const SERVICE = process.env['S3_SERVICE'] || "s3";
9091

9192
/**
9293
* Transform the headers returned from S3 such that there isn't information
@@ -165,12 +166,7 @@ function s3date(r) {
165166
function s3auth(r) {
166167
const bucket = process.env['S3_BUCKET_NAME'];
167168
const region = process.env['S3_REGION'];
168-
let server;
169-
if (S3_STYLE === 'path') {
170-
server = process.env['S3_SERVER'] + ':' + process.env['S3_SERVER_PORT'];
171-
} else {
172-
server = process.env['S3_SERVER'];
173-
}
169+
const host = r.variables.s3_host;
174170
const sigver = process.env['AWS_SIGS_VERSION'];
175171

176172
let signature;
@@ -180,7 +176,7 @@ function s3auth(r) {
180176
let req = _s3ReqParamsForSigV2(r, bucket);
181177
signature = awssig2.signatureV2(r, req.uri, req.httpDate, credentials);
182178
} else {
183-
let req = _s3ReqParamsForSigV4(r, bucket, server);
179+
let req = _s3ReqParamsForSigV4(r, bucket, host);
184180
signature = awssig4.signatureV4(r, awscred.Now(), region, SERVICE,
185181
req.uri, req.queryParams, req.host, credentials);
186182
}
@@ -221,15 +217,11 @@ function _s3ReqParamsForSigV2(r, bucket) {
221217
* @see {@link https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html | AWS V4 Signing Process}
222218
* @param r {NginxHTTPRequest} HTTP request object
223219
* @param bucket {string} S3 bucket associated with request
224-
* @param server {string} S3 host associated with request
220+
* @param host {string} S3 host associated with request
225221
* @returns {S3ReqParams} s3ReqParams object (host, uri, queryParams)
226222
* @private
227223
*/
228-
function _s3ReqParamsForSigV4(r, bucket, server) {
229-
let host = server;
230-
if (S3_STYLE === 'virtual' || S3_STYLE === 'default' || S3_STYLE === undefined) {
231-
host = bucket + '.' + host;
232-
}
224+
function _s3ReqParamsForSigV4(r, bucket, host) {
233225
const baseUri = s3BaseUri(r);
234226
const computed_url = !utils.parseBoolean(r.variables.forIndexPage)
235227
? r.variables.uri_path

common/etc/nginx/nginx.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ env S3_REGION;
2020
env AWS_SIGS_VERSION;
2121
env DEBUG;
2222
env S3_STYLE;
23+
env S3_SERVICE;
2324
env ALLOW_DIRECTORY_LIST;
2425
env PROVIDE_INDEX_PAGE;
2526
env APPEND_SLASH_FOR_POSSIBLE_DIRECTORY;

common/etc/nginx/templates/default.conf.template

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ map $uri_full_path $uri_path {
1919
default $PREFIX_LEADING_DIRECTORY_PATH$uri_full_path;
2020
}
2121

22-
map $S3_STYLE $s3_host_hdr {
23-
virtual "${S3_BUCKET_NAME}.${S3_SERVER}";
24-
path "${S3_SERVER}:${S3_SERVER_PORT}";
25-
default "${S3_BUCKET_NAME}.${S3_SERVER}";
26-
}
22+
# S3_HOST_HEADER is set in the startup script
23+
# (either ./common/docker-entrypoint.sh or ./standalone_ubuntu_oss_install.sh)
24+
# based on the S3_STYLE configuration option.
25+
js_var $s3_host ${S3_HOST_HEADER};
2726

2827
js_var $indexIsEmpty true;
2928
js_var $forIndexPage true;
@@ -141,7 +140,7 @@ server {
141140
proxy_set_header X-Amz-Security-Token $awsSessionToken;
142141

143142
# We set the host as the bucket name to inform the S3 API of the bucket
144-
proxy_set_header Host $s3_host_hdr;
143+
proxy_set_header Host $s3_host;
145144

146145
# Use keep alive connections in order to improve performance
147146
proxy_http_version 1.1;
@@ -202,7 +201,7 @@ server {
202201
proxy_set_header X-Amz-Security-Token $awsSessionToken;
203202

204203
# We set the host as the bucket name to inform the S3 API of the bucket
205-
proxy_set_header Host $s3_host_hdr;
204+
proxy_set_header Host $s3_host;
206205

207206
# Use keep alive connections in order to improve performance
208207
proxy_http_version 1.1;
@@ -265,7 +264,7 @@ server {
265264
proxy_set_header X-Amz-Security-Token $awsSessionToken;
266265

267266
# We set the host as the bucket name to inform the S3 API of the bucket
268-
proxy_set_header Host $s3_host_hdr;
267+
proxy_set_header Host $s3_host;
269268

270269
# Use keep alive connections in order to improve performance
271270
proxy_http_version 1.1;

common/etc/nginx/templates/gateway/s3_location_common.conf.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ proxy_set_header Authorization $s3auth;
1919
proxy_set_header X-Amz-Security-Token $awsSessionToken;
2020

2121
# We set the host as the bucket name to inform the S3 API of the bucket
22-
proxy_set_header Host $s3_host_hdr;
22+
proxy_set_header Host $s3_host;
2323

2424
# Use keep alive connections in order to improve performance
2525
proxy_http_version 1.1;

deployments/s3_express/.terraform.lock.hcl

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deployments/s3_express/.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform 1.8.1

0 commit comments

Comments
 (0)