Короткий ответ

Секрет, который читает other или лежит в git, считайте украденным: ротация, потом гигиена. На хосте host.example (10.0.20.10) храните секреты в файле 0600 root (или сервисный пользователь), подключайте EnvironmentFile=-/etc/myservice/myservice.env в systemd, не в ExecStart=...password=. Репозиторий — шаблоны без значений. Не лечите утечку chmod 777. Не отключайте MAC, чтобы приложению «было проще читать секрет».

Пользователь admin с sudo копирует секреты, не держит их в /home/admin/Downloads.

Симптомы и как отличить

  • grep -R password /etc/myservice находит plaintext;
  • git log -p показывает старый ключ;
  • docker-compose.yml с MYSQL_ROOT_PASSWORD: в git;
  • wiki «пароль SMTP».
МестоРиск
/etc/myservice/app.env 640 user myserviceнорма
/etc/myservice/app.conf 644 с passwordдыра
unit Environment=SECRET=…видно всем systemctl cat с правами, плюс journal
cloud-init userdata в логечасто 700 дней в /var/log

Отличие от широких прав на каталоги без секрета: 777/world-writable. Cron, который пишет секрет в /tmp: cron.

Возможные причины

  1. Пример конфига с сайта скопировали вместе с changeme.
  2. Ansible template без mode: '0600'.
  3. «Чтобы php-fpm читал» поставили 644.
  4. Docker secrets забыли, взяли env в compose.
  5. Скриншот .env в тикете.
  6. Backup /etc в публичный S3.

Диагностика

Поиск подозрительных строк (ограничьте каталоги, не сканируйте чужие home без мандата):

sudo grep -RInE 'password|secret|api[_-]?key|BEGIN (RSA |OPENSSH |EC )?PRIVATE' \
  /etc/myservice /opt/myservice /var/www 2>/dev/null | head
sudo find /etc/myservice /opt/myservice -type f -printf '%m %u %g %p\n' 2>/dev/null

Git на сервере (если вдруг клонирован):

sudo git -C /opt/myservice rev-parse --is-inside-work-tree
sudo git -C /opt/myservice grep -I -nE 'BEGIN OPENSSH|AKIA[0-9A-Z]{16}' || true

Права:

stat /etc/myservice/app.env
namei -l /etc/myservice/app.env

Если каталог 755 и файл 600 — other не читает файл. Если файл 644 — читает любой локальный пользователь, включая www-data и скомпрометированный PHP.

Journal:

sudo journalctl -u myservice -n 20 --output=cat

Секреты в логах — отдельный баг приложения.

Решение

Сценарий A. Файл на диске world-readable

sudo install -d -m 750 -o root -g myservice /etc/myservice
sudo install -m 640 -o root -g myservice /dev/null /etc/myservice/myservice.env
sudo nano /etc/myservice/myservice.env

Формат systemd EnvironmentFile: KEY=value без кавычек-сюрпризов. Unit:

[Service]
User=myservice
EnvironmentFile=/etc/myservice/myservice.env

Уберите пароль из app.conf в git-шаблоне. Reload:

sudo systemctl daemon-reload
sudo systemctl restart myservice

Сценарий B. Секрет в git

  1. Ротация всех ключей/паролей, которые когда-либо коммитили.
  2. Вынести файл в myservice.env на хост, в git — myservice.env.example без значений.
  3. .gitignore на .env, *.pem, id_*.
  4. History: git filter-repo на копии репозитория — отдельная операция; не считайте squash последним коммитом достаточным, если репозиторий уже клонировали.

Не публикуйте новый секрет в том же MR, что и фильтр history.

Сценарий C. Docker Compose

env_file с правами 600 на хосте, не environment: plaintext в yaml в git. Лучше Docker secrets / bind file uid=. Не монтируйте .env chmod 777.

Сценарий D. Нужен доступ нескольким сервисам

Группа myservice, файл 640 root:myservice, не 644. Не общая группа users.

После утечки проверьте sudo/ключи: sudo, SSH-ключ.

Где ещё ищут секреты после «починили app.env». cloud-init пишет userdata в /var/lib/cloud/instance/user-data.txt — часто 600, но бэкап этого дерева уезжает на NAS. Вынесите пароли из userdata в Secret Manager / файл, который cloud-init скачивает с ограниченным IAM, не в plaintext YAML.

Ansible: diff в логе CI с secret= — включите no_log, ротируйте всё, что светилось в pipeline. group_vars/all.yml в git с vault лучше, чем plaintext; незашифрованный group_vars = git-секрет.

Панели: phpMyAdmin config.inc.php с $cfg['Servers'][$i]['password'], .pgpass в /root mode 644, ~/.my.cnf у admin. stat на них в том же обходе, что /etc/myservice.

Kernel cmdline и GRUB не место для паролей БД. systemctl show и docker inspect доступны тем, кто имеет право читать процессы: ограничьте admin и не давайте docker.sock разработчику.

После ротации проверьте старые секреты в: снимках VM, object storage бэкапов, ticket-вложениях, Slack/Mattermost. Ротация в приложении без отзыва в облаке (IAM access key) оставляет рабочий ключ.

Шаблон деплоя: в репозитории только app.env.example с пустыми значениями и комментарием «положить на хост 0640». CI должен падать на regex AKIA[0-9A-Z]{16}|BEGIN OPENSSH PRIVATE KEY в tracked files.

Как проверить, что проблема устранена

stat -c '%a %U:%G %n' /etc/myservice/myservice.env
sudo -u nobody cat /etc/myservice/myservice.env; echo nobody_exit:$?
sudo grep -RInE 'password|SECRET' /opt/myservice --exclude='*.example' || true
systemctl show myservice -p User,EnvironmentFiles

nobody не читает. В git grep по рабочей копии — пусто. Приложение живо (healthcheck). Старые ключи в облаке disabled.

Если не помогло

  • Приложение не стартует: User= не в группе файла — 640 root:myservice и User=myservice.
  • PHP open_basedir / AppArmor DENIED — правьте профиль, не chmod 644.
  • Cloud-init снова кладёт пароль в /var/lib/cloud: уберите из userdata, ротируйте.
  • Секрет в docker inspect Env — пересоберите без env plaintext.
  • Backup-агент копирует /etc на шару 777 — это новая утечка.

Профилактика

  • Линтер CI: запрет password= в репо, gitleaks/trufflehog.
  • Ansible mode: '0640', no_log: true на задачах с секретом.
  • Секретница (Vault и т.п.) когда парк вырос; до этого — файлы 640 и ротация.
  • Не логировать Environment.
  • Права на файлы в том же обходе.

FAQ

Хватит ли chmod 600 при каталоге 777?

Нет. World-writable каталог позволяет подменить файл. Каталог 750/755 без write для other.

Можно ли секрет в /home/admin?

Хуже: бэкапы home, агенты, www-data иногда имеют обход. /etc/myservice + сервисный uid.

EnvironmentFile vs pass vs Vault

Для одного сервера EnvironmentFile 640 достаточно, если не коммитится. Vault — когда много хостов и ротация. Не смешивайте секрет в git «пока внедряем Vault».

docker.sock и секреты

Кто в группе docker, читает env всех контейнеров. См. статью про сокет.

Нужно ли ротировать, если файл был 644 один день?

Да. Любой локальный пользователь и любой RCE от www-data могли прочитать.