Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений
This commit is contained in:
@@ -1,51 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Первичная подготовка Ubuntu 24.04 для HAN Chat.
|
||||
# Fresh Ubuntu 24.04 bootstrap for production VM1. This script never starts
|
||||
# Compose/application containers and never creates DNS, PG, S3, TLS or secrets.
|
||||
# Run twice: first with HARDEN_SSH=false, then only after independent deploy and
|
||||
# admin login/sudo checks with HARDEN_SSH=true.
|
||||
#
|
||||
# Скрипт настраивает только VM: пользователя развертывания, базовые пакеты,
|
||||
# Docker/Compose, UFW, fail2ban, DOCKER-USER, swap и каталоги проекта.
|
||||
# PostgreSQL и S3 остаются внешними управляемыми сервисами. Скрипт не создает
|
||||
# .env, секреты, DNS, S3-бакеты, схемы БД и TLS-сертификаты.
|
||||
# Required:
|
||||
# DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub
|
||||
# ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub
|
||||
#
|
||||
# Запуск на свежей VM:
|
||||
# chmod +x deployment/scripts/setup-vm.sh
|
||||
# sudo deployment/scripts/setup-vm.sh
|
||||
#
|
||||
# Основные параметры:
|
||||
# DEPLOY_USER=deploy
|
||||
# DEPLOY_DIR=/opt/han-chat/backend
|
||||
# SSH_PORT=22
|
||||
# TIMEZONE=Europe/Moscow
|
||||
# SWAP_SIZE_GB=4
|
||||
# EXTERNAL_IF=ens3
|
||||
# PUBLIC_DOCKER_PORTS=80,443
|
||||
# COPY_SSH_KEYS=true
|
||||
# HARDEN_SSH=false
|
||||
# LOCK_ACCOUNT_PASSWORDS=true
|
||||
# HSTS_MAX_AGE_SECONDS=31536000
|
||||
# RESET_UFW=false
|
||||
# SKIP_APT_UPGRADE=false
|
||||
#
|
||||
# Парольный SSH-вход, X11 forwarding и локальные пароли root/deploy отключаются
|
||||
# по умолчанию после проверки authorized_keys. HARDEN_SSH=true дополнительно
|
||||
# запрещает прямой root-вход и SSH TCP forwarding.
|
||||
# Optional: SSH_PORT=22 EXTERNAL_IF=ens3 SWAP_SIZE_GB=4 HARDEN_SSH=false
|
||||
|
||||
set -Eeuo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
DEPLOY_USER="${DEPLOY_USER:-deploy}"
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-/opt/han-chat/backend}"
|
||||
ADMIN_USER="${ADMIN_USER:-admin}"
|
||||
DEPLOY_AUTHORIZED_KEY_FILE="${DEPLOY_AUTHORIZED_KEY_FILE:-}"
|
||||
ADMIN_AUTHORIZED_KEY_FILE="${ADMIN_AUTHORIZED_KEY_FILE:-}"
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-/opt/han-chat/current/backend}"
|
||||
INCOMING_DIR="${INCOMING_DIR:-/var/lib/han-deploy/incoming}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
TIMEZONE="${TIMEZONE:-Europe/Moscow}"
|
||||
SWAP_SIZE_GB="${SWAP_SIZE_GB:-4}"
|
||||
EXTERNAL_IF="${EXTERNAL_IF:-}"
|
||||
PUBLIC_DOCKER_PORTS="${PUBLIC_DOCKER_PORTS:-80,443}"
|
||||
COPY_SSH_KEYS="${COPY_SSH_KEYS:-true}"
|
||||
HARDEN_SSH="${HARDEN_SSH:-false}"
|
||||
LOCK_ACCOUNT_PASSWORDS="${LOCK_ACCOUNT_PASSWORDS:-true}"
|
||||
HSTS_MAX_AGE_SECONDS="${HSTS_MAX_AGE_SECONDS:-31536000}"
|
||||
RESET_UFW="${RESET_UFW:-false}"
|
||||
RESET_UFW="${RESET_UFW:-true}"
|
||||
SKIP_APT_UPGRADE="${SKIP_APT_UPGRADE:-false}"
|
||||
LOG_FILE="${LOG_FILE:-/var/log/han-chat-vm-setup.log}"
|
||||
LOG_FILE="${LOG_FILE:-/var/log/han-chat-vm1-setup.log}"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
|
||||
@@ -73,17 +55,26 @@ require_root() {
|
||||
}
|
||||
|
||||
validate_parameters() {
|
||||
[[ "$DEPLOY_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] || die "Некорректный DEPLOY_USER"
|
||||
[[ "$DEPLOY_DIR" == /* ]] || die "DEPLOY_DIR должен быть абсолютным путем"
|
||||
[[ "$DEPLOY_USER" == deploy ]] || die "Production unit/sudoers требуют DEPLOY_USER=deploy"
|
||||
[[ "$ADMIN_USER" == admin ]] || die "Break-glass роль должна называться admin"
|
||||
[[ "$DEPLOY_AUTHORIZED_KEY_FILE" =~ ^/[A-Za-z0-9._/-]+$ ]] \
|
||||
|| die "Задайте безопасный абсолютный DEPLOY_AUTHORIZED_KEY_FILE"
|
||||
[[ "$ADMIN_AUTHORIZED_KEY_FILE" =~ ^/[A-Za-z0-9._/-]+$ ]] \
|
||||
|| die "Задайте безопасный абсолютный ADMIN_AUTHORIZED_KEY_FILE"
|
||||
[[ "$DEPLOY_AUTHORIZED_KEY_FILE" != "$ADMIN_AUTHORIZED_KEY_FILE" ]] \
|
||||
|| die "deploy и admin должны использовать разные key files"
|
||||
[[ "$DEPLOY_DIR" =~ ^/[A-Za-z0-9._/-]+$ ]] || die "Некорректный DEPLOY_DIR"
|
||||
[[ "$INCOMING_DIR" =~ ^/[A-Za-z0-9._/-]+$ ]] || die "Некорректный INCOMING_DIR"
|
||||
[[ "$DEPLOY_DIR" != "$INCOMING_DIR" ]] || die "Production и incoming должны различаться"
|
||||
[[ "$SSH_PORT" =~ ^[0-9]+$ ]] || die "SSH_PORT должен быть числом"
|
||||
((SSH_PORT >= 1 && SSH_PORT <= 65535)) || die "SSH_PORT вне диапазона"
|
||||
[[ "$SWAP_SIZE_GB" =~ ^[0-9]+$ ]] || die "SWAP_SIZE_GB должен быть целым числом"
|
||||
[[ "$HSTS_MAX_AGE_SECONDS" =~ ^[0-9]+$ ]] \
|
||||
|| die "HSTS_MAX_AGE_SECONDS должен быть целым числом"
|
||||
((HSTS_MAX_AGE_SECONDS >= 31536000)) \
|
||||
|| die "HSTS_MAX_AGE_SECONDS должен быть не меньше 31536000"
|
||||
[[ "$PUBLIC_DOCKER_PORTS" =~ ^[0-9]+(,[0-9]+)*$ ]] \
|
||||
|| die "PUBLIC_DOCKER_PORTS должен иметь вид 80,443"
|
||||
[[ "$HARDEN_SSH" == true || "$HARDEN_SSH" == false ]] || die "Некорректный HARDEN_SSH"
|
||||
[[ "$LOCK_ACCOUNT_PASSWORDS" == true || "$LOCK_ACCOUNT_PASSWORDS" == false ]] \
|
||||
|| die "Некорректный LOCK_ACCOUNT_PASSWORDS"
|
||||
[[ "$RESET_UFW" == true || "$RESET_UFW" == false ]] || die "Некорректный RESET_UFW"
|
||||
[[ "$SKIP_APT_UPGRADE" == true || "$SKIP_APT_UPGRADE" == false ]] \
|
||||
|| die "Некорректный SKIP_APT_UPGRADE"
|
||||
}
|
||||
|
||||
check_os() {
|
||||
@@ -107,7 +98,7 @@ update_system() {
|
||||
apt-get install -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
dos2unix \
|
||||
certbot \
|
||||
fail2ban \
|
||||
git \
|
||||
gnupg \
|
||||
@@ -117,11 +108,10 @@ update_system() {
|
||||
netcat-openbsd \
|
||||
openssl \
|
||||
python3 \
|
||||
python3-venv \
|
||||
rsync \
|
||||
sudo \
|
||||
unattended-upgrades \
|
||||
ufw
|
||||
apt-get autoremove -y
|
||||
}
|
||||
|
||||
configure_time() {
|
||||
@@ -130,65 +120,79 @@ configure_time() {
|
||||
timedatectl set-ntp true
|
||||
}
|
||||
|
||||
create_deploy_user() {
|
||||
step "Пользователь развертывания"
|
||||
if ! id "$DEPLOY_USER" >/dev/null 2>&1; then
|
||||
useradd --create-home --shell /bin/bash "$DEPLOY_USER"
|
||||
log "Создан пользователь ${DEPLOY_USER}"
|
||||
else
|
||||
log "Пользователь ${DEPLOY_USER} уже существует"
|
||||
fi
|
||||
install_authorized_key() {
|
||||
local user=$1 source=$2 target="/home/${1}/.ssh/authorized_keys"
|
||||
[[ -f "$source" && ! -L "$source" ]] || die "Не найден обычный key file ${source}"
|
||||
[[ "$(wc -l <"$source")" -eq 1 ]] || die "${source} должен содержать один ключ"
|
||||
ssh-keygen -l -f "$source" >/dev/null || die "Некорректный SSH key ${source}"
|
||||
grep -Eq '^ssh-ed25519[[:space:]]+[A-Za-z0-9+/=]+([[:space:]].*)?$' "$source" \
|
||||
|| die "Разрешены только Ed25519 ключи"
|
||||
install -d -m 0700 -o "$user" -g "$user" "/home/${user}/.ssh"
|
||||
install -m 0600 -o "$user" -g "$user" "$source" "$target"
|
||||
}
|
||||
|
||||
install -d -m 700 -o "$DEPLOY_USER" -g "$DEPLOY_USER" \
|
||||
"/home/${DEPLOY_USER}/.ssh"
|
||||
|
||||
local source_user="${SUDO_USER:-}"
|
||||
local source_keys=""
|
||||
local target_keys="/home/${DEPLOY_USER}/.ssh/authorized_keys"
|
||||
if [[ -n "$source_user" && "$source_user" != "root" ]]; then
|
||||
source_keys="/home/${source_user}/.ssh/authorized_keys"
|
||||
elif [[ -s /root/.ssh/authorized_keys ]]; then
|
||||
source_user="root"
|
||||
source_keys="/root/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
if [[ "$COPY_SSH_KEYS" == "true" && ! -s "$target_keys" && -s "$source_keys" ]]; then
|
||||
install -m 600 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$source_keys" "$target_keys"
|
||||
log "SSH-ключи скопированы от ${source_user}"
|
||||
fi
|
||||
|
||||
if [[ ! -s "$target_keys" ]]; then
|
||||
log "ПРЕДУПРЕЖДЕНИЕ: у ${DEPLOY_USER} отсутствует authorized_keys"
|
||||
create_host_roles() {
|
||||
step "Роли deploy и break-glass admin"
|
||||
id "$DEPLOY_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$DEPLOY_USER"
|
||||
id "$ADMIN_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$ADMIN_USER"
|
||||
install_authorized_key "$DEPLOY_USER" "$DEPLOY_AUTHORIZED_KEY_FILE"
|
||||
install_authorized_key "$ADMIN_USER" "$ADMIN_AUTHORIZED_KEY_FILE"
|
||||
local deploy_key admin_key group
|
||||
deploy_key="$(awk '{print $2}' "$DEPLOY_AUTHORIZED_KEY_FILE")"
|
||||
admin_key="$(awk '{print $2}' "$ADMIN_AUTHORIZED_KEY_FILE")"
|
||||
[[ "$deploy_key" != "$admin_key" ]] || die "deploy/admin используют один ключ"
|
||||
if [[ -s /root/.ssh/authorized_keys ]] &&
|
||||
{ grep -Fq "$deploy_key" /root/.ssh/authorized_keys ||
|
||||
grep -Fq "$admin_key" /root/.ssh/authorized_keys; }; then
|
||||
die "deploy/admin key совпадает с root key"
|
||||
fi
|
||||
for group in docker sudo lxd adm systemd-journal; do
|
||||
if getent group "$group" >/dev/null &&
|
||||
id -nG "$DEPLOY_USER" | tr ' ' '\n' | grep -qx "$group"; then
|
||||
gpasswd -d "$DEPLOY_USER" "$group"
|
||||
fi
|
||||
done
|
||||
usermod -aG sudo "$ADMIN_USER"
|
||||
for group in docker lxd; do
|
||||
if getent group "$group" >/dev/null &&
|
||||
id -nG "$ADMIN_USER" | tr ' ' '\n' | grep -qx "$group"; then
|
||||
gpasswd -d "$ADMIN_USER" "$group"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
configure_account_passwords() {
|
||||
step "Блокировка локальных паролей привилегированных учетных записей"
|
||||
if [[ "$LOCK_ACCOUNT_PASSWORDS" != "true" ]]; then
|
||||
log "LOCK_ACCOUNT_PASSWORDS=false: локальные пароли root и ${DEPLOY_USER} не изменены"
|
||||
return
|
||||
step "Пароли host-ролей"
|
||||
if [[ "$LOCK_ACCOUNT_PASSWORDS" == true ]]; then
|
||||
passwd --lock root
|
||||
passwd --lock "$DEPLOY_USER"
|
||||
fi
|
||||
|
||||
[[ -s "/home/${DEPLOY_USER}/.ssh/authorized_keys" ]] \
|
||||
|| die "Нельзя заблокировать пароль ${DEPLOY_USER}: authorized_keys пользователя пуст"
|
||||
|
||||
passwd --lock root
|
||||
passwd --lock "$DEPLOY_USER"
|
||||
log "Локальные пароли root и ${DEPLOY_USER} заблокированы; вход по SSH-ключам сохранен"
|
||||
local status
|
||||
status="$(passwd --status "$ADMIN_USER" | awk '{print $2}')"
|
||||
if [[ "$HARDEN_SSH" == true && "$status" != P ]]; then
|
||||
die "До HARDEN_SSH=true задайте отдельный sudo-пароль: passwd ${ADMIN_USER}"
|
||||
fi
|
||||
[[ "$status" == P ]] || log "ПРЕДУПРЕЖДЕНИЕ: задайте sudo-пароль admin"
|
||||
}
|
||||
|
||||
configure_layout() {
|
||||
step "Каталоги HAN Chat"
|
||||
install -d -m 755 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$DEPLOY_DIR"
|
||||
install -d -m 700 -o "$DEPLOY_USER" -g "$DEPLOY_USER" \
|
||||
"${DEPLOY_DIR}/secrets" \
|
||||
"${DEPLOY_DIR}/secrets/pg" \
|
||||
"${DEPLOY_DIR}/backups"
|
||||
|
||||
local env_file="${DEPLOY_DIR}/.env"
|
||||
if [[ -f "$env_file" ]]; then
|
||||
chown "$DEPLOY_USER:$DEPLOY_USER" "$env_file"
|
||||
chmod 600 "$env_file"
|
||||
step "Root-owned releases и incoming staging"
|
||||
install -d -m 0755 -o root -g root /opt/han-chat /opt/han-chat/releases
|
||||
install -d -m 0755 -o root -g root /var/lib/han-deploy
|
||||
install -d -m 0750 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$INCOMING_DIR"
|
||||
install -d -m 0700 -o root -g root /etc/han /etc/han/secrets /etc/han/credentials
|
||||
install -d -m 0755 -o root -g root /etc/han/ca /var/lib/han-chat/acme
|
||||
if [[ -f /etc/han/vm1.env ]]; then
|
||||
[[ "$(stat -c '%U:%G:%a' /etc/han/vm1.env)" == root:root:600 ]] \
|
||||
|| die "/etc/han/vm1.env должен быть root:root 0600"
|
||||
fi
|
||||
if [[ -d "$DEPLOY_DIR" ]]; then
|
||||
if find "$DEPLOY_DIR" ! -user root -print -quit | grep -q .; then
|
||||
die "Активный release содержит файлы не root-owned"
|
||||
fi
|
||||
if find "$DEPLOY_DIR" -perm /022 -print -quit | grep -q .; then
|
||||
die "Активный release доступен на запись group/other"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -261,7 +265,6 @@ install_docker() {
|
||||
EOF
|
||||
systemctl enable --now docker
|
||||
systemctl restart docker
|
||||
usermod -aG docker "$DEPLOY_USER"
|
||||
|
||||
docker compose version >/dev/null \
|
||||
|| die "Docker Compose plugin не установлен"
|
||||
@@ -276,8 +279,7 @@ configure_ufw() {
|
||||
fi
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow "${SSH_PORT}/tcp" comment 'HAN Chat SSH'
|
||||
ufw limit "${SSH_PORT}/tcp" comment 'HAN Chat SSH rate limit'
|
||||
ufw allow "$SSH_PORT"/tcp comment 'HAN VM1 SSH temporary public access'
|
||||
ufw allow 80/tcp comment 'HAN Chat HTTP'
|
||||
ufw allow 443/tcp comment 'HAN Chat HTTPS'
|
||||
ufw logging medium
|
||||
@@ -317,7 +319,6 @@ configure_docker_firewall() {
|
||||
step "Фильтрация опубликованных Docker-портов"
|
||||
cat >/etc/default/han-chat-docker-firewall <<EOF
|
||||
EXTERNAL_IF=${EXTERNAL_IF}
|
||||
PUBLIC_DOCKER_PORTS=${PUBLIC_DOCKER_PORTS}
|
||||
EOF
|
||||
|
||||
cat >/usr/local/sbin/han-chat-docker-firewall <<'FIREWALL'
|
||||
@@ -341,14 +342,10 @@ iptables -F HAN-CHAT-DOCKER
|
||||
iptables -A HAN-CHAT-DOCKER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
|
||||
iptables -A HAN-CHAT-DOCKER -i lo -j RETURN
|
||||
|
||||
IFS=',' read -ra ports <<<"$PUBLIC_DOCKER_PORTS"
|
||||
for port in "${ports[@]}"; do
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || {
|
||||
echo "Некорректный порт: $port" >&2
|
||||
exit 1
|
||||
}
|
||||
iptables -A HAN-CHAT-DOCKER -i "$external_if" -p tcp --dport "$port" -j RETURN
|
||||
done
|
||||
iptables -A HAN-CHAT-DOCKER -i "$external_if" -p tcp \
|
||||
-m conntrack --ctorigdstport 80 -j RETURN
|
||||
iptables -A HAN-CHAT-DOCKER -i "$external_if" -p tcp \
|
||||
-m conntrack --ctorigdstport 443 -j RETURN
|
||||
|
||||
# Блокируется только новый входящий трафик с внешнего интерфейса в Docker bridge.
|
||||
# Исходящий и межконтейнерный трафик этой цепочкой не затрагивается.
|
||||
@@ -384,7 +381,8 @@ EOF
|
||||
ExecStartPost=-/usr/local/sbin/han-chat-docker-firewall
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now han-chat-docker-firewall.service
|
||||
systemctl enable han-chat-docker-firewall.service
|
||||
systemctl restart han-chat-docker-firewall.service
|
||||
}
|
||||
|
||||
configure_ssh() {
|
||||
@@ -396,6 +394,9 @@ configure_ssh() {
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
PermitEmptyPasswords no
|
||||
AllowAgentForwarding no
|
||||
AllowTcpForwarding no
|
||||
X11Forwarding no
|
||||
MaxAuthTries 3
|
||||
ClientAliveInterval 120
|
||||
@@ -403,13 +404,13 @@ ClientAliveCountMax 2
|
||||
Port ${SSH_PORT}
|
||||
EOF
|
||||
if [[ "$HARDEN_SSH" == "true" ]]; then
|
||||
cat >>/etc/ssh/sshd_config.d/00-han-chat.conf <<'EOF'
|
||||
cat >>/etc/ssh/sshd_config.d/00-han-chat.conf <<EOF
|
||||
PermitRootLogin no
|
||||
AllowTcpForwarding no
|
||||
AllowUsers ${DEPLOY_USER} ${ADMIN_USER}
|
||||
EOF
|
||||
log "Расширенный SSH hardening включен: root-вход и TCP forwarding запрещены"
|
||||
log "Root SSH отключён; разрешены deploy и admin"
|
||||
else
|
||||
log "Базовый SSH hardening включен; root-вход и TCP forwarding не изменены"
|
||||
log "Root SSH пока не отключён: выполните двухфазную проверку"
|
||||
fi
|
||||
|
||||
rm -f /etc/ssh/sshd_config.d/99-han-chat.conf
|
||||
@@ -417,40 +418,45 @@ EOF
|
||||
systemctl reload ssh
|
||||
}
|
||||
|
||||
configure_application_security() {
|
||||
step "Безопасные HTTP-заголовки приложения"
|
||||
local env_file="${DEPLOY_DIR}/.env"
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
if grep -q '^NGINX_HSTS_MAX_AGE=' "$env_file"; then
|
||||
sed -i "s/^NGINX_HSTS_MAX_AGE=.*/NGINX_HSTS_MAX_AGE=${HSTS_MAX_AGE_SECONDS}/" "$env_file"
|
||||
else
|
||||
printf '\nNGINX_HSTS_MAX_AGE=%s\n' "$HSTS_MAX_AGE_SECONDS" >>"$env_file"
|
||||
fi
|
||||
chown "$DEPLOY_USER:$DEPLOY_USER" "$env_file"
|
||||
chmod 600 "$env_file"
|
||||
log "HSTS настроен на ${HSTS_MAX_AGE_SECONDS} секунд в ${env_file}"
|
||||
else
|
||||
log "Проект еще не настроен: HSTS будет взят из безопасного значения Compose по умолчанию"
|
||||
fi
|
||||
install_deploy_sudoers() {
|
||||
step "Exact sudoers для deploy"
|
||||
cat >/etc/sudoers.d/deploy <<'EOF'
|
||||
Cmnd_Alias HAN_VM1_UNITS = /usr/bin/systemctl start han-secrets@production.service, /usr/bin/systemctl restart han-secrets@production.service, /usr/bin/systemctl start han-stack@production.service, /usr/bin/systemctl restart han-stack@production.service, /usr/bin/systemctl stop han-stack@production.service
|
||||
Cmnd_Alias HAN_VM1_STATUS = /usr/bin/systemctl --no-pager status han-secrets@production.service, /usr/bin/systemctl --no-pager status han-stack@production.service, /usr/bin/journalctl --no-pager -u han-secrets@production.service, /usr/bin/journalctl --no-pager -u han-stack@production.service
|
||||
deploy ALL=(root) NOPASSWD: HAN_VM1_UNITS, HAN_VM1_STATUS
|
||||
EOF
|
||||
chmod 0440 /etc/sudoers.d/deploy
|
||||
visudo -cf /etc/sudoers.d/deploy >/dev/null || die "Некорректный sudoers"
|
||||
}
|
||||
|
||||
install_secret_loader_if_possible() {
|
||||
step "Загрузчик секретов"
|
||||
install_release_helpers_if_possible() {
|
||||
step "Root-owned helpers, TLS hook и units"
|
||||
local source_dir="${DEPLOY_DIR}/deployment/secrets"
|
||||
if [[ ! -f "${source_dir}/secrets_loader.py" || ! -f "${source_dir}/han-secrets" ]]; then
|
||||
log "Проект еще не скопирован: загрузчик секретов будет установлен при повторном запуске"
|
||||
local deployment="${DEPLOY_DIR}/deployment"
|
||||
local tls_group=han-nginx-tls tls_gid=11001
|
||||
if [[ ! -f "${DEPLOY_DIR}/docker-compose.yml" ||
|
||||
! -f "${source_dir}/secrets_loader.py" ||
|
||||
! -f "${deployment}/han-stack@.service" ]]; then
|
||||
log "Активный релиз отсутствует; повторите после root-активации"
|
||||
return
|
||||
fi
|
||||
chmod 0750 "${source_dir}/han-secrets" "${source_dir}/han-compose"
|
||||
|
||||
install -d -m 0700 -o root -g root \
|
||||
/etc/han \
|
||||
/etc/han/secrets \
|
||||
/etc/han/credentials
|
||||
install -d -m 0755 -o root -g root \
|
||||
/usr/local/lib/han-secrets \
|
||||
/usr/local/share/doc/han-secrets
|
||||
if find "$DEPLOY_DIR" -type l -print -quit | grep -q .; then
|
||||
die "Внутри активного release обнаружен symlink"
|
||||
fi
|
||||
[[ -x "${deployment}/preflight.sh" ]] || die "preflight.sh не executable"
|
||||
[[ -x "${deployment}/scripts/tls-deploy-hook.sh" ]] || die "TLS hook не executable"
|
||||
[[ -x "${source_dir}/han-compose" ]] || die "han-compose не executable"
|
||||
[[ -x "${source_dir}/han-secrets" ]] || die "han-secrets не executable"
|
||||
if getent group "$tls_group" >/dev/null; then
|
||||
[[ "$(getent group "$tls_group" | cut -d: -f3)" == "$tls_gid" ]] \
|
||||
|| die "han-nginx-tls имеет неожиданный GID"
|
||||
elif getent group "$tls_gid" >/dev/null; then
|
||||
die "GID ${tls_gid} уже занят"
|
||||
else
|
||||
groupadd --system --gid "$tls_gid" "$tls_group"
|
||||
fi
|
||||
install -d -m 0750 -o root -g "$tls_group" /var/lib/han-chat/public-tls
|
||||
install -d -m 0755 -o root -g root /usr/local/lib/han-secrets /usr/local/share/doc/han-secrets
|
||||
install -m 0750 -o root -g root \
|
||||
"${source_dir}/secrets_loader.py" \
|
||||
/usr/local/lib/han-secrets/secrets_loader.py
|
||||
@@ -459,56 +465,23 @@ install_secret_loader_if_possible() {
|
||||
/usr/local/lib/han-secrets/han-secrets
|
||||
install -m 0750 -o root -g root \
|
||||
"${source_dir}/han-compose" \
|
||||
/usr/local/bin/han-compose
|
||||
/usr/local/sbin/han-vm1-compose
|
||||
install -m 0644 -o root -g root \
|
||||
"${source_dir}/han-secrets@.service" \
|
||||
/etc/systemd/system/han-secrets@.service
|
||||
install -m 0644 -o root -g root \
|
||||
"${deployment}/han-stack@.service" \
|
||||
/etc/systemd/system/han-stack@.service
|
||||
install -d -m 0755 -o root -g root /etc/letsencrypt/renewal-hooks/deploy
|
||||
install -m 0755 -o root -g root \
|
||||
"${deployment}/scripts/tls-deploy-hook.sh" \
|
||||
/etc/letsencrypt/renewal-hooks/deploy/han-chat-nginx
|
||||
install -m 0644 -o root -g root \
|
||||
"${source_dir}/SELECTEL_RUNBOOK.ru.md" \
|
||||
/usr/local/share/doc/han-secrets/SELECTEL_RUNBOOK.ru.md
|
||||
if [[ ! -e /etc/han/secrets/production.selectel.json.example ]]; then
|
||||
install -m 0600 -o root -g root \
|
||||
"${source_dir}/config.example.json" \
|
||||
/etc/han/secrets/production.selectel.json.example
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
log "Загрузчик установлен, но не включен: сначала выполните SELECTEL_RUNBOOK.ru.md"
|
||||
}
|
||||
|
||||
install_ssl_timer_if_possible() {
|
||||
step "Таймер продления TLS"
|
||||
local renew_script="${DEPLOY_DIR}/deployment/scripts/ssl-renew.sh"
|
||||
if [[ ! -x "$renew_script" ]]; then
|
||||
log "Проект еще не скопирован: таймер TLS будет установлен при повторном запуске"
|
||||
return
|
||||
fi
|
||||
|
||||
cat >/etc/systemd/system/han-chat-ssl-renew.service <<EOF
|
||||
[Unit]
|
||||
Description=Renew HAN Chat TLS certificate
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=${DEPLOY_USER}
|
||||
WorkingDirectory=${DEPLOY_DIR}
|
||||
ExecStart=${renew_script}
|
||||
EOF
|
||||
|
||||
cat >/etc/systemd/system/han-chat-ssl-renew.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Run HAN Chat TLS renewal twice daily
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03,15:20:00
|
||||
RandomizedDelaySec=30m
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now han-chat-ssl-renew.timer
|
||||
systemctl enable certbot.timer
|
||||
log "Helpers установлены; application units не запущены"
|
||||
}
|
||||
|
||||
verify() {
|
||||
@@ -520,7 +493,21 @@ verify() {
|
||||
iptables -C DOCKER-USER -j HAN-CHAT-DOCKER 2>/dev/null \
|
||||
|| { log "FAIL: цепочка HAN-CHAT-DOCKER не подключена"; failed=1; }
|
||||
docker compose version >/dev/null || { log "FAIL: Compose недоступен"; failed=1; }
|
||||
[[ -d "$DEPLOY_DIR" ]] || { log "FAIL: отсутствует ${DEPLOY_DIR}"; failed=1; }
|
||||
if id -nG "$DEPLOY_USER" | tr ' ' '\n' |
|
||||
grep -Eq '^(docker|sudo|lxd|adm|systemd-journal)$'; then
|
||||
log "FAIL: deploy состоит в привилегированной группе"; failed=1
|
||||
fi
|
||||
id -nG "$ADMIN_USER" | tr ' ' '\n' | grep -qx sudo \
|
||||
|| { log "FAIL: admin не состоит в sudo"; failed=1; }
|
||||
[[ "$(stat -c '%U:%G:%a' "$INCOMING_DIR")" == deploy:deploy:750 ]] \
|
||||
|| { log "FAIL: неверные права incoming"; failed=1; }
|
||||
local external_if="${EXTERNAL_IF:-}"
|
||||
[[ -n "$external_if" ]] || external_if="$(ip -4 route show default | awk '{print $5; exit}')"
|
||||
for port in 80 443; do
|
||||
iptables -C HAN-CHAT-DOCKER -i "$external_if" -p tcp \
|
||||
-m conntrack --ctorigdstport "$port" -j RETURN 2>/dev/null \
|
||||
|| { log "FAIL: нет original host port ${port}"; failed=1; }
|
||||
done
|
||||
((failed == 0)) || die "Базовая проверка VM не пройдена"
|
||||
log "Базовая проверка VM пройдена"
|
||||
}
|
||||
@@ -529,35 +516,17 @@ summary() {
|
||||
step "Настройка VM завершена"
|
||||
cat <<EOF | tee -a "$LOG_FILE"
|
||||
|
||||
Пользователь развертывания: ${DEPLOY_USER}
|
||||
Каталог Compose: ${DEPLOY_DIR}
|
||||
Открытые порты: ${SSH_PORT}, 80, 443
|
||||
Парольный SSH/X11: отключены
|
||||
Локальные пароли: ${LOCK_ACCOUNT_PASSWORDS}
|
||||
HSTS max-age: ${HSTS_MAX_AGE_SECONDS}
|
||||
Лог настройки: ${LOG_FILE}
|
||||
Deploy: ${DEPLOY_USER} (без docker/sudo/lxd/adm/journal)
|
||||
Break-glass: ${ADMIN_USER} (sudo с отдельным паролем)
|
||||
Incoming: ${INCOMING_DIR}
|
||||
Активный release: ${DEPLOY_DIR}
|
||||
Public ingress: 80,443
|
||||
SSH ingress: ${SSH_PORT}/tcp из любой сети (временно)
|
||||
Root SSH hardening: ${HARDEN_SSH}
|
||||
Лог: ${LOG_FILE}
|
||||
|
||||
Следующие действия:
|
||||
1. Проверьте вход в новой SSH-сессии:
|
||||
ssh ${DEPLOY_USER}@<VM_IP>
|
||||
2. Скопируйте содержимое codebase/backend в:
|
||||
${DEPLOY_DIR}
|
||||
3. Поместите CA PostgreSQL:
|
||||
${DEPLOY_DIR}/secrets/pg/ca.pem
|
||||
4. Создайте только несекретный config:
|
||||
cd ${DEPLOY_DIR}
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
./scripts/validate-env .env
|
||||
5. Настройте Selectel, encrypted bootstrap credential и fallback map:
|
||||
deployment/secrets/SELECTEL_RUNBOOK.ru.md
|
||||
6. Выполняйте Compose только через:
|
||||
sudo deployment/secrets/han-compose <command>
|
||||
7. Продолжите с Gate 7 в:
|
||||
deployment/RUNBOOK.ru.md
|
||||
8. После копирования проекта повторно запустите этот скрипт для установки unit-файлов.
|
||||
|
||||
Важно: членство в группе docker начнет действовать после нового входа в систему.
|
||||
Продолжайте только по deployment/RUNBOOK.production.ru.md.
|
||||
Скрипт не запускал Compose и прикладные сервисы.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -570,7 +539,7 @@ main() {
|
||||
check_os
|
||||
update_system
|
||||
configure_time
|
||||
create_deploy_user
|
||||
create_host_roles
|
||||
configure_account_passwords
|
||||
configure_layout
|
||||
configure_swap
|
||||
@@ -581,9 +550,8 @@ main() {
|
||||
configure_unattended_upgrades
|
||||
configure_docker_firewall
|
||||
configure_ssh
|
||||
configure_application_security
|
||||
install_secret_loader_if_possible
|
||||
install_ssl_timer_if_possible
|
||||
install_deploy_sudoers
|
||||
install_release_helpers_if_possible
|
||||
verify
|
||||
summary
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user