#!/usr/bin/env bash # glchat: аудит безопасности инстанса (AGENT.md 9.2, 9.6, 9.7). set -uo pipefail GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}" ENV_FILE="${GLCHAT_ETC_DIR}/.env" ISSUES=0 say_ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } say_warn() { printf ' \033[33m!\033[0m %s\n' "$*" ISSUES=$((ISSUES + 1)) } say_info() { printf ' · %s\n' "$*"; } printf '\n=== glchat security-check ===\n' printf '\n[SSH]\n' if sshd -T 2>/dev/null | grep -q '^passwordauthentication no'; then say_ok "вход по паролю запрещён" else say_warn "PasswordAuthentication включён (AGENT.md 9.2 требует ключи)" fi if sshd -T 2>/dev/null | grep -qE '^permitrootlogin (no|prohibit-password)'; then say_ok "root-логин ограничен" else say_warn "PermitRootLogin разрешён полностью" fi max_tries="$(sshd -T 2>/dev/null | awk '/^maxauthtries/ {print $2}')" if [ -n "$max_tries" ] && [ "$max_tries" -le 3 ]; then say_ok "MaxAuthTries=${max_tries}" else say_warn "MaxAuthTries=${max_tries:-unknown} (рекомендуется ≤ 3)" fi printf '\n[UFW]\n' if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q 'Status: active'; then say_ok "UFW активен" ufw status numbered 2>/dev/null | sed -n 's/^\[[ 0-9]*\]\s*/ · /p' | head -20 else say_warn "UFW не активен" fi printf '\n[DOCKER-USER]\n' if command -v iptables >/dev/null 2>&1 && iptables -nL DOCKER-USER >/dev/null 2>&1; then if iptables -nL DOCKER-USER | grep -q glchat-block-internal; then say_ok "внутренние порты приложения закрыты снаружи" else say_warn "нет правил glchat в DOCKER-USER" fi fi printf '\n[fail2ban]\n' if command -v fail2ban-client >/dev/null 2>&1; then jails="$(fail2ban-client status 2>/dev/null | sed -n 's/.*Jail list:\s*//p')" for jail in sshd glchat-auth recidive; do if printf '%s' "$jails" | grep -q "$jail"; then banned="$(fail2ban-client status "$jail" 2>/dev/null | sed -n 's/.*Currently banned:\s*//p')" say_ok "jail ${jail} (забанено: ${banned:-0})" else say_warn "jail ${jail} не активен" fi done else say_warn "fail2ban не установлен" fi printf '\n[Слушающие порты]\n' if command -v ss >/dev/null 2>&1; then ss -tulnH | awk '{print $1, $5}' | sort -u | while read -r proto addr; do case "$addr" in 127.0.0.1:* | [::1]:*) say_info "${proto} ${addr} (локальный)" ;; 0.0.0.0:* | [::]:* | *:*) say_info "${proto} ${addr}" ;; esac done fi printf '\n[Секреты и права]\n' if [ -f "$ENV_FILE" ]; then perms="$(stat -c '%a' "$ENV_FILE")" if [ "$perms" = "600" ]; then say_ok ".env: права 600" else say_warn ".env: права ${perms} (нужно 600)" fi for key in SESSION_PEPPER MASTER_KEY LIVEKIT_API_SECRET TOTP_ENCRYPTION_KEY WEBHOOK_SECRET; do value="$(grep -E "^${key}=" "$ENV_FILE" | cut -d= -f2- || true)" if [ -z "$value" ]; then say_warn "${key} не задан" elif [ "${#value}" -lt 32 ]; then say_warn "${key} короче 32 символов" else say_ok "${key} задан (${#value} символов)" fi done else say_warn "не найден ${ENV_FILE}" fi age_key="${GLCHAT_ETC_DIR}/data/backups/age-key.txt" if [ -f "$age_key" ]; then say_warn "приватный age-ключ всё ещё на сервере (${age_key}): сохраните его вне сервера и удалите" fi printf '\n[Обновления пакетов]\n' if command -v apt-get >/dev/null 2>&1; then upgradable="$(apt-get -s upgrade 2>/dev/null | grep -c '^Inst' || true)" say_info "пакетов с доступными обновлениями: ${upgradable}" fi if [ -f /var/run/reboot-required ]; then say_warn "требуется перезагрузка сервера (обновления ядра)" fi printf '\n=== Замечаний: %d ===\n\n' "$ISSUES" [ "$ISSUES" -eq 0 ]