Безопасность статического контента на веб-сервере: права доступа, заголовки и защита .git и .env | AdminWiki

Безопасность статического контента на веб-сервере: права доступа, заголовки и защита .git и .env

22 сентября 2026 14 мин. чтения
Содержание статьи

Статический контент читается кем угодно, если сервер отдаёт его без ограничений. Запрос 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-семействе.

ОбъектПраваВладелец:группа
Каталоги статики755root:www-data
Файлы статики: html, css, js, изображения644root:www-data
.env, файлы с токенами600root:root
Логи и метрики640root: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 не логируйте в ответах и не выводите в отладочных страницах: одна такая страница в проде равна открытому файлу.

Что делать, если утечка уже произошла

  1. Закрыть доступ в nginx: добавить location с return 404 и перезагрузить конфиг командой nginx -s reload.
  2. Сменить все ключи, токены и пароли из файла: доступ к базе, API-ключи, секреты подписи. Даже короткая утечка приводит к смене ключей, потому что сканер сохраняет ответ.
  3. Сохранить копию .env в защищённое место до смены ключей, чтобы не потерять конфигурацию, и только затем удалить файл из веб-корня.
  4. Проверить логи на обращения к служебным путям: grep -E '\.(env|git|bak|sql)' /var/log/nginx/access.log.
  5. Удалить .git из продакшена и пересобрать деплой без репозитория.
  6. Проверить дампы и архивы в document root: find /var/www -type f \( -name '*.sql' -o -name '*.zip' -o -name '*.tar.gz' \).
  7. Записать причину: деплой без проверки, распаковка архива в веб-корень, отладка с копией .env на проде.

Чек-лист проверки перед публикацией статики

Проверка занимает несколько минут и выполняется после каждого деплоя. Полный аудит TLS, заголовков и контроля доступа описан в руководстве по аудиту безопасности Nginx.

  1. nginx -t проходит без ошибок.
  2. autoindex off задан явно.
  3. location с return 404 закрывает пути, начинающиеся с точки: .git, .env, .htaccess.
  4. location по расширениям закрывает .bak, .old, .swp, .sql, .zip, .tar.gz.
  5. Каталоги 755, файлы 644, владелец root:www-data.
  6. .env лежит вне document root либо имеет права 600 и владельца root.
  7. Бэкапы и дампы отсутствуют в веб-корне.
  8. Заголовок X-Content-Type-Options: nosniff приходит на всех ответах.
  9. HSTS включён после проверки HTTPS и всех поддоменов.
  10. CSP проверен в report-only и затем включён в enforce.
  11. curl -I на .env, .git/config и архив возвращает 404.
  12. Логи веб-сервера собираются, настроен алерт на всплеск запросов к служебным путям, проверка повторяется после каждого деплоя.

Команды для быстрой проверки

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 в веб-корень и снимает заголовки.

Поделиться:
Сохранить гайд? В закладки браузера