Контракт в репозитории достаёт только агентов, у которых этот репозиторий
есть. Astra работает на fr1 как tgclaude и infra у неё не склонирован, так
что до неё не дошло бы ничего.
Заведено на сервере:
~rus/CLAUDE.md и ~tgclaude/CLAUDE.md — указатель на репо, правило и пять
граблей, стоивших больше всего; AGENTS.md рядом симлинком, для агентов с
другой конвенцией. Claude Code читает CLAUDE.md вверх по дереву, поэтому
файл в хоме работает и при работе в ~tgclaude/imgbot.
~rus/MyVault/TOOLS.md, раздел SSH — то же для Astra. Файл смонтирован в
claude-channels как /vault, изменение видно агенту сразу, в Gitea уезжает
кроном каждые 5 минут.
MOTD и .bashrc сознательно не использованы: .bashrc выходит на строке 6 для
неинтерактивных шеллов, MOTD показывается только при интерактивном входе, а
агенты обычно дёргают `ssh host 'команда'` и не увидят ни того, ни другого.
Ограничения механизма и причина, по которой детектор дрейфа не заведён,
описаны в servers/fr1.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Сверка fr1 показала, что дока расходится с фактом по десятку пунктов —
значит нужен не разовый проход, а контракт и инструмент.
CLAUDE.md — правило «тронул сервер, обнови servers/<хост>.md в том же
прогоне» плюс стиль: факт + причина + что сломается молча. Лежит именно
в CLAUDE.md, потому что AGENTS.md автоматически агенту не подгружается.
.claude/skills/server-audit/ — метод сверки и накопленные грабли: где
дока врёт чаще всего, почему ufw не отвечает на вопрос об открытых
портах, почему нужен up -d --dry-run, почему «executed successfully»
не доказывает работу задания.
scripts/snapshot.sh — инвентаризация живого состояния за один проход:
только чтение, без sudo, секретов не печатает, недоступное под root
помечает и подсказывает команду.
Первый же прогон скрипта нашёл две вещи, обе описаны:
- IPv6 не «починился», а мигает: за один день FAILED -> REACHABLE ->
FAILED без единой правки конфига. Описан как мигающий, с выводом
ничего на него не завязывать.
- В основном sshd_config стоит PermitRootLogin yes (правка шаблона
Contabo). Root-вход закрыт только потому, что Include дроп-инов
стоит выше, а sshd берёт первое значение. Уберёшь дроп-ин —
включится.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Дока расходилась с фактом по десятку пунктов. Что исправлено по
результатам обхода сервера:
- ядро 6.8.0-137 (было 134); IPv6 заработал — шлюз отвечает на NDP,
хотя конфиг не менялся, так что может отвалиться обратно
- Gitea 1.27.3, 1.9 ГБ, 20 репо (было 1.25.4 / 4.3 ГБ / 16)
- апекс ruzzy.dev уже на fr1; появились vhost'ы share и www
- мониторинг шире описанного: таргеты ru1 и job gitea, дашборды
OpenVPN и Gitea; убраны устаревшие упоминания выведенного de1
- vault-синк claude-channels работает (дока говорила обратное), том
claude-home раздулся заново до 2.8 ГБ
Добавлены разделы, которых не было вовсе:
- tgclaude — общий вход для агентов; цепочка Telegram-бот → агент в
контейнере → SSH → группа docker → root на хосте
- imgbot и его переезд в контейнеры
- расписания: полная инвентаризация cron и таймеров
- открытые хвосты, включая непогашенные токены в Gitea
Разобрано и описано попутно: ufw не управляет docker-портами (ipcam
месяцами держал 2221 и 30000-30010 наружу без единого правила);
sysstat хранил историю 7 дней при HISTORY=7 — поднято до 365 (при >28
sa1 сам переключает имена на saYYYYMMDD, иначе смысла нет); /etc/cron.
hourly/free час за часом выбрасывал страничный кэш (до -3.89 ГиБ,
подтверждено по Prometheus) — удалён.
Уборка диска: 104 → 52 ГБ занято. Ушли ipcam, sc2-parser, booty,
booty-collector, vacuum-scraper, ollama (8.1 ГБ), легаси-архив
картинок, docker build cache (43.4 ГБ).
В AGENTS.md поправлены две неверные строки: de1 значился действующей
прод-нодой, хотя выведен, а туннель ru1 — идущим до de1. Сами
servers/de1.md и servers/ru1.md не сверялись.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Проект агента (~tgclaude/imgbot) работал тремя голыми python-процессами,
запущенными руками из SSH-сессии: без юнита, без restart-политики, при
Linger=no. Держались только на KillUserProcesses=no и не пережили бы
перезагрузку, причём молча.
Один образ на базе playwright/python:v1.62.0-noble (версия пакета
playwright обязана совпадать с тегом, иначе разъезжается ревизия
chromium), два сервиса:
- imgbot-daemon — APScheduler: refresh 30 мин, analyze 5 мин
- imgbot-http — объединил прежние mcp-http (8765) и file-server
(8766): MCP как под-приложение Starlette плюс маршрут /photo/<id>
Неочевидное, из-за чего это ломается молча (в compose есть комментарии):
- данные монтируются в тот же путь, что на хосте: images.file_path в
SQLite абсолютный, и его читают обе стороны
- корней хранения может быть больше одного — проверять все префиксы
file_path, а не только data_dir
- HOME обязателен: collector.py вызывает Path.home(), а uid 1001 не
имеет записи в /etc/passwd образа
- у MCP своя защита от DNS-rebinding: без IMGBOT_ALLOWED_HOSTS отдаёт
421 на любой Host кроме localhost
Порт не публикуется на хост, сервис доступен из общей docker-сети с
claude-channels — агент наконец ходит по MCP, а не по SSH.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ipcam/FTPS-приёмник удалён с fr1 целиком: контейнер, образ, том, сеть,
каталоги /srv/ipcam и /srv/ftps, deploy-хук certbot и сертификат
cam.ruzzy.dev. Видео так и не начали складываться — на момент удаления
в /srv/ipcam было 0 файлов.
Пин образа Gitea в стеке держал 1.25.4, тогда как работает 1.27.3
(обновлён 2026-08-31). Любой `docker compose up -d` откатил бы сервис
на две минорные версии назад.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:26:00 +03:00
12 changed files with 659 additions and 59 deletions
description:Сверить фактическое состояние сервера с его описанием в servers/*.md и обновить описание. Использовать после любых работ на сервере (поднял/погасил сервис, поменял конфиг, удалил каталог, завёл ключ), а также когда просят проверить, соответствует ли дока реальности, актуализировать описание хоста или разобраться, что на сервере вообще происходит.
---
# Сверка описания сервера с фактом
Цель — чтобы `servers/<хост>.md` описывал сервер таким, какой он есть, и
чтобы по нему можно было принимать решения. Правила оформления — в
`CLAUDE.md` репозитория, здесь метод и грабли.
## Порядок
1.**Собери факт до чтения доки.** Сначала `./scripts/snapshot.sh <хост>`,
потом читай описание. Наоборот — начнёшь подгонять наблюдения под текст.
2.**Сверяй по пунктам, не по впечатлению.** Прогоняй утверждения доки
через факт по одному: версии, размеры, пути, порты, «включено/выключено».
При сверке fr1 2026-09-08 неверными оказались десять утверждений, включая
«IPv6 не работает» про работавший IPv6 и версию Gitea на два минора назад.
3.**Добери то, что требует root** — snapshot это помечает. Неинтерактивный
sudo не проходит: нужен `ssh -t <хост> 'sudo ...'`, то есть попроси
пользователя выполнить, а не пытайся обойти.
4.**Правь описание в том же прогоне.** Не откладывай на «потом опишу».
5.**Незакрытое — в «Открытые хвосты»** файла хоста. Токен, который не
удалось отозвать; права, которые не поправил; решение, которое за
пользователя не принимаешь.
## Где дока врёт чаще всего
- **Версии и размеры** — меняются молча при обновлении контейнера. Смотри
фактический тег у запущенного контейнера, а не запись в compose: они
расходятся, и тогда `docker compose up -d` откатывает сервис.
- **«Не работает»** — могло починиться на стороне провайдера. И наоборот:
IPv6 на fr1 за один день был `FAILED`, потом `REACHABLE`, потом снова
`FAILED` — такое надо описывать как мигающее, а не как состояние.
- **Открытые порты.** `ufw status` не отвечает на вопрос «что торчит
наружу»: docker публикует порты в iptables напрямую, минуя ufw. Только
`ss -tlnH` + `docker ps`. Непубликуемый порт контейнера всё равно
доступен с хоста по адресу контейнера — «внутренняя сеть» не периметр.
- **Процессы вне контейнеров.** `docker ps` их не покажет, а они не
переживут перезагрузку. Ищи `ps`с`PPID=1` и чужими пользователями.
- **Кроны других пользователей** — в `docker ps` и в `crontab -l` их нет.
Осиротевшее задание может годами падать молча: MTA не установлен, вывод
крона теряется.
- **Планы, записанные как факт.** «Планируется перенести DNS» при уже
перенесённом DNS. Проверяй раздел «планируется» отдельно.
## Грабли, которые стоили времени (не повторяй)
- **Пути в БД абсолютные.** Прежде чем контейнеризовать сервис, проверь
**все** префиксы путей в его базе, а не только основной каталог данных:
`select substr(file_path,1,30), count(*) from images group by 1;`
Монтируй в тот же путь, что на хосте, иначе часть данных пропадёт молча.
- **`up -d --dry-run` перед любым compose-действием.** Он показывает, что
compose собирается сделать, — например поднять забытый контейнер того же
проекта и словить конфликт портов.
- **Задание может глушить свои ошибки.** У imgbot `analyze` логирует
исключение и возвращается нормально, поэтому планировщик пишет
`executed successfully` после падения. Отсутствие ошибок не доказывает
работу — проверяй результат, а не статус.
- **Ретенция.** journald был на 500M (11 дней), sysstat на `HISTORY=7`.
Сервер, где разбирают аварии, обязан хранить историю дольше, чем живёт
проблема. У sysstat при `HISTORY>28` имена переключаются на
`saYYYYMMDD` — иначе поднятие числа бессмысленно.
- **Группа `docker` = root.** Пользователь без sudo, но в группе docker,
монтирует `/` в контейнер и делает что угодно. Учитывай при описании
доступа.
## Проверка результата
Перед отчётом прогони по получившемуся файлу: каждое ⚠️ подтверждено
наблюдением? у каждого изменения указана дата и прежнее значение? не
попали ли в текст секреты? если роль хоста изменилась — поправлена ли
- [`servers/de1.md`](servers/de1.md) — `ruzzy.dev` (213.136.74.247). Действующая прод-нода: прокси/VPN (telemt, AmneziaWG, OpenVPN), gitea, openclaw, боты. Источник для переноса.
- [`servers/de1.md`](servers/de1.md) — 213.136.74.247. **Выведен** (все сервисы остановлены, метрики удалены): был прокси/VPN-нодой (telemt, AmneziaWG, OpenVPN) плюс gitea, openclaw, боты — всё уехало на fr1 или в бэкапы. ⚠️ Сам файл описания под вывод не переписан.
pro status --format json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print("Ubuntu Pro:", d["account"]["name"]); [print(" ",s["name"],s["status"]) for s in d["services"] if s["status"]=="enabled"]' 2>/dev/null
**ОС:** Ubuntu 24.04.4 LTS · kernel 6.8.0-137-generic (с 2026-08-07; ранее 6.8.0-134 с 2026-07-31, до того 6.8.0-106)
**Ресурсы:** 8 vCPU / 23 GB RAM / 387 GB
**Ресурсы:** 8 vCPU / 23 GB RAM / 387 GB
**hostname:**`fr1` (FQDN `fr1.ruzzy.dev` через `/etc/hosts`)
**hostname:**`fr1` (FQDN `fr1.ruzzy.dev` через `/etc/hosts`)
---
---
## Пользователи и доступ
## Пользователи и доступ
- Пользователь `rus` (uid 1000, группы `rus`, `sudo`, `users`), sudo по паролю.
- Пользователь `rus` (uid 1000, группы `rus`, `sudo`, `users`, `docker`, `tgclaude`), sudo по паролю.
- Пользователь `tgclaude` (uid 1001, группы `tgclaude`, `users`, `docker`) — аккаунт агента **Astra**, вход по SSH разрешён, sudo нет. `/home/tgclaude` — **21 ГБ**, крупнейший потребитель диска. Подробно — в разделе «Пользователь `tgclaude`».
- ⚠️ **Группа `docker` = root-эквивалент.**`docker:x:988:rus,tgclaude` — любой её член монтирует `/` в контейнер и делает что угодно от рута (этим же способом чистились root-овы файлы 2026-09-08). Поэтому «у `tgclaude` нет sudo» **не** означает ограничения прав на хосте.
- Вход с Mac: `ssh rus-161` (алиас в `~/.ssh/config`) → ключ `~/.ssh/id_161_97_93_252` (ed25519).
- Вход с Mac: `ssh rus-161` (алиас в `~/.ssh/config`) → ключ `~/.ssh/id_161_97_93_252` (ed25519).
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
⚠️ В основном `/etc/ssh/sshd_config` строкой 42 стоит `PermitRootLogin yes` (правка шаблона Contabo). Побеждает всё равно дроп-ин: `Include /etc/ssh/sshd_config.d/*.conf` стоит строкой 12, а sshd берёт **первое** встреченное значение. То есть root-вход закрыт, но держится на порядке строк — уберёшь дроп-ин, и root-вход по ключу включится. ⏳ Рядом лежит `50-cloud-init.conf`, который из-под `rus` не читается: под root стоит проверить `sshd -T | grep -i permitrootlogin`.
```
```
PermitRootLogin no
PermitRootLogin no
PasswordAuthentication no
PasswordAuthentication no
@@ -22,18 +26,34 @@ PubkeyAuthentication yes
KbdInteractiveAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
ChallengeResponseAuthentication no
MaxAuthTries 3
MaxAuthTries 3
AllowUsers rus
AllowUsers rus tgclaude
```
```
Через socket-activation (`ssh.socket`).
Через socket-activation (`ssh.socket`).
## Сеть
- Управляется **netplan → systemd-networkd**, конфиг `/etc/netplan/50-cloud-init.yaml` (наследие cloud-init, сейчас просто статический файл).
- Матч интерфейса **по MAC `00:50:56:64:ff:15`** + `set-name: eth0`. Ядро называет карту `ens18`, netplan переименовывает её в **`eth0`** через сгенерированный `/run/systemd/network/10-netplan-eth0.link`.
- IPv4 `161.97.93.252/18`, GW `161.97.64.1`. DNS `195.179.224.53` / `209.126.15.53`.
- ⚠️ IPv6 `2a02:c207:2338:7709::1/64`, дефолт `via fe80::1 onlink` — **мигает, а не «работает» или «не работает»**. Хронология одного дня, 2026-09-08: утром `fe80::1` в `ip -6 neigh` был `REACHABLE` и `ping6` наружу шёл без потерь (до этого месяцами `FAILED`); к 15:50 снова `FAILED`, `ping6` — 100% потерь. Конфиг на сервере не менялся ни разу, то есть причина целиком на стороне Contabo. Практический вывод: **не завязывать ничего на IPv6 этого хоста** и не считать очередное «заработало» починкой. IPv4 при этом не страдает.
- ⚠️ **На имя `eth0` завязано** NAT-правило AmneziaWG (`-o eth0 MASQUERADE` в `wg0.conf`) и правила ufw. Если netplan не отработает, интерфейс останется `ens18` и VPN-выход молча сломается.
- **Страховка `net-rescue`** (заведена 2026-08-01): `/usr/local/sbin/net-rescue.sh` + `net-rescue.timer` (`OnBootSec=150`, enabled). Идемпотентно находит карту по MAC — независимо от её имени — и доставляет адрес, дефолтный маршрут и DNS, если их нет. Пишет в journal тегом `net-rescue`. Смысл: пережить повторение аварии 2026-07-31 без похода в VNC.
- **Третий механизм правки маршрута — `/etc/cron.d/staticroute`** (из образа Contabo, не наш): `@reboot root ip route replace $(link-scope сеть eth0) via $(дефолтный GW) dev eth0`. Аргументы вычисляет на ходу из текущей таблицы, поэтому если адреса на eth0 нет вообще — тихо ничего не делает. Дублирует задачу `net-rescue`, но вслепую и без привязки к MAC. Знать о нём надо: при разборе «кто поправил маршрут» это третий кандидат после netplan и net-rescue.
- cloud-init установлен, но сетевой конфиг ему запрещён: `/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg` (`network: {config: disabled}`) — иначе перезапишет netplan.
## DNS
## DNS
- Зона `ruzzy.dev` — Namecheap (NS `dns1/dns2.registrar-servers.com`).
- Зона `ruzzy.dev` — Namecheap (NS `dns1/dns2.registrar-servers.com`).
-`fr1.ruzzy.dev` A → 161.97.93.252.
-`fr1.ruzzy.dev` A → 161.97.93.252. Апекс `ruzzy.dev` A → 161.97.93.252 (переведён с de1).
-`9100/tcp` — только `from 172.16.0.0/12` (node-exporter для Prometheus в docker).
- Роутинг туннеля: `allow FWD wg0 ↔ eth0` (в обе стороны) + `allow in on wg0`.
- Плюс динамические REJECT-правила от fail2ban.
- ⚠️ **ufw НЕ управляет docker-портами.** Публикацию `ports: - "X:Y"` docker вписывает в iptables напрямую, минуя цепочки ufw: порт открыт в мир независимо от `deny incoming` и в `ufw status` не виден. Проверено 2026-09-08 — ipcam-FTPS месяцами держал наружу `2221` и `30000-30010`, правил ufw для них не было вообще. Список выше описывает **только host-сервисы**; чтобы контейнер не светил порт, биндить надо на `127.0.0.1:` (как у grafana/prometheus/alertmanager/gitea-web). Аудит открытого наружу — `ss -tlnH` + `docker ps`, а не `ufw status`.
- livepatch приходит **снапом**`canonical-livepatch` (v11.0.2, rev 414); рядом только `core22` и `snapd` 2.76.3 — больше снапов на хосте нет. Состояние: `canonical-livepatch status` → ядро `6.8.0-137.137-generic`, `patchState: nothing-to-apply`, tier `updates`, поддержка этого ядра до **2027-08-17** (`upgradeRequiredDate` — после этой даты livepatch потребует апгрейда ядра).
- ⚠️ snapd обновляет себя и livepatch **сам, 4 раза в сутки, без окна обслуживания**: `snap refresh --time` → `timer: 00:00~24:00/4`, holds не выставлены. Это отдельный от apt канал изменений на хосте, и отчёт в Telegram про него не приходит (`uu-telegram-report.sh` висит только на apt).
`SystemMaxUse=8G`, `MaxRetentionSec=1year`. **Было `SystemMaxUse=500M`** — журнал упирался в потолок и хранил всего ~11 дней, из-за чего при разборе аварии логи за 15 июля уже не существовали.
- История bash (в `.bashrc`у`rus` и у`root`): `HISTTIMEFORMAT='%F %T '`, `HISTSIZE=100000`, `HISTFILESIZE=200000`,
`HISTCONTROL=ignoredups` (было `ignoreboth` — команды с ведущим пробелом исчезали бесследно),
`PROMPT_COMMAND="history -a"` — сброс после каждой команды, чтобы жёсткий ресет не съедал сессию.
- ⚠️ Записи в `~/.bash_history`с датой **`1970-01-01` — это легаси до 2026-08-01**, реального времени у них нет. Проставлен сентинел `#1` перед каждой такой строкой, иначе bash рисовал бы им текущее время и вводил в заблуждение при разборе.
- rsyslog дублирует auth/syslog в `/var/log/*.log` (ротация ~4 недели) — это спасло разбор, когда journald уже вычистил июль.
- **sysstat / `sar`** — исторические CPU, память, диски, сеть с шагом 10 минут. Собирает `sysstat-collect.timer` (`OnCalendar=*:00/10` → `/usr/lib/sysstat/sa1`), бинарные срезы в `/var/log/sysstat/saNN` (NN — день месяца), ежедневный текстовый отчёт `sarNN` от `sysstat-summary.timer`. Читать: `sar -u -f /var/log/sysstat/sa07` (CPU), `-r` память, `-d` диски, `-n DEV` сеть. Ценность именно в том, что пишет **системный таймер, а не контейнер**: данные остаются, даже когда лежал сам docker с Prometheus.
- **Ретенция sysstat — пересобрана 2026-09-08.** Было `HISTORY=7`: история жила 7–8 дней, файлы затирались по кругу номером дня месяца — ровно та же ловушка, что была с journald на 500M (при разборе аварии месячной давности загрузки уже нет, а sysstat держат именно за этим). Стало **`HISTORY=365`**, `ENABLED="true"` в `/etc/default/sysstat`, мёртвый `/etc/cron.d/sysstat` удалён.
- Важная механика: при `HISTORY > 28` скрипт `/usr/lib/sysstat/sa1` сам добавляет `sadc -D`, и имена файлов переключаются с `saDD` на **`saYYYYMMDD`** — без этого поднятие HISTORY было бы бесполезным, срезы всё равно перезаписывались бы каждый месяц. Первый датированный файл появляется на следующем срабатывании таймера. Чистку и сжатие делает `sa2` (`find -mtime +HISTORY` → удаление, `+COMPRESSAFTER=10` → xz); при старом `HISTORY=7` сжатие не срабатывало никогда, теперь работает.
- Старые файлы эры 7 дней (`sa31`, `sa01…sa08`) остаются рядом ещё год и читаются как обычно: `sar -u -f /var/log/sysstat/sa07`.
- Почему `/etc/cron.d/sysstat` удалён без сожалений: оба его правила искали `debian-sa1`, которого в Ubuntu 24.04 нет (бинарь называется `sa1`) — то есть это был no-op. Сбором всё время занимался systemd-таймер, дублей в данных не было (срезы ровно в :00/:10/:20…). Флаг `ENABLED="false"` там же был легаси и на таймер не влиял — выставлен в `true` для соответствия факту.
- **Известные дыры:** команды без sudo (`docker stop`, `mpstat`, …) не фиксируются нигде; `sudo su` — слепая зона. `snoopy`/`auditd` и `log_output` в sudoers сознательно НЕ ставили.
- ⚠️ **Открытый баг:**`rsyslogd` периодически валится в `action 'action-0-builtin:omfile' suspended` и в эти окна теряет строки. Наблюдался и 25.07, и перед падением 31.07. Не чинилось.
## Docker
## Docker
- Docker 29.6 + Compose v5.1 (официальный репозиторий). `rus` в группе docker.
- Docker 29.6 + Compose v5.1 (официальный репозиторий). `rus` в группе docker.
- vhost `/etc/nginx/sites-available/mon.ruzzy.dev.conf` → проксирует на Grafana (`127.0.0.1:3000`), HTTPS-редирект, websocket.
- vhost `/etc/nginx/sites-available/mon.ruzzy.dev.conf` → проксирует на Grafana (`127.0.0.1:3000`), HTTPS-редирект, websocket.
- vhost'ы `share.ruzzy.dev.conf` и `www.ruzzy.dev.conf` — статика с**общим** корнем `/var/www/share.ruzzy.dev` (`autoindex off`, `try_files $uri =404`, `server_tokens off`), у каждого свой сертификат. Используется для раздачи клиентских конфигов OpenVPN — сейчас там `marina-de.ovpn`, `masha-de.ovpn`.
- vhost `default` — catch-all `server_name _`, `listen 80 default_server`, отдаёт дефолтную страницу `/var/www/html/index.nginx-debian.html`. На `:443``default_server`**не задан**, поэтому TLS-запрос с неизвестным SNI попадает на первый по алфавиту vhost (`git.ruzzy.dev`) и получает его сертификат.
- Сертификаты LE: `mon`, `git`, `share`, `www`. `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
- Prometheus: retention 90d, слушает `127.0.0.1:9090`. Job'ы: `node` и `cadvisor` (file_sd, instance'ы **fr1 и ru1**), `openvpn` (file_sd, только ru1 — `85.198.109.86:9176`), `gitea` (static, `gitea:3000` через внешнюю docker-сеть `monitoring_monitoring`), `prometheus` (self). На 2026-09-08 все таргеты up, активных алертов нет.
- Grafana: `127.0.0.1:3000`, доступ снаружи через **https://mon.ruzzy.dev** (nginx + Let's Encrypt). Datasource Prometheus (uid `prometheus`) и дашборды (Node Exporter Full, cAdvisor, папка Infra) провижатся автоматически. Пароль admin — в `~/monitoring/docker-compose.yml` на сервере (в репо вынесен в `${GF_ADMIN_PASSWORD}`).
- Grafana: `127.0.0.1:3000`, доступ снаружи через **https://mon.ruzzy.dev** (nginx + Let's Encrypt). Datasource Prometheus (uid `prometheus`) и дашборды (Node Exporter Full, cAdvisor, Network/Traffic, Gitea, OpenVPN — папка Infra) провижатся автоматически. Пароль admin — в `~/monitoring/docker-compose.yml` на сервере (в репо вынесен в `${GF_ADMIN_PASSWORD}`).
- **Тома:** `monitoring_prometheus_data` — **6.5 ГБ** (при retention 90d; самый крупный том на сервере, учитывать при планировании диска), `monitoring_grafana_data` — 50 МБ, `monitoring_alertmanager_data` — пустой.
- **Multi-server:** таргеты через file_sd (`prometheus/targets/{node,cadvisor}/*.yml`) — новый сервер = добавить файл + `curl -X POST .../-/reload`. См. `stacks/monitoring/README.md`.
- **Multi-server:** таргеты через file_sd (`prometheus/targets/{node,cadvisor}/*.yml`) — новый сервер = добавить файл + `curl -X POST .../-/reload`. См. `stacks/monitoring/README.md`.
- **node-exporter — `network_mode: host`** (видит реальные интерфейсы хоста: eth0, wg0-туннель и т.д.; иначе показывал бы только сеть docker-бриджа). Слушает host:9100, доступ ограничен ufw (`from 172.16.0.0/12 to port 9100`), Prometheus скрейпит через `host.docker.internal:9100`. nodename берётся с хоста (fr1).
- **node-exporter — `network_mode: host`** (видит реальные интерфейсы хоста: eth0, wg0-туннель и т.д.; иначе показывал бы только сеть docker-бриджа). Слушает host:9100, доступ ограничен ufw (`from 172.16.0.0/12 to port 9100`), Prometheus скрейпит через `host.docker.internal:9100`. nodename берётся с хоста (fr1).
- Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты). ⚠️ для корректной сети на `de1` его агент тоже надо перевести в host-net (пока bridge).
- Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты).
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
- Планируется: VPN-экспортёры (wireguard/openvpn/telemt) + подключение `de1` по туннелю.
- Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
- Перенесён с`de1` 2026-06-21. **Gitea 1.27.3** (обновлён 2026-08-31 с 1.25.4), БД SQLite (всё в `/srv/gitea`, 1.9 ГБ, 20 репо).
- Пин образа в `~/gitea/docker-compose.yml` и в `stacks/gitea/` приведён к `1.27.3`**2026-09-08** — до этого оба файла держали `1.25.4`, и любой `docker compose up -d` откатил бы Gitea на две минорные версии. Бэкап прежнего файла — `~/gitea/docker-compose.yml.bak-20260908`.
- Запущенный контейнер `gitea` создан вне compose, поэтому `docker compose up -d` его **пересоздаст** — короткий простой, но данные на bind-mount не теряются. Конфликта больше нет: мешавший `gitea_old_1254` (он нёс метки того же compose-проекта и поднимался вторым, забирая порты 2222/3001) удалён 2026-09-08, `--dry-run` теперь показывает ровно одно `Container gitea Creating`.
- **Откат на 1.25.4 возможен — предмиграционный снимок есть:** `/home/tgclaude/backups/gitea-backup-20260831-045458.tar.gz`, 4.5 ГБ (`config/app.ini` + `data/`). Проверено 2026-09-08 распаковкой `data/gitea.db`: версия схемы в архиве — **323**, в живой БД — **343**, репозиториев 20, пользователей 4. То есть архив снят до миграции 1.25→1.27 и для откатa годен. Контейнер `gitea_old_1254` и образ `1.25.4` удалены 2026-09-08 (они как раз откатом **не** были — работали бы на уже миграцированных данных); при необходимости образ просто `docker pull gitea/gitea:1.25.4`.
- ⚠️ Этот снимок лежит в хоме `tgclaude` и до 2026-09-08 нигде не был описан — при уборке диска его легко снести по незнанию. Правило на будущее: перед апгрейдом Gitea снимать `/srv/gitea`, и складывать снимок туда, где он описан.
- Контейнер `gitea`: SSH `2222` (наружу), web `127.0.0.1:3001` (3000 занят Grafana → перевесил на 3001).
- Контейнер `gitea`: SSH `2222` (наружу), web `127.0.0.1:3001` (3000 занят Grafana → перевесил на 3001).
-`ROOT_URL`/`SSH_DOMAIN` = `git.ruzzy.dev` (не менялись). Host-ключи SSH и user/deploy-ключи перенесены с данными — `ssh://git@git.ruzzy.dev:2222` работает как раньше.
-`ROOT_URL`/`SSH_DOMAIN` = `git.ruzzy.dev` (не менялись). Host-ключи SSH и user/deploy-ключи перенесены с данными — `ssh://git@git.ruzzy.dev:2222` работает как раньше.
- Docker-томов нет — все данные на bind-mount `/srv/gitea` (1.9 ГБ), это и есть точка бэкапа.
- **ufw:** `36360/udp` открыт + `ufw route allow in on wg0 out on eth0` (и обратно) + `ufw allow in on wg0` — **обязательно**, иначе ufw режет туннельный трафик (на `de1` ufw был выключен).
- **ufw:** `36360/udp` открыт + `ufw route allow in on wg0 out on eth0` (и обратно) + `ufw allow in on wg0` — **обязательно**, иначе ufw режет туннельный трафик (на `de1` ufw был выключен).
- Пиры: `ru1` (10.8.1.4) переключён сюда; прямые 10.8.1.1/2/3 пока на `de1`.
- Пиры: `ru1` (10.8.1.4) переключён сюда; прямые 10.8.1.1/2/3 пока на `de1`.
- Проверено: зарубежный exit RU-клиентов идёт через `fr1` (exit-IP 161.97.93.252).
- Проверено: зарубежный exit RU-клиентов идёт через `fr1` (exit-IP 161.97.93.252).
- ⏳ Остаётся (фаза 2): telemt MTProxy, OpenVPN, прямые AWG-клиенты, перенос DNS `ruzzy.dev`/`www`.
- ⏳ Остаётся (фаза 2): telemt MTProxy, OpenVPN, прямые AWG-клиенты. DNS `ruzzy.dev`/`www` — **уже переведены** на fr1.
## claude-channels (Telegram-бот @iluha_rabobot)
## claude-channels (Telegram-бот @iluha_rabobot)
- Перенесён с`de1` 2026-06-22. `~/claude-channels/` (Dockerfile Node22+Bun+Claude Code, compose, `.env` с токеном). Запущен, авторизован (Sonnet 4.6), подключён к Telegram.
- Перенесён с`de1` 2026-06-22. `~/claude-channels/` (Dockerfile Node22+Bun+Claude Code, compose, `.env` с токеном). Запущен, авторизован, подключён к Telegram. Claude Code 2.1.197; в `.claude.json` модели `sonnet-5[1m]`, `opus-4-8[1m]`, `opus-4-7`.
- Volume `claude-channels_claude-home` — только нужное: `.claude.json` + `.claude/.credentials.json` (авторизация) + settings + channels (allowlist). Плагины/кэш/история НЕ копировались (на ext4 раздувались до 6.5G из-за CoW на de1).
- Volume `claude-channels_claude-home` — при переносе положили только нужное: `.claude.json` + `.claude/.credentials.json` (авторизация) + settings + channels (allowlist). Плагины/кэш/история НЕ копировались (на de1 раздувались до 6.5 ГБ).
- ⚠️ Но том **раздулся заново: 2.8 ГБ** (проверено 2026-09-08) — второй по величине после prometheus_data. Экономия от аккуратного переноса съедена работой самого бота (кэш/история/плагины нарастают внутри контейнера). Если снова упрёмся в диск — смотреть сюда.
- ⚠️ В томе, помимо авторизации Claude, лежат **SSH-ключи**: `/root/.ssh/id_ed25519` (в Gitea как deploy-ключ, `Host git.ruzzy.dev`) и `/root/.ssh/astra_fr1` — ключ к самому хосту под `tgclaude` (см. раздел `tgclaude`). Том — чувствительный артефакт, обращаться как с секретом.
- **telegram-плагин переустановлен заново** в контейнере (`claude plugin install telegram@claude-plugins-official`, v0.0.6) — копировать его нельзя.
- **telegram-плагин переустановлен заново** в контейнере (`claude plugin install telegram@claude-plugins-official`, v0.0.6) — копировать его нельзя.
- Монтирует `~/MyVault` (`/vault`). ⚠️ Vault-синк в gitea пока НЕ активен (заведём с openclaw) — заметки бота копятся локально.
- Монтирует `~/MyVault` (`/vault`, 186 МБ). Vault-синк в gitea **работает**: cron у`rus``*/5 * * * *` → `~/openclaw/sync.sh` — `git pull --rebase --autostash`, затем при изменениях коммит «vault sync <дата>» и push в `ruzzy/MyVault.git`. Ключ `~/.ssh/openclaw_gitea`, лог `~/.openclaw/logs/vault-sync.log`.
- Gmail MCP — без OAuth-кредов (gmail-инструменты не работают, на бот не влияет).
- Gmail MCP — без OAuth-кредов (gmail-инструменты не работают, на бот не влияет).
-`/usr/local/bin/uu-telegram-report.sh` — сводка автообновлений + reboot-required; повешен на `apt-daily-upgrade.service` через drop-in `/etc/systemd/system/apt-daily-upgrade.service.d/telegram.conf` (`ExecStartPost`). Молчит, если нечего сообщать.
-`/usr/local/bin/uu-telegram-report.sh` — сводка автообновлений + reboot-required; повешен на `apt-daily-upgrade.service` через drop-in `/etc/systemd/system/apt-daily-upgrade.service.d/telegram.conf` (`ExecStartPost`). Молчит, если нечего сообщать.
## Пользователь `tgclaude` (агенты) и его нагрузки
Аккаунт uid 1001 — **общий вход для агентов на этот же хост**, не персональный. До 2026-09-08 был описан только в `~rus/MyVault/TOOLS.md` (шпаргалке агентов), в репо `infra` не значился вовсе. Его нагрузки не видны ни в `docker ps` (они не в контейнерах), ни при обходе `~rus`, поэтому при инвентаризации их легко пропустить. `rus` состоит в группе `tgclaude`, так что большая часть этого хома читается без sudo.
- Кто заходит: агент **Astra**, работающий **внутри контейнера `claude-channels`** (бот @iluha_rabobot). Ключ и конфиг лежат в самом контейнере: `/root/.ssh/astra_fr1` + `Host fr1-astra → HostName 161.97.93.252, User tgclaude, IdentitiesOnly yes` (созданы 2026-09-07), а публичная часть — в `authorized_keys`у`tgclaude`. То есть бот ходит по SSH на свой же хост.
- ⚠️ **Цепочка привилегий, которую стоит осознавать целиком:** Telegram-бот → агент в контейнере → SSH как `tgclaude` → группа `docker` → **root на хосте**. Ни sudo, ни пароля на этом пути не требуется. Всё, что ограничивает агента, — его собственная сдержанность.
- Ключ `astra_fr1` живёт в томе `claude-channels_claude-home`: пересоздание контейнера доступ не отберёт, а бэкап тома содержит рабочий ключ к хосту.
**imgbot** — `/home/tgclaude/imgbot` (209 МБ; python/uv, `.venv`, Playwright + chromium, есть `.git`). Headless-поиск и скачивание картинок по текстовому запросу: источники Bing, Yandex, Google и Reddit (по сохранённым кукам), Pexels API; дедуп SHA-256 + перцептивный dHash; SQLite-манифест; фоновая проверка релевантности vision-моделью через OpenRouter; MCP-сервер с инструментами `request_photos`, `list_saved`, `export_by_query`. Правится активно — README и `.git` от 2026-09-08.
- **Оба сервиса живут в контейнерах, стек — `stacks/imgbot/` в репо, на сервере `~/imgbot-daemon/`** (переведено 2026-09-08). Один образ `imgbot-daemon:local` (база `mcr.microsoft.com/playwright/python:v1.62.0-noble`), два сервиса:
-`imgbot-daemon` — APScheduler: `refresh` каждые 30 мин (докачка картинок) и `analyze` каждые 5 мин (vision-проверка релевантности, 3 запроса к OpenRouter на картинку). ~15% ядра постоянно, поэтому `cpus: 2.0`, `mem_limit: 3g`.
-`imgbot-http` — **объединил прежние `imgbot-mcp-http` (8765) и `imgbot-file-server` (8766)**: MCP отдаётся как под-приложение Starlette (`/mcp`), рядом маршрут `/photo/<id>`. Один процесс, один порт 8765.
- Ключевые решения по контейнерам (все с комментариями в compose, ломается молча, если забыть):
- **Данные монтируются в тот же путь, что на хосте** — `/home/tgclaude/imgbot-data:/home/tgclaude/imgbot-data`. В SQLite `images.file_path` абсолютный; при монтировании в `/data` analyze падал с`FileNotFoundError` на старых строках, а новые стали бы нечитаемыми для чего угодно вне контейнера.
- ⚠️ **Корень хранения может быть не один.** При переезде был пропущен второй — легаси-архив `~tgclaude/images`, куда смотрели 1350 строк манифеста, — и `/photo/<id>` полтора часа отдавал 404 на 19.5% коллекции **молча**: в логах ничего, потому что `analyze` эти строки уже не трогал. Архив с тех пор удалён (см. «Выведено»), сейчас корень один. **Правило: перед контейнеризацией проверять все префиксы `file_path`, а не только `data_dir`** — `select substr(file_path,1,30), count(*) from images group by 1;`
- Исходники монтируются **read-only из рабочей копии**`/home/tgclaude/imgbot:/app:ro` — правки применяются `docker restart`, без пересборки. В образе только зависимости и chromium.
-`HOME=/tmp` обязателен: `collector.py` безусловно вызывает `Path.home()`, а uid 1001 не имеет записи в `/etc/passwd` образа.
- Версия `playwright` в образе (1.62.0) должна совпадать с `uv.lock` и тегом базового образа — иначе разъезжается ревизия chromium.
-`/etc/localtime` + `/etc/timezone` внутрь: иначе контейнер писал в общий `daemon.log` в UTC поверх строк в CEST.
-`shm_size: 1gb` — chromium падает на дефолтных 64 МБ.
- **Сеть и доступ:** `imgbot-http` подключён к `imgbot_default` и к внешней `claude-channels_default`, порт на хост **не публикуется**. Агент внутри `claude-channels` обращается по имени `http://imgbot-http:8765`. Проверено 2026-09-08: `MCP initialize` → 200 с`serverInfo`, `/photo/1` → 200 `image/jpeg`, несуществующий id → 404.
- ⚠️ **Авторизации нет сознательно.** Прежде токен `IMGBOT_FILE_SERVER_TOKEN` закрывал только `/photo`, а`/mcp` пускал кого угодно — проверено запросом без заголовков. Держать одну дверь на замке при открытой второй смысла не было, поэтому убрали и токен, и упоминания о нём в docstring и README. Обоснование: снаружи адреса нет, попасть можно лишь с самой машины и из подключённых docker-сетей. **Если сервис когда-нибудь выставят наружу — авторизацию вернуть первым делом:**`request_photos` тратит бюджет API и работает под живыми куками Google/Reddit, `export_by_query` копирует файлы.
- ⚠️ У MCP **своя защита от DNS-rebinding**: по умолчанию принимает только `localhost`, на `Host: imgbot-http:8765` отвечал `421 Invalid Host header`. Список задаётся `IMGBOT_ALLOWED_HOSTS` (сейчас `imgbot-http:8765,localhost:8765,127.0.0.1:8765`) — при смене имени сервиса или порта его надо править, иначе получишь загадочный 421.
- **Код проекта теперь в Gitea** — `ruzzy/imgbot` (private), создан 2026-09-08. До этого репозиторий существовал единственной копией на fr1: без remote, без бэкапа. Push идёт по deploy-ключу `~tgclaude/.ssh/imgbot_gitea` (с правом записи), remote `ssh://git@git.ruzzy.dev:2222/ruzzy/imgbot.git`. Разовая копия перед переездом — `~rus/imgbot-repo-pre-merge-20260908-133444.tar.gz`.
- ⚠️ `_job_analyze` глушит исключения через `log.exception` и возвращается нормально, поэтому APScheduler пишет `executed successfully`**даже после падения**. Мониторить надо `job failed`/`Traceback` в логе, отсутствие ошибок ничего не доказывает.
- README описывает те же `refresh`/`analyze` как задачи cron с`flock` — при живом daemon это дубль. Проверить `crontab -l -u tgclaude` (нужен root): если записи есть, работа делается дважды.
- Данные: `/home/tgclaude/imgbot-data` (2.6 ГБ) — `images/`, живой `manifest.db`, `exports/` 21 МБ, логи `daemon.log`/`refresh.log`/`bulk_fill.log`, состояние и куки Google/Reddit (mode 600).
- **Ещё один Telegram-бот** — `send_galleries.py`, `send_random_photo.py`, `send_retry.py`, `gallery_paths.txt`, токен в `~/.tg_bot_token` (600). Отдельный и от @ultimate_log_bot, и от @iluha_rabobot.
-`/home/tgclaude/backups/gitea-backup-20260831-045458.tar.gz` — 4.5 ГБ, предмиграционный снимок Gitea, см. раздел Gitea.
- ⚠️ `/home/tgclaude/.openclaw-auth-profile-secrets` — права **777** на каталоге, предназначенном для секретов (сейчас пуст).
- ⏳ Не проверено, нужен root: crontab пользователя `tgclaude` (README imgbot упоминает периодические задачи «run via cron» — значит, что-то запланировано), а также `.cache`, `.docker`, `.ssh`.
## Прочие нагрузки и каталоги (не оформлены в стеки)
- **openclaw — две независимые копии, не путать:**
-`/home/tgclaude/openclaw-deploy/openclaw` (947 МБ) — **именно отсюда запущены контейнеры** (по метке `com.docker.compose.project.config_files`), состояние в `/home/tgclaude/.openclaw` (135 МБ, владелец при этом `rus`, mode 700). Контейнеры `openclaw-openclaw-{gateway,cli}-1` (образ `openclaw:local`) — exited с 2026-08-31, причём у gateway `restart: unless-stopped`, то есть он остановлен явно, а не упал. Порты gateway `18789`, `18790`, `3978`; cli делит с ним netns. Сеть `openclaw_default` (172.22.0.0/16) и пустой том `openclaw_whisper_models` живы.
-`~rus/openclaw` (212 МБ, **не git-репозиторий** — просто распакованное дерево) + `~rus/.openclaw` (2.0 ГБ, из них `agents/` почти всё) + `~rus/openclaw-work` (147 МБ) + `~rus/.openclaw-auth-profile-secrets` — отдельная копия, своих контейнеров не имеет. Единственная живая часть — `sync.sh` из cron `rus` (см. claude-channels), лог пишется.
- Образ-снимок `openclaw:backup-20260831` (1.55 ГБ) — путь откатa для openclaw.
- **`~/backups` — 5.4 ГБ**, всё с уже выведенного `de1`: `openclaw-de1-2026-06-22.tgz` (3.1 ГБ), `claude-channels-de1-2026-06-22.tgz` (1.1 ГБ), `postgres-de1-2026-06-22/` (1.3 ГБ, 11 дампов — крупнейший `pers_data.dump` 1.2 ГБ), `var-www-envs/` (`.env`-ы 8 проектов), 5 pre-wipe-снимков claude-channels от 2026-07-16/18. Кандидат №1 на выселение с сервера, если понадобится место.
-`~/downloads` — пустой. `~/MyVault` (186 МБ) — см. claude-channels.
## Выведено с fr1
- **ipcam / FTPS-приёмник** (был `~/ipcam`, `fauria/vsftpd`, порты `2221:21` + `30000-30010`, домен `cam.ruzzy.dev`, данные `/srv/ipcam/videos`, сертификат `/srv/ftps/certs` + deploy-хук certbot) — **удалён 2026-09-08**: контейнер, образ, том `ipcam_ipcam_logs`, сеть, каталоги `/srv/ipcam` и `/srv/ftps`, хук и LE-сертификат `cam.ruzzy.dev`. Видео так и не начали складываться (в `/srv/ipcam` на момент удаления 0 файлов). Хвосты вне сервера: DNS-запись в Namecheap и настройка аплоада на самой камере.
- **Уборка 2026-09-08** — удалены неиспользуемые каталоги и их образы: `~/sc2-parser` (+образ `sc2-parser:latest`), `~/booty` (пустой), `~/booty-collector` (+сиротский том `booty_data`, 812 МБ), `~/vacuum-scraper` (2.0 ГБ, из них `output/` от root — дочищен через контейнер, +образ 887 МБ), `~/downloads/vacuum-scraper-images.tar.gz` (121 МБ). Код обоих проектов остался в Gitea (`booty-collector.git`, `vacuum-scraper.git`). Плюс вычищены docker build cache (43.4 ГБ), 6 анонимных томов, базовые образы `node:22`, `node:24-bookworm-slim`, `node:22-alpine`, `busybox` и связка `gitea_old_1254` + `gitea/gitea:1.25.4`. Итог: занято на `/`**104 → 58 ГБ** (27% → 15%).
- **ollama удалён 2026-09-08** — `/home/tgclaude/ollama` (3.5 ГБ: `ollama.tar.zst` 1.4 ГБ + `lib` 2.1 ГБ), `/home/tgclaude/.ollama` (4.6 ГБ моделей) и `ollama-serve.log`. Не запускался с 2026-08-25, процесса не было, юнитов и бинарей вне хома `tgclaude` не существовало. Итого 8.1 ГБ; хом `tgclaude` 21 → 13 ГБ, занято на `/`**58 → 50 ГБ** (15% → 13%).
- **Легаси-архив `~tgclaude/images` удалён 2026-09-08** — 1386 файлов, 1.9 ГБ на диске (0.84 ГиБ по сумме `size_bytes`). Это был не мусор, а второй корень хранения imgbot: `migrate_legacy.py` зарегистрировал файлы в манифесте **на месте**, поэтому вместе с каталогом удалены и 1350 строк таблицы `images` (запрос «жопки-архив»; 61 из них была признана релевантной и уже доставлена ботом). Удаление **необратимо**: у этих строк `source_url` пустой — imgbot не знал, откуда файлы взялись, и докачать их не сможет. Метаданные сохранены в `~rus/imgbot-legacy-archive-rows-20260908.json` (808 КБ) — хеши, размеры, вердикты, но не источники. Монтирование каталога убрано из compose (иначе docker пересоздал бы его пустым от root). После удаления: 5578 строк, все файлы на месте, один корень хранения.
- **Синк `vacuum-scraper → ~tgclaude/images` признан мёртвым 2026-09-08**, задание из крона root снято. Его роль забрал imgbot.
## Расписания (cron + systemd-таймеры)
Полная инвентаризация на 2026-09-08 — чтобы «кто это сделал в 3 утра» не приходилось выяснять заново.
**Наше:**
-`crontab -u rus`: `*/5 * * * *` → `~/openclaw/sync.sh` — синк MyVault в Gitea (см. claude-channels).
-`/etc/cron.d/staticroute` — `@reboot` правка маршрута, из образа Contabo (см. раздел «Сеть»).
-`certbot.timer` + `/etc/cron.d/certbot` — продление сертификатов (крон-вариант выходит no-op на systemd).
-`sysstat-collect.timer` (`*:00/10`) и `sysstat-summary.timer` (00:07) — сбор и суточный отчёт sar.
-`apt-daily.timer` / `apt-daily-upgrade.timer` — unattended-upgrades, на второй повешен `uu-telegram-report.sh`.
- Внутри контейнера `imgbot-daemon` — свой APScheduler (`refresh` 30 мин, `analyze` 5 мин), в системный крон не входит.
**Разобрано 2026-09-08 — что тут было и почему убрано:**
- **`crontab -u root` пуст.** Там было `*/5 * * * * rsync -a --delete … /home/rus/vacuum-scraper/output/images/ /home/tgclaude/images/` — зеркалило вывод vacuum-scraper в хом `tgclaude` каждые 5 минут. Пайплайн умер вместе с удалением `vacuum-scraper` (роль забрал imgbot), задание снято 2026-09-08. Это же объясняло, почему в `~tgclaude/images` лежали те же UUID-имена, что в удалённом `output/images`: живое зеркало, а не старая копия. Мина, которую заодно убрали: `rsync --delete` мог удалять файлы, на которые ссылался манифест imgbot, — то есть в любой момент осиротить строки в БД.
- **`/etc/cron.hourly` пуст, оба скрипта удалены 2026-09-08.** Что там было и почему убрано:
-`free` — одна строка `echo 1 > /proc/sys/vm/drop_caches`, то есть **сброс страничного кэша каждый час**. Артефакт шаблона Contabo ради «красивого» вывода `free`. Срабатывание доказано по Prometheus: `node_memory_Cached_bytes` проваливался ровно в **:17** каждого часа (именно в это время `/etc/crontab` запускает `run-parts /etc/cron.hourly`) — например 13:17 минус **3.89 ГиБ**, 10:17 минус 2.22 ГиБ. При этом платы на диске **не обнаружено**: чтение после провалов 0.14–0.48 МиБ/с, рабочий набор мал и возвращается в кэш дешёво. Убрано не ради производительности, а потому что это регулярная пила на графиках памяти, не связанная ни с чем реальным, — прямое препятствие для разборов, ради которых на этом сервере задрана ретенция журналов.
-`fstrim` — `fstrim /`**каждый час** при том, что штатный `fstrim.timer` (`OnCalendar=weekly`) на этой машине уже `enabled`. То есть trim шёл дважды; часовой убран, недельный таймер оставлен.
**Проверено и оставлено как есть:**
-`/etc/cron.daily/sysstat` — no-op: первой строкой `[ ! -d /run/systemd/system ] || exit 0`, то есть на systemd сразу выходит. Дубля с `sysstat-summary.timer` нет, трогать не надо.
- Таймеры `fwupd-refresh.timer` (раз в сутки ходил в сеть за метаданными прошивок LVFS) и `motd-news.timer` (`/etc/default/motd-news`: `ENABLED=1`, `URLS=https://motd.ubuntu.com` — раз в сутки запрашивал у Canonical новостной текст для приветствия) — `disabled`.
- Демоны `ModemManager`, `fwupd`, `multipathd`, `udisks2` — `masked`. Все четыре висели с 0.0% CPU и ~100 МБ RSS суммарно, и обслуживать им было нечего: multipath-устройств нет (`/dev/mapper` — только `control`, диск один), модемов нет (в `/sys/class/net` только `eth0`, `wg0`, `lo` и docker-интерфейсы), прошивок в KVM-гостe нет, съёмных носителей тоже.
## Открытые хвосты (на 2026-09-08)
Мелочи, оставленные сознательно, — чтобы не искать их заново.
- ⚠️ **В Gitea живут два временных токена** — `imgbot-setup` и `imgbot-setup2`, scope `write:repository,write:user`. Созданы через `docker exec -u 1000 gitea gitea admin user generate-access-token` для создания репо `ruzzy/imgbot` и deploy-ключа. Удалить через API не удалось: Gitea отвечает 401 на управление токенами по токен-авторизации, нужна basic-auth. **Отозвать вручную:** Настройки → Приложения.
-В таблице `queries` манифеста imgbot осталась строка `жопки-архив`, хотя все её 1350 картинок удалены. `refresh` может попытаться пополнить её, ища по этой строке в поисковиках, — получится мусор. Снести строку или оставить.
-В`~tgclaude/imgbot/.env` осталась строка `IMGBOT_FILE_SERVER_TOKEN` — после объединения HTTP-сервисов её никто не читает.
-`~tgclaude/.openclaw-auth-profile-secrets` — права **777** на каталоге для секретов (сейчас пуст), стоит `chmod 700`.
- Не решено, нужны ли на fr1: `~rus/backups` (5.4 ГБ архивов с выведенного de1) и спящий openclaw (947 МБ дерева + 2.0 ГБ состояния + образ-снимок 1.55 ГБ).
- ⏳ Дроп-ин `/etc/ssh/sshd_config.d/50-cloud-init.conf` не читается из-под `rus` — под root проверить `sshd -T | grep -iE "permitrootlogin|passwordauth"`, чтобы знать эффективные значения, а не собранные из файлов вручную.
- ⚠️ Описания **других** серверов не сверялись: `servers/de1.md` и `servers/ru1.md` датируются временем до вывода de1, так что могут врать.
## Как это описание поддерживается
Механизм заведён 2026-09-08, после того как сверка нашла десяток расхождений.
**В репозитории:**`CLAUDE.md` — контракт «тронул сервер, обнови описание в
том же прогоне» и правила оформления; `.claude/skills/server-audit/` — метод
сверки и накопленные грабли; `scripts/snapshot.sh` — инвентаризация живого
состояния за один проход (только чтение, без sudo, секретов не печатает).
**На самом сервере**, для агентов, у которых репозитория нет:
-`~rus/CLAUDE.md` и `~tgclaude/CLAUDE.md` — указатель на репо, правило и
пять граблей, которые дороже всего стоили (ufw не видит docker-портов;
группа docker равна root; процесс из SSH не переживёт ребут; абсолютные
пути в чужих базах; мигающий IPv6). Рядом `AGENTS.md` — симлинк на тот же
файл, для агентов, читающих эту конвенцию. Claude Code подхватывает
`CLAUDE.md` вверх по дереву каталогов, поэтому файл в хоме виден и при
работе в подкаталогах вроде `~tgclaude/imgbot`.
-`~rus/MyVault/TOOLS.md`, раздел SSH — та же обязанность для агента Astra.
Файл смонтирован в контейнер `claude-channels` как `/vault`, так что
изменение видно ему сразу, и синкается в Gitea кроном каждые 5 минут.
⚠️ **Чего этот механизм не гарантирует.** MOTD и `.bashrc` не помогут:
`.bashrc` выходит на строке 6 для неинтерактивных шеллов, а MOTD
показывается только при интерактивном входе — то есть агент, дёргающий
`ssh fr1 'команда'`, не увидит ничего. Файлы в хомах достанут только тех,
кто читает `CLAUDE.md`/`AGENTS.md` и чей рабочий каталог оказался внутри
хома. Серверного хука «агент закончил работу» не существует.
Детектор дрейфа (таймер, сравнивающий отпечаток состояния с прошлым и
пишущий в Telegram) обсуждался и **сознательно не заводился** — если
незадокументированные изменения снова начнут накапливаться, начинать
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.