Как боты положили мне Gitea и сожрали 92% RAM на VPS

В какой-то момент я обнаружил, что мой сервер, на котором запущена Gitea, стал потреблять много ресурсов. Захожу, смотрю free -h — а там:

Mem:  7.8Gi  7.2Gi used  368Mi free
Swap:    0B     0B    0B

368 мегабайт свободной памяти, свопа нет. Одна нагрузка — и OOM-killer начнёт убивать процессы. Полез разбираться.

Кто ест память?

docker stats --no-stream
КонтейнерRAMCPU
nexus2.44 GiB0.2%
gitea2.34 GiB173%
всё остальное~2.4 GiB

Nexus — JVM, 2.4 гига для него нормально, если не ограничивать heap. Но Gitea на 2.3 GiB и 173% CPU — это ненормально. Это Go-приложение, оно столько не должно есть.

Что происходит?

Смотрю логи Gitea:

GET /OpenSourceArk/mkdocs-material/compare/7.1.0...7.3.5  — 5148ms
GET /OpenSourceArk/mkdocs-material/compare/8.0.0b1...9.0.4 — 5897ms
GET /OpenSourceArk/mkdocs-material/commits/commit/...      — 190ms
GET /OpenSourceArk/mkdocs-material/blame/commit/...

Десятки разных IP из Вьетнама, Бразилии, Нигерии. Каждую секунду — новый запрос на compare, blame, commits. Это самые тяжёлые операции в git — сравнение деревьев между коммитами. Каждый такой запрос — 3-6 секунд CPU.

Мои публичные репозитории краулят боты. Причём не просто главную страницу, а именно тяжёлые эндпоинты.

Внутри контейнера Gitea активно выполняла множество git-процессов: read-tree, rev-list, cat-file, check-attr, batch-check. Всё это — следствие обработки запросов к compare и blame.

Что пробовал сначала

Отключение фоновых задач

Первым делом я заподозрил фоновые задачи Gitea — у меня было много зеркал с GitHub. Проверить можно через API:

curl -X GET "http://localhost:3000/api/v1/admin/cron" \
  -H "Authorization: token YOUR_ACCESS_TOKEN"

Отключил все задачи кроме критичных. На первый взгляд помогло, но через час Gitea снова потребляла 41% CPU и 14% RAM — при том, что пользователей не было.

Самодиагностика

Провёл автоматическую диагностику Gitea — все проверки прошли успешно. Проблема была не в самой Gitea.

Решение: защита от ботов

1. robots.txt

Первое и самое простое — попросить честных ботов не ходить куда не надо:

User-agent: *
Disallow: /*/compare/
Disallow: /*/blame/
Disallow: /*/commits/
Disallow: /*/raw/
Disallow: /*/rss/

Отдаю через Nginx location до proxy_pass — запрос даже не попадает в Gitea:

location = /robots.txt {
    alias /var/www/gitea/robots.txt;
    access_log off;
}

2. Rate limiting в Nginx

robots.txt — это просьба. Rate limiting — это закон.

limit_req_zone $binary_remote_addr zone=gitea:10m rate=5r/s;
 
# Тяжёлые эндпоинты — жёсткий лимит
location ~* /(compare|blame|commits|raw|rss)/ {
    limit_req zone=gitea burst=3 nodelay;
    limit_req_status 429;
    proxy_pass http://gitea:3000;
}
 
# Остальное — мягче
location / {
    limit_req zone=gitea burst=15 nodelay;
    limit_req_status 429;
    proxy_pass http://gitea:3000;
}

5 запросов в секунду на IP. Для тяжёлых страниц — burst всего 3 (4 запроса залпом и жди). Для обычных — burst 15, чтобы легитимные пользователи не страдали.

3. Memory limits в Docker

Даже если боты пролезут — контейнеры не сожрут всю память:

gitea:
  deploy:
    resources:
      limits:
        memory: 2G
 
nexus:
  deploy:
    resources:
      limits:
        memory: 1536M
  environment:
    - INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx1g -XX:MaxDirectMemorySize=256m

Для Nexus ещё ограничил JVM heap до 1 гига — по дефолту JVM забирает столько памяти, сколько может.

4. Swap как страховка

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Swap на VPS — не для постоянного использования, а чтобы при пиковой нагрузке система не убивала процессы, а начинала свопить.

Результат

МетрикаДоПосле
RAM used7.2 GiB3.5 GiB
RAM available578 MiB4.2 GiB
Gitea CPU173%26%
Gitea RAM2.34 GiB303 MiB
Nexus RAM2.44 GiB832 MiB

С 578 мегабайт свободных до 4.2 гигабайта. Gitea с 173% CPU упал до 26%.

Выводы

  1. Публичные git-репозитории краулят. Если у вас self-hosted Gitea/Gitlab/Gogs с публичными репами — проверьте логи. Compare и blame — это дорогие операции.

  2. robots.txt не спасает один. Агрессивные боты его игнорируют. Rate limiting на уровне reverse proxy — обязательно.

  3. Memory limits в Docker — не опция, а необходимость. Без них один контейнер может сожрать всю память на хосте.

  4. JVM-приложения без -Xmx — классическая ловушка. Nexus, Jenkins, SonarQube — все они по дефолту заберут 25-50% RAM.

  5. Swap на VPS. Многие хостеры не создают swap по умолчанию. 2 GiB swap file — 5 минут работы и страховка от OOM-kill.

Раунд 2: return 444 вместо rate limiting

Через год боты вернулись — на этот раз распределённый ботнет с тысячами уникальных IP. Каждый IP делает 1-2 запроса и уходит. Rate limiting и fail2ban оказались бесполезны: 431 match в fail2ban, 0 banned. Из 3.2 млн запросов только 13 получили 429.

Основная цель — src/commit/[hash], commits/commit/[hash], blame/commit/[hash], archive/[hash]. Нормальные пользователи ходят по веткам (/src/branch/main/), а не по хешам коммитов.

Решение — блокировка по паттерну URL с return 444 (Nginx закрывает соединение без ответа):

location ~* /(src|commits|blame|raw)/commit/[a-f0-9] {
    return 444;
}
location ~* /archive/[a-f0-9] {
    return 444;
}
location ~* /compare/ {
    return 444;
}

Также добавлена отдельная rate-limit зона для тяжёлых запросов:

limit_req_zone $binary_remote_addr zone=gitea_heavy:10m rate=1r/s;
МетрикаДоПосле
Gitea CPU136%12.7%
Gitea RAM2 GiB (лимит)306 MiB

Не ломает: просмотр по веткам/тегам, git clone/push/pull, API, скачивание релизов.