Скрипты и ПО
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.xmlbashфайлов: 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}' завершена."
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 "$@"
bash
Проверка состояния кластера Proxmox
Быстрый health-check узлов и кворума.
Показать код
#!/usr/bin/env bash
set -euo pipefail
pvecm status
pvesh get /cluster/resources --type node