issayev.kz
RU EN

Скрипты и ПО

bash4
bashфайлов: 16
Пошаговое развёртывание: Samba AD + Postfix/Dovecot/LDAP + PMG + EAS на двух площадках
Домен AD: ad.example.com (NetBIOS EXAMPLE) · Почта: example.com ОС всех узлов: Debian 12 · Топология: Active-Active, по узлу каждой роли на площадку Периметр: PMG1/PMG2 (релей + DKIM/DMARC) · EAS: grommunio-sync · Бэкенд почты: Postfix+Dovecot, LDAP-аутентификация в AD GUI (всё FOSS): LAM Community (каталог) · Roundcube (веб-почта) · PMG web UI (антиспам/очереди)
Показать код
#!/usr/bin/env bash
# 00-base.sh — базовая подготовка узла (все 6 узлов: dc1,dc2,mx1,mx2,pmg1,pmg2)
# Идемпотентен. Запуск: sudo FQDN=dc1.ad.example.com IP=10.10.10.10 ./00-base.sh
#   опц.: TZ_NAME=UTC

source "$(dirname "$0")/lib.sh"
require_root
require_env FQDN
require_env IP
TZ_NAME="${TZ_NAME:-UTC}"

valid_fqdn "$FQDN" || die "FQDN некорректен: $FQDN"
valid_ipv4 "$IP"   || die "IP некорректен: $IP"

info "Узел: ${FQDN} (${IP}), TZ=${TZ_NAME}"

# ── время ──
timedatectl set-timezone "$TZ_NAME"
hostnamectl set-hostname "$FQDN"

export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get -y -qq dist-upgrade
apt-get -y -qq install chrony curl gnupg ca-certificates dnsutils \
    rsync vim less acl attr ufw net-tools

systemctl enable --now chrony
chronyc -a makestep >/dev/null 2>&1 || warn "chronyc makestep отложен (нет источника?)"

# ── /etc/hosts: FQDN → статический IP, без 127.0.1.1 ──
SHORT="${FQDN%%.*}"
backup_once /etc/hosts
sed -i '/127\.0\.1\.1/d' /etc/hosts
ensure_line "${IP}  ${FQDN} ${SHORT}" /etc/hosts

ok "Базовая подготовка ${FQDN} завершена"
Файлы комплекта10-dc1-provision.sh bash20-dc2-join.sh bash30-directory-objects.sh bash40-mail-install.sh bash50-eas-install.sh bash60-pmg-relay.sh bash70-firewall-mx.sh bash80-lam-install.sh bash85-sysvol-sync.sh bash90-roundcube-install.sh bash95-domain-rebuild.sh bashlib.sh bashautodiscover.php phpexample.com.zoneconfig-v1.1.xml
bashфайлов: 14
Proxmox VE Cluster Autoconfig
Комплект идемпотентных bash-скриптов для автоматической сборки production-кластера Proxmox VE 8/9 с выбором модели хранилища (Ceph / FC-hybrid) и программно-определяемой сетью (SDN), построенной по образцу VMware vSphere Distributed Switch (vDS).
Показать код
#!/usr/bin/env bash
# deploy-all.sh — оркестратор полного развёртывания. Выполняет этапы в
# правильном порядке с учётом роли узла и DEPLOY_MODE. Идемпотентен —
# безопасно перезапускать. Поддерживает выборочный запуск (--only) и
# продолжение с этапа (--from).
#
# Порядок фаз:
#   phase-local  (на КАЖДОМ узле): 00-preflight, 10-network, 40-fc(multipath)
#   phase-init   (на ИНИЦИАТОРЕ):  20-cluster
#   [join]       (на остальных):   20-cluster
#   phase-cluster(на ИНИЦИАТОРЕ):  30-ceph, 40-fc(vg/storage), 50-sdn, 60-ha
#
# Рекомендуемый сценарий:
#   1) на каждом узле:      ./deploy-all.sh --phase local
#   2) на инициаторе:       ./deploy-all.sh --phase init
#   3) на остальных узлах:  ./deploy-all.sh --phase join
#   4) на инициаторе:       ./deploy-all.sh --phase cluster
# Либо всё сразу на инициаторе с настроенным SSH: ./deploy-all.sh --phase all

set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../lib/common.sh"

CONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/../config/cluster.conf}"
PHASE="all"

usage() {
    cat <<EOF
Использование: $0 [--config FILE] [--phase local|init|join|cluster|all]
                  [--dry-run] [--yes]

  --config FILE   путь к cluster.conf (по умолчанию config/cluster.conf)
  --phase         какую фазу выполнить (см. заголовок скрипта)
  --dry-run       только печатать команды (экспорт DRY_RUN=1)
  --yes           не запрашивать подтверждений (экспорт ASSUME_YES=1)
EOF
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --config) CONFIG_FILE="$2"; shift 2 ;;
        --phase)  PHASE="$2"; shift 2 ;;
        --dry-run) export DRY_RUN=1; shift ;;
        --yes)     export ASSUME_YES=1; shift ;;
        -h|--help) usage; exit 0 ;;
        *) die "Неизвестный аргумент: $1" ;;
    esac
done

require_root
load_config "$CONFIG_FILE"
export CONFIG_FILE

run_step() {
    local script="$1"
    log "═══ Этап: ${script} ═══"
    bash "${SCRIPT_DIR}/${script}" "$CONFIG_FILE"
    ok "═══ Завершён: ${script} ═══"
}

phase_local() {
    run_step 00-preflight.sh
    run_step 10-network.sh
    # multipath-часть 40 запускается на каждом узле (VG/storage внутри скрипта
    # выполняется только на инициаторе — он сам это определяет).
    if [[ "$DEPLOY_MODE" == "fc" || "$DEPLOY_MODE" == "hybrid" ]]; then
        run_step 40-fc-storage.sh
    fi
}

phase_init()    { run_step 20-cluster.sh; }
phase_join()    { run_step 20-cluster.sh; }

phase_cluster() {
    [[ "$DEPLOY_MODE" == "ceph" || "$DEPLOY_MODE" == "hybrid" ]] && run_step 30-ceph.sh
    [[ "$DEPLOY_MODE" == "fc"   || "$DEPLOY_MODE" == "hybrid" ]] && run_step 40-fc-storage.sh
    run_step 50-sdn.sh
    run_step 60-ha.sh
}

case "$PHASE" in
    local)   phase_local ;;
    init)    phase_init ;;
    join)    phase_join ;;
    cluster) phase_cluster ;;
    all)
        warn "Фаза 'all' предполагает настроенный SSH и запуск с инициатора."
        phase_local
        phase_init
        phase_cluster
        warn "НЕ забудьте выполнить фазы 'local' и 'join' на остальных узлах!"
        ;;
    *) die "Неизвестная фаза: $PHASE" ;;
esac

ok "Оркестрация фазы '${PHASE}' завершена."
Файлы комплектаcommon.sh bash00-preflight.sh bash10-network.sh bash20-cluster.sh bash30-ceph.sh bash40-fc-storage.sh bash50-sdn.sh bash60-ha.sh bashadd-ha-vm.sh bashteardown.sh bashvalidate.sh bashsdn-model.reference.yaml yamlcluster.conf.example
bash
Angie Reverse Proxy для 1С (IIS) — руководство по развёртыванию
Автоматизированное развёртывание Angie в роли защищённого reverse proxy для публикации 1С на IIS наружу через HTTPS, с веб-мониторингом (Angie Console Light) и защитой от типовых веб-атак и L7-DDoS.
Показать код
#!/usr/bin/env bash
#===============================================================================
# deploy-angie-rp.sh  (v2 — публикация 1С/IIS + Console Light мониторинг)
#
#   Внешний вход:  TCP/443 (TLS 1.2/1.3), server_name = FQDN
#   Проксирование: на upstream IP:PORT публикации 1С в IIS (по умолчанию 5743)
#   Мониторинг:    Angie Console Light на ОТДЕЛЬНОМ внутреннем server-блоке
#                  (порт по умолчанию 8443, доступ только из доверенных сетей)
#   Защита:        TLS hardening, security headers, L7 rate/conn-limit,
#                  anti-slowloris, ACL, опционально ModSecurity+CRS.
#
# Специфика 1С/IIS (аутентификация анонимная/внутри 1С):
#   - WebSocket-апгрейд для тонкого клиента (map Connection)
#   - длинные таймауты (отчёты/проведение/обмен) -> 1800s
#   - большие тела (внешние обработки/обмен) -> client_max_body_size 512m
#   - раздельные rate-зоны: щедрая для веб-клиента, отдельная для веб-сервисов
#   - сохранение пути публикации (/base) и корректный proxy_redirect
#
# Поддерживаемые ОС: Debian 11/12, Ubuntu 22.04/24.04, RHEL/Rocky/Alma 8/9.
# Идемпотентность: повторный запуск безопасен (бэкап конфигов, перегенерация).
#===============================================================================

set -Eeuo pipefail

#------------------------------- Константы -------------------------------------
readonly SCRIPT_NAME="${0##*/}"
readonly LOG_FILE="/var/log/deploy-angie-rp.log"
readonly ANGIE_CONF_DIR="/etc/angie"
readonly SITE_CONF="${ANGIE_CONF_DIR}/http.d/reverse-proxy.conf"
readonly SEC_CONF="${ANGIE_CONF_DIR}/http.d/00-security.conf"
readonly CONSOLE_CONF="${ANGIE_CONF_DIR}/http.d/10-console.conf"
readonly TLS_DIR="${ANGIE_CONF_DIR}/tls"
readonly CONSOLE_ROOT="/usr/share/angie-console-light/html"
readonly BACKUP_DIR="${ANGIE_CONF_DIR}/backup-$(date +%Y%m%d-%H%M%S)"

ENABLE_MODSEC="${ENABLE_MODSEC:-ask}"
ENABLE_CONSOLE="${ENABLE_CONSOLE:-ask}"
UPDATE_SYSTEM="${UPDATE_SYSTEM:-ask}"     # yes|no|ask — обновить пакеты ОС перед установкой
MANAGE_BASE_REPOS="${MANAGE_BASE_REPOS:-yes}"  # yes|no — добавлять недостающие базовые репозитории ОС
RATE_RPS="${RATE_RPS:-50}"
RATE_BURST="${RATE_BURST:-100}"
CONN_PER_IP="${CONN_PER_IP:-50}"
CONSOLE_PORT="${CONSOLE_PORT:-8443}"
NONINTERACTIVE="${NONINTERACTIVE:-0}"

FQDN=""
UPSTREAM_IP=""
UPSTREAM_PORT=""
UPSTREAM_SCHEME="http"
ONEC_BASE=""
WS_PATHS="ws hs"                          # пути веб-сервисов 1С (через пробел): ws hs [api ...]
TRUSTED_NETS=""
CONSOLE_USER=""
CONSOLE_PASS=""

#------------------------------- Утилиты ---------------------------------------
log()  { echo -e "[$(date +'%F %T')] $*" | tee -a "$LOG_FILE" >&2; }
info() { log "\033[0;34m[INFO]\033[0m  $*"; }
ok()   { log "\033[0;32m[ OK ]\033[0m  $*"; }
warn() { log "\033[0;33m[WARN]\033[0m  $*"; }
err()  { log "\033[0;31m[FAIL]\033[0m  $*"; }
die() { err "$*"; exit 1; }

on_err() { local ec=$?; err "Ошибка на строке ${BASH_LINENO[0]} (код $ec). Смотрите $LOG_FILE"; exit "$ec"; }
trap on_err ERR

require_root() { [[ ${EUID} -eq 0 ]] || die "Запускать от root (sudo $SCRIPT_NAME)."; }

#------------------------------- Валидаторы ------------------------------------
is_valid_fqdn() {
  local f="$1"
  [[ ${#f} -le 253 ]] || return 1
  [[ "$f" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+([a-zA-Z]{2,63}|xn--[a-zA-Z0-9-]{2,59}[a-zA-Z0-9])$ ]]
}
is_valid_ipv4() {
  local ip="$1" o
  [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
  IFS='.' read -r -a oct <<< "$ip"
  for o in "${oct[@]}"; do
    (( o >= 0 && o <= 255 )) || return 1
    [[ "$o" == "0" || "$o" != 0* ]] || return 1
  done
  return 0
}
is_valid_port() { local p="$1"; [[ "$p" =~ ^[0-9]+$ ]] || return 1; (( p >= 1 && p <= 65535 )); }
is_valid_cidr() {
  local c="$1" ip mask
  if [[ "$c" == */* ]]; then
    ip="${c%/*}"; mask="${c#*/}"
    [[ "$mask" =~ ^[0-9]+$ ]] || return 1
    (( mask >= 0 && mask <= 32 )) || return 1
  else ip="$c"; fi
  is_valid_ipv4 "$ip"
}
is_valid_onec_base() {
  local b="$1"; [[ -z "$b" ]] && return 0
  [[ "$b" =~ ^(/[A-Za-z0-9._-]+)+$ ]]
}
# Список путей веб-сервисов через пробел: каждый сегмент из [A-Za-z0-9._-], хотя бы один.
is_valid_ws_paths() {
  local s="$1" p
  [[ -z "${s// /}" ]] && return 1
  for p in $s; do
    [[ "$p" =~ ^[A-Za-z0-9._-]+$ ]] || return 1
    [[ "$p" == -* ]] && return 1     # сегмент не должен начинаться с дефиса
  done
  return 0
}
# Собирает альтернацию для регекспа: "ws hs api" -> "ws|hs|api"
ws_paths_regex() {
  local s="$1" out="" p
  for p in $s; do out+="${out:+|}${p}"; done
  printf '%s' "$out"
}

#------------------------------- Ввод данных -----------------------------------
prompt_inputs() {
  if [[ "$NONINTERACTIVE" == "1" ]]; then
    FQDN="${FQDN:-${IN_FQDN:-}}"
    UPSTREAM_IP="${UPSTREAM_IP:-${IN_UPSTREAM_IP:-}}"
    UPSTREAM_PORT="${UPSTREAM_PORT:-${IN_UPSTREAM_PORT:-5743}}"
    UPSTREAM_SCHEME="${IN_UPSTREAM_SCHEME:-http}"
    ONEC_BASE="${IN_ONEC_BASE:-}"
    WS_PATHS="${IN_WS_PATHS:-ws hs}"
    TRUSTED_NETS="${IN_TRUSTED_NETS:-127.0.0.1}"
    [[ "$ENABLE_CONSOLE" == "ask" ]] && ENABLE_CONSOLE="${IN_ENABLE_CONSOLE:-yes}"
    [[ "$ENABLE_MODSEC" == "ask" ]] && ENABLE_MODSEC="${IN_ENABLE_MODSEC:-no}"
    is_valid_fqdn "$FQDN"          || die "NONINTERACTIVE: некорректный IN_FQDN='$FQDN'"
    is_valid_ipv4 "$UPSTREAM_IP"   || die "NONINTERACTIVE: некорректный IN_UPSTREAM_IP='$UPSTREAM_IP'"
    is_valid_port "$UPSTREAM_PORT" || die "NONINTERACTIVE: некорректный IN_UPSTREAM_PORT='$UPSTREAM_PORT'"
    is_valid_onec_base "$ONEC_BASE"|| die "NONINTERACTIVE: некорректный IN_ONEC_BASE='$ONEC_BASE'"
    is_valid_ws_paths "$WS_PATHS"  || die "NONINTERACTIVE: некорректный IN_WS_PATHS='$WS_PATHS'"
    return 0
  fi

  while :; do
    read -r -p "FQDN внешнего ресурса (напр. 1c.example.kz): " FQDN
    FQDN="${FQDN,,}"; FQDN="${FQDN#"${FQDN%%[![:space:]]*}"}"; FQDN="${FQDN%"${FQDN##*[![:space:]]}"}"
    is_valid_fqdn "$FQDN" && break
    warn "Некорректный FQDN. Пример: 1c.example.kz"
  done

  local sc
  read -r -p "Протокол до IIS-бэкенда [http/https] (по умолчанию http): " sc
  sc="${sc,,}"
  case "$sc" in
    https) UPSTREAM_SCHEME="https" ;;
    http|"") UPSTREAM_SCHEME="http" ;;
    *) warn "Неизвестно '$sc', использую http"; UPSTREAM_SCHEME="http" ;;
  esac

  while :; do
    read -r -p "IP-адрес сервера IIS с публикацией 1С: " UPSTREAM_IP
    UPSTREAM_IP="${UPSTREAM_IP//[[:space:]]/}"
    is_valid_ipv4 "$UPSTREAM_IP" && break
    warn "Некорректный IPv4. Пример: 10.20.30.40"
  done

  while :; do
    read -r -p "Порт публикации 1С на IIS (по умолчанию 5743): " UPSTREAM_PORT
    UPSTREAM_PORT="${UPSTREAM_PORT//[[:space:]]/}"
    [[ -z "$UPSTREAM_PORT" ]] && UPSTREAM_PORT="5743"
    is_valid_port "$UPSTREAM_PORT" && break
    warn "Некорректный порт (1-65535)."
  done

  while :; do
    read -r -p "Имя публикации 1С в IIS, напр. /accounting (Enter = корень /): " ONEC_BASE
    ONEC_BASE="${ONEC_BASE//[[:space:]]/}"
    [[ "$ONEC_BASE" == "/" ]] && ONEC_BASE=""
    ONEC_BASE="${ONEC_BASE%/}"
    is_valid_onec_base "$ONEC_BASE" && break
    warn "Некорректный путь. Пример: /accounting"
  done

  # Пути веб-сервисов 1С (SOAP/HTTP) — отдельная зона лимитов и стриминг.
  while :; do
    read -r -p "Пути веб-сервисов 1С через пробел (Enter = 'ws hs'): " WS_PATHS
    WS_PATHS="${WS_PATHS# }"; WS_PATHS="${WS_PATHS% }"
    [[ -z "$WS_PATHS" ]] && WS_PATHS="ws hs"
    is_valid_ws_paths "$WS_PATHS" && break
    warn "Некорректно. Пример: ws hs  или  ws hs api  (латиница/цифры/_/-, через пробел)"
  done

  if [[ "$ENABLE_CONSOLE" == "ask" ]]; then
    local a
    read -r -p "Установить веб-мониторинг Angie Console Light? [Y/n]: " a
    [[ "${a,,}" == "n" || "${a,,}" == "no" ]] && ENABLE_CONSOLE="no" || ENABLE_CONSOLE="yes"
  fi

  if [[ "$ENABLE_CONSOLE" == "yes" ]]; then
    while :; do
      read -r -p "Доверенные сети для консоли (через пробел, напр. '10.30.0.0/16 192.168.1.5'): " TRUSTED_NETS
      TRUSTED_NETS="${TRUSTED_NETS# }"; TRUSTED_NETS="${TRUSTED_NETS% }"
      [[ -z "$TRUSTED_NETS" ]] && { warn "Пусто нельзя — консоль не должна быть открыта всем."; continue; }
      local bad=0 n
      for n in $TRUSTED_NETS; do is_valid_cidr "$n" || { warn "Некорректная сеть/IP: $n"; bad=1; }; done
      (( bad == 0 )) && break
    done
    while :; do
      read -r -p "Порт внутреннего server-блока консоли (по умолчанию 8443): " CONSOLE_PORT
      CONSOLE_PORT="${CONSOLE_PORT//[[:space:]]/}"
      [[ -z "$CONSOLE_PORT" ]] && CONSOLE_PORT="8443"
      is_valid_port "$CONSOLE_PORT" && break
      warn "Некорректный порт."
    done
  fi

  if [[ "$ENABLE_MODSEC" == "ask" ]]; then
    local m
    read -r -p "Установить ModSecurity + OWASP CRS (WAF)? [y/N]: " m
    [[ "${m,,}" == "y" || "${m,,}" == "yes" ]] && ENABLE_MODSEC="yes" || ENABLE_MODSEC="no"
  fi

  echo
  info "Подтвердите параметры:"
  info "  FQDN:            $FQDN"
  info "  1С/IIS backend:  ${UPSTREAM_SCHEME}://${UPSTREAM_IP}:${UPSTREAM_PORT}${ONEC_BASE:-/}"
  info "  Веб-сервисы:     $(ws_paths_regex "$WS_PATHS") (пути: ${WS_PATHS})"
  info "  Rate limit:      ${RATE_RPS} r/s (burst ${RATE_BURST}), conn/IP ${CONN_PER_IP}"
  info "  Console Light:   ${ENABLE_CONSOLE}$( [[ "$ENABLE_CONSOLE" == yes ]] && echo " (порт ${CONSOLE_PORT}, сети: ${TRUSTED_NETS})" )"
  info "  ModSecurity:     ${ENABLE_MODSEC}"
  local c
  read -r -p "Продолжить? [Y/n]: " c
  [[ "${c,,}" == "n" || "${c,,}" == "no" ]] && die "Отменено пользователем."
}

#------------------------------- Детект ОС -------------------------------------
detect_os() {
  [[ -r /etc/os-release ]] || die "Не найден /etc/os-release."
  # shellcheck disable=SC1091
  source /etc/os-release
  OS_ID="${ID:-}"; OS_LIKE="${ID_LIKE:-}"
  OS_VERSION_ID="${VERSION_ID:-}"
  # Кодовое имя релиза (bookworm/jammy/noble...): из os-release либо lsb_release
  OS_CODENAME="${VERSION_CODENAME:-}"
  [[ -z "$OS_CODENAME" ]] && OS_CODENAME="$(lsb_release -cs 2>/dev/null || true)"
  # Мажор EL для RHEL-семейства (8/9...)
  EL_MAJOR="${OS_VERSION_ID%%.*}"
  case "$OS_ID" in
    debian|ubuntu) PKG="deb" ;;
    rhel|centos|rocky|almalinux|fedora) PKG="rpm" ;;
    *)
      if [[ "$OS_LIKE" == *debian* ]]; then PKG="deb"
      elif [[ "$OS_LIKE" == *rhel* || "$OS_LIKE" == *fedora* ]]; then PKG="rpm"
      else die "Неподдерживаемая ОС: $OS_ID"; fi ;;
  esac
  ok "ОС: ${PRETTY_NAME:-$OS_ID} (пакеты: $PKG, релиз: ${OS_CODENAME:-$OS_VERSION_ID})"
}

#--------------------- Базовые репозитории дистрибутива -------------------------
# Проверяет наличие штатных репо ОС и добавляет недостающие:
#   Debian:  main + security + updates
#   Ubuntu:  main, universe, updates, security
#   RHEL/*:  baseos, appstream, CRB/PowerTools, EPEL
ensure_base_repos() {
  [[ "$MANAGE_BASE_REPOS" == "yes" ]] || { info "Управление базовыми репо отключено (MANAGE_BASE_REPOS=no)."; return 0; }
  info "Проверяю базовые репозитории дистрибутива..."
  if [[ "$PKG" == "deb" ]]; then ensure_base_repos_deb; else ensure_base_repos_rpm; fi
}

ensure_base_repos_deb() {
  local ts backup added=0 list sources cn
  ts="$(date +%Y%m%d-%H%M%S)"
  backup="/etc/apt/sources.backup-${ts}"
  mkdir -p "$backup"
  [[ -f /etc/apt/sources.list ]] && cp -a /etc/apt/sources.list "$backup/" 2>/dev/null || true
  [[ -d /etc/apt/sources.list.d ]] && cp -a /etc/apt/sources.list.d "$backup/" 2>/dev/null || true

  sources="$(grep -rhE '^\s*(deb|Types:|URIs:|Suites:|Components:)' \
              /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null || true)"
  cn="${OS_CODENAME:-stable}"
  list="/etc/apt/sources.list.d/90-base-added.list"

  add_deb_line() {  # $1 маркер, $2 строка, $3 описание
    if ! echo "$sources" | grep -q -- "$1"; then
      echo "$2" >> "$list"
      info "Добавлен репозиторий: $3"
      added=1
      sources="${sources}"$'\n'"$2"
    fi
  }

  if [[ "$OS_ID" == "ubuntu" ]]; then
    local url="http://archive.ubuntu.com/ubuntu" surl="http://security.ubuntu.com/ubuntu"
    add_deb_line " ${cn} .*universe" "deb ${url} ${cn} main restricted universe multiverse" "${cn} main/universe"
    add_deb_line " ${cn}-updates"    "deb ${url} ${cn}-updates main restricted universe multiverse" "${cn}-updates"
    add_deb_line " ${cn}-security"   "deb ${surl} ${cn}-security main restricted universe multiverse" "${cn}-security"
  elif [[ "$OS_ID" == "debian" || "$OS_LIKE" == *debian* ]]; then
    local url="http://deb.debian.org/debian" surl="http://security.debian.org/debian-security"
    add_deb_line " ${cn} .*main"  "deb ${url} ${cn} main contrib non-free non-free-firmware" "${cn} main"
    add_deb_line " ${cn}-updates" "deb ${url} ${cn}-updates main contrib non-free non-free-firmware" "${cn}-updates"
    add_deb_line "${cn}-security" "deb ${surl} ${cn}-security main contrib non-free non-free-firmware" "${cn}-security"
  fi

  if (( added )); then
    ok "Базовые репозитории дополнены (бэкап: $backup). Обновляю индексы..."
    apt-get update -y || warn "apt-get update с ошибкой — проверьте $list"
  else
    ok "Все необходимые базовые репозитории уже подключены."
  fi
}

ensure_base_repos_rpm() {
  local mgr; mgr="$(command -v dnf >/dev/null && echo dnf || echo yum)"
  $mgr -y install "dnf-command(config-manager)" >/dev/null 2>&1 \
    || $mgr -y install yum-utils >/dev/null 2>&1 || true

  case "$EL_MAJOR" in
    9) $mgr config-manager --set-enabled crb >/dev/null 2>&1 \
         && ok "Включён репозиторий CRB." || warn "CRB включить не удалось (возможно уже включён)." ;;
    8) ($mgr config-manager --set-enabled powertools >/dev/null 2>&1 \
         || $mgr config-manager --set-enabled PowerTools >/dev/null 2>&1) \
         && ok "Включён репозиторий PowerTools." || warn "PowerTools включить не удалось." ;;
    *) info "EL${EL_MAJOR}: CRB/PowerTools не требуется/не определён." ;;
  esac

  if ! $mgr repolist enabled 2>/dev/null | grep -qi '^epel'; then
    if $mgr -y install epel-release >/dev/null 2>&1; then
      ok "Установлен EPEL."
    elif [[ -n "$EL_MAJOR" ]]; then
      $mgr -y install "https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm" >/dev/null 2>&1 \
        && ok "Установлен EPEL (прямой RPM)." || warn "EPEL установить не удалось."
    fi
  else
    ok "EPEL уже подключён."
  fi
  $mgr -y makecache >/dev/null 2>&1 || true
}

#--------------------- Обновление системы --------------------------------------
update_system() {
  case "$UPDATE_SYSTEM" in
    no)  info "Обновление системы пропущено (UPDATE_SYSTEM=no)."; return 0 ;;
    yes) : ;;
    ask)
      if [[ "$NONINTERACTIVE" == "1" ]]; then info "Обновление системы пропущено (не задано)."; return 0; fi
      local a
      read -r -p "Обновить пакеты ОС перед установкой (может занять время)? [y/N]: " a
      [[ "${a,,}" == "y" || "${a,,}" == "yes" ]] || { info "Обновление системы пропущено."; return 0; }
      ;;
  esac
  info "Обновляю пакеты системы..."
  if [[ "$PKG" == "deb" ]]; then
    export DEBIAN_FRONTEND=noninteractive
    apt-get update -y
    apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade \
      && ok "Система обновлена." || warn "apt upgrade завершился с предупреждениями."
  else
    local mgr; mgr="$(command -v dnf >/dev/null && echo dnf || echo yum)"
    $mgr -y update && ok "Система обновлена." || warn "$mgr update завершился с предупреждениями."
  fi
}

#--------------------- Репозиторий Angie ---------------------------------------
setup_angie_repo() {
  info "Настраиваю репозиторий Angie ($OS_ID $OS_VERSION_ID)..."
  if [[ "$PKG" == "deb" ]]; then
    export DEBIAN_FRONTEND=noninteractive
    apt-get install -y curl gnupg2 ca-certificates lsb-release >/dev/null 2>&1 \
      || apt-get install -y curl gnupg2 ca-certificates lsb-release
    curl -fsSL https://angie.software/keys/angie-signing.gpg \
      -o /usr/share/keyrings/angie-signing.gpg || die "Не удалось скачать ключ подписи Angie."
    local rel="${OS_VERSION_ID}" cn="${OS_CODENAME}"
    [[ -z "$rel" ]] && rel="$(lsb_release -rs 2>/dev/null || echo "")"
    echo "deb [signed-by=/usr/share/keyrings/angie-signing.gpg] https://download.angie.software/angie/${OS_ID}/${rel}/ ${cn} main" \
      > /etc/apt/sources.list.d/angie.list
    apt-get update -y || die "apt-get update для репо Angie не удался (${OS_ID}/${rel}/${cn})."
    ok "Репозиторий Angie подключён (${OS_ID}/${rel}/${cn})."
  else
    local rid="${OS_ID/centos/rhel}"
    curl -fsSL -o /etc/yum.repos.d/angie.repo \
      "https://download.angie.software/angie/${rid}/${EL_MAJOR}/x86_64/angie.repo" \
      || die "Не удалось скачать angie.repo для ${rid}/${EL_MAJOR}."
    ok "Репозиторий Angie подключён (${rid}/${EL_MAJOR})."
  fi
}

#------------------------------- Установка -------------------------------------
install_angie() {
  if command -v angie >/dev/null 2>&1; then
    ok "Angie уже установлен ($(angie -v 2>&1 | head -1))"; return 0
  fi
  info "Устанавливаю Angie..."
  if [[ "$PKG" == "deb" ]]; then
    apt-get install -y angie
  else
    (command -v dnf >/dev/null && dnf install -y angie) || yum install -y angie
  fi
  command -v angie >/dev/null 2>&1 || die "Установка Angie не удалась."
  ok "Angie установлен: $(angie -v 2>&1 | head -1)"
}

install_console() {
  [[ "$ENABLE_CONSOLE" == "yes" ]] || { info "Console Light пропущен."; return 0; }
  if [[ -d "$CONSOLE_ROOT" ]] || [[ -d /usr/share/angie-console-light ]]; then
    ok "Console Light уже установлен."; return 0
  fi
  info "Устанавливаю angie-console-light..."
  if [[ "$PKG" == "deb" ]]; then
    apt-get install -y angie-console-light || { warn "Пакет недоступен — мониторинг отключён."; ENABLE_CONSOLE="no"; return 0; }
  else
    (command -v dnf >/dev/null && dnf install -y angie-console-light) || yum install -y angie-console-light || { warn "Пакет недоступен — мониторинг отключён."; ENABLE_CONSOLE="no"; return 0; }
  fi
  ok "Console Light установлен."
}

install_modsecurity() {
  [[ "$ENABLE_MODSEC" == "yes" ]] || { info "ModSecurity пропущен."; return 0; }
  info "Устанавливаю ModSecurity + OWASP CRS..."
  if [[ "$PKG" == "deb" ]]; then
    apt-get install -y angie-module-modsecurity modsecurity-crs || { warn "Модуль недоступен — WAF отключён."; ENABLE_MODSEC="no"; return 0; }
  else
    (command -v dnf >/dev/null && dnf install -y angie-module-modsecurity) || yum install -y angie-module-modsecurity || { warn "Модуль недоступен — WAF отключён."; ENABLE_MODSEC="no"; return 0; }
  fi
  ok "ModSecurity установлен."
}

#------------------------------- TLS -------------------------------------------
generate_tls() {
  mkdir -p "$TLS_DIR"; chmod 750 "$TLS_DIR"
  local crt="${TLS_DIR}/${FQDN}.crt" key="${TLS_DIR}/${FQDN}.key" dh="${TLS_DIR}/dhparam.pem"
  if [[ ! -s "$crt" || ! -s "$key" ]]; then
    info "Генерирую self-signed сертификат-заглушку для $FQDN..."
    openssl req -x509 -nodes -newkey rsa:2048 -days 397 -keyout "$key" -out "$crt" -subj "/CN=${FQDN}/O=ReverseProxy" -addext "subjectAltName=DNS:${FQDN}" >/dev/null 2>&1
    chmod 600 "$key"; chmod 644 "$crt"
    ok "Сертификат-заглушка создан."
  else
    ok "Сертификат уже существует."
  fi
  if [[ ! -s "$dh" ]]; then
    info "Генерирую DH-параметры (2048 бит)..."
    openssl dhparam -out "$dh" 2048 >/dev/null 2>&1
    chmod 644 "$dh"
  fi
}

#------------------------------- Конфигурация ----------------------------------
backup_configs() {
  mkdir -p "$BACKUP_DIR"
  local f
  for f in "$SITE_CONF" "$SEC_CONF" "$CONSOLE_CONF" "${ANGIE_CONF_DIR}/angie.conf"; do
    [[ -f "$f" ]] && cp -a "$f" "$BACKUP_DIR/"
  done
  ok "Бэкап конфигов: $BACKUP_DIR"
}

write_security_conf() {
  mkdir -p "${ANGIE_CONF_DIR}/http.d"
  cat > "$SEC_CONF" <<EOF
#=== 00-security.conf — глобальные http-level настройки =========================
server_tokens off;

client_max_body_size        512m;
client_body_buffer_size     128k;
client_header_buffer_size   1k;
large_client_header_buffers 4 16k;

client_body_timeout   30s;
client_header_timeout 15s;
send_timeout          30s;
keepalive_timeout     65s;
reset_timedout_connection on;

limit_req_zone  \$binary_remote_addr zone=req_web:10m rate=${RATE_RPS}r/s;
limit_req_zone  \$binary_remote_addr zone=req_ws:10m  rate=30r/s;
limit_conn_zone \$binary_remote_addr zone=conn_main:10m;
limit_req_status  429;
limit_conn_status 429;

set_real_ip_from 127.0.0.1;
# set_real_ip_from <IP_фронтового_балансировщика>;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

map \$http_upgrade \$connection_upgrade {
    default upgrade;
    ""      close;
}

map \$request_method \$bad_method {
    default 1;
    GET 0; HEAD 0; POST 0; PUT 0; PATCH 0; DELETE 0; OPTIONS 0;
}

map \$http_user_agent \$bad_ua {
    default 0;
    ""                     1;
    "~*(nikto|sqlmap|nmap|masscan|acunetix|fimap|nessus)" 1;
}
EOF
  ok "Записан $SEC_CONF"
}

write_site_conf() {
  local modsec_on="" base loc_prefix ws_re
  if [[ "$ENABLE_MODSEC" == "yes" ]]; then
    modsec_on=$'    modsecurity on;\n    modsecurity_rules_file /etc/modsecurity.d/include.conf;'
  fi
  base="${ONEC_BASE:-}"
  loc_prefix="${base:-/}"
  ws_re="$(ws_paths_regex "$WS_PATHS")"

  cat > "$SITE_CONF" <<EOF
#=== reverse-proxy.conf — публикация 1С (IIS) ==================================
upstream onec_iis {
    zone onec_iis 64k;
    server ${UPSTREAM_IP}:${UPSTREAM_PORT} max_fails=3 fail_timeout=15s;
    keepalive 32;
}

server {
    listen 80;
    listen [::]:80;
    server_name ${FQDN};
    location /.well-known/acme-challenge/ { root /var/www/acme; }
    location / { return 301 https://\$host\$request_uri; }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name ${FQDN};
    status_zone onec_site;

    ssl_certificate     ${TLS_DIR}/${FQDN}.crt;
    ssl_certificate_key ${TLS_DIR}/${FQDN}.key;
    ssl_dhparam         ${TLS_DIR}/dhparam.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    ssl_stapling on; ssl_stapling_verify on;
    resolver 1.1.1.1 9.9.9.9 valid=300s ipv6=off;
    resolver_timeout 5s;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Frame-Options           "SAMEORIGIN" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy    "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' wss: https:; frame-ancestors 'self'" always;

    limit_conn conn_main ${CONN_PER_IP};
    if (\$bad_method) { return 405; }
    if (\$bad_ua)     { return 444; }

    location ~ /\\.(?!well-known) { deny all; }

${modsec_on}

    location ~* ^${base}/(${ws_re})/ {
        limit_req zone=req_ws burst=60 nodelay;
        proxy_pass ${UPSTREAM_SCHEME}://onec_iis;
        proxy_http_version 1.1;
        proxy_set_header Host              \$host;
        proxy_set_header X-Real-IP         \$remote_addr;
        proxy_set_header X-Forwarded-For   \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_set_header Connection        "";
        proxy_connect_timeout 10s;
        proxy_send_timeout    1800s;
        proxy_read_timeout    1800s;
        proxy_buffering off;
    }

    location ${loc_prefix} {
        limit_req zone=req_web burst=${RATE_BURST} nodelay;

        proxy_pass ${UPSTREAM_SCHEME}://onec_iis;
        proxy_http_version 1.1;

        proxy_set_header Upgrade    \$http_upgrade;
        proxy_set_header Connection \$connection_upgrade;

        proxy_set_header Host              \$host;
        proxy_set_header X-Real-IP         \$remote_addr;
        proxy_set_header X-Forwarded-For   \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_set_header X-Forwarded-Host  \$host;
        proxy_set_header X-Forwarded-Port  \$server_port;

        proxy_connect_timeout 10s;
        proxy_send_timeout    1800s;
        proxy_read_timeout    1800s;

        proxy_buffering on;
        proxy_buffers 16 32k;
        proxy_buffer_size 32k;
        proxy_busy_buffers_size 64k;

        proxy_redirect ${UPSTREAM_SCHEME}://${UPSTREAM_IP}:${UPSTREAM_PORT}/ https://${FQDN}/;
        proxy_redirect ${UPSTREAM_SCHEME}://${FQDN}/ https://${FQDN}/;

        proxy_hide_header X-Powered-By;
        proxy_hide_header X-AspNet-Version;
        proxy_hide_header Server;
    }

    location = /healthz { access_log off; return 200 "ok\\n"; }
}
EOF
  ok "Записан $SITE_CONF"
}

write_console_conf() {
  if [[ "$ENABLE_CONSOLE" != "yes" ]]; then
    rm -f "$CONSOLE_CONF" 2>/dev/null || true
    info "Console-конфиг не создаётся."
    return 0
  fi
  local allows="" n
  for n in $TRUSTED_NETS; do allows+="        allow ${n};"$'\n'; done

  cat > "$CONSOLE_CONF" <<EOF
#=== 10-console.conf — Angie Console Light (ВНУТРЕННИЙ доступ) ==================
server {
    listen ${CONSOLE_PORT} ssl;
    server_name ${FQDN};
    status_zone console;

    ssl_certificate     ${TLS_DIR}/${FQDN}.crt;
    ssl_certificate_key ${TLS_DIR}/${FQDN}.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
${allows}        deny all;
        auth_basic "Angie Console";
        auth_basic_user_file ${ANGIE_CONF_DIR}/console.htpasswd;
        alias ${CONSOLE_ROOT}/;
        index index.html;
    }

    location /status/ {
        api /status/;
${allows}        deny all;
        auth_basic "Angie Console";
        auth_basic_user_file ${ANGIE_CONF_DIR}/console.htpasswd;
    }
}
EOF
  ok "Записан $CONSOLE_CONF"

  local htp="${ANGIE_CONF_DIR}/console.htpasswd"
  if [[ ! -s "$htp" ]]; then
    local pw hash
    pw="$(openssl rand -base64 12 2>/dev/null || echo "ChangeMe$RANDOM")"
    hash="$(openssl passwd -apr1 "$pw" 2>/dev/null)"
    if [[ -n "$hash" ]]; then
      echo "admin:${hash}" > "$htp"; chmod 640 "$htp"
      CONSOLE_USER="admin"; CONSOLE_PASS="$pw"
      ok "Создан доступ к консоли (логин admin)."
    else
      warn "Не удалось сгенерировать htpasswd — создайте вручную."
    fi
  else
    ok "console.htpasswd уже существует."
  fi
}

ensure_include() {
  local main="${ANGIE_CONF_DIR}/angie.conf"
  [[ -f "$main" ]] || die "Не найден $main"
  if ! grep -qE 'include\s+.*http\.d/\*\.conf' "$main"; then
    warn "http.d/*.conf не подключён — добавляю в http{}"
    if grep -q 'include .*conf.d/\*.conf' "$main"; then
      sed -i 's#\(include .*conf.d/\*.conf;\)#\1\n    include /etc/angie/http.d/*.conf;#' "$main"
    else
      warn "Авто-подключение не удалось. Добавьте вручную: include /etc/angie/http.d/*.conf; в http{}"
    fi
  fi
  mkdir -p /var/www/acme
}

configure_firewall() {
  local console_rule=""
  [[ "$ENABLE_CONSOLE" == "yes" ]] && console_rule="$CONSOLE_PORT"
  if command -v ufw >/dev/null 2>&1; then
    ufw allow 80/tcp  >/dev/null 2>&1 || true
    ufw allow 443/tcp >/dev/null 2>&1 || true
    if [[ -n "$console_rule" ]]; then
      local n
      for n in $TRUSTED_NETS; do ufw allow from "$n" to any port "$console_rule" proto tcp >/dev/null 2>&1 || true; done
    fi
    ok "UFW: 80,443$( [[ -n "$console_rule" ]] && echo ", $console_rule (только доверенным)" )"
  elif command -v firewall-cmd >/dev/null 2>&1; then
    firewall-cmd --permanent --add-service=http  >/dev/null 2>&1 || true
    firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true
    if [[ -n "$console_rule" ]]; then
      local n
      for n in $TRUSTED_NETS; do
        firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$n port port=$console_rule protocol=tcp accept" >/dev/null 2>&1 || true
      done
    fi
    firewall-cmd --reload >/dev/null 2>&1 || true
    ok "firewalld настроен."
  else
    warn "Не найден ufw/firewalld — откройте порты вручную."
  fi
}

validate_and_start() {
  info "Проверяю конфигурацию (angie -t)..."
  if ! angie -t 2>>"$LOG_FILE"; then
    err "Конфигурация невалидна. Восстанавливаю из бэкапа $BACKUP_DIR"
    local f
    for f in reverse-proxy.conf 00-security.conf 10-console.conf; do
      [[ -f "$BACKUP_DIR/$f" ]] && cp -a "$BACKUP_DIR/$f" "${ANGIE_CONF_DIR}/http.d/$f"
    done
    die "Откат выполнен. Смотрите $LOG_FILE"
  fi
  ok "Конфигурация валидна."
  systemctl enable angie >/dev/null 2>&1 || true
  if systemctl is-active --quiet angie; then
    systemctl reload angie && ok "Angie перезагружен (reload)."
  else
    systemctl restart angie && ok "Angie запущен (restart)."
  fi
}

smoke_test() {
  info "Smoke-тест..."
  sleep 1
  ss -tlnp 2>/dev/null | grep -q ':443' && ok "Порт 443 слушается." || warn "443 не найден — systemctl status angie"
  if [[ "$ENABLE_CONSOLE" == "yes" ]]; then
    ss -tlnp 2>/dev/null | grep -q ":${CONSOLE_PORT}" && ok "Порт консоли ${CONSOLE_PORT} слушается." || warn "Порт ${CONSOLE_PORT} не найден."
  fi
  if command -v curl >/dev/null 2>&1; then
    local code
    code="$(curl -kso /dev/null -w '%{http_code}' "https://127.0.0.1/healthz" -H "Host: ${FQDN}" || echo 000)"
    [[ "$code" == "200" ]] && ok "healthz -> HTTP $code" || warn "healthz -> HTTP $code"
  fi
}

print_summary() {
  local base="${ONEC_BASE:-}" ws_list="" p
  for p in $WS_PATHS; do ws_list+="${ws_list:+ , }${base}/${p}/"; done
  cat <<EOF | tee -a "$LOG_FILE"

================= РАЗВЁРТЫВАНИЕ ЗАВЕРШЕНО =================
 FQDN:              ${FQDN}
 Вход (1С):         https://${FQDN}${ONEC_BASE:-/}   (TCP/443, TLS1.2/1.3)
 Бэкенд IIS:        ${UPSTREAM_SCHEME}://${UPSTREAM_IP}:${UPSTREAM_PORT}${ONEC_BASE:-/}
 Веб-сервисы 1С:    ${ws_list}  (отдельная зона лимитов)
 Rate limit:        web ${RATE_RPS} r/s (burst ${RATE_BURST}); ws 30 r/s; conn/IP ${CONN_PER_IP}
 ModSecurity/CRS:   ${ENABLE_MODSEC}
EOF
  if [[ "$ENABLE_CONSOLE" == "yes" ]]; then
    cat <<EOF | tee -a "$LOG_FILE"
 Console Light:     https://<внутр.IP>:${CONSOLE_PORT}/   (только сети: ${TRUSTED_NETS})
   Логин/пароль:    ${CONSOLE_USER:-admin} / ${CONSOLE_PASS:-<см. console.htpasswd>}
   ВНИМАНИЕ:        консоль НЕ опубликована на 443 и не должна быть доступна извне.
EOF
  fi
  cat <<EOF | tee -a "$LOG_FILE"
 Конфиги:           ${SITE_CONF}
                    ${SEC_CONF}
 TLS:               ${TLS_DIR}/${FQDN}.{crt,key}  (ЗАГЛУШКА — замените!)
 Бэкап:             ${BACKUP_DIR}
 Лог:               ${LOG_FILE}

 СЛЕДУЮЩИЕ ШАГИ:
  1) Замените self-signed сертификат на боевой (ACME/корп. CA).
  2) Укажите set_real_ip_from для фронтового LB/FortiGate в 00-security.conf.
  3) Проверьте вход: тонкий клиент 1С + один веб-сервис (/ws/).
  4) Если UI 1С "поехал" — ослабьте CSP под конфигурацию.
  5) Настройте fail2ban на 429/444.
=========================================================
EOF
}

main() {
  require_root
  : > "$LOG_FILE" 2>/dev/null || true
  info "Старт $SCRIPT_NAME"
  detect_os
  prompt_inputs
  ensure_base_repos
  update_system
  setup_angie_repo
  install_angie
  install_console
  install_modsecurity
  generate_tls
  backup_configs
  write_security_conf
  write_site_conf
  write_console_conf
  ensure_include
  configure_firewall
  validate_and_start
  smoke_test
  print_summary
}

main "$@"
скачать →angie, bash, 1C, IIS
bash
Проверка состояния кластера Proxmox
Быстрый health-check узлов и кворума.
Показать код
#!/usr/bin/env bash
set -euo pipefail
pvecm status
pvesh get /cluster/resources --type node
proxmox,bash,healthcheck