Development Guide

August 29, 2026 · View on GitHub

Integrating with AWS Signature

Update the following files when enhancing nginx-s3-gateway to integrate with AWS signature whenever AWS releases a new version of signature or you have a new PR:

AWS signature flow across the njs modules

The flow above signs proxied client requests. Requests the gateway originates itself - the STS AssumeRole credential fetch (GH-122) - are signed by awssig4.signRequestV4, which takes every request component as an explicit parameter and bypasses the signing-key cache (whose entries are bound to the date and access key id, but not to a region/service pair) so an sts-scoped key can never poison the S3 signatures.

Extending the Gateway

Extending gateway configuration via container images

conf.d Directory

On the container image, all files with the extension .conf in the directory /etc/nginx/conf.d will be loaded into the configuration of the base http block within the main NGINX configuration.

This allows for extension of the configuration by adding additional configuration files into the container image extending the base gateway image.

Stub Files

The NGINX configuration templates render into /etc/nginx/conf.d when the container starts. The gateway ships three empty stub files in the gateway/ subdirectory that exist purely as extension points:

Each of these files can be overwritten in a container image that inherits from the S3 Gateway container image, so that additional NGINX configuration directives can be inserted into the gateway configuration.

Two similarly located files in the same directory are not extension points — the entrypoint writes their contents at container start:

Examples

In the examples/ directory, there are Dockerfile examples that show how to extend the base functionality of the NGINX S3 Gateway by adding additional modules.

Testing

The GNUmakefile (GNU Make 4.x) is the only supported interface for build and test workflows. The test logic lives in test/run_unit_tests.sh and test/run_integration_tests.sh, which make drives; the legacy test.sh script is deprecated and only forwards to the equivalent make targets — do not invoke it directly.

Automated tests require docker, docker compose, curl, md5sum (or md5 on macOS), and the AWS CLI (used as a generic S3 client against the RustFS test origin) to be installed; run make check-tools to verify all prerequisites are present.

To build the gateway image and run the full unit and integration test suite:

$ make test                  # NGINX OSS (default)
$ make test NGINX_TYPE=plus  # NGINX Plus

NGINX Plus builds require your NGINX repository certificates (nginx-repo.crt and nginx-repo.key) in the plus/etc/ssl/nginx directory, a docker login private-registry.nginx.com (the Plus base image is pulled from NGINX's private registry), and a license.jwt in the repository root or at /etc/nginx/license.jwt for the integration tests.

Other useful targets:

  • make retest — rerun tests against the already-built image. Note that unit tests import the njs modules baked into the image, so after editing common/etc/nginx/include/*.js use make test to rebuild first.
  • make test-unit / make test-integration — run just the unit or just the integration half of the suite against the already-built image.
  • make test-latest-njs / make test-unprivileged — build and test the image variants.
  • make test-matrix — reproduce the CI matrix locally.
  • make test S3_STYLE=path (or virtual / virtual-v2) — reproduce a single CI matrix leg; plain make test covers only the default virtual-v2 style.
  • make lint — run the linters (checkmake + shellcheck).

Run make help for the full target list.

Agent users can also run an adaptive live smoke test of the gateway via the skill in .claude/skills/smoke-test/, which exercises features the fixed integration matrix does not cover.

Adding tests

Unit tests live in test/unit/ and run under the njs CLI inside the built image. New files are discovered automatically: test/run_unit_tests.sh runs every test/unit/*_test.js file twice — once with and once without AWS_SESSION_TOKEN — so no wiring is needed. Environment variables that a module under test reads at import time belong in the unit_test_env list in that runner.

Integration tests live in test/integration/ and are driven by test/run_integration_tests.sh, which starts the compose environment (test/docker-compose.yaml, with the test/docker-compose.dynamic-credentials.yaml override supplying an ECS credential-endpoint mock for the dynamic-credentials phase, the test/docker-compose.secret-file-credentials.yaml override supplying the static credentials as mounted secret files, and the test/docker-compose.assume-role.yaml override switching the gateway to STS AssumeRole credentials for the assume-role phase), seeds the RustFS S3 origin with the fixtures in test/data/, and invokes the test scripts across a matrix of gateway configurations, including HTTPS origins with TLS verification and a CORS-enabled phase. New shell scripts under test/ and test/integration/ are picked up by make lint automatically and must pass shellcheck --severity=warning. Six of the eight test_entrypoint_*.sh scripts source test/integration/entrypoint_test_lib.sh for the shared docker run wrapper and the validation and banner assertions; the baseline container environment lives there, so a newly required gateway variable is added in one place. The other two invoke docker run directly: test_entrypoint_ipv6.sh needs none of the baseline gateway variables, and test_entrypoint_output_settings.sh varies S3_STYLE, which the baseline pins.

The make targets guard against image/target mismatches: the variant targets (retest-latest-njs, retest-unprivileged) verify the floating nginx-s3-gateway tag actually points at the matching variant image, and the non-variant targets verify the inverse, failing with an actionable error instead of a confusing test failure.