Хранить статику в S3 или совместимом object storage — удобно, но далеко не всегда хочется светить «сырой» адрес бакета и отдавать клиенту служебные заголовки провайдера. В этой статье собираем production-конфиг Nginx как reverse proxy перед S3: кастомный домен, корректный SSL, строгая гигиена заголовков без утечек, понятная политика кэширования и подготовка к работе с CDN. Всё с пояснениями, зачем каждая строчка нужна.
Зачем ставить Nginx перед S3
Слой Nginx между клиентом и S3/object storage решает сразу несколько задач:
- Кастомный домен и контроль над TLS-конфигурацией (алгоритмы, OCSP Stapling, HSTS, ALPN и т.д.).
- Сокрытие специфичных для S3 заголовков (
x-amz-*,Serverпровайдера, и прочего служебного). - Единая политика
Cache-ControlиVaryдля браузеров и CDN, fallback на случай пустых метаданных объекта. - Локальный кэш на уровне Nginx: снижение латентности, защита от коротких перебоев origin и экономия трафика.
- Гибкость логики: переопределение MIME,
Content-Dispositionдля скачиваний, CORS для шрифтов и API, ограничения методов.
Идея простая: S3 хранит и масштабирует, Nginx контролирует протокол, заголовки, кэш и доменное имя. Классическая связка reverse proxy + object storage.
Схема и предпосылки
Базовая схема: Клиент → Nginx (HTTPS) → S3/object storage (HTTPS). Nginx служит единственной публичной точкой входа. В идеале у вас есть отдельный бакет/префикс под сайт и вы отдаёте только GET/HEAD. Для крупных проектов обычно поверх добавляют CDN, но это необязательно на старте. Если нужен самостоятельный контроль над конфигом и ресурсами — поднимайте фронт на VDS.
Дальше будут примеры для virtual-hosted-style S3-эндпоинта (то есть bucket.provider-region.example). Для S3-совместимых хранилищ меняется только имя хоста и поведение некоторых заголовков, основные принципы те же. Если ещё нет домена — начните с регистрации доменов, а затем выпустите SSL-сертификаты.
Базовый каркас Nginx
Сначала подготовим общий контекст: зону кэша, fallback для Cache-Control и рекомендации по сжатию. Все блоки ниже можно размещать в соответствующих контекстах (http, затем server и location).
http {
# Диск для кэша прокси
proxy_cache_path /var/cache/nginx/s3 levels=1:2 keys_zone=s3_cache:200m max_size=10g inactive=7d use_temp_path=off;
# Fallback для Cache-Control, если у объекта нет метаданных
map $upstream_http_cache_control $cc {
default $upstream_http_cache_control;
"" "public, max-age=31536000, immutable";
}
# Подготовка для OCSP Stapling и динамических резолвов при необходимости
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=on;
resolver_timeout 5s;
# Сжатие на стороне Nginx; Vary обязателен
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml text/plain application/xml;
gzip_vary on;
}
HTTP → HTTPS редирект
server {
listen 80;
listen [::]:80;
server_name static.example.com;
return 301 https://$host$request_uri;
}

HTTPS-сервер и прокси на S3
Важные моменты: включаем HTTP/2, настраиваем сертификат, формируем корректный Host к upstream, сохраняем $request_uri, отключаем ре-компрессию на стороне origin и чётко управляем заголовками.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name static.example.com;
# Ваши пути к сертификату/ключу
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# Для статического контента HSTS полезен, включайте осознанно
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Минимальная диагностическая метка запроса
add_header X-Request-ID $request_id always;
# Безопасные методы: только GET/HEAD наружу
location / {
limit_except GET HEAD { deny all; }
# Адрес вашего origin в стиле virtual-hosted
set $s3_upstream my-bucket.s3.region.example.com;
proxy_set_header Host $s3_upstream; # Для правильной виртуализации у S3
proxy_ssl_server_name on; # SNI к upstream
proxy_ssl_name $s3_upstream;
# Важно: не просим gzip у origin, чтобы не ломать ETag сильной хеш-суммой из S3
proxy_set_header Accept-Encoding identity;
proxy_pass https://$s3_upstream;
proxy_redirect off;
# Таймауты и буферы
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
proxy_buffers 64 8k;
proxy_busy_buffers_size 128k;
# Кэш прокси и поведение при сбоях
proxy_cache s3_cache;
proxy_cache_valid 200 301 302 24h;
proxy_cache_valid 404 10m;
proxy_cache_revalidate on; # If-None-Match/If-Modified-Since
proxy_cache_use_stale updating error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_background_update on;
# Возможность отключить кэш по query ?nocache=1
set $nocache 0;
if ($arg_nocache) { set $nocache 1; }
proxy_no_cache $nocache;
proxy_cache_bypass $nocache;
# Гигиена заголовков: не проливаем служебное наружу
proxy_hide_header Server; # Прячем Server origin
proxy_hide_header X-Powered-By;
proxy_hide_header X-Cache;
proxy_hide_header X-Cache-Hits;
proxy_hide_header X-Amz-Id-2;
proxy_hide_header X-Amz-Request-Id;
proxy_hide_header X-Amz-Version-Id;
proxy_hide_header X-Amz-Replication-Status;
proxy_hide_header X-Amz-Expiration;
proxy_hide_header X-Amz-Website-Redirect-Location;
proxy_hide_header X-Amz-Server-Side-Encryption;
proxy_hide_header X-Amz-Server-Side-Encryption-Customer-Algorithm;
proxy_hide_header X-Amz-Server-Side-Encryption-Customer-Key-Md5;
proxy_hide_header X-Amz-Storage-Class;
proxy_hide_header ETag; # Смотрите раздел про ETag ниже
# Полезные клиентские заголовки безопасности
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy no-referrer-when-downgrade always;
# Единая политика кэша на клиенте/CDN: сохраняем origin-значение, иначе fallback
add_header Cache-Control $cc always;
# Маркер состояния кэша Nginx в ответе (для диагностики)
add_header X-Cache-Status $upstream_cache_status always;
}
}
Сертификат можно оформить и автоматизировать ротацию через SSL-сертификаты. При необходимости заведите домен заранее — это упростит проверку и выпуск: воспользуйтесь регистрацией доменов.
Почему так: про ETag, Accept-Encoding и Vary
S3 часто возвращает «сильный» ETag — это MD5 тела файла без сжатия. Если вы попросите у origin gzip, а затем ещё и сожмёте ответ на стороне Nginx, ETag перестанет соответствовать конкретному представлению контента. Чтобы избежать путаницы:
- Просим у origin несжатое тело:
proxy_set_header Accept-Encoding identity. - Сжимаем на стороне Nginx для клиента:
gzip onиgzip_vary on. - Не прокидываем исходный
ETagнаружу:proxy_hide_header ETag. Сильный ETag от origin относится к варианту «identity», а у клиента может быть «gzip». - Опираемся на
Cache-ControlиLast-Modifiedдля валидации, либо строим кэшную стратегию на immutable-файлах с версионированием (например,style.abc123.css).
Альтернатива — не сжимать на лету и отдавать ровно то, что хранится в S3 (включая ETag). Но так вы теряете экономию трафика к клиенту.

Корректное кэширование и подготовка к CDN
Задача кэша Nginx — уменьшить латентность, защитить от кратковременных ошибок origin и не мешать Cache-Control политики. Несколько практических моментов:
- Revalidate:
proxy_cache_revalidate onавтоматически будет слатьIf-None-Match/If-Modified-Sinceк S3 при истечении срока кэша. - Stale-ответы:
proxy_cache_use_stale updating error timeout ...позволит отдавать «несвежий» кэш при апстрим-ошибках или фоновой реквалидации. - Lock:
proxy_cache_lock onне допустит лавинообразной догрузки одного и того же объекта параллельными воркерами. - Ключ кэша: по умолчанию подходит
$scheme$proxy_host$request_uri. При необходимости учитывайте заголовки черезVary. - CDN: для статики используйте
immutable, для HTML — небольшойmax-ageи отсутствиеimmutable. CDN корректно поймётVary: Accept-Encoding.
Если вы оптимизируете картинки, пригодится разбор по modern-форматам и map-конфигурации в материале «WebP/AVIF и map-кэш в Nginx» — см. подробности про WebP/AVIF и кэш.
Range/HEAD и большие файлы
S3 поддерживает частичные запросы, и Nginx их корректно проксирует по умолчанию. Несколько советов:
- Не отключайте прокси-буферизацию без необходимости. Буферы помогают сгладить разницу скоростей клиент ↔ origin и повышают устойчивость.
- Если поток очень большой и диск ограничен, можно уменьшить нагрузку на tmp, но помните о компромиссе в производительности.
- Для скачиваний добавляйте
Content-Dispositionна уровне локации, если в объекте нет нужных метаданных.
location ~* \.(zip|tar|gz|bz2|7z|pdf)$ {
add_header Content-Disposition "attachment" always;
}
Подробно про особенности диапазонных запросов и кешей мы разбирали в статье «HTTP Range в Nginx и Apache» — см. работу Range-запросов и кешей.
Контроль MIME и защита от сниффинга
Иногда в бакете забывают прописать корректный Content-Type. Браузеры могут угадать тип, но лучше не полагаться на это. Два решения:
- Наводить порядок в метаданных объектов на стороне хранилища.
- Переопределить типы на уровне Nginx для конкретных путей/расширений.
Обязательно включите X-Content-Type-Options: nosniff (в примере выше он уже есть) — это снижает риск XSS на статике с неверным типом.
CORS для шрифтов и статики
Если статику забирают с другого домена (шрифты, SPA-запросы), настройте CORS точечно:
location ~* \.(woff2|woff|ttf|otf)$ {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
}
Для API-объектов оставьте CORS более строгим, лучше whitelist по доменам. Не выставляйте CORS-глобально без необходимости.
Без утечек служебных заголовков
Главная боль при прямой раздаче из S3 — экспонирование внутренних заголовков: X-Amz-*, Server, иногда Via/X-Cache, метаданные x-amz-meta-* и т.д. Стратегия такова:
- Статично спрятать известные заголовки через
proxy_hide_header(см. конфиг выше). - Избегать хранения чувствительных данных в
x-amz-meta-*, так как штатными средствами Nginx нельзя «масочно» вычистить все пользовательские мета-заголовки из ответа. - Не прокидывать в клиентский ответ сырой
ETag, если у вас включено сжатие на стороне Nginx. - Проверять ответы реальным браузером и curl — заголовки могут отличаться у разных провайдеров object storage.
Если вам нужно удалять произвольные заголовки по шаблону, понадобится сторонний модуль управления заголовками. В базовой поставке Nginx этого нет, поэтому лучше не хранить лишнего в метаданных объектов.
Частые ошибки и диагностика
- 403 от S3: проверьте
Hostк upstream (proxy_set_header Host), права на объект и точность имени бакета в виртуальном хосте. - 307/301 или странные редиректы: это признак обращения не к тому эндпоинту (website-style против virtual-hosted), либо попытка запроса к корню без завершающего слеша.
- ETag/304 не работает: если вы прячете
ETag, браузер не сможет валидировать по нему. ОставьтеLast-Modifiedили используйте «версионирование файла в имени» плюс долгийmax-ageсimmutable. - Голые заголовки S3 у клиента: убедитесь, что локация именно проксирует ответ (нет
try_filesна локальную статику), и списокproxy_hide_headerактуален. - Проблемы с TLS к upstream: включите
proxy_ssl_server_name onи корректно задайтеproxy_ssl_nameилиHost— многие object storage требуют SNI. - IPv6/резолв: при использовании OCSP stapling задайте рабочий
resolver, иначе в ошибках появятся таймауты в валидации цепочки.
Плюс немного жёсткости в сервере
Ужесточим методы и уберём то, чего не должно быть в статике:
location = /robots.txt { try_files $uri =404; }
location = /favicon.ico { try_files $uri =404; }
# Никаких POST/PUT/DELETE наружу
location /upload/ { deny all; }
В реальном мире это может быть не нужно, но полезно ещё раз проверить конфиг на отсутствие лишних публичных путей.
Мини-чеклист перед продакшеном
- Сертификат валиден, OCSP stapling работает, HSTS включён осознанно.
- В ответах нет
X-Amz-*,Serverorigin, лишних внутренних заголовков. Cache-Controlрасставлен: для версионированной статики — долго иimmutable, для HTML — коротко.Vary: Accept-Encodingприсутствует, если включили сжатие на стороне Nginx.- Кэш Nginx создаётся,
X-Cache-Statusпоказывает HIT/MISS/STALE как ожидается. - Range-запросы и HEAD корректны для больших файлов.
- В логах нет неожиданных 301/307: проверен тип эндпоинта и
Hostк upstream.
Итоги
Схема «Nginx как reverse proxy → S3/object storage» закрывает типичные пробелы прямой раздачи из бакета: даёт контроль над TLS, доменом, заголовками и кэшем, упрощает подключение CDN и диагностику. Главное — держать в чистоте границы ответственности: объектное хранилище хранит, а Nginx навешивает протокольные и кэшные политики. С такой архитектурой вы получаете быстрый и предсказуемый стек для статики и медиаконтента без утечек внутренних деталей поставщика хранилища.


