Setting up an NGINX AWS S3 proxy is one of the most effective ways to serve private S3 objects without exposing your AWS credentials. Many implementations make a critical security mistake: they store IAM secret keys directly on the web server. The ngx_aws_auth module solves this problem by using AWS Signature Version 4 scoped signing keys, which expire after seven days and never expose your master credentials.
In this guide, you will learn how to install and configure the NGINX AWS S3 proxy module to securely forward authenticated requests to Amazon S3 backends.
Why Use an NGINX AWS S3 Proxy?
Placing NGINX in front of S3 offers several advantages over serving S3 URLs directly to clients:
- Access control: Restrict S3 access through NGINX authentication, rate limiting, or IP-based rules, without making buckets public.
- URL abstraction: Clients see your domain (e.g.,
assets.example.com/file.pdf) instead of S3 URLs. You can migrate storage backends without changing client-facing URLs. - Caching: Use NGINX’s
proxy_cachedirective to cache frequently accessed S3 objects, reducing both latency and AWS data transfer costs. - Logging and monitoring: Gain full visibility into access patterns through NGINX access logs.
- Security isolation: Keep AWS credentials off application servers entirely. Only the NGINX AWS S3 proxy server needs signing keys.
How the Module Works
The ngx_aws_auth module intercepts outgoing proxy requests and automatically adds the required AWS Signature Version 4 authentication headers. For every matching request, the module generates three headers:
Authorization— The AWS4-HMAC-SHA256 credential and signature string.x-amz-date— The current UTC timestamp in ISO 8601 format.x-amz-content-sha256— A SHA-256 hash of the request body (always the empty-body hash for GET and HEAD requests).
The key security principle is that the module uses a scoped signing key, not the IAM secret key itself. The signing key is derived from the secret key but is limited to a specific date, region, and service. Therefore, even if the signing key is compromised, the blast radius is limited to one AWS region and service for a maximum of seven days.
Supported HTTP Methods
The module currently supports GET and HEAD requests only. POST, PUT, and DELETE requests return HTTP 405 (Method Not Allowed) because signing request bodies is not yet implemented. This is acceptable for most NGINX AWS S3 proxy use cases, where you read objects through NGINX and manage uploads through a separate, authenticated channel.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
Install the module from the GetPageSpeed RPM repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-aws-auth
Then load the module by adding this line at the top of /etc/nginx/nginx.conf, before any http block:
load_module modules/ngx_http_aws_auth_module.so;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-aws-auth
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Generating a Signing Key
Before configuring NGINX, you need to generate a scoped signing key from your IAM secret key. The module includes a Python script for this purpose.
First, download the key generation script:
curl -O https://raw.githubusercontent.com/anomalizer/ngx_aws_auth/master/generate_signing_key
chmod +x generate_signing_key
Then generate the signing key:
./generate_signing_key --secret-key YOUR_IAM_SECRET_KEY --region us-east-1
This produces two lines of output:
jcUxLpCwJR0mSxAnb6gcJ8dBw1+x+2TNMwABi0eLyLc=
20260226/us-east-1/s3/aws4_request
- Line 1 is the base64-encoded signing key — use this as the
aws_signing_keyvalue. - Line 2 is the key scope — use this as the
aws_key_scopevalue.
Important: Never store or pass the IAM secret key in NGINX configuration. Only the derived signing key should be placed on the server.
Configuration
Directive Reference
The module provides six directives. All directives can be placed in http, server, or location blocks.
| Directive | Arguments | Default | Description |
|---|---|---|---|
aws_access_key |
1 (string) | — | Your AWS access key ID (e.g., AKIAIOSFODNN7EXAMPLE). |
aws_signing_key |
1 (string) | — | The base64-encoded scoped signing key generated by the helper script. |
aws_key_scope |
1 (string) | — | The scope string in the format YYYYMMDD/region/service/aws4_request. |
aws_s3_bucket |
1 (string) | — | The name of the target S3 bucket. |
aws_endpoint |
1 (string) | s3.amazonaws.com |
The S3 endpoint hostname. Change this for regional or non-standard endpoints. |
aws_sign |
none | — | Enables request signing in the current location. Without this directive, no authentication headers are added. |
Basic S3 Proxy
This configuration creates a basic NGINX AWS S3 proxy that forwards all requests to a private S3 bucket:
server {
listen 443 ssl;
server_name assets.example.com;
ssl_certificate /etc/ssl/certs/assets.example.com.pem;
ssl_certificate_key /etc/ssl/private/assets.example.com.key;
aws_access_key AKIAIOSFODNN7EXAMPLE;
aws_key_scope 20260226/us-east-1/s3/aws4_request;
aws_signing_key jcUxLpCwJR0mSxAnb6gcJ8dBw1+x+2TNMwABi0eLyLc=;
aws_s3_bucket my-private-bucket;
location / {
aws_sign;
proxy_pass https://my-private-bucket.s3.amazonaws.com;
}
}
The aws_sign directive inside the location block activates signing for all GET and HEAD requests matched by that location. The authentication credentials defined at the server level are inherited by all location blocks within it.
Subpath Proxy
You can proxy a specific URL prefix to S3 while keeping other locations for different purposes:
server {
listen 443 ssl;
server_name www.example.com;
# ... SSL and other configuration ...
aws_access_key AKIAIOSFODNN7EXAMPLE;
aws_key_scope 20260226/us-east-1/s3/aws4_request;
aws_signing_key jcUxLpCwJR0mSxAnb6gcJ8dBw1+x+2TNMwABi0eLyLc=;
aws_s3_bucket my-assets-bucket;
location /downloads {
rewrite /downloads/(.*) /$1 break;
proxy_pass https://my-assets-bucket.s3.amazonaws.com;
aws_sign;
}
location / {
# Regular website content
proxy_pass http://backend;
}
}
In this example, only requests to /downloads/ are proxied to S3 with authentication. The rewrite strips the /downloads prefix so that /downloads/report.pdf fetches /report.pdf from the S3 bucket.
Custom S3 Endpoint
For S3 buckets in non-default regions, or for S3-compatible services like MinIO or Wasabi, use the aws_endpoint directive:
location /china-assets {
rewrite /china-assets/(.*) /$1 break;
proxy_pass https://my-bucket.s3.cn-north-1.amazonaws.com.cn;
aws_sign;
aws_endpoint "s3.cn-north-1.amazonaws.com.cn";
aws_access_key AKIAIOSFODNN7EXAMPLE;
aws_key_scope 20260226/cn-north-1/s3/aws4_request;
aws_signing_key dGVzdGtleWJhc2U2NA==;
aws_s3_bucket my-bucket;
}
Note that the aws_key_scope region must match the endpoint’s region, and the proxy_pass URL must use the virtual-hosted-style bucket format (bucket.endpoint).
Adding Caching
Combine the NGINX AWS S3 proxy with NGINX’s proxy cache to reduce latency and S3 transfer costs:
proxy_cache_path /var/cache/nginx/s3
levels=1:2
keys_zone=s3_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
listen 443 ssl;
server_name assets.example.com;
# ... SSL and AWS auth configuration ...
location / {
aws_sign;
proxy_pass https://my-private-bucket.s3.amazonaws.com;
proxy_cache s3_cache;
proxy_cache_valid 200 60m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status;
}
}
For best results, also tune your proxy buffer settings to match your typical S3 object sizes.
Testing Your Configuration
After configuring the module, verify the syntax:
nginx -t
Then reload NGINX:
sudo systemctl reload nginx
Test with curl to confirm that the proxy is authenticating correctly:
curl -I https://assets.example.com/test-file.txt
A successful response returns the S3 object’s headers (e.g., Content-Type, ETag, x-amz-request-id). If you see a 403 error from S3, verify the following:
- The
aws_key_scopedate matches the date when the signing key was generated. - The region in
aws_key_scopematches your bucket’s actual region. - The signing key has not expired (keys are valid for seven days from the generation date).
- The
aws_s3_bucketvalue matches the exact bucket name.
Security Best Practices
Never Store IAM Secret Keys on the Server
The most important security benefit of this module is that the IAM secret key never touches the NGINX server. Only the derived signing key is stored in the configuration. If the server is compromised, the attacker gains a signing key that expires within seven days and is limited to a single AWS region and service.
Automate Signing Key Rotation
Signing keys expire after seven days. Automate the rotation using a cron job or configuration management tool:
#!/bin/bash
# rotate-aws-signing-key.sh
# Run daily via cron: 0 3 * * * /usr/local/bin/rotate-aws-signing-key.sh
set -euo pipefail
OUTPUT=$(./generate_signing_key --secret-key "$AWS_SECRET_KEY" --region us-east-1)
SIGNING_KEY=$(echo "$OUTPUT" | head -1)
KEY_SCOPE=$(echo "$OUTPUT" | tail -1)
# Update NGINX configuration
sed -i "s|aws_signing_key .*|aws_signing_key ${SIGNING_KEY};|" /etc/nginx/conf.d/s3-proxy.conf
sed -i "s|aws_key_scope .*|aws_key_scope ${KEY_SCOPE};|" /etc/nginx/conf.d/s3-proxy.conf
# Reload NGINX to apply the new configuration
nginx -t && systemctl reload nginx
Store the IAM secret key in a secrets manager or environment variable — never hard-code it in the rotation script.
Restrict Bucket Permissions
Create a dedicated IAM user with the minimum required S3 permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-private-bucket",
"arn:aws:s3:::my-private-bucket/*"
]
}
]
}
Since the module only supports GET and HEAD, there is no need to grant s3:PutObject, s3:DeleteObject, or other write permissions.
Use HTTPS for the Proxy Connection
Always use https://` in theproxy_pass` directive when connecting to S3. This encrypts the traffic between NGINX and S3, preventing man-in-the-middle attacks on the authentication headers.
Combine with NGINX Access Controls
Layer additional security on top of the S3 proxy using NGINX’s built-in access control directives:
location /internal-docs {
# Restrict to internal network
allow 10.0.0.0/8;
deny all;
aws_sign;
proxy_pass https://my-private-bucket.s3.amazonaws.com;
}
You can also use NGINX JWT authentication to require token-based access before granting access to S3 objects. Additionally, consider enabling Brotli compression for text-based S3 objects to reduce bandwidth usage, and adding security headers to responses served through the proxy.
Performance Considerations
The module adds minimal overhead to each request. The signing process involves computing HMAC-SHA256 hashes, which completes in microseconds on modern hardware. However, consider these factors:
- Proxy latency: Each request adds a round trip to S3. Use
proxy_cacheto mitigate this for frequently accessed objects. - Connection reuse: NGINX can maintain persistent connections to S3 using
keepalivein anupstreamblock, reducing TCP and TLS handshake overhead. - DNS resolution: S3 bucket endpoints resolve to multiple IP addresses. Use NGINX’s
resolverdirective to handle DNS changes:
resolver 8.8.8.8 valid=300s;
resolver_timeout 5s;
Troubleshooting
403 Forbidden from S3
This is the most common error. Possible causes:
- Expired signing key: Check if the signing key was generated more than seven days ago. Regenerate it using the
generate_signing_keyscript. - Region mismatch: The region in
aws_key_scopemust exactly match the bucket’s region. Useaws s3api get-bucket-location --bucket BUCKET_NAMEto verify. - Clock skew: AWS rejects requests with timestamps more than 15 minutes off. Ensure your server’s time is synchronized with NTP.
- Bucket name mismatch: The
aws_s3_bucketdirective must contain the exact bucket name, without any path prefix or trailing slash.
405 Method Not Allowed
The module only signs GET and HEAD requests. If your application sends POST, PUT, or DELETE requests through the proxy, it returns 405. Use a different mechanism (such as pre-signed URLs or direct SDK calls) for write operations.
Module Not Loading
If NGINX reports unknown directive "aws_sign", verify that:
- The module file exists at
/usr/lib64/nginx/modules/ngx_http_aws_auth_module.so. - The
load_moduledirective is placed at the top ofnginx.conf, before thehttpblock. - The module version matches your installed NGINX version.
Conclusion
The ngx_aws_auth module provides a secure, efficient way to set up an NGINX AWS S3 proxy for serving private bucket content. By using scoped signing keys instead of IAM secret keys, it significantly reduces the security risk of server compromise. Combined with NGINX’s caching, access control, and logging capabilities, it offers a robust alternative to serving S3 objects directly.
The module’s source code is available on GitHub. Pre-built packages for RHEL-based distributions are available from the GetPageSpeed RPM repository, and Debian/Ubuntu packages from the GetPageSpeed APT repository.

