@@ -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_DI R " = = /* ] ] || die "DEPLOY_DIR должен быть абсолютным путем "
[ [ " $DEPLOY_USER " = = deploy ] ] || die "Production unit/sudoers требуют DEPLOY_USER=deploy "
[ [ " $ADMIN_USE R " = = 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_load er_if_possible( ) {
step "Загрузчик секретов "
install_release_help ers _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/s bin/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_DI R " ] ] || { log " FAIL: отсутствует ${ DEPLOY_DIR } " ; failed = 1 ; }
if id -nG " $DEPLOY_USE R " | 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_load er_if_possible
install_ssl_timer_if_possible
install_deploy_sudoers
install_release_help ers _if_possible
verify
summary
}