Безопасность скриптов администрирования в 2026 году: права, секреты и аудит | AdminWiki

Безопасность скриптов администрирования в 2026 году: права, секреты и аудит

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

Почему безопасность скриптов администрирования сводится к трём вещам: секреты, права и аудит

Безопасность скриптов администрирования в 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.

  1. Создайте файл и сразу закройте права:
    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. Владелец - тот пользователь, от которого запускается скрипт.
  2. Загрузите переменные в начале скрипта:
    set -a
    source /opt/backup.env
    set +a
    
    Флаг -a (allexport) экспортирует все переменные, назначенные после его включения, в окружение дочерних процессов. Без него DB_PASSWORD останется локальной переменной shell и не дойдёт до mysqldump. Команда set +a выключает режим.
  3. Используйте переменную вместо литерального пароля:
    mysqldump -u backup -p"$DB_PASSWORD" --single-transaction --all-databases \
      | gzip > "/var/backups/all-$(date +%F).sql.gz"
    
    Пароль базы держим не в скрипте, а в отдельном файле, и подгружаем его через set -a && source - этот рецепт разобран в нашем гайде по автоматическим бэкапам VPS.
  4. Для cron передайте файл прямо в командной строке задачи:
    30 4 * * * . /opt/backup.env && /opt/backup.sh >> /var/log/backup.log 2>&1
    
    Точка в начале - это команда source в shell cron. Она загружает переменные в окружение задачи, после чего запускается скрипт.
  5. Не коммитьте файл с секретами. Добавьте в .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.envsource файла секретов в окружение задачи
/opt/backup.shсам скрипт
>> /var/log/backup.logstdout дописывается в лог
2>&1stderr идёт туда же, ошибки не теряются

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

Чек-лист проверки скрипта перед запуском в прод

  1. Секреты вне кода. Пароли, ключи и токены лежат в /opt/backup.env или менеджере секретов, литеральных значений в скрипте нет. Права на файл 600, владелец - пользователь запуска, файл вне git и проверен по истории коммитов.
  2. Права минимальны. Скрипт запускается от непривилегированного пользователя, вывод sudo -l просмотрен, wildcard и NOPASSWD убраны или обоснованы. Для systemd заданы User, NoNewPrivileges, ProtectSystem, ReadWritePaths.
  3. Каркас на месте. В начале файла set -euo pipefail и IFS, переменные в кавычках, аргументы сверены со списком допустимых, eval и динамически собранные команды отсутствуют.
  4. Аудит настроен. cron или systemd timer пишут stdout и stderr в лог, лог закрыт правами 640 и ротируется, heartbeat в healthchecks.io подключён.
  5. Анализ и dry-run пройдены. shellcheck без критичных замечаний, bash -n проходит, сценарий прогнан на тестовом стенде.
  6. Страховка сделана. Снапшот или свежий бэкап есть до обновления системы, миграции базы или установки нового движка.
  7. Внешняя копия существует. Хотя бы одна копия лежит вне основной площадки, восстановление из неё проверялось на тестовом стенде.

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

Быстрая проверка за 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 по логу показывает последние ошибки и время успешного запуска.

Что делать, если секрет уже утёк: короткий план действий

  1. Отзовите секрет: смените пароль базы, ротируйте ключ или токен. Ротация важнее удаления файла: если секрет был доступен кому-то постороннему, его считают скомпрометированным.
  2. Проверьте историю git: git log -S '<секрет>' --all --oneline покажет коммиты со строкой. Переписывание истории через git filter-repo убирает секрет из текущих ссылок, но копии в форках, кэшах CI и локальных клонах остаются.
  3. Изучите логи доступа сервиса, к которому вёл утёкший секрет: время входов, IP-адреса, необычные запросы. Смена пароля без проверки логов оставляет вопрос открытым.
  4. Проверьте уведомления: healthchecks.io и другие алерты могли сообщать о пропущенном запуске, который вы не заметили.
  5. Ограничьте доступ и зафиксируйте инцидент: лишние выданные права отзовите, файл с секретом уберите с хостов, дату и факт запишите в журнал изменений.

Регулярный просмотр логов бэкапа и прав на файлы с секретами - минимальный ритм, который помогает заметить утечку раньше, чем она превратится в инцидент.

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