Безопасная автоматизация для системных администраторов - MSB support

Рубрики

Безопасная автоматизация для системных администраторов


Пишем надежные скрипты для VPN-серверов

Аудитория: Системные администраторы и DevOps-инженеры, которые начинают писать свои скрипты для автоматизации управления серверами.


Введение: От «Готово» до «А работает ли?»

Автоматизация — это сила. Но без надежности она становится ловушкой.

Представьте: вы запускаете скрипт, он выводит «ГОТОВО», но в этот момент ваш VPN-сервер перестает отвечать. Такие ситуации возникают, когда код работает «в идеале», но не учитывает реальную жизнь — разные версии ПО, нестандартные конфигурации и человеческий фактор.

В этой статье мы разберем, как написать скрипт, который будет работать не только на вашей машине, но и в любой другой среде. Мы будем строить идеальный скрипт пошагово, объясняя, почему каждый шаг важен.


Ключевые принципы надежности

Любой качественный скрипт должен быть построен на четырех китах: строгой изоляции, обнаружения (discovery), безопасности и грамотной обработки ошибок.

Принцип 1: Строгая изоляция и защита (set -euo pipefail)

Первое, что должно быть в любом серьезном скрипте — это строка set -euo pipefail. Она делает три важных вещи:

  1. -e (errexit): Скрипт немедленно останавливается, если любая команда возвращает ненулевой код (ошибку). Это предотвращает «катастрофическое падение», когда одна ошибка ведет за собой десятки других.
  2. -u (nounset): Скрипт выдаст ошибку, если вы попытаетесь использовать необъявленную переменную. Это ловит опечатки в именах ($USER_NMAE вместо $USER_NAME).
  3. -o pipefail: Позволяет отлавливать ошибки в цепочках команд (конвейерах). Если команда в середине cmd1 | cmd2 | cmd3 упадет, скрипт это заметит.

Почему это важно: Без этой строки скрипт будет пытаться продолжить работу даже после того, как команда mv или systemctl restart провалилась, создавая катастрофическую ситуацию.


Принцип 2: Обнаружение (Service Discovery), а не угадывание

Жестко прописывать имя сервиса (systemctl restart hysteria2.service) — это самое слабое место. Реальная система может называть его hysteria@80.service или просто hysteria.

Решение — динамическое обнаружение.

Вот пример идеальной функции:

safe_restart() {
    local service_name="$1"
    # Ищем точное имя сервиса, игнорируя возможные различия
    local actual_service
    actual_service=$(systemctl list-units --type=service --all 2>/dev/null | grep -oP "${service_name}(@\w+)?\.service" | head -n 1 || echo "")

    if [[ -n "$actual_service" ]]; then
        log_info "Перезапуск $actual_service..."
        if systemctl restart "$actual_service"; then
            log_success "$actual_service запущен."
        else
            log_error "Не удалось запустить $actual_service! Проверьте: journalctl -u $actual_service"
        fi
    else
        log_info "Сервис $service_name не найден, пропускаю."
    fi
}

Почему это важно: Это делает скрипт адаптивным к любой среде. Он не «угадывает», он «находит». Вы можете запустить этот скрипт на сервере с любыми названиями сервисов, и он сработает.


Принцип 3: Безопасность — Бэкапы, бэкапы и еще раз бэкапы

Изменение системных файлов — операция рискованная. Решение? Обязательное резервное копирование перед каждой операцией.

create_backup() {
    local file="$1"
    if [[ -f "$file" ]]; then
        # Используем формат времени YYYYMMDD_HHMMSS для уникальности
        local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
        cp "$file" "$backup"
        log_info "Бэкап создан: $backup"
    fi
}

Почему это важно: Бэкап — это ваш «undo» (отмена) в системе. Если jq ошибется и испортит JSON, вы сможете мгновенно вернуть всё как было за долю секунды.


Принцип 4: Обработка ошибок и пользовательский ввод

Ключевая ошибка новичков — полагаться на «ввод» пользователя.

  • Проблема: Если скрипт вызывается не в интерактивном режиме (например, через cron), команда read приведет к зависанию сервера.
  • Решение: Использовать параметр по умолчанию и проверку.
USER_NAME="${1:-}" # Используем аргумент или пустую строку
if [[ -z "$USER_NAME" ]]; then
    read -p "Введите имя нового пользователя: " USER_NAME
fi
# Проверяем, что пользователь ввел имя
[[ -z "$USER_NAME" ]] && log_error "Имя пользователя обязательно."

Также любой внешний вызов (как curl для определения IP) должен быть ограничен по времени: curl -s --max-time 5 ...


Идеальный скрипт (минимальный рабочий фрагмент)

Вот как выглядит его основа:

#!/bin/bash

set -euo pipefail

GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

log_info()    { echo -e "${YELLOW}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC}   $1"; }
log_error()   { echo -e "${RED}[ERR]${NC}  $1" >&2; exit 1; }

create_backup() {
    local file="$1"
    if [[ -f "$file" ]]; then
        local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
        cp "$file" "$backup"
        log_info "Бэкап создан: $backup"
    fi
}

safe_restart() {
    local service_name="$1"
    local actual_service
    actual_service=$(systemctl list-units --type=service --all 2>/dev/null | grep -oP "${service_name}(@\w+)?\.service" | head -n 1 || echo "")

    if [[ -n "$actual_service" ]]; then
        log_info "Перезапуск $actual_service..."
        if systemctl restart "$actual_service"; then
            log_success "$actual_service запущен."
        else
            log_error "Не удалось запустить $actual_service! Проверьте: journalctl -u $actual_service"
        fi
    else
        log_info "Сервис $service_name не найден, пропускаю."
    fi
}

# --- Инициализация ---
USER_NAME="${1:-}"
if [[ -z "$USER_NAME" ]]; then
    read -p "Введите имя нового пользователя: " USER_NAME
fi
[[ -z "$USER_NAME" ]] && log_error "Имя пользователя обязательно."

Заключение

Следуя этим простым, но мощным принципам, вы превращаете «хакерский» скрипт в надежный инструмент, которым можно доверять. Это основа профессиональной автоматизации.