Выполнение с минимальными привилегиями и дисциплина sudo
Снижайте привилегии, жёстко ограничивайте правила sudo и проверяйте эффективный UID перед рискованными операциями
«Выполнение с минимальными привилегиями и дисциплина sudo» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.
Зачем нужны минимальные привилегии в скриптах оболочки
Большинство нарушений безопасности в автоматизации происходит не из-за экзотических эксплойтов, а потому, что скрипты работают с большими привилегиями, чем им необходимо. Задание cron, запущенное от root и которому нужно всего лишь повернуть файл журнала, — это почти гарантированная предпосылка для инцидента.
Принцип минимальных привилегий гласит: каждый процесс должен использовать только права, необходимые для выполнения его задачи, и не более. В скриптах Bash это означает:
- по возможности работать от имени пользователя без привилегий
- повышать привилегии до root только для конкретных команд, которым это необходимо
- сбрасывать привилегии сразу после завершения привилегированной работы
- никогда не хранить и не наследовать учётные данные за пределами их области действия
В этом уроке рассматриваются конкретные приёмы: ограничение области действия sudo, сброс привилегий через su, проверки UID и усиление настроек sudoers — формирование дисциплинированной модели привилегий для рабочих скриптов.
Проверка эффективного UID перед рискованными операциями
Перед любым блоком кода, которому действительно нужен root, скрипт должен проверить, что он запущен с ожидаемым эффективным UID. Никогда не предполагайте — всегда проверяйте явно.
$EUID — специальная переменная Bash, содержащая идентификатор эффективного пользователя текущего процесса. Для root значение EUID всегда равно 0. Проверка в начале скрипта или вокруг привилегированного блока предотвращает случайное выполнение от имени неправильного пользователя.
Используйте следующий шаблон проверки:
#!/usr/bin/env bash
set -euo pipefail
# Guard: this script must NOT run as root.
if [[ "$EUID" -eq 0 ]]; then
echo "ERROR: Do not run this script as root. Use a normal user account." >&2
exit 1
fi
echo "Running as UID $EUID — proceeding safely."Проверка наличия root только при необходимости
Некоторым скриптам действительно нужен root. В таком случае проверка должна работать наоборот: завершайте выполнение сразу, если root отсутствует, вместо того чтобы позволять скрипту дойти до привилегированного системного вызова и выдать непонятную ошибку доступа в середине работы.
Сочетание раннего завершения с понятным сообщением об использовании делает скрипты самодокументируемыми:
#!/usr/bin/env bash
set -euo pipefail
require_root() {
if [[ "$EUID" -ne 0 ]]; then
echo "ERROR: $(basename "$0") must be run as root." >&2
echo " Try: sudo $(basename "$0") $*" >&2
exit 1
fi
}
require_root "$@"
echo "Root confirmed (EUID=0). Starting privileged work..."Ограничение sudo отдельными командами
Самая распространённая ошибка — поместить sudo в начало скрипта, а затем выполнять всё от имени root. Вместо этого применяйте sudo только к конкретной команде, которой оно необходимо, — всё остальное должно выполняться от имени обычного пользователя.
Это ограничивает масштаб возможного ущерба: если злоумышленник внедрит код в ваш скрипт, с правами root он сможет выполнить только те части, перед которыми указано sudo.
Сравните два приведённых ниже варианта. Второй значительно безопаснее:
#!/usr/bin/env bash
set -euo pipefail
# BAD: escalate early, do everything as root (avoid this)
# sudo bash -c '
# cp config.conf /etc/app/config.conf
# chown app:app /etc/app/config.conf
# systemctl restart app
# '
# GOOD: escalate only for the commands that require it
LOCAL_CONF="./config.conf"
DEST="/etc/app/config.conf"
# Unprivileged: validate the config before touching anything as root
if ! grep -q '^[[:space:]]*\[main\]' "$LOCAL_CONF"; then
echo "ERROR: config.conf is missing [main] section" >&2
exit 1
fi
# Privileged: only these three commands run under sudo
sudo cp "$LOCAL_CONF" "$DEST"
sudo chown app:app "$DEST"
sudo systemctl restart app
echo "Config deployed and service restarted."Составление строгих правил sudoers
Вызов sudo somecommand в скрипте безопасен только в том случае, если файл sudoers настроен на разрешение именно этой команды — и ничего больше. Избегайте правил вроде ALL=(ALL) NOPASSWD: ALL для служебных учётных записей.
Вместо этого ограничивайте правила конкретными командами с конкретными аргументами, используя безопасный для visudo синтаксис. Ключевые поля правила sudoers:
- Пользователь — кто может вызывать sudo
- Узел — на каком компьютере (для переносимости используйте
ALL) - RunAs — какую учётную запись следует принять (почти всегда
root) - Команда — полный абсолютный путь, при необходимости с буквальными аргументами
Примеры строгих правил для служебной учётной записи развёртывания (deployer):
# /etc/sudoers.d/deployer (edit with: sudo visudo -f /etc/sudoers.d/deployer)
#
# Allow 'deployer' to restart exactly one service — nothing else
deployer ALL=(root) NOPASSWD: /usr/bin/systemctl restart app
# Allow copying a config file to a fixed destination only
deployer ALL=(root) NOPASSWD: /usr/bin/cp /home/deployer/staging/config.conf /etc/app/config.conf
# Allow chown of that specific file only
deployer ALL=(root) NOPASSWD: /usr/bin/chown app\:app /etc/app/config.conf
# NEVER do this — gives full root shell:
# deployer ALL=(ALL) NOPASSWD: ALLСброс привилегий с помощью su и runuser
Если скрипт запускается от имени root (например, системой инициализации или заданием cron от root), но основная работа должна выполняться пользователем без привилегий, явно сбрасывайте привилегии, а не запускайте весь скрипт от имени root.
Для этого есть два инструмента:
su -s /bin/bash -c 'command' username— запускает оболочку от имени пользователя username и выполняет командуrunuser -u username -- command args— предпочтительный в Linux способ переключения пользователя внутри скриптов, запущенных от root; он удобнее, чемsu
В приведённом ниже примере оболочка развёртывания, принадлежащая root, переключается на пользователя app для выполнения фактической логики приложения:
#!/usr/bin/env bash
# This script is called by systemd as root during pre-deployment
set -euo pipefail
APP_USER="app"
DEPLOY_DIR="/opt/myapp"
# Step 1: privileged — fix ownership of deploy directory
chown -R "${APP_USER}:${APP_USER}" "$DEPLOY_DIR"
# Step 2: drop to app user for the actual migration/startup logic
# runuser is available on most modern Linux systems
runuser -u "$APP_USER" -- bash -c "
cd $DEPLOY_DIR
./bin/migrate.sh
./bin/start.sh
"
echo "Deploy complete. Privileged wrapper exiting."Использование sudo -u для запуска отдельной команды от имени другого пользователя
Не всегда нужно переходить к полноценному сеансу оболочки. sudo -u username command выполняет одну команду от имени указанного пользователя, а затем возвращается к исходной учётной записи. Это удобно для работы с файлами, принадлежащими служебной учётной записи, без предоставления этой учётной записи интерактивного доступа.
Сочетайте это с правилом sudoers, разрешающим именно такую комбинацию пользователя и команды:
#!/usr/bin/env bash
set -euo pipefail
# Scenario: deploy script runs as 'deployer'; DB migrations must run as 'postgres'
# sudoers entry needed:
# deployer ALL=(postgres) NOPASSWD: /opt/app/bin/run_migrations.sh
DB_MIGRATION_SCRIPT="/opt/app/bin/run_migrations.sh"
if [[ ! -x "$DB_MIGRATION_SCRIPT" ]]; then
echo "ERROR: migration script not found or not executable: $DB_MIGRATION_SCRIPT" >&2
exit 1
fi
echo "Running DB migrations as postgres user..."
sudo -u postgres "$DB_MIGRATION_SCRIPT"
echo "Migrations done. Returning to deployer context (EUID=$EUID)."Предотвращение повышения привилегий через переменные окружения
Одна из малозаметных поверхностей атаки — переменные окружения, унаследованные сеансом sudo. По умолчанию sudo сбрасывает окружение, но неправильно настроенные параметры env_keep или env_reset могут передать в привилегированные команды переменные, контролируемые злоумышленником, например LD_PRELOAD, PATH или PYTHONPATH.
Рекомендации:
- Всегда используйте абсолютные пути в скриптах, запускаемых через
sudo, — никогда не полагайтесь на$PATH - Передавайте только явно необходимые переменные:
sudo env VAR=value /path/to/cmd - В sudoers избегайте записей
env_keep += PATHилиenv_keep += LD_* - Используйте
sudo -Eтолько при полном контроле над вызывающим окружением и доверии к нему
#!/usr/bin/env bash
set -euo pipefail
# BAD: relies on $PATH — attacker who controls PATH can hijack 'cp'
# sudo cp config.conf /etc/app/
# GOOD: absolute paths for every command called under elevated context
SUDO_BIN="/usr/bin/sudo"
CP_BIN="/usr/bin/cp"
CHOWN_BIN="/usr/bin/chown"
SYSTEMCTL_BIN="/usr/bin/systemctl"
"$SUDO_BIN" "$CP_BIN" ./config.conf /etc/app/config.conf
"$SUDO_BIN" "$CHOWN_BIN" app:app /etc/app/config.conf
"$SUDO_BIN" "$SYSTEMCTL_BIN" restart app
echo "Deployed with hardened absolute-path invocations."Защита sudo с помощью проверки аргументов команды
Даже если правило sudoers разрешает конкретный скрипт, ему можно передать произвольные аргументы, если правило не ограничивает и их. Распространённая ошибка:
deployer ALL=(root) NOPASSWD: /opt/scripts/manage.shЭто разрешает sudo /opt/scripts/manage.sh restart, но также и sudo /opt/scripts/manage.sh --arbitrary-flag. Если manage.sh без проверки передаёт аргументы привилегированным дочерним командам, возникает проблема.
Используйте эшелонированную защиту: проверяйте аргументы внутри привилегированного скрипта, а также в sudoers:
#!/usr/bin/env bash
# /opt/scripts/manage.sh — called via sudo; must validate its own args
set -euo pipefail
# Allowlist of valid actions
declare -A ALLOWED_ACTIONS=(
[restart]=1
[status]=1
[reload]=1
)
ACTION="${1:-}"
if [[ -z "$ACTION" ]]; then
echo "Usage: $(basename "$0") <restart|status|reload>" >&2
exit 1
fi
if [[ -z "${ALLOWED_ACTIONS[$ACTION]:-}" ]]; then
echo "ERROR: Unknown action '${ACTION}'. Allowed: ${!ALLOWED_ACTIONS[*]}" >&2
exit 2
fi
/usr/bin/systemctl "$ACTION" app
echo "Action '$ACTION' executed successfully."Временное повышение привилегий с очисткой через ловушку
Если скрипту нужно ненадолго получить доступ к привилегированному файлу, учётным данным или ресурсу, используйте trap в Bash, чтобы очистка выполнялась даже при ошибке или сигнале. Это предотвращает утечку привилегий — например, появление временного двоичного файла с setuid или сокета, принадлежащего root, если скрипт завершится аварийно.
Приведённый ниже шаблон создаёт временный файл от имени root, использует его, а затем удаляет — это гарантируется ловушкой для EXIT:
#!/usr/bin/env bash
set -euo pipefail
# Must run as root for this demo
if [[ "$EUID" -ne 0 ]]; then
echo "Run as root" >&2; exit 1
fi
TMP_SECRET=""
cleanup() {
local exit_code=$?
if [[ -n "$TMP_SECRET" && -f "$TMP_SECRET" ]]; then
# Overwrite before deletion to reduce forensic recovery risk
shred -u "$TMP_SECRET" 2>/dev/null || rm -f "$TMP_SECRET"
echo "[cleanup] Removed privileged temp file." >&2
fi
exit "$exit_code"
}
trap cleanup EXIT INT TERM
# Create a root-owned temp file for a short-lived secret
TMP_SECRET="$(mktemp /tmp/deploy_secret.XXXXXXXX)"
chmod 600 "$TMP_SECRET"
# Simulate fetching a secret into the temp file
echo "super-secret-token" > "$TMP_SECRET"
# Use the secret (e.g., pass to a sub-command via file descriptor)
/usr/bin/some-privileged-tool --key-file "$TMP_SECRET"
echo "Privileged operation complete."Аудит и журналирование привилегированных действий
Принцип минимальных привилегий проще соблюдать, если каждое событие повышения привилегий журналируется с контекстом: кто, что, когда и зачем выполнил. Объедините два уровня:
- сам sudo —
/var/log/auth.log(Debian/Ubuntu) или/var/log/secure(RHEL) автоматически фиксирует каждый вызов sudo - журнал аудита на уровне скрипта — записывайте структурированную запись в начале каждой привилегированной функции, чтобы намерение фиксировалось вместе с системным журналом
Использование простой функции структурированного журналирования помогает поддерживать единообразие аудиторских следов и делает их удобными для поиска с помощью grep:
#!/usr/bin/env bash
set -euo pipefail
AUDIT_LOG="/var/log/app_deploy_audit.log"
log_privileged_action() {
local action="$1"
local reason="${2:-unspecified}"
local ts
ts="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf '{"ts":"%s","user":"%s","euid":%d,"action":"%s","reason":"%s"}\n' \
"$ts" "${SUDO_USER:-$USER}" "$EUID" "$action" "$reason" \
| sudo tee -a "$AUDIT_LOG" > /dev/null
}
# Each privileged step is logged before execution
log_privileged_action "cp_config" "deploy release v2.4.1"
sudo /usr/bin/cp ./config.conf /etc/app/config.conf
log_privileged_action "chown_config" "ensure app user owns config"
sudo /usr/bin/chown app:app /etc/app/config.conf
log_privileged_action "restart_service" "activate new config"
sudo /usr/bin/systemctl restart app
echo "Deployment complete. Audit entries written to $AUDIT_LOG"Проверка знаний: область действия sudo
Проверьте, насколько хорошо Вы понимаете выполнение скриптов Bash с минимальными привилегиями.
Итоги: выполнение с минимальными привилегиями и дисциплина использования sudo
В этом уроке Вы освоили методы запуска скриптов Bash с минимально необходимыми привилегиями на каждом шаге. Ниже приведено краткое резюме основных принципов:
- Проверяйте с помощью
$EUID— немедленно завершайте работу, если скрипт выполняется не от нужного имени, независимо от того, требуется ли запретить запуск от root или, наоборот, обеспечить его - Ограничивайте область действия
sudoотдельными командами — никогда не повышайте привилегии для всего скрипта; применяйтеsudoтолько к строкам, которым это действительно необходимо - Пишите строгие правила sudoers — указывайте полные абсолютные пути и буквальные аргументы; избегайте подстановочных знаков и
ALL - Снижайте привилегии с помощью
runuserилиsudo -u— если запущенному от root скрипту нужно передать выполнение непривилегированному пользователю, используйте подходящий инструмент, а не запускайте всё от root - Используйте абсолютные пути — никогда не полагайтесь на
$PATHв привилегированном коде; задавайте пути к двоичным файлам явно, чтобы предотвратить перехват - Проверяйте аргументы внутри привилегированных скриптов — правила sudoers являются первой, но не единственной линией защиты; используйте списки разрешённых значений
- Перехватывайте сигналы и выполняйте очистку — используйте
trap cleanup EXIT, чтобы гарантировать удаление временных привилегированных ресурсов даже при ошибке - Журналируйте каждое повышение привилегий — структурированные записи аудита в сочетании со встроенным системным журналом sudo обеспечивают отслеживание каждого привилегированного действия
При последовательном применении эти методы уменьшают поверхность атаки автоматизации: от режима root всегда до режима root только там, где это доказуемо необходимо — отличительного признака защищённого Bash производственного уровня.
Часто задаваемые вопросы
Урок «Выполнение с минимальными привилегиями и дисциплина sudo» бесплатный?
Да — полный текст урока «Выполнение с минимальными привилегиями и дисциплина sudo» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.
Чему я научусь в уроке «Выполнение с минимальными привилегиями и дисциплина sudo»?
Снижайте привилегии, жёстко ограничивайте правила sudo и проверяйте эффективный UID перед рискованными операциями Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DevOps Bootcamp?
Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Выполнение с минимальными привилегиями и дисциплина sudo»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DevOps Bootcamp?
Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Предотвращение внедрения команд и аргументов
- Безопасная работа с секретами и чистота окружения
- Выполнение с минимальными привилегиями и дисциплина sudo
- Статический анализ и аудит с ShellCheck