0Pricing
DevOps Bootcamp · Урок

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

Аудит и журналирование привилегированных действий

Принцип минимальных привилегий проще соблюдать, если каждое событие повышения привилегий журналируется с контекстом: кто, что, когда и зачем выполнил. Объедините два уровня:

  1. сам sudo — /var/log/auth.log (Debian/Ubuntu) или /var/log/secure (RHEL) автоматически фиксирует каждый вызов sudo
  2. журнал аудита на уровне скрипта — записывайте структурированную запись в начале каждой привилегированной функции, чтобы намерение фиксировалось вместе с системным журналом

Использование простой функции структурированного журналирования помогает поддерживать единообразие аудиторских следов и делает их удобными для поиска с помощью 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 — локальная установка не требуется.

Все уроки этого курса

  1. Предотвращение внедрения команд и аргументов
  2. Безопасная работа с секретами и чистота окружения
  3. Выполнение с минимальными привилегиями и дисциплина sudo
  4. Статический анализ и аудит с ShellCheck
← Назад к DevOps Bootcamp