Site icon GetPageSpeed

NGINX AWS S3 Proxy: Secure Module Guide

How to Securely Proxy AWS S3 with NGINX Using the AWS Auth Module

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:

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:

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_module directive 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

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:

  1. The aws_key_scope date matches the date when the signing key was generated.
  2. The region in aws_key_scope matches your bucket’s actual region.
  3. The signing key has not expired (keys are valid for seven days from the generation date).
  4. The aws_s3_bucket value 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:

resolver 8.8.8.8 valid=300s;
resolver_timeout 5s;

Troubleshooting

403 Forbidden from S3

This is the most common error. Possible causes:

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:

  1. The module file exists at /usr/lib64/nginx/modules/ngx_http_aws_auth_module.so.
  2. The load_module directive is placed at the top of nginx.conf, before the http block.
  3. 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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version