Почему безопасность скриптов администрирования сводится к трём вещам: секреты, права и аудит
Безопасность скриптов администрирования в 2026 году держится на трёх практиках: секреты хранятся вне кода, привилегии ограничены необходимым минимумом, а выполнение скрипта подтверждается аудитом. Скрипт бэкапа или деплоя, где пароль базы записан прямо в теле, запуск идёт от root, а вывод cron никуда не перенаправляется, создаёт три независимых риска: утечку секрета, полный доступ к серверу при компрометации и незамеченный отказ задачи.
Типовой сценарий выглядит так: /opt/backup.sh с паролем БД лежит в git, cron запускает его от root, ошибки уходят в никуда. Через полгода выясняется, что архивов нет уже месяц, а пароль может прочитать каждый, у кого есть доступ к репозиторию. Молча сломавшийся бэкап опасен именно этим: все уверены, что копии есть, а их нет.
Дальше идём по порядку: убираем пароли из кода через /opt/backup.env, ограничиваем права и разбираем sudo -l, настраиваем логирование cron с внешним heartbeat, добавляем валидацию аргументов, чистим содержимое бэкапа и заканчиваем чек-листом перед продом.
Граница материала: детальный разбор с командами и конфигами у нас есть для сценария резервного копирования на VPS, он и служит опорой для примеров ниже. Приёмы по sudo, менеджерам секретов и валидации входных данных приводим как общие практики администрирования: прогоняйте их на тестовом стенде, прежде чем менять прод.
Три риска, которые закрываем в этой статье
- Секреты в открытом виде. Пароль в теле скрипта виден в истории shell, в аргументах процесса и в git.
- Избыточные права. Скрипт бэкапа запускается от root, а в sudoers остаётся правило вида ALL=(ALL) NOPASSWD: ALL или wildcard /usr/bin/systemctl restart *.
- Отсутствие аудита и валидации. Нет set -euo pipefail, нет логирования, аргументы подставляются в команды без проверки.
Готовый список проверок собран в конце статьи, в разделе с чек-листом перед запуском в прод.
Хранение секретов в скриптах: переменные окружения и файл с ограниченными правами
Начнём с антипаттерна, который встречается чаще всего:
#!/usr/bin/env bash mysqldump -u root -p'SuperSecret123' --all-databases | gzip > /var/backups/all.sql.gz
Проблем здесь сразу три. Аргументы процесса видны другим пользователям системы через ps, строка с паролем остаётся в history, а сам файл скрипта обычно лежит в git и уезжает туда вместе с секретом. Архив с копией каталога /opt или /etc тоже тащит пароль внутрь бэкапа.
Файл /opt/backup.env: создание, права 600 и загрузка через set -a && source
Порядок действий от создания файла до строки в cron.
- Создайте файл и сразу закройте права:
install -m 600 /dev/null /opt/backup.env printf 'DB_PASSWORD=%s\n' 'SuperSecret123' > /opt/backup.env chown backup:backup /opt/backup.env stat -c '%a %U:%G %n' /opt/backup.env
Ожидаемый вывод последней команды: 600 backup:backup /opt/backup.env. Владелец - тот пользователь, от которого запускается скрипт. - Загрузите переменные в начале скрипта:
set -a source /opt/backup.env set +a
Флаг -a (allexport) экспортирует все переменные, назначенные после его включения, в окружение дочерних процессов. Без него DB_PASSWORD останется локальной переменной shell и не дойдёт до mysqldump. Команда set +a выключает режим. - Используйте переменную вместо литерального пароля:
mysqldump -u backup -p"$DB_PASSWORD" --single-transaction --all-databases \ | gzip > "/var/backups/all-$(date +%F).sql.gz"
Пароль базы держим не в скрипте, а в отдельном файле, и подгружаем его через set -a && source - этот рецепт разобран в нашем гайде по автоматическим бэкапам VPS. - Для cron передайте файл прямо в командной строке задачи:
30 4 * * * . /opt/backup.env && /opt/backup.sh >> /var/log/backup.log 2>&1
Точка в начале - это команда source в shell cron. Она загружает переменные в окружение задачи, после чего запускается скрипт. - Не коммитьте файл с секретами. Добавьте в .gitignore строки /opt/backup.env и *.env, затем проверьте историю:
git log -S 'SuperSecret123' --all --oneline git log --all --diff-filter=A -- /opt/backup.env
Если секрет уже попал в коммит, считайте его скомпрометированным и меняйте, а не просто удаляйте строку.
Файл с правами 600 на одиночном сервере закрывает основную часть задачи: секрет не читается другими пользователями и не лежит в репозитории. Дальше начинаются требования, которые одним файлом не решаются: ротация, разграничение доступа и журнал обращений к секрету.
Когда файла .env уже недостаточно: менеджеры секретов
| Инструмент | Как работает | Когда уместен |
|---|---|---|
| pass | хранит секреты в GPG-зашифрованном каталоге с синхронизацией через git | небольшой парк машин, один администратор |
| sops + age | шифрует отдельные поля YAML/JSON, ключ расшифровки в age | секреты лежат рядом с кодом в git, несколько окружений |
| HashiCorp Vault | централизованное хранилище с политиками доступа и аудитом обращений | команда, много сервисов, нужны ротация и роли |
| systemd credentials | передаёт секрет в юнит без файла на диске, например LoadCredential=dbpass:/opt/backup.env | сервисы под systemd, нужна изоляция секрета от файловой системы |
Критерии выбора простые: число серверов, количество секретов, требования к ротации и аудиту доступа. Один VPS и один скрипт бэкапа обходятся /opt/backup.env с chmod 600. Как только серверов становится несколько или в команде больше одного администратора, копия файла на каждом хосте превращается в точку рассинхронизации: смена пароля требует обойти все машины вручную. Сценарии с менеджерами секретов проверяйте на своём стенде: в опорных материалах этот пласт не разобран, поэтому относитесь к таблице как к карте вариантов, а не как к готовому рецепту.
Права скриптов: запуск от непривилегированного пользователя и проверка sudo -l
Принцип минимальных привилегий: скрипту бэкапа нужен доступ к своей базе, своей директории и своему логу. Root ему не нужен. Первый шаг аудита - посмотреть, что реально разрешено пользователю.
sudo -l: как читать вывод и находить опасные правила
sudo -l -U backup
Команда от root показывает правила для пользователя backup. Структура вывода: строка вида «User backup may run the following commands on web1:» и список правил после неё. Читаем построчно и сверяем с таблицей.
| Правило в sudoers | Что даёт на практике |
|---|---|
| (ALL : ALL) ALL | полный root, в большинстве случаев лишний |
| (ALL) NOPASSWD: ALL | полный root без пароля: достаточно захватить сессию пользователя |
| /usr/bin/systemctl restart * | перезапуск любого сервиса; при слабых правах на /etc/systemd подмена unit-файла даёт выполнение своего кода |
| /bin/bash, /usr/bin/vi, /usr/bin/less, /usr/bin/find | оболочка или редактор с root равны полному доступу |
| /usr/bin/cp * /etc/ | запись в системные каталоги в обход прав |
Синтаксис проверяйте после каждой правки: visudo -c читает /etc/sudoers и файлы из /etc/sudoers.d, ловит опечатки, которые иначе блокируют sudo целиком. Точечная проверка одного файла: visudo -cf /etc/sudoers.d/backup.
Заменяйте wildcard на конкретные цели: вместо /usr/bin/systemctl restart * выдайте /usr/bin/systemctl restart nginx.service. Убирайте NOPASSWD там, где оно не нужно: для cron правило без пароля оправдано, для интерактивного доступа нет. Запускайте sudo -l от имени того пользователя, под которым работает скрипт, иначе увидите не свою картину прав.
Смежные темы по управлению sudo, SSH и firewall разобраны в гайде по hardening Linux-сервера, там же Lynis и OpenSCAP для автоматического аудита конфигурации.
systemd и cron: как запускать скрипт с минимальными правами
systemd даёт больше контроля, чем cron: ограничения файловой системы, окружения и привилегий описываются прямо в юните.
[Unit] Description=Nightly backup [Service] Type=oneshot User=backup Group=backup NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true ReadWritePaths=/var/backups ExecStart=/opt/backup.sh
NoNewPrivileges запрещает процессу получать дополнительные привилегии, в том числе через setuid-бинарники. ProtectSystem=strict переводит корень файловой системы в режим только для чтения, а ReadWritePaths оставляет запись только в /var/backups. Запуск по расписанию оформляется таймером:
[Unit] Description=Run nightly backup [Timer] OnCalendar=*-*-* 04:30:00 Persistent=true [Install] WantedBy=timers.target
Проверка: systemctl list-timers backup.timer и journalctl -u backup.service -n 50. Для cron запускайте задачу от отдельного пользователя через crontab -u backup -e: тогда в задаче не будет root-окружения, а права на скрипт останутся минимальными.
Про группу docker: добавление пользователя в неё даёт возможность смонтировать корень хоста внутрь контейнера, что фактически равно правам root. Если скрипт управляет контейнерами, ограничивайте не группу, а набор разрешённых команд. Скрипту, которому root действительно необходим, закройте доступ к телу файла: chmod 700 /opt/backup.sh, владелец root:root, запись в родительский каталог только для root.
Аудит скриптов: cron, логи и внешние уведомления через healthchecks.io
Выполнение скрипта нужно подтверждать извне. Строка cron с логированием и внешний heartbeat закрывают вопрос, работает ли бэкап на самом деле.
Настройка cron с логированием и heartbeat
30 4 * * * . /opt/backup.env && /opt/backup.sh >> /var/log/backup.log 2>&1
| Элемент | Назначение |
|---|---|
| 30 4 * * * | расписание: каждую ночь в 4:30 |
| . /opt/backup.env | source файла секретов в окружение задачи |
| /opt/backup.sh | сам скрипт |
| >> /var/log/backup.log | stdout дописывается в лог |
| 2>&1 | stderr идёт туда же, ошибки не теряются |
В конец скрипта добавьте heartbeat. Создайте check в healthchecks.io, получите UUID и вставьте вызов:
curl -fsS -m 10 https://hc-ping.com/<uuid> > /dev/null
Флаги: -f завершает работу с ошибкой при HTTP-коде 4xx или 5xx, -s убирает прогресс, -S показывает сообщение об ошибке, -m 10 задаёт таймаут 10 секунд. Бесплатные сервисы вроде healthchecks.io присылают письмо, если скрипт не отчитался вовремя, - эта схема описана в материале по бэкапам VPS.
Закройте права на лог: chmod 640 /var/log/backup.log, владелец backup, группа adm. В логе встречаются имена баз и пути, читать его всем незачем. Ротацию настройте через logrotate, иначе файл вырастет и займёт раздел.
Схемы регулярных проверок, аудит конфигураций и оповещения разобраны в отдельном материале по автоматизации аудита безопасности.
Еженедельная проверка: лог, даты файлов, права
- Поищите ошибки в логе: grep -iE 'error|fail|denied|warning' /var/log/backup.log.
- Проверьте даты и размеры архивов: ls -lh /var/backups. Файл старше суток или нулевого размера - сигнал.
- Проверьте права на файл секретов: stat -c '%a %U:%G %n' /opt/backup.env, ожидаем 600.
- Посмотрите, не приходили ли уведомления о пропущенном heartbeat.
- Раз в месяц восстановите копию на тестовом стенде и убедитесь, что данные читаются.
Проверка работает только тогда, когда стоит в календаре. Пятнадцать минут в неделю дешевле, чем месяц жизни без рабочих копий.
Валидация входных данных и защита от ошибок в скриптах
Скрипт ломается на пустой переменной, лишнем пробеле в пути и на аргументе, который пришёл не оттуда, откуда вы ждали.
set -euo pipefail и кавычки: минимальный каркас безопасного скрипта
#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t'
- -e останавливает скрипт при первой команде с ненулевым кодом возврата.
- -u превращает обращение к необъявленной переменной в ошибку. Без него опечатка в имени даёт пустую строку и команду с неожиданным аргументом.
- -o pipefail делает код возврата пайплайна равным коду последней неуспешной команды цепочки.
- IFS ограничивает разбиение строк переводами строк и табами, из-за которых ломаются пути с пробелами.
Кавычки вокруг переменных обязательны: rm -rf "$TARGET" и "$@" вместо незакавыченных вариантов. Подробный разбор каркаса, getopts и trap в статье по bash-скриптам для администрирования Linux.
Проверка аргументов и запрет на опасные конструкции
Скрипт, который удаляет данные по аргументу, без проверки становится оружием:
rm -rf "$1"/*
При пустом $1 команда превращается в rm -rf /*. С явными проверками и списком допустимых путей такого не происходит:
if [ "$#" -ne 2 ]; then echo "usage: $0 <backup_dir> <retention_days>" >&2 exit 1 fi case "$1" in /var/backups|/opt/backups) ;; *) echo "unsupported path: $1" >&2; exit 1 ;; esac if [ ! -d "$1" ]; then echo "no such directory: $1" >&2 exit 1 fi
- Проверяйте число аргументов до любых действий.
- Проверяйте путь по списку допустимых значений, а не набор запрещённых подстрок.
- Не используйте eval и не собирайте команды из пользовательского ввода.
- Не подставляйте ввод в sh -c и не запускайте curl ... | bash внутри скрипта.
- Прогоните файл через shellcheck: статический анализ ловит незакавыченные переменные, неверные проверки и опасные конструкции.
Валидация нужна любому скрипту, который запускают вручную, а не только cron-задаче. Аргумент из терминала ничем не надёжнее строки из файла.
Бэкапы и данные внутри скриптов: что архивировать, а что нет
Скрипт бэкапа - такой же скрипт администрирования, и к нему применимы те же правила по секретам, правам и аудиту. Отдельный вопрос - содержимое архива: чем больше данных едет в копию, тем дольше восстановление и тем больше поверхность утечки при компрометации.
Что включать в архив, а что исключать
- Включать: дампы баз через mysqldump с флагом --single-transaction, файлы сайта, конфиги nginx, systemd и сертификаты.
- Исключать: образы Docker, кэши, временные файлы, node_modules и всё, что восстанавливается из lock-файла.
tar --exclude='/var/lib/docker' \
--exclude='*/node_modules' \
--exclude='*/cache' \
-czf "/var/backups/site-$(date +%F).tar.gz" \
/var/www /etc/nginx /etc/systemd/system /etc/letsencrypt
В гайде по бэкапам VPS отдельно отмечено: не стоит бэкапить то, что скачивается заново, в том числе образы Docker, это раздувает архивы в разы без пользы. Исключение лишнего уменьшает и поверхность атаки: при компрометации копии утекает меньше данных.
Внешняя копия и снапшоты: как не остаться без данных
- Держите копию вне основной площадки: отдельный сервер, объектное хранилище, холодное хранилище. Если сервер потерян целиком или к нему получил доступ посторонний, локальные копии не помогут.
- Снапшоты применяйте как страховку перед обновлением системы, миграцией базы или установкой нового движка: сделали снимок, провели работы, убедились, что всё живо.
- Как основной способ бэкапа снапшоты не годятся: они живут на той же площадке и часто под той же учётной записью, что и сервер.
- Раз в месяц собирайте тестовый стенд и восстанавливайте копию целиком, а не отдельный файл.
Готовые сценарии с проверкой восстановления, интеграцией с Prometheus и Alertmanager собраны в материале по автоматизации резервного копирования.
Чек-лист проверки скрипта перед запуском в прод
- Секреты вне кода. Пароли, ключи и токены лежат в /opt/backup.env или менеджере секретов, литеральных значений в скрипте нет. Права на файл 600, владелец - пользователь запуска, файл вне git и проверен по истории коммитов.
- Права минимальны. Скрипт запускается от непривилегированного пользователя, вывод sudo -l просмотрен, wildcard и NOPASSWD убраны или обоснованы. Для systemd заданы User, NoNewPrivileges, ProtectSystem, ReadWritePaths.
- Каркас на месте. В начале файла set -euo pipefail и IFS, переменные в кавычках, аргументы сверены со списком допустимых, eval и динамически собранные команды отсутствуют.
- Аудит настроен. cron или systemd timer пишут stdout и stderr в лог, лог закрыт правами 640 и ротируется, heartbeat в healthchecks.io подключён.
- Анализ и dry-run пройдены. shellcheck без критичных замечаний, bash -n проходит, сценарий прогнан на тестовом стенде.
- Страховка сделана. Снапшот или свежий бэкап есть до обновления системы, миграции базы или установки нового движка.
- Внешняя копия существует. Хотя бы одна копия лежит вне основной площадки, восстановление из неё проверялось на тестовом стенде.
Прогоняйте список перед каждым изменением скрипта, а не только перед первым запуском. Правка в одну строку ломает продакшен ровно так же, как новый сценарий.
Быстрая проверка за 5 минут
ls -l /opt/backup.env sudo -l -U backup shellcheck /opt/backup.sh bash -n /opt/backup.sh tail -n 50 /var/log/backup.log
- ls -l показывает права на файл секретов: ожидаем -rw------- и владельца запуска.
- sudo -l -U backup показывает, что пользователю реально разрешено.
- shellcheck выполняет статический анализ без запуска скрипта.
- bash -n проверяет синтаксис, не выполняя команды.
- tail по логу показывает последние ошибки и время успешного запуска.
Что делать, если секрет уже утёк: короткий план действий
- Отзовите секрет: смените пароль базы, ротируйте ключ или токен. Ротация важнее удаления файла: если секрет был доступен кому-то постороннему, его считают скомпрометированным.
- Проверьте историю git: git log -S '<секрет>' --all --oneline покажет коммиты со строкой. Переписывание истории через git filter-repo убирает секрет из текущих ссылок, но копии в форках, кэшах CI и локальных клонах остаются.
- Изучите логи доступа сервиса, к которому вёл утёкший секрет: время входов, IP-адреса, необычные запросы. Смена пароля без проверки логов оставляет вопрос открытым.
- Проверьте уведомления: healthchecks.io и другие алерты могли сообщать о пропущенном запуске, который вы не заметили.
- Ограничьте доступ и зафиксируйте инцидент: лишние выданные права отзовите, файл с секретом уберите с хостов, дату и факт запишите в журнал изменений.
Регулярный просмотр логов бэкапа и прав на файлы с секретами - минимальный ритм, который помогает заметить утечку раньше, чем она превратится в инцидент.