#!/usr/bin/env bash
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}"
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
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
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
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
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
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
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
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
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
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
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
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