Статический контент читается кем угодно, если сервер отдаёт его без ограничений. Запрос curl -I https://site.example/.env выполняется за секунду, и nginx по умолчанию не мешает такой попытке: файл из document root уходит в ответ вместе с ключами и токенами. По разбору утечек через историю коммитов, абсолютный лидер по частоте среди утёкших имён файлов — .env, рядом с ним index.js, application.properties, app.js, server.js, .env.example и docker-compose.yml. Открытый .env находят не только целенаправленной атакой: чаще его вытаскивает автоматическое сканирование по всей сети без разбора конкретных целей.
Без дополнительного ПО риски закрывают три настройки. Права 755 для каталогов, 644 для файлов и 600 для секретов. Директива autoindex off плюс блокировка служебных путей в location. Заголовки X-Content-Type-Options: nosniff и Strict-Transport-Security. CSP ставят следующим шагом, когда раздача статики работает стабильно.
Дальше: что именно утекает, команды chmod и chown, готовый конфиг nginx, разбор nosniff, CSP и HSTS, два сценария утечек через .git и .env и чек-лист проверки перед публикацией.
Примеры в статье опираются на открытые описания развёртывания конкретных приложений (Hermes Agent, nvtt). Конфигурации nginx даны как стандартные директивы и требуют проверки командой nginx -t на вашей версии сервера.
Почему статический контент - это риск, а не «просто файлы»
Nginx отдаёт файл из document root любому, кто знает или угадал путь. Расширение и назначение файла сервер не проверяет. Поэтому .env с ключом платёжного шлюза, дамп базы и архив резервной копии уходят наружу тем же способом, что и logo.png.
Конфигурация из пакета дистрибутива не блокирует .git, .env, .bak и .swp. Пока в location нет явного запрета, защищают только права доступа и расположение файла: внутри веб-корня или вне него.
В описании развёртывания Hermes Agent файл с секретами и API-ключами защищён правами доступа, а исполняемый код отделён от пользовательских данных. Оба принципа переносятся на статику: веб-сервер получает только чтение кода, секреты и данные живут отдельно и с ограниченными правами.
Отдельно стоит сказать о правах: при режиме 644 файл .env прочитает любой пользователь на том же сервере, даже без рутовых прав. Это делает неверные права самостоятельным вектором утечки, а не только следствием ошибки в конфиге.
Что именно утекает: .env, .git, бэкапы и листинги
- .env: пароли к базе, API-ключи, токены, секрет подписи сессий. Один запрос отдаёт файл целиком, разбирать его не нужно.
- .git: каталог репозитория. Через пути /.git/HEAD и /.git/config выкачивают объекты и восстанавливают исходный код вместе с историей коммитов, где часто остаются удалённые пароли.
- Резервные копии и временные файлы: .bak, .old, .orig, .save, .swp, .swo, имена с тильдой на конце, архивы .tar.gz, .zip, .sql, дампы.
- Конфигурации с паролями: settings.py, config.php, .htaccess, docker-compose.yml, выгруженный kubeconfig.
- Листинг каталога: при autoindex on nginx строит страницу со списком файлов, и структура проекта с внутренними именами документов становится публичной.
Сканеры перебирают именно эти пути по готовым словарям. SecLists — это набор множества типов списков, используемых при тестировании безопасности: имена пользователей, пароли, URL, шаблоны чувствительных данных, fuzzing-пейлоады, веб-шеллы и другое. В нём есть раздел Discovery/Web-Content с файлом common.txt, предназначенным для content discovery. Если файл доступен по HTTP, его найдут: скрывать имя бесполезно.
Чем опасен MIME-sniffing для статики
Браузер определяет тип файла по заголовку Content-Type, но не всегда ему доверяет. Спецификация WHATWG MIME Sniffing Standard описывает алгоритм определения вычисленного MIME-типа ресурса (MIME type sniffing algorithm), по которому user agent может прийти к иному выводу о типе содержимого, чем сервер. Если сервер считает, что клиент обработает файл как изображение и потому как безопасный, а user agent считает содержимое HTML и потому имеющим право исполнять скрипты, атакующий может украсть учётные данные аутентификации пользователя и провести другие XSS-атаки.
Рабочий сценарий: загрузчик аватаров проверяет расширение и отдаёт файл как text/plain. Пользователь присылает файл с HTML и JavaScript, ссылка уходит жертве, скрипт выполняется в домене сайта. Права на файл при этом не нарушены, и в логах запрос выглядит как обычная отдача статики.
Закрывается одной строкой: add_header X-Content-Type-Options "nosniff" always;. Флаг nosniff запрещает браузеру угадывать тип. Для сайтов с загрузкой файлов и любым пользовательским контентом заголовок обязателен.
Права доступа к файлам и каталогам: базовые правила для nginx
Веб-серверу нужен доступ на чтение статики. Каталоги: 755, владелец пишет, группа и остальные читают и входят внутрь. Файлы: 644. Исключение - конфиги, ключи и секреты: 600 или 640, владелец root.
Права root нужны на этапе установки системных пакетов и настройки компонентов (описание установки Hermes Agent), а рабочий процесс nginx запускается от непривилегированного пользователя: www-data в Debian и Ubuntu, nginx в RHEL-семействе.
| Объект | Права | Владелец:группа |
|---|---|---|
| Каталоги статики | 755 | root:www-data |
| Файлы статики: html, css, js, изображения | 644 | root:www-data |
| .env, файлы с токенами | 600 | root:root |
| Логи и метрики | 640 | root:www-data |
| Каталог загрузок пользователя | 755 на каталог, 644 на файл | www-data:www-data |
Бит выполнения на каталоге нужен для прохода внутрь, поэтому 644 на каталог ломает сайт: nginx получает 403 на файлы, лежащие на уровень глубже. Бит записи для группы www-data на статике не нужен нигде, кроме каталога загрузок.
Как выставить права: пошаговые команды
chown -R root:www-data /var/www/site
find /var/www/site -type d -exec chmod 755 {} \;
find /var/www/site -type f -exec chmod 644 {} \;
chown root:root /var/www/site/.env
chmod 600 /var/www/site/.env
Права применяют к конкретному сайту, а не к /var/www целиком: рядом могут лежать проекты с другими требованиями. При распаковке архива выставляйте umask 022 до распаковки, тогда новые файлы получат 644, а каталоги 755. Для каталога с секретами подойдёт umask 027: файлы получат 640, лишние пользователи в них не попадут.
umask 022 unzip -q dist.zip -d /var/www/site
Деплой через rsync сохраняет права источника, поэтому их задают явно:
rsync -a --chmod=D755,F644 --chown=root:www-data ./dist/ deploy@server:/var/www/site/
В контейнерах права приходят из образа, но bind mount и volume сохраняют биты хоста. Статику монтируют только для чтения: -v /srv/site:/usr/share/nginx/html:ro. На NAS, например TrueNAS, статику отдают датасетом с доступом только на чтение для веб-сервиса.
Типичные ошибки с правами и как их избежать
- chmod -R 777 «чтобы заработало». Любой процесс на сервере и любой пользователь по FTP перезаписывает index.html, а вместе с ним и точку входа сайта.
- Владелец root и права 700 на статике: nginx под www-data файлы не читает, сайт отдаёт 403, и права «чинят» через 777.
- Запись для nginx в каталог с кодом: уязвимость в CMS или загрузчике превращается в правку любых файлов сайта.
- Права из архива: unzip под root даёт владельца root, отдельные архивы приносят 777 на исполняемые файлы.
- Забытый .env с правами 644 рядом с index.html: файл читают все, кто дошёл до веб-корня.
- Деплой под личной учётной записью: файлы принадлежат деплой-пользователю, и он же получает права на прод.
Правило простое: nginx имеет только чтение статики, секреты читает root, запись в код есть лишь у процесса деплоя.
Отключаем листинг каталогов и закрываем служебные пути в nginx
autoindex off убирает навигацию по каталогу, но не мешает прямому запросу к файлу. Нужны оба механизма: листинг выключен, служебные пути закрыты.
Готовый конфиг nginx: autoindex off и блокировка .git/.env
server {
listen 443 ssl;
server_name site.example;
root /var/www/site;
index index.html;
autoindex off;
location ~ /\.(?!well-known) {
return 404;
}
location ~* \.(bak|old|orig|save|swp|swo|log|sql|tar|gz|tgz|zip|7z|rar)$ {
return 404;
}
location / {
try_files $uri $uri/ =404;
}
}
- autoindex off отключает листинг. Значение по умолчанию совпадает, но явная строка не потеряется при следующей правке конфига.
- location ~ /\.(?!well-known) закрывает пути, начинающиеся с точки, кроме /.well-known/. Этот каталог нужен для проверки домена при выпуске сертификата Let's Encrypt: при проверке методом HTTP-01 центр сертификации ожидает, что сервер отдаст токен по URL вида /.well-known/acme-challenge/. Под правило попадают .git, .env, .htaccess, .svn, .DS_Store.
- location по расширениям закрывает архивы, дампы и файлы редакторов.
- return 404 вместо deny all: 403 подтверждает, что файл существует, 404 такой информации не даёт.
Важная деталь для ACME: в конфигурациях nginx для challenge используют location ^~ /.well-known/acme-challenge/, потому что ^~ повышает приоритет и не даёт regex-location «перехватить» challenge. В официальном примере это объясняется прямо: ^~ нужен, чтобы не проверять другие regex, поскольку в других конфигах есть regex-правило, запрещающее доступ к файлам с точками в имени. Если вы выпускаете сертификат через webroot, добавьте такой блок до общего правила с точкой.
Проверка занимает секунды:
nginx -t curl -I https://site.example/.env curl -I https://site.example/.git/config
Ожидаемый ответ: HTTP/2 404. Если nginx стоит перед приложением как reverse proxy, блокировка ставится на внешнем сервере, до проксирования: иначе запрос уйдёт в бэкенд, и тот отдаст .env сам. Схема с nginx перед приложением встречается в описании приложения nvtt: HTTPS-прокси там работает единственной точкой входа, и правила блокировки задаются именно на нём.
Что делать с резервными копиями и временными файлами
Резервные копии держите вне document root: /var/backups/site вместо /var/www/site. Если копия нужна рядом, закрывайте её тем же location по расширениям.
Частые имена для проверки: site.tar.gz, site.zip, dump.sql, index.html.bak, config.php.old, .index.html.swp. Файлы редакторов содержат кусок исходного текста, иногда вместе с паролями и токенами. В описании приложения nvtt данные, файлы, фото и журнал, лежат в отдельной папке и требуют бэкапа. Схема правильная: данные отделены от кода, а копия данных живёт вне веб-корня и не отдаётся по HTTP.
Security-заголовки: CSP, X-Content-Type-Options, HSTS
Заголовки работают на уровне ответа и не зависят от прав на файлы. nosniff закрывает подмену типа, CSP ограничивает выполнение чужого кода, HSTS убирает обращения по HTTP. Готовые связки HTTPS, HSTS и CSP для Nginx и Apache собраны в разборе защиты веб-серверов.
Одна деталь nginx: add_header наследуется в дочерние блоки только тогда, когда в них нет своих add_header. Как только в location появляется собственный add_header, заголовки из server теряются. Проверяйте итоговые ответы на всех типах страниц, включая 404.
X-Content-Type-Options: как настроить и зачем
add_header X-Content-Type-Options "nosniff" always;
Параметр always добавляет заголовок и к ответам 4xx и 5xx, включая страницы ошибок. Без него браузер снова получает свободу угадывать тип. Проверка:
curl -sI https://site.example/ | grep -i x-content-type-options
CSP для статического сайта: минимальный рабочий набор
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
- default-src 'self': базовое правило, ресурсы загружаются только с того же origin.
- img-src 'self' data:: схема data: нужна для встроенных картинок и base64-иконок в CSS.
- style-src 'self' 'unsafe-inline': встроенные стили часто остаются в собранной статике. Для скриптов такое исключение делать не стоит.
- script-src 'self': блокирует инлайн-скрипты и внешние домены, сторонний тег script не выполнится.
- object-src 'none', base-uri 'self', frame-ancestors 'none': запрет плагинов, подмена тега base и встраивание сайта в iframe.
Порядок включения, безопасный для продакшена: сначала Content-Security-Policy-Report-Only, сбор отчётов о нарушениях, затем переход на enforce. Для приложений с динамикой и API директива расширяется connect-src и form-action, разбор таких политик есть в материале о защите приложений с динамическим контентом.
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;
HSTS: когда включать и какие риски
add_header Strict-Transport-Security "max-age=300" always;
Заголовок заставляет браузер обращаться к домену только по HTTPS в течение max-age. Правило живёт на стороне клиента, и отозвать его мгновенно нельзя. Начните с max-age=300, убедитесь, что весь сайт работает по HTTPS без смешанного контента, и наращивайте срок до 31536000 секунд (год).
includeSubDomains добавляйте после проверки всех поддоменов: любой из них, живущий по HTTP, перестанет открываться в браузерах, где уже получен заголовок. С preload стоит быть особенно осторожным: по данным официальной страницы удаления из HSTS preload list, удаление домена из списка может занять 6–12 недель, чтобы дойти до большинства пользователей Chrome, и может занять больше времени для других браузеров. Чтобы запросить удаление через форму, сайт должен быть preloaded или pending preload через hstspreload.org и обслуживать HTTPS с валидным сертификатом. Поэтому preload включают последним, когда домен и все поддомены стабильно работают только по HTTPS. HSTS не работает без валидного сертификата: если он истёк, сайт станет недоступен.
HTTPS нужен и для функциональности: камера в браузере работает только по HTTPS, поэтому приложению требуется домен с сертификатом, а перед ним ставится nginx с HTTPS (описание приложения nvtt). Подготовка сертификата с TLS 1.3 и проверкой в SSL Labs описана в руководстве по установке SSL-сертификата.
Защита от утечек через .git и .env: практические сценарии
Два сценария дают основную часть громких утечек на статике, и оба закрываются правами плюс конфигом.
Сценарий 1: .env с правами 644 в корне сайта. Файл читает любой, кто знает адрес, и любой пользователь на сервере. Внутри обычно ключи к платёжному шлюзу, доступ к базе и токены сторонних API. Смена одного ключа недостаточна: сервисы часто связаны, и утечка тянет за собой сброс всего набора.
Сценарий 2: .git в document root. Через /.git/HEAD, /.git/config и объекты репозитория восстанавливают исходный код. Секреты, удалённые из текущей версии, остаются в истории коммитов, поэтому проверка текущего кода успокаивает зря.
Меры: .env вынести за document root либо оставить в проекте с правами 600 и владельцем root; закрыть служебные пути в nginx; на прод не деплоить репозиторий, а собирать артефакт через git archive, CI или сборку образа. Разделение кода и данных снижает ущерб, но не отменяет проверку путей.
Как проверить, не утекли ли .git и .env
curl -s -o /dev/null -w "%{http_code}" https://site.example/.env; echo
curl -s -o /dev/null -w "%{http_code}" https://site.example/.git/config; echo
curl -s -o /dev/null -w "%{http_code}" https://site.example/.git/HEAD; echo
Ожидаемый результат: 404 на всех трёх адресах. Код 200 означает, что файл отдаётся. Ответ 403 тоже плохой знак: путь существует, а запрет стоит на уровне deny all, а не возврата 404. Проверяйте основной домен, поддомены и staging: тестовый контур часто копирует прод без заголовков и без блокировок.
Содержимое .env не логируйте в ответах и не выводите в отладочных страницах: одна такая страница в проде равна открытому файлу.
Что делать, если утечка уже произошла
- Закрыть доступ в nginx: добавить location с return 404 и перезагрузить конфиг командой nginx -s reload.
- Сменить все ключи, токены и пароли из файла: доступ к базе, API-ключи, секреты подписи. Даже короткая утечка приводит к смене ключей, потому что сканер сохраняет ответ.
- Сохранить копию .env в защищённое место до смены ключей, чтобы не потерять конфигурацию, и только затем удалить файл из веб-корня.
- Проверить логи на обращения к служебным путям: grep -E '\.(env|git|bak|sql)' /var/log/nginx/access.log.
- Удалить .git из продакшена и пересобрать деплой без репозитория.
- Проверить дампы и архивы в document root: find /var/www -type f \( -name '*.sql' -o -name '*.zip' -o -name '*.tar.gz' \).
- Записать причину: деплой без проверки, распаковка архива в веб-корень, отладка с копией .env на проде.
Чек-лист проверки перед публикацией статики
Проверка занимает несколько минут и выполняется после каждого деплоя. Полный аудит TLS, заголовков и контроля доступа описан в руководстве по аудиту безопасности Nginx.
- nginx -t проходит без ошибок.
- autoindex off задан явно.
- location с return 404 закрывает пути, начинающиеся с точки: .git, .env, .htaccess.
- location по расширениям закрывает .bak, .old, .swp, .sql, .zip, .tar.gz.
- Каталоги 755, файлы 644, владелец root:www-data.
- .env лежит вне document root либо имеет права 600 и владельца root.
- Бэкапы и дампы отсутствуют в веб-корне.
- Заголовок X-Content-Type-Options: nosniff приходит на всех ответах.
- HSTS включён после проверки HTTPS и всех поддоменов.
- CSP проверен в report-only и затем включён в enforce.
- curl -I на .env, .git/config и архив возвращает 404.
- Логи веб-сервера собираются, настроен алерт на всплеск запросов к служебным путям, проверка повторяется после каждого деплоя.
Команды для быстрой проверки
nginx -t curl -I https://site.example/.env curl -I https://site.example/.git/config curl -I https://site.example/backup.zip curl -sI https://site.example/ | grep -i -E 'x-content-type-options|strict-transport-security|content-security-policy'
Ответ 404 на служебные пути и наличие трёх заголовков в последней строке означают, что базовая проверка пройдена. Ответ 200 требует немедленной правки конфига: файл отдаётся наружу прямо сейчас.
Автоматизация проверок в CI/CD
Ручная проверка забывается, поэтому шаг добавляют в pipeline сразу после деплоя. Если хотя бы один служебный путь отдаёт не 404, сборка падает и деплой откатывается.
for path in .env .git/config backup.zip dump.sql; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://site.example/$path")
if [ "$code" != "404" ]; then
echo "Утечка: $path -> $code"
exit 1
fi
done
Тот же скрипт проверяет заголовки: сравните вывод curl -sI с ожидаемым списком и уроните сборку при расхождении. Для статики в Docker, на VPS или на NAS логика одинаковая, отличается только адрес стенда.
Итог: минимальный набор мер, который закрывает 90% рисков
Три приоритета, если времени мало. Права: каталоги 755, файлы 644, .env 600 с владельцем root. Конфиг: autoindex off плюс блокировка .git, .env, .bak и архивов через return 404. Заголовки: X-Content-Type-Options: nosniff и Strict-Transport-Security после проверки HTTPS.
CSP идёт следующим шагом: от чтения .env он не спасает, зато ограничивает ущерб от стороннего кода, выполненного в браузере. Ни одна из мер не требует дополнительной инфраструктуры и работает на любом nginx.
Пройдите чек-лист выше и добавьте проверку служебных путей в pipeline. Безопасность статики держится не на разовой настройке, а на повторении проверки после каждого деплоя: новая сборка легко возвращает .env в веб-корень и снимает заголовки.