gitea_security_audit

 
#!/usr/bin/env bash
#===============================================================================
# GITEA SECURITY HARDENING AUDIT SCRIPT
# Версия: 1.0 | Дата: 2026-07-16
# Запуск: sudo bash gitea_security_audit.sh
# Результат: ничего не меняет, только читает конфигурацию
#===============================================================================

set -u

# ---------- Цвета и счётчики ----------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
PASS_COUNT=0; FAIL_COUNT=0; WARN_COUNT=0

pass() { ((PASS_COUNT++)); echo -e "  ${GREEN}[PASS]${NC} $1"; }
fail() { ((FAIL_COUNT++)); echo -e "  ${RED}[FAIL]${NC} $1"; }
warn() { ((WARN_COUNT++)); echo -e "  ${YELLOW}[WARN]${NC} $1"; }
header() { echo -e "\n${BLUE}=== $1 ===${NC}"; }

# Проверка прав запуска
if [[ $EUID -ne 0 ]]; then
    echo -e "${RED}[ERROR]${NC} Скрипт требует прав root. Запустите: sudo bash $0"
    exit 1
fi

echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║  GITEA SECURITY HARDENING AUDIT — $(date '+%Y-%m-%d %H:%M')  ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"

# ---------- 1. Пользователь git ----------
header "1. Проверка системного пользователя git"
GIT_SHELL=$(getent passwd git 2>/dev/null | cut -d: -f7)
if [[ -z "$GIT_SHELL" ]]; then
    fail "Пользователь 'git' не найден в системе"
elif [[ "$GIT_SHELL" == "/usr/sbin/nologin" || "$GIT_SHELL" == "/bin/false" ]]; then
    pass "Оболочка пользователя git: $GIT_SHELL (интерактивный вход запрещён)"
else
    fail "Оболочка пользователя git: $GIT_SHELL (должна быть nologin/false)"
fi

# ---------- 2. Сетевая безопасность / порты ----------
header "2. Проверка открытых портов"
if ss -tlnp 2>/dev/null | grep -q ":3000.*0\.0\.0\.0\|:3000.*:::"; then
    fail "Gitea (порт 3000) слушает на всех интерфейсах — должен быть только 127.0.0.1"
elif ss -tlnp 2>/dev/null | grep -q ":3000.*127\.0\.0\.1"; then
    pass "Gitea слушает только на 127.0.0.1:3000"
else
    warn "Порт 3000 не обнаружен (Gitea может быть остановлен)"
fi

if command -v ufw >/dev/null 2>&1; then
    UFW_STATUS=$(ufw status 2>/dev/null | head -1)
    if [[ "$UFW_STATUS" == *"active"* ]]; then
        pass "UFW (брандмауэр) активен"
    else
        fail "UFW не активен — сервер открыт"
    fi
else
    warn "UFW не установлен"
fi

# ---------- 3. Systemd hardening ----------
header "3. Проверка Systemd hardening (gitea.service)"
SERVICE_FILE="/etc/systemd/system/gitea.service"
if [[ ! -f "$SERVICE_FILE" ]]; then
    fail "Файл $SERVICE_FILE не найден"
else
    for directive in NoNewPrivileges ProtectSystem PrivateTmp; do
        if grep -qE "^${directive}=yes|^${directive}=strict|^${directive}=true" "$SERVICE_FILE"; then
            pass "$directive включён в gitea.service"
        else
            fail "$directive отсутствует в gitea.service"
        fi
    done
    if grep -q "ProtectHome" "$SERVICE_FILE"; then
        pass "ProtectHome настроен"
    else
        warn "ProtectHome не задан (рекомендуется read-only)"
    fi
fi

# ---------- 4. Права на конфигурацию Gitea ----------
header "4. Проверка прав на /etc/gitea"
if [[ -d /etc/gitea ]]; then
    DIR_PERMS=$(stat -c "%a" /etc/gitea)
    DIR_OWNER=$(stat -c "%U:%G" /etc/gitea)
    if [[ "$DIR_PERMS" == "750" || "$DIR_PERMS" == "770" ]] && [[ "$DIR_OWNER" == "root:git" ]]; then
        pass "Права на /etc/gitea: $DIR_PERMS, владелец: $DIR_OWNER"
    else
        fail "Небезопасные права на /etc/gitea: $DIR_PERMS, владелец: $DIR_OWNER (ожидается 750 root:git)"
    fi

    if [[ -f /etc/gitea/app.ini ]]; then
        INI_PERMS=$(stat -c "%a" /etc/gitea/app.ini)
        INI_OWNER=$(stat -c "%U:%G" /etc/gitea/app.ini)
        if [[ "$INI_PERMS" == "640" || "$INI_PERMS" == "600" ]] && [[ "$INI_OWNER" == "root:git" ]]; then
            pass "Права на app.ini: $INI_PERMS, владелец: $INI_OWNER"
        else
            fail "Небезопасные права на app.ini: $INI_PERMS (ожидается 640 root:git)"
        fi
    fi
fi

# ---------- 5. Настройки безопасности в app.ini ----------
header "5. Проверка app.ini (секция [security] и [picture])"
APP_INI="/etc/gitea/app.ini"
if [[ -f "$APP_INI" ]]; then
    check_ini() {
        local section="$1" key="$2" expected="$3"
        local value
        value=$(awk -v s="[$section]" -v k="$key" '
            $0==s {in_section=1; next}
            /^\[/ {in_section=0}
            in_section && $0 ~ "^"k" *= *" {gsub(/ *= */,"=", $0); split($0,a,"="); print a[2]; exit}
        ' "$APP_INI" | tr -d ' ')
        if [[ "$value" == "$expected" ]]; then
            pass "[$section] $key = $value"
        elif [[ -z "$value" ]]; then
            warn "[$section] $key не задан (рекомендуется: $expected)"
        else
            fail "[$section] $key = $value (ожидается: $expected)"
        fi
    }
    check_ini "security" "DISABLE_REGISTRATION" "true"
    check_ini "security" "COOKIE_SECURE" "true"
    check_ini "security" "ENABLE_2FA" "true" 2>/dev/null || true
    check_ini "picture" "DISABLE_GRAVATAR" "true"
    check_ini "picture" "ENABLE_FEDERATED_AVATAR" "false"
else
    fail "Файл $APP_INI не найден (Gitea не был установлен через веб?)"
fi

# ---------- 6. MariaDB пользователь gitea ----------
header "6. Проверка пользователя БД gitea"
MY_CNF="/home/cloudpanel/.my.cnf"
if [[ -f "$MY_CNF" ]]; then
    DB_EXISTS=$(mysql -u root -e "SELECT User,Host FROM mysql.user WHERE User='gitea';" 2>/dev/null | grep -c gitea)
    if [[ "$DB_EXISTS" -ge 1 ]]; then
        HOST=$(mysql -u root -N -e "SELECT Host FROM mysql.user WHERE User='gitea' LIMIT 1;" 2>/dev/null)
        if [[ "$HOST" == "localhost" || "$HOST" == "127.0.0.1" ]]; then
            pass "Пользователь gitea привязан к $HOST"
        else
            fail "Пользователь gitea привязан к '$HOST' (должен быть localhost)"
        fi
    else
        fail "Пользователь БД 'gitea' не найден"
    fi
else
    warn "Не найден $MY_CNF — не удалось проверить БД автоматически"
fi

# ---------- 7. Nginx reverse proxy ----------
header "7. Проверка Nginx конфигурации"
NGINX_CONF=$(find /etc/nginx/sites-available /etc/nginx/conf.d -name "*gitea*" 2>/dev/null | head -1)
if [[ -n "$NGINX_CONF" ]]; then
    pass "Конфиг Nginx для Gitea найден: $NGINX_CONF"
    if grep -q "server_tokens off" /etc/nginx/nginx.conf "$NGINX_CONF" 2>/dev/null; then
        pass "server_tokens off (версия Nginx скрыта)"
    else
        warn "server_tokens не отключён — версия Nginx видна"
    fi
    if grep -q "proxy_pass.*127.0.0.1:3000" "$NGINX_CONF"; then
        pass "proxy_pass настроен на 127.0.0.1:3000"
    else
        fail "proxy_pass не указывает на локальный порт 3000"
    fi
    if grep -q "Strict-Transport-Security" "$NGINX_CONF"; then
        pass "HSTS заголовок настроен"
    else
        warn "HSTS не настроен"
    fi
else
    fail "Конфиг Nginx для Gitea не найден"
fi

# ---------- 8. SSL сертификат ----------
header "8. Проверка SSL сертификата"
DOMAIN=$(grep -hoE "server_name [^;]+" /etc/nginx/sites-available/*gitea* 2>/dev/null | awk '{print $2}' | head -1)
if [[ -n "$DOMAIN" ]]; then
    if command -v openssl >/dev/null 2>&1; then
        EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
        if [[ -n "$EXPIRY" ]]; then
            EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null)
            NOW_EPOCH=$(date +%s)
            DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
            if [[ "$DAYS_LEFT" -gt 30 ]]; then
                pass "SSL сертификат для $DOMAIN действителен ещё $DAYS_LEFT дней"
            elif [[ "$DAYS_LEFT" -gt 0 ]]; then
                warn "SSL сертификат истекает через $DAYS_LEFT дней"
            else
                fail "SSL сертификат ПРОСРОЧЕН!"
            fi
        fi
    fi
else
    warn "Не удалось определить домен Gitea для проверки SSL"
fi

# ---------- 9. SSH конфигурация ----------
header "9. Проверка SSH-конфигурации"
if grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config 2>/dev/null; then
    pass "SSH: вход по паролю отключён"
else
    warn "SSH: вход по паролю не отключён явно"
fi
if grep -qE "^Match User git" /etc/ssh/sshd_config 2>/dev/null; then
    pass "SSH: есть Match User git (ограничение команд git-пользователя)"
else
    warn "SSH: Match User git не настроен (рекомендуется для ограничения команд)"
fi

# ---------- 10. Fail2ban ----------
header "10. Проверка Fail2ban"
if systemctl is-active --quiet fail2ban 2>/dev/null; then
    BANNED=$(fail2ban-client status 2>/dev/null | grep -c "Currently banned" || echo 0)
    pass "Fail2ban активен"
    if [[ "$BANNED" -gt 0 ]]; then
        warn "Заблокировано IP-адресов: $BANNED (проверьте легитимность)"
    fi
else
    warn "Fail2ban не запущен (рекомендуется установить)"
fi

# ---------- 11. Версия Gitea ----------
header "11. Проверка версии Gitea"
if [[ -x /usr/local/bin/gitea ]]; then
    CURRENT_VER=$(/usr/local/bin/gitea --version 2>/dev/null | awk '{print $3}')
    pass "Установленная версия Gitea: $CURRENT_VER"
    warn "Сравните с актуальной на https://dl.gitea.com/gitea/ — обновляйтесь ежемесячно"
else
    fail "Бинарник /usr/local/bin/gitea не найден или не исполняемый"
fi

# ---------- 12. Подозрительная активность ----------
header "12. Проверка на наличие подозрительной активности"
SUSPECT_CRON=$(find /var/spool/cron/crontabs /etc/cron.d 2>/dev/null -type f -exec grep -lE "curl.*\|.*wget.*\|nc -e\|bash -i" {} \; 2>/dev/null | wc -l)
if [[ "$SUSPECT_CRON" -eq 0 ]]; then
    pass "Подозрительных cron-задач не обнаружено"
else
    fail "Найдено подозрительных cron-задач: $SUSPECT_CRON — требуется ручной аудит!"
fi

LISTENING=$(ss -tlnp 2>/dev/null | awk 'NR>1 {print $4}' | grep -vE ":(22|80|443|3000|3306)$" | wc -l)
if [[ "$LISTENING" -eq 0 ]]; then
    pass "Нет неожиданных слушающих портов"
else
    warn "Обнаружено $LISTENING дополнительных слушающих портов — проверьте вручную (ss -tlnp)"
fi

# ---------- Итоговый отчёт ----------
echo -e "\n${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║                    ИТОГОВЫЙ ОТЧЁТ                          ║${NC}"
echo -e "${BLUE}╠══════════════════════════════════════════════════════════════╣${NC}"
echo -e "║  ${GREEN}PASS: $PASS_COUNT${NC}                                                   ║"
echo -e "║  ${YELLOW}WARN: $WARN_COUNT${NC}                                                   ║"
echo -e "║  ${RED}FAIL: $FAIL_COUNT${NC}                                                   ║"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"

if [[ "$FAIL_COUNT" -eq 0 && "$WARN_COUNT" -eq 0 ]]; then
    echo -e "${GREEN}✓ Отличная работа! Сервер соответствует всем требованиям hardening.${NC}"
    exit 0
elif [[ "$FAIL_COUNT" -eq 0 ]]; then
    echo -e "${YELLOW}⚠ Критических проблем нет, но есть рекомендации к улучшению.${NC}"
    exit 0
else
    echo -e "${RED}✗ Обнаружены критические проблемы безопасности. Требуется исправление!${NC}"
    exit 1
fi

Share your love