Что мы настраиваем и какие риски закрываем
Nginx отдаёт файлы прямо с диска: вы описываете каталог в директиве root или alias, закрываете его location, и воркер отдаёт содержимое без обращения к бэкенду. Для файлового архива с прямыми ссылками этого достаточно, если одновременно закрыть три канала утечки: листинг каталогов, чужие Referer'ы и доступ по HTTP без шифрования.
Безопасность раздачи держится на трёх опорах. Первая: права на файловой системе, где Nginx читает файлы и не имеет права писать. Вторая: ограничения в конфиге, а именно autoindex off, deny для служебных файлов, valid_referers против хотлинка и limit_rate для тяжёлых файлов. Третья: транспорт, то есть TLS с HSTS и auth_basic для закрытых каталогов. Отключение одной опоры не компенсируется двумя другими: права 777 при выключенном autoindex всё равно дают лишний доступ, а HTTPS без auth_basic оставляет приватный архив публичным.
Дальше разбираем по порядку: конфигурацию location и autoindex, ограничение скорости отдачи, защиту от хотлинка через valid_referers, корректные права на каталоги, HTTPS и базовую аутентификацию для закрытых архивов, проверку конфигурации и типичные ошибки при раздаче больших файлов. Держим один принцип: не открыть лишнего наружу. Каждый шаг заканчивается командой, которой результат проверяется.
Кому подойдёт этот гайд и что вы получите на выходе
Материал рассчитан на DevOps-инженеров и системных администраторов, которые знают, что такое server-блок и location, и хотят собрать файловый архив без сюрпризов. Новичку тоже будет понятно: конфиги даём целиком, объясняем назначение директив и указываем, что можно убрать под свою задачу.
На выходе у вас: рабочий server-блок для раздачи каталога, набор директив безопасности (autoindex off, valid_referers, limit_rate, auth_basic), корректные права на каталоги и файлы, команды проверки через nginx -t и curl, а также чек-лист перед публикацией. Шаблоны воспроизводимы на типовых дистрибутивах: Debian и Ubuntu с пакетом nginx и пользователем www-data, RHEL-семейство с пользователем nginx и SELinux. Пути и имена доменов подставьте свои.
Три уровня защиты файлового архива
Модель угроз для архива простая: файл должен уходить тем, кому он адресован, и не уходить всем остальным. Раскладываем защиту по слоям.
- Файловая система: владелец каталога, группа воркеров Nginx, права 750 на каталоги и 640 на файлы, umask 027, а на дистрибутивах с мандатным контролем - метки SELinux или профиль AppArmor.
- Конфиг Nginx: autoindex off или точечное включение, deny для служебных файлов вроде .env и .git, valid_referers против хотлинка, limit_rate для больших файлов, auth_basic для приватных подкаталогов.
- Транспорт: TLS современных версий, редирект с 80 на 443, HSTS после проверки сертификата.
Слои не взаимозаменяемы. Открытый autoindex покажет имена файлов даже при аккуратных правах, а валидные Referer'ы не спасут от скачивания по прямой ссылке, если сам файл должен быть закрыт. Для приватных данных ставьте auth_basic и права, для публичных - хотлинк-защиту и лимиты.
Базовая конфигурация location и autoindex для раздачи файлов
Начните с рабочего server-блока. Здесь нет ничего лишнего: только раздача каталога и две зоны, публичная с листингом и приватная с deny.
server {
listen 80;
server_name files.example.com;
root /var/www/files;
index index.html;
location /files/ {
try_files $uri $uri/ =404;
}
location /files/public/ {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
}
location ^~ /files/private/ {
deny all;
}
}
Разберём ключевые директивы. try_files $uri $uri/ =404 отдаёт существующий файл, затем каталог, и возвращает 404 вместо пустой страницы, если ничего не нашлось. Без него запрос к несуществующему файлу уйдёт в лог как лишний 404 или приведёт к неожиданному поведению. Директива index нужна только для каталогов с индексным файлом, для архива с прямыми ссылками она почти не используется.
Про сжатие и MIME-типы для статики подробно писали отдельно: раздача статического контента через Nginx. Там же разобраны ошибки 403 и 404 из-за прав и симлинков.
root против alias: что выбрать для архива
Разница проявляется на конкретном пути. Пусть файл лежит в /var/www/files/report.zip, а URL должен быть https://files.example.com/files/report.zip.
# root: URL /files/report.zip превращается в /var/www/files/files/report.zip
root /var/www/files;
# alias: URL /files/report.zip превращается в /var/www/files/report.zip
location /files/ {
alias /var/www/files/;
}
С root Nginx дописывает путь из location к корню, поэтому появляется дублирующий сегмент files и файл не находится. Правило простое: берите alias, когда URL-префикс не совпадает с именем каталога на диске. Не забывайте завершающий слэш: alias /var/www/files/; и location /files/ должны заканчиваться одинаково, иначе соберёте путь с ошибкой.
Когда autoindex - это удобно, а когда - утечка
autoindex on рисует листинг каталога: имена файлов, даты, размеры. Для публичного архива с прямыми ссылками листинг удобен, для каталога с черновиками и внутренними документами опасен, потому что раскрывает структуру и подсказывает имена. Включайте его точечно в отдельном location, как в примере выше, а не глобально в server-блоке.
Для приватных подкаталогов добавьте явный запрет: location ^~ /files/private/ { deny all; }. Префикс ^~ отключает проверку регулярных выражений в других location и гарантирует, что запрет сработает раньше. Если листинг нужен, но читать его должно быть удобно, оставьте autoindex_exact_size off (размеры в КБ и МБ вместо байтов) и autoindex_localtime on (местное время вместо UTC).
Права доступа к каталогам и файлам: что выставить и почему
Права решают, сможет ли воркер Nginx прочитать файл, и не получит ли он лишних возможностей. Рабочая схема: владелец каталога root или отдельный сервисный пользователь, группа та же, что у воркеров Nginx, каталоги 750, файлы 640, umask 027 для новых файлов.
chown -R root:www-data /var/www/files
chmod -R 750 /var/www/files
find /var/www/files -type f -exec chmod 640 {} \;
umask 027
sudo -u www-data cat /var/www/files/example.zip > /dev/null
Последняя команда - быстрый тест от имени воркера: если файл читается без ошибки, права выставлены верно. Если cat возвращает Permission denied, смотрите группу и SELinux, а не добавляйте 777. Права на запись в каталог раздачи Nginx не нужны: архив только читается, а загрузки и обновления выполняет отдельный процесс или пользователь с ограниченными правами.
Подробнее про закрытие служебных файлов и разграничение прав читайте в статье про безопасность статического контента: права доступа и защита .git и .env.
Владелец процессов Nginx и группа каталога
Пользователя воркеров задаёт директива user в nginx.conf, обычно это nginx или www-data. Проверить фактического владельца процессов можно так:
ps aux | grep nginx grep -E "^user" /etc/nginx/nginx.conf
Группа каталога архива должна совпадать с группой воркеров, иначе вы получите 403 при абсолютно корректных, на первый взгляд, правах. У master-процесса владелец root, это нормально: он только читает конфиг и управляет воркерами.
SELinux и AppArmor: почему права выставлены, а 403 остаётся
На RHEL, CentOS, Fedora и их производных метка SELinux запрещает httpd читать файлы из нестандартных путей, даже если chmod и chown в порядке. Присвойте каталогу правильный тип:
chcon -R -t httpd_sys_content_t /var/www/files # постоянный вариант через политику semanage fcontext -a -t httpd_sys_content_t "/var/www/files(/.*)?" restorecon -Rv /var/www/files
На Ubuntu и Debian похожую роль выполняет AppArmor: проверьте профиль /etc/apparmor.d/usr.sbin.nginx и добавьте каталог в разрешённые пути, если он лежит вне /var/www. Диагностика отказов: ausearch -m avc -ts recent для SELinux и dmesg | grep -i apparmor для AppArmor. Запись permission denied в error.log при корректных правах почти всегда означает мандатный контроль.
Защита от хотлинка через valid_referers
Хотлинк - это встраивание вашего файла на чужой странице: трафик идёт через ваш сервер, а посетитель видит чужой сайт. Блокируется директивой valid_referers, которая проверяет заголовок Referer и поднимает переменную invalid_referer.
location /files/ {
alias /var/www/files/;
valid_referers none blocked server_names *.example.com example.com;
if ($invalid_referer) {
return 403;
}
}
Значения читаются так: none разрешает запросы без Referer (прямой ввод адреса, curl, часть мобильных клиентов), blocked разрешает Referer, скрытый прокси или файрволом, server_names разрешает ваш домен из server_name, дальше идут явные маски вроде *.example.com. Любой другой Referer даёт invalid_referer, и запрос получает 403.
Мягкая и жёсткая блокировка: что выбрать для архива
Жёсткий вариант - return 403 для всего, что не прошло проверку. Он рубит и хотлинк, и часть легитимных сценариев: клиенты, которые не отправляют Referer, корпоративные прокси, иногда мессенджеры. Для файлового архива с прямыми ссылками это риск: ссылка, отправленная коллеге в закрытый чат, может перестать открываться.
Поэтому разделяйте контент. Для картинок и тяжёлых медиафайлов, которые обычно и воруют, ставьте жёсткий 403. Для документов, которые должны открываться по прямой ссылке откуда угодно, защиту не включайте или ограничьте её лимитом скорости. Проверить поведение можно одной командой: curl -H "Referer: https://evil.example/" -I https://files.example.com/files/archive.zip. Ожидаемый ответ - 403, а без заголовка Referer тот же URL должен отдавать 200.
Ограничение скорости отдачи: limit_rate и limit_rate_after
limit_rate ограничивает скорость передачи для одного соединения, а limit_rate_after задаёт порог, после которого лимит включается. Первые мегабайты уходят на полной скорости, дальше включается ограничение, и на коротких файлах клиент его не замечает.
location ~* ^/files/.*\.(iso|img|zip)$ {
limit_rate_after 10m;
limit_rate 1m;
}
Это защита от абьюза и от ситуации, когда один качальщик забирает весь канал. Помните про единицы измерения: в Nginx m означает мегабайты, k - килобайты, суффикса g нет. Лимит действует на соединение, поэтому десять параллельных сессий одного клиента дадут десятикратную скорость. Для ограничения по IP нужны limit_conn и limit_req по ключу $binary_remote_addr, их разбирали в материале про защиту веб-серверов: HTTPS, заголовки и противодействие атакам.
HTTPS и базовая аутентификация для закрытых архивов
Приватный архив закрывается парой директив: auth_basic и auth_basic_user_file. Файл с хешами паролей создаётся утилитой htpasswd из пакета apache2-utils или httpd-tools.
htpasswd -c /etc/nginx/.htpasswd archive chown root:www-data /etc/nginx/.htpasswd chmod 640 /etc/nginx/.htpasswd
После создания уберите ключ -c: он перезаписывает файл целиком, и вы потеряете остальные записи. Сам файл лежит вне корня раздачи, иначе его можно скачать по прямой ссылке.
server {
listen 443 ssl;
server_name files.example.com;
ssl_certificate /etc/letsencrypt/live/files.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/files.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location /files/private/ {
alias /var/www/files/private/;
auth_basic "Restricted archive";
auth_basic_user_file /etc/nginx/.htpasswd;
}
}
Закрывайте аутентификацией только приватный location, публичную часть оставляйте открытой. Пароль в базовой аутентификации передаётся в base64, поэтому без HTTPS он читается на любом промежуточном узле. Разбор сертификатов и типичных ошибок валидации собран в статье про ручную установку SSL-сертификата на Nginx и Apache. Более широкий набор правил доступа, включая ограничение по IP и диагностику кодов 401 и 403, описан в гайде про права доступа к данным в Nginx.
Порядок применения правил: auth_basic, valid_referers, limit_rate
Директивы срабатывают на разных этапах обработки запроса. auth_basic проверяется до отдачи файла и возвращает 401, если нет корректных учётных данных. valid_referers работает на этапе обработки запроса и до этой проверки не доходит, когда клиент уже получил 401. limit_rate применяется при передаче тела ответа.
Практический вывод: для приватного архива аутентификация важнее хотлинк-защиты. Пока файл закрыт паролем, чужой сайт его не встроит, потому что не сможет получить содержимое. valid_referers ставьте на публичную часть, чтобы ограничить расход трафика, а на приватную его можно не добавлять.
HSTS и редирект на HTTPS без потери прямых ссылок
Редирект с 80 на 443 добавляет один server-блок:
server {
listen 80;
server_name files.example.com;
return 301 https://$host$request_uri;
}
Переменная $request_uri сохраняет полный путь с query-параметрами, поэтому старые прямые ссылки продолжают работать после перехода на HTTPS. HSTS включайте после того, как убедились, что сертификат валиден на всех поддоменах: после первого ответа с заголовком Strict-Transport-Security браузер будет ходить только по HTTPS. Директиву preload добавляйте, только если контролируете домен целиком и готовы к тому, что откат занимает недели.
Проверка конфигурации и результата
Перед любым изменением в продакшене проверьте синтаксис и только потом перезагружайте. Команда nginx -t собирает конфиг и сообщает точный файл и строку при ошибке. Для применения используйте reload, а не restart: reload перечитывает конфиг без разрыва активных соединений, restart уронит текущие скачивания больших файлов.
nginx -t systemctl reload nginx curl -I https://files.example.com/files/archive.zip curl -H "Referer: https://evil.example/" -I https://files.example.com/files/archive.zip curl -I https://files.example.com/files/private/report.zip
Ожидаемые результаты: первый запрос отдаёт 200 и заголовки Content-Length, Accept-Ranges и Last-Modified; второй - 403, если для каталога включён valid_referers; третий - 401 с заголовком WWW-Authenticate. Код 200 на приватном файле означает, что auth_basic не попал в нужный location.
Чек-лист перед публикацией архива
- autoindex выключен или включён точечно для публичного каталога.
- Служебные файлы закрыты: location ~ /\. { deny all; } для .env, .git и подобных.
- Права каталогов 750, файлов 640, владелец и группа совпадают с воркерами Nginx.
- Учтён SELinux или AppArmor, если каталог лежит вне стандартного пути.
- valid_referers настроен для публичных файлов, которые нужно защитить от встраивания.
- limit_rate и limit_rate_after заданы для тяжёлых файлов.
- HTTPS работает, редирект с 80 на 443 добавлен, HSTS включён после проверки сертификата.
- auth_basic закрывает приватные подкаталоги, файл .htpasswd лежит вне корня.
- nginx -t пройден, выполнен reload, а не restart.
- Проверки curl пройдены: 200 на публичном файле, 403 на хотлинке, 401 на приватном.
Типичные ошибки при раздаче больших файлов
Большие файлы проявляют всё, что скрыто на мелких. Разберём частые сбои и их причины.
- 413 Request Entity Too Large. Ошибка относится к телу запроса, то есть к загрузке файла на сервер, а не к отдаче. Если архив принимает uploads, поднимите client_max_body_size до нужного размера. Для чистой раздачи эта директива ни на что не влияет, но её часто правят напрасно.
- 504 Gateway Timeout. Возникает при проксировании, когда бэкенд не отвечает за proxy_read_timeout. При отдаче статики из каталога прокси не участвует, поэтому таймауты прокси здесь не при чём.
- Обрыв скачивания на середине. Проверьте send_timeout и keepalive_timeout: при медленном канале клиента соединение может закрыться раньше, чем файл дойдёт. Значения 60s для send_timeout и 30s для keepalive_timeout покрывают типовые сценарии.
- Медленная первая отдача. sendfile on передаёт файл без копирования через пользовательское пространство, tcp_nopush собирает заголовки и начало данных в один пакет. Без них на больших файлах растёт нагрузка на CPU.
- Нехватка дескрипторов при тысячах одновременных скачиваний. Проверьте worker_connections и worker_rlimit_nofile, а также системный лимит open files.
Диагностика по логам: что искать в error.log
Начинайте с error.log, он показывает причину, а не симптом.
tail -f /var/log/nginx/error.log grep -i "permission denied" /var/log/nginx/error.log grep -i "open() .* failed" /var/log/nginx/error.log
Соответствие записей и причин:
| Запись в логе | Причина | Что делать |
|---|---|---|
| open() failed (13: Permission denied) | Права на каталог или файл | Проверить владельца, группу и права 750/640 |
| Permission denied при корректных правах | SELinux или AppArmor | chcon -t httpd_sys_content_t, проверить профиль AppArmor |
| directory index of ... is forbidden | Нет index-файла и выключен autoindex | Включить autoindex точечно или добавить index.html |
| upstream timed out | Проксирование, а не статика | Увеличить proxy_read_timeout у проксируемого location |
| client intended to send too large body | Загрузка больше client_max_body_size | Поднять лимит для location с upload |
access.log дополняет картину: коды 403 и 404 показывают, где защита сработала правильно, а где вы случайно закрыли нужное. Резкий рост 200 с одного IP намекает на массовую выкачку и даёт повод проверить лимиты.
Производительность и кэширование статики
Для отдачи файлов с диска включите базовый набор директив в http или server-блоке:
sendfile on; tcp_nopush on; tcp_nodelay on; open_file_cache max=10000 inactive=30s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on;
open_file_cache держит открытые дескрипторы и метаданные недавно запрошенных файлов: при частых повторных скачиваниях одного архива это экономит системные вызовы. Параметр inactive=30s закрывает дескриптор, если файл не запрашивали полминуты, open_file_cache_valid 60s задаёт интервал перепроверки метаданных. Если файлы в каталоге меняются часто, уменьшите open_file_cache_valid, иначе клиенты получат устаревшие размеры и даты.
Заголовки кэширования для архива зависят от того, меняется ли файл под тем же именем. Правило: immutable только для версионированных файлов с хешем в имени.
# версионированные файлы: имя меняется при обновлении
location ~* ^/files/.+-[a-f0-9]{8}\.(zip|tar\.gz)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
# файлы с постоянным именем: короткий срок и перепроверка
location ~* ^/files/.+\.(zip|tar\.gz)$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
Порядок location важен: регулярные выражения проверяются в порядке появления, поэтому шаблон для версионированных имён ставьте выше общего. Заголовок immutable снимает условные запросы при перезагрузке страницы: браузер не перепроверяет файл, пока действует max-age. Заголовки ETag и Last-Modified Nginx отдаёт по умолчанию, они позволяют получить 304 и не качать файл заново. Файлу с неизменным именем immutable не выдавайте: пользователи застрянут на старой версии, а обновление придётся ждать вручную после очистки кэша.
Итог: минимальный безопасный конфиг и что проверить
Финальный server-блок целиком
server {
listen 443 ssl;
server_name files.example.com;
ssl_certificate /etc/letsencrypt/live/files.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/files.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
root /var/www/files;
# служебные файлы наружу не отдаём
location ~ /\. {
deny all;
}
# публичный архив с прямыми ссылками
location /files/ {
alias /var/www/files/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
valid_referers none blocked server_names *.example.com example.com;
if ($invalid_referer) {
return 403;
}
limit_rate_after 10m;
limit_rate 1m;
try_files $uri $uri/ =404;
}
# приватная часть закрыта паролем
location /files/private/ {
alias /var/www/files/private/;
auth_basic "Restricted archive";
auth_basic_user_file /etc/nginx/.htpasswd;
}
sendfile on;
tcp_nopush on;
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
}
server {
listen 80;
server_name files.example.com;
return 301 https://$host$request_uri;
}
Комментарии в блоке объясняют назначение частей, а лишнее можно убрать под свою задачу: autoindex, если листинг не нужен; valid_referers, если все файлы публичные и встраивание вас не беспокоит; limit_rate, если канал широкий и абьюза нет. auth_basic и deny для служебных файлов лучше оставить всегда. Оба location с /files/ сводятся в один, если структура на диске это позволяет.
Короткий чек-лист перед публикацией
- Права 750 на каталоги и 640 на файлы, владелец и группа совпадают с воркерами Nginx.
- autoindex выключен глобально или ограничен публичным каталогом.
- Служебные файлы закрыты через deny all в регулярном location.
- valid_referers настроен и проверен curl с чужим Referer.
- limit_rate и limit_rate_after заданы для тяжёлых файлов.
- TLS включён, редирект с 80 на 443 работает, HSTS добавлен.
- auth_basic закрывает приватный подкаталог, .htpasswd лежит вне корня раздачи.
- nginx -t пройден, выполнен systemctl reload nginx.
- Проверки curl дают ожидаемые 200, 403 и 401.
- error.log и access.log просмотрены после первого скачивания.
Принцип остаётся одним: не открыть лишнего наружу. Проверяйте каждый слой отдельно, потому что работающая раздача и защищённая раздача - два разных результата, и второй получается только осознанной настройкой.