Короткий ответ
Секрет, который читает 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.
Возможные причины
- Пример конфига с сайта скопировали вместе с
changeme. - Ansible
templateбезmode: '0600'. - «Чтобы php-fpm читал» поставили
644. - Docker secrets забыли, взяли env в compose.
- Скриншот
.envв тикете. - 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/nullGit на сервере (если вдруг клонирован):
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
- Ротация всех ключей/паролей, которые когда-либо коммитили.
- Вынести файл в
myservice.envна хост, в git —myservice.env.exampleбез значений. .gitignoreна.env,*.pem,id_*.- 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,EnvironmentFilesnobody не читает. В git grep по рабочей копии — пусто. Приложение живо (healthcheck). Старые ключи в облаке disabled.
Если не помогло
- Приложение не стартует: User= не в группе файла —
640root:myservice и User=myservice. - PHP open_basedir / AppArmor DENIED — правьте профиль, не
chmod 644. - Cloud-init снова кладёт пароль в
/var/lib/cloud: уберите из userdata, ротируйте. - Секрет в
docker inspectEnv — пересоберите без 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 могли прочитать.