Files
infra/servers/fr1.md
T

322 lines
75 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fr1 — `fr1.ruzzy.dev` (161.97.93.252)
Снимок состояния сервера. Секреты (пароли/токены) — только на сервере, не здесь.
**Провайдер:** Contabo VPS · France / Lauterbourg (Grand Est)
**ОС:** 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
**hostname:** `fr1` (FQDN `fr1.ruzzy.dev` через `/etc/hosts`)
---
## Пользователи и доступ
- Пользователь `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).
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
- ~~Пользователь `marusia-tunnel`~~ — удалён 2026-10-11 вместе со всей интеграцией с Марусей (отменена владельцем): пользователь и `/home/marusia-tunnel`, `sshd_config.d/70-marusia-tunnel.conf`, запись в `AllowUsers`, vhost `astra.ruzzy.dev` (`sites-available/astra.ruzzy.dev.conf`, ссылка `zz-…`, `conf.d/marusia-webhook.conf`), сертификат LE `astra.ruzzy.dev`, `~rus/marusia-audit/`. Скрипт демонтажа — `~rus/marusia-removal.sh`, копии удалённого — `/root/marusia-removed-20261011/` (root 700). nginx/sshd перезагружались после успешных `-t`; проверено: git/mon открываются, SSH-вход работает.
- **Пуш-ключ `metrics-push@rpi`** (с 2026-10-11) в `~rus/.ssh/authorized_keys` с `restrict,command="/home/rus/bin/pushgw-ingest rpi"`: этим ключом домашняя малина может только залить метрики в pushgateway — шелла, форвардинга и pty нет. Бэкап прежнего файла — `~/.ssh/authorized_keys.bak-20261011`.
## SSH (`/etc/ssh/sshd_config.d/00-hardening.conf`)
⚠️ В основном `/etc/ssh/sshd_config` строкой 42 стоит `PermitRootLogin yes` (правка шаблона Contabo). Побеждает всё равно дроп-ин: `Include /etc/ssh/sshd_config.d/*.conf` стоит строкой 12, а sshd берёт **первое** встреченное значение. То есть root-вход закрыт, но держится на порядке строк — уберёшь дроп-ин, и root-вход по ключу включится. Рядом лежит `50-cloud-init.conf` (root 600) — эффективные значения проверены под root 2026-10-11, см. ниже.
```
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
MaxAuthTries 3
AllowUsers rus tgclaude
```
Через socket-activation (`ssh.socket`), слушает `0.0.0.0:22` и `[::]:22`.
- `marusia-tunnel` был в `AllowUsers` 2026-10-11 (для туннеля Маруси), убран в тот же день вместе с интеграцией; бэкап исходного — `00-hardening.conf.bak-20261011-171345`.
- Проверено под root 2026-10-11: `50-cloud-init.conf` содержит `PasswordAuthentication yes`, но эффективное значение — `no` (`00-hardening` читается первым); `sshd -T` → `permitrootlogin no`, `passwordauthentication no`. ⚠️ То же «держится на порядке файлов»: переименуй `00-hardening` во что-то после `50-`, и вход по паролю включится.
- Отпечатки host-ключей: ED25519 `SHA256:M5J9nH+svyA3+beD7ZllUn1tayMMO2cxw/t3Z/kj6KU`, ECDSA `SHA256:fMZEytu7XI3W+Mzo4WQ6WZHDn0nYiagtbKb4JyUIX5w`, RSA `SHA256:53olW/dhoPclFgp8X6OWh0euoOfSmwXWZb6c026OGAw`.
## Сеть
- Управляется **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
- Зона `ruzzy.dev` — Namecheap (NS `dns1/dns2.registrar-servers.com`).
- `fr1.ruzzy.dev` A → 161.97.93.252. Апекс `ruzzy.dev` A → 161.97.93.252 (переведён с de1).
- CNAME → `fr1.ruzzy.dev`: `git`, `mon`, `www`, `share`, `books` (с 2026-10-13).
- ⚠️ `cam.ruzzy.dev` (CNAME → fr1) остался в Namecheap после удаления ipcam 2026-09-08 — снести руками.
- PTR (reverse DNS): дефолтный `vmi3387709.contaboserver.net` (не менялся).
## Firewall (ufw)
- Default: deny incoming, allow outgoing.
- Открыто (IPv4 + IPv6): `22/tcp` (OpenSSH), `80/tcp`, `443/tcp`, `2222/tcp` (gitea-ssh), `36360/udp` (awg-de-exit).
- `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`.
## fail2ban (`/etc/fail2ban/jail.local`)
```
[DEFAULT]
bantime=1h findtime=10m maxretry=5
ignoreip = 127.0.0.1/8 ::1 31.153.37.106 46.199.208.45
backend=systemd banaction=ufw
[sshd] enabled=true
[recidive] enabled=true bantime=1w findtime=1d maxretry=5
backend=auto logpath=/var/log/fail2ban.log
```
- `46.199.208.45` — текущий домашний IP (Cyta, за ним rpi и Mac), добавлен 2026-10-11 (было только `31.153.37.106`), бэкап `jail.local.bak-20261011-175800`. Зачем: туннель Маруси с rpi переподключается сам, и серия ошибок входа забанила бы весь дом — вместе с `ssh rus-161` — на час, а `recidive` на неделю.
- ⚠️ **Домашний IP динамический** (PPPoE Cyta, см. `servers/rpi.md`). После смены адреса защита молча пропадёт, а в `ignoreip` останется чужой адрес — сверять с `router status` на rpi.
- ⏳ Происхождение `31.153.37.106` нигде не записано (стоит с 2026-06-21) — выяснить и убрать, если не нужен.
## Обновления
- Ubuntu Pro подключён (аккаунт ruzzyrullezz@gmail.com): **livepatch**, **esm-infra**, **esm-apps** — enabled.
- 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).
- unattended-upgrades: `Update-Package-Lists "1"`, `Unattended-Upgrade "1"` (ежедневно, security). Авто-reboot выключен.
## Система
- swap: `/swapfile` 2 GB (в `/etc/fstab`), `vm.swappiness=10` (`/etc/sysctl.d/99-swappiness.conf`).
- AppArmor включён; время — systemd-timesyncd.
## Логирование (для разборов) — пересобрано 2026-08-01
- journald: `Storage=persistent`; ретенция — дроп-ин `/etc/systemd/journald.conf.d/10-retention.conf`:
`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 29.6 + Compose v5.1 (официальный репозиторий). `rus` в группе docker.
## nginx + TLS
- nginx 1.24 (apt) + certbot 2.9 (`python3-certbot-nginx`), автопродление через systemd-таймер certbot.
- 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`; с 2026-10-12 ещё `astra/index.html` и `astra/privacy.html` — страницы OAuth-приложения Astra для Google (`stacks/www-ruzzy/`, см. `servers/rpi.md`).
- 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`) и получает его сертификат. Так отвечают и `https://ruzzy.dev`, и `https://fr1.ruzzy.dev` — своих vhost'ов и сертификатов у них нет, браузер видит несовпадение имени (проверено 2026-10-11).
- vhost `astra.ruzzy.dev` (вебхук Маруси) — **удалён 2026-10-11** вместе с сертификатом. ⚠️ DNS-запись `astra.ruzzy.dev` (CNAME → fr1.ruzzy.dev) у регистратора — удалить вручную; пока она есть, запросы к этому имени отвечает vhost `git` с чужим сертификатом.
- Сертификаты LE: `mon`, `git`, `share`, `www`, `books` (с 2026-10-13), `astra` (с 2026-10-11, `certbot certonly --nginx`, продление тем же таймером). `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager + Pushgateway** (docker compose).
- **Алерты:** Alertmanager → Telegram (бот @ultimate_log_bot, chat 607015). Правила `prometheus/rules/alerts.yml`: TargetDown, HostHighCPU, HostLowMemory, HostDiskLow/Critical; группа `rpi` (с 2026-10-11) — SMART и бэкапы малины: PushMetricsStale, SmartHealthFailed, SmartBadSectors(+Growing), SmartUdmaCrcErrors, SmartSelftestFailed, SmartTemperatureHigh, BackupFailed, BackupStale. Токен — в `~/monitoring/alertmanager/telegram_token` (chown 65534, не в git).
- 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, Network/Traffic, Gitea, OpenVPN, **RPi — диск и бэкап OneDrive** (`rpi-disk.json`, с 2026-10-11) — папка 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`.
- **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 скрыты).
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
- **Pushgateway** (с 2026-10-11) — для хостов за NAT, которых не скрейпнуть (домашняя `rpi`, см. `servers/rpi.md`). Слушает только `127.0.0.1:9091`; job `pushgateway` с `honor_labels: true` (иначе job/instance заменились бы на `pushgateway:9091`). Персистентность в томе `monitoring_pushgateway_data` — рестарт не обнуляет метрики. **Как приходят данные:** хост делает `ssh rus@fr1 <job> < metrics.prom`, forced command `~/bin/pushgw-ingest <instance>` делает `PUT /metrics/job/<job>/instance/<instance>`. instance зашит в authorized_keys, а не берётся от клиента — хост не может писать от чужого имени; имя job фильтруется по `[a-z0-9_]`. Наружу порт не открыт, nginx/ufw не трогали.
- В `~/bin/pushgw-ingest` есть режим `health` (с 2026-10-11): `ssh … health` отдаёт key=value о здоровье Prometheus/Alertmanager/pushgateway, свежести пушей хоста и счётчиках доставки в Telegram — только чтение. Им пользуется внешний сторож `monwatch` на rpi.
- ⚠️ Pushgateway хранит последнее значение вечно: если хост умер, метрики не исчезнут, а «застынут». Поэтому свежесть ловит отдельный алерт `PushMetricsStale` по `push_time_seconds`.
- ⚠️ Живой `~/monitoring/docker-compose.yml` расходится с репо (пароль Grafana вписан литералом), поэтому при деплое 2026-10-11 compose **не копировался**, а блок pushgateway вставлен на месте. Бэкапы: `docker-compose.yml.bak-20261011`, `prometheus/prometheus.yml.bak-20261011`.
- Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
## 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).
- nginx vhost `git.ruzzy.dev` → `127.0.0.1:3001`, TLS Let's Encrypt, `client_max_body_size 512M`.
- DNS: `git.ruzzy.dev` (CNAME → fr1) → 161.97.93.252.
- `ROOT_URL`/`SSH_DOMAIN` = `git.ruzzy.dev` (не менялись). Host-ключи SSH и user/deploy-ключи перенесены с данными — `ssh://git@git.ruzzy.dev:2222` работает как раньше.
- Docker-томов нет — все данные на bind-mount `/srv/gitea` (1.9 ГБ), это и есть точка бэкапа.
- Это origin репо `infra` и vault openclaw.
- **Пользователь `astra`** (id 5, с 2026-10-11) — агент на домашней малине (`servers/rpi.md`): коллаборатор с записью в `ruzzy/infra` и `ruzzy/astra-bot`, свои репо в `astra/…`, постоянный токен `astra-rpi` (хранится только на малине). Пароль случайный и нигде не сохранён — вход только ключом/токеном; сбросить — `gitea admin user change-password`. Deploy-ключи astra (id 10, 11) удалены.
- Как сделать разовую операцию через API без следов: `gitea admin user generate-access-token --scopes write:repository --raw` (конфиг по умолчанию верный — работающая Gitea тоже читает `/data/gitea/conf/app.ini`, БД `/data/gitea.db`) → `POST /api/v1/repos/<owner>/<repo>/keys` → удалить токен прямо из БД: `docker exec -u 1000 gitea sqlite3 /data/gitea.db "delete from access_token where name='<имя>'"` (API требует basic-auth, как выяснилось с imgbot-токенами). Проверено 2026-10-11: после удаления токен получает 401.
## books.ruzzy.dev — читалка с ИИ (с 2026-10-13, `~/books`, код — репо `ruzzy/books`)
- Деплой: из клона `ruzzy/books` (`~/pets/books` на Mac) — `rsync -a --exclude data --exclude .env --exclude .git ./ rus-161:books/ && ssh rus-161 'cd ~/books && docker compose up -d --build'` (подробно — README репо). `~/books` на fr1 — копия, не git-клон.
- 2026-10-13: vhost и сертификат LE `books.ruzzy.dev` (до 2027-01-11, продление — таймер certbot) поставил владелец `sudo sh ~/books/nginx/install.sh` (у `rus` sudo по паролю, root-вход закрыт); DNS — CNAME `books` → `fr1.ruzzy.dev` в Namecheap (добавлен в тот же день; ⚠️ wildcard-записи у зоны нет — новый поддомен = новая CNAME, иначе certbot падает с NXDOMAIN). Проверено: https 200, http→https 301, `/` без входа → `/login`, `X-Robots-Tag: noindex`.
- 2026-10-13 `CLAUDE_CODE_OAUTH_TOKEN` вписан, проверено на «SRE» (592 с.): индексация 75 с, рекап главы ~8 с, перевод выделенного — первые слова ~5 с (целиком 10–17 с), вопрос с WebSearch ~10 с; у агента «нет инструментов». ⚠️ Токен владелец передал через чат с ассистентом — перевыпустить (`claude setup-token`) и вписать напрямую.
- Контейнер `books` (образ `books:local`, compose `~/books/docker-compose.yml`), `127.0.0.1:3010` → nginx vhost `books.ruzzy.dev` (`sites-available/books.ruzzy.dev.conf`, `client_max_body_size 210M`, `proxy_buffering off`, таймаут 900 с — ответы ИИ потоком). Данные — `~/books/data` (uid 1000 = rus): `books.db`, `files/`, `models/`, `claude/`. Лимиты: 4 ГБ RAM, 4 CPU, `cap_drop ALL`, `no-new-privileges`.
- ИИ — Claude Code 2.1.197 внутри контейнера в фоновом режиме по **подписке владельца** (`CLAUDE_CODE_OAUTH_TOKEN` в `~/books/.env`, 600), без инструментов; для проверки фактов — только WebSearch. ⚠️ Лимиты подписки общие с claude-channels и работой владельца.
- Вход — логин и пароль (решение владельца 2026-10-13, вместо Telegram Login): в `.env` `BOOKS_LOGIN` и scrypt-хеш `BOOKS_PASSWORD_HASH` (`scrypt:соль:хеш`; ⚠️ без `$` — compose подставляет переменные в `.env`, с `$` хеш молча портился и пароль «не подходил»). Не больше 5 неудачных попыток за 15 мин с IP (IP — последний в `X-Forwarded-For`, его ставит nginx) и 30 — всего. Секреты `.env`: `BOOKS_PASSWORD_HASH`, `SESSION_SECRET`, `CLAUDE_CODE_OAUTH_TOKEN`.
- 2026-10-13: в образе ещё Go 1.23.4 + goimports + staticcheck (проверка листингов книг; кэш модулей — `~/books/data/go`, качает с proxy.golang.org) и pymupdf4llm. Фоновая подготовка «книжных» глав — Claude по подписке, ~5–10 мин/глава, параллельно ≤2 запроса (`LLM_PARALLEL_BG`), интерактив — отдельно ≤3. Картинки глав — `~/books/data/books/<id>/img/`.
- ⚠️ 2026-10-13: Claude Code по умолчанию «размышляет» (thinking) — перевод шёл до минуты; в контейнере `MAX_THINKING_TOKENS=0` (через код, `LLM_THINKING_TOKENS`). Для интерактива держатся 2 прогретых процесса `claude -p --input-format stream-json` (haiku и sonnet, ~200 МБ каждый) — первые слова ответа за ~0,7 с.
- **gVisor** (с 2026-10-13): `runsc` release-20261005.0 из apt-репозитория gvisor.dev (`/etc/apt/sources.list.d/gvisor.list`), `/etc/docker/daemon.json` — runtime `runsc` с `--host-uds=create` (файла раньше не было). Подключён `systemctl reload docker` без перезапуска контейнеров (поставил владелец `sudo sh ~/books/runner/gvisor-install.sh`). Используется только `books-runner`. Откат — убрать runtime из daemon.json, reload, `apt-get remove runsc`.
- **Песочница `books-runner`** (с 2026-10-13, тот же compose `~/books`): запуск примеров кода из книг. **runtime runsc (gVisor)**, без сети (`network_mode: none`), ФС только для чтения, `cap_drop ALL` + SETUID/SETGID/KILL/CHOWN, 1 CPU / 768 МБ / 128 pid, tmpfs `/work`; код — от nobody, ≤40 процессов, ≤384 МБ (сторож RSS); бинарник собирает контейнер books. ⚠️ Бенчмарки под gVisor в 1,5–2,5 раза медленнее нативных. Связь — unix-сокет `~/books/run/runner.sock`; ⚠️ каталог `~/books/run` — root:rus 0770 (ставился через контейнер — у rus нет sudo без пароля): если сделать 0777, код примеров сможет писать в каталог хоста и стучаться в сокет песочницы.
- ⚠️ Агенту книг сознательно не дан доступ к хосту (в отличие от claude-channels с ключами к fr1/gitea): книги и выделения — чужой текст, возможны внедрённые инструкции.
- 2026-10-18: в `app/prep.py` логи планировщика (`docker compose logs books | grep prep:` — взял слот, пропустил слот и почему, пауза после лимита, глава готова). Повод: в ночь на 18.10 не подготовилась ни одна глава, причину (вероятно, пауза после лимита в памяти процесса) без логов было не установить; контейнер пересоздан.
- 2026-10-18: MCP-токены `bot:…` — для ботов в общих чатах: без `get_notes`, поиск без заметок/стикеров владельца. Выдан `bot:iluha_rabobot` (claude-channels); список — `docker compose exec books python /app/books_mcp.py list`, отзыв — `… revoke bot:iluha_rabobot`.
## AmneziaWG exit (для ru1) — перенос VPN, фаза 1
- Зеркало exit-узла `de1`: AmneziaWG server `wg0` = **10.8.1.0/24**, ListenPort **36360/udp**, те же server-ключи/PSK и obfuscation-параметры (Jc/Jmin/Jmax/S1/S2/H1-4), что и на `de1` (server-pubkey `Lumb1o…`) → на `ru1` свитч = только смена Endpoint.
- Установка: `amneziawg-dkms`+`amneziawg-tools` (PPA amnezia), host-ом через `awg-quick@wg0` (enabled). Конфиг `/etc/amnezia/amneziawg/wg0.conf` (секрет, не в git).
- NAT: PostUp masquerade `10.8.1.0/24 → eth0` + `ip_forward=1`.
- **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`.
- Проверено: зарубежный exit RU-клиентов идёт через `fr1` (exit-IP 161.97.93.252).
- ⏳ Остаётся (фаза 2): telemt MTProxy, OpenVPN, прямые AWG-клиенты. DNS `ruzzy.dev`/`www` — **уже переведены** на fr1.
## claude-channels (Telegram-бот @iluha_rabobot)
- Перенесён с `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). Плагины/кэш/история НЕ копировались (на 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) — копировать его нельзя.
- **Доступа к Obsidian-vault у бота нет с 2026-10-11** (решение владельца: vault — его личная база, а не рабочая папка агентов; заметки — к Astra на rpi). Убрано: монтирование `/home/rus/MyVault:/vault` из `~/claude-channels/docker-compose.yml` (бэкап `docker-compose.yml.bak-20261011`), разделы «Obsidian Vault»/«Git и Obsidian Vault» в `/root/CLAUDE.md` бота заменены пометкой «доступа нет» (бэкап `/root/CLAUDE.md.bak-20261011`), синк-cron снят, **`~/MyVault` удалён** (227 МБ; перед удалением всё было в gitea, незапушенного не было). История — `ruzzy/MyVault`, архив на 2026-09-23.
- Мост `ask-astra` в контейнере (`/root/agent-bridge/bin/ask-astra`) по умолчанию работал в `/vault` — переключён на `/root/astra-work` (пустой git-репозиторий; `ask-astra` требует git). Бэкапы — `ask-astra.bak-20261011`, `astra-section.md.bak-20261011`.
- Gmail MCP — без OAuth-кредов (gmail-инструменты не работают, на бот не влияет).
- **MCP `books` с 2026-10-18** (книги читалки, только чтение): `claude mcp add --scope user --transport http books https://books.ruzzy.dev/mcp` с Bearer-токеном `bot:iluha_rabobot` (токен — только в `/root/.claude.json` тома и хешем в `books.db`). В `/root/CLAUDE.md` бота дописан раздел «Книги владельца» (без бэкапа: `cp` в `/root` не прошёл — нет прав; раздел — в конце файла, удаляется целиком): ответы по книгам, публикация в Telegram-канал только по команде владельца, без дословных глав. Контейнер перезапущен (ушёл в новую сессию: прежняя была >5 МБ и заархивирована entrypoint-ом). ⚠️ Текст книг — чужие PDF, теперь он попадает к агенту с SSH-ключами к fr1/gitea и `--dangerously-skip-permissions`: защита от внедрённых инструкций — только правило в CLAUDE.md. Канал для публикаций бот подключает сам по команде владельца «подключи канал @имя» (раздел «Подключение канала» в его CLAUDE.md): `getChat`/`getChatMember` по Bot API → id в `groups` в `/root/.claude/channels/telegram/access.json` (без этого `reply` в канал отклоняется) → строка в «Подключённые каналы» в конце CLAUDE.md. Подключён канал **DulyNotes** (приватный, без @username, id `-1004305290483`, бот — админ с правом публикации) 2026-10-18; бэкап access.json до правки — `/tmp/access.json.bak` в контейнере. ⚠️ У приватного канала `getChat` по имени не работает — id узнавали `getUpdates` при приостановленном (`kill -STOP`) плагине, без `offset`, потом вернули `allowed_updates=[]` (параметр `getUpdates` запоминается Telegram и действует на плагин).
- **DulyNotes: разбор книги по расписанию** (с 2026-10-18). `/root/publisher/` в томе (создан от root: `/root` в контейнере — root:claude 755, бот сам туда не пишет): `publish.mjs` — в 9:00 и 21:00 по Никосии по 2 главы «Learning Go» (book_id=2, 16 глав → 4 дня) в канал `-1004305290483`, с 2026-10-18 каждая отдельным сообщением как самостоятельная техническая заметка о Go: заголовок по теме, без упоминания книги, автора и глав, связный рассказ с переходом от прошлой заметки (её текст — `log/ch<idx>.html`), 2800–3500 символов; длиннее 3800 — второй проход «сократи», длиннее 4000 — продолжение ответом на первое сообщение; `claude -p` запускается из пустого `/tmp/publisher-run`, иначе подхватывает `/root/CLAUDE.md` бота; очередь и сделанные слоты — `learning-go.json`, лог и тексты глав — `log/`. Пост пишет `claude -p` (sonnet, по той же подписке) в **изолированном конфиге** `.claude-config` (только `.credentials.json` — симлинк на основной, пустые settings): без плагинов/хуков/встроенных инструментов, только MCP `books` (`books-mcp.json`, 600, токен `bot:iluha_rabobot`); ⚠️ `MCP_CONNECTION_NONBLOCKING=false` обязателен — иначе MCP «pending» и модель пишет вызов инструмента текстом. ⚠️ Основной конфиг для `claude -p` не годится: поднимется второй telegram-плагин, а он при старте убивает чужой опрос (`server.ts`, «Kill any stale holder»). Отправка — Bot API напрямую; HTML чистится от «<» не по делу, при отказе Telegram — текстом; ошибки — владельцу в личку. Ручные режимы: `--now`, `--preview`.
- **Комментарии DulyNotes — ограниченный режим** (с 2026-10-18). Хук `PreToolUse` `/root/.claude/hooks/restricted_guard.js` (в `/root/.claude/settings.json`, бэкап `settings.json.bak-20261018`): если последнее входящее из чата из `/root/.claude/hooks/restricted_chats.json` (`chats`) не от `trusted_users` (607015), разрешены только WebSearch/WebFetch, `mcp__books__*`, reply/react/edit в тот же чат (файлы — только `draw/out` и inbox), Read/Write своей истории, Write `draw/*.txt` и Bash ровно `astra-draw <draw/*.txt>`. Проверено: 24 сценария на поддельных транскриптах + живой `claude -p --dangerously-skip-permissions` (deny соблюдается в bypass). ⚠️ Без транскрипта на диске хук пропускает (у бота он есть всегда; в тестах с `--no-session-persistence` — нет). Группа комментариев ещё не привязана к каналу (на 2026-10-18 `linked_chat_id` пуст) — подключает бот по команде «подключи комментарии» (раздел в CLAUDE.md; `requireMention:false`, id → `restricted_chats.json`). Privacy mode у бота выключен (`can_read_all_group_messages`), админство в группе не нужно.
- **`/root/agent-bridge/bin/astra-draw`** (с 2026-10-18) — Astra только для картинок: `codex exec --sandbox read-only` в пустом temp-каталоге с выключенными shell_tool, unified_exec, apps, plugins, browser_use*, computer_use, multi_agent, goals и `mcp_servers={}`; `code_mode_host` оставлен — через него идёт image_gen. Проверено: прочитать `/etc/hostname` не может (JS без ФС, apply_patch запрещён режимом), дочерний агент тоже без shell, внедрённая в описание просьба прочитать `~/.ssh` проигнорирована. Остаётся `web__run`. Картинки → `/root/agent-bridge/draw/out/`. Бэкап CLAUDE.md бота до этих правок — `/root/agent-bridge/CLAUDE.md.bak-20261018`.
## Telegram-уведомления
- Бот @ultimate_log_bot. Конфиг `/etc/tg-notify.conf` (chmod 600, токен+chat_id — секрет на сервере).
- `/usr/local/bin/tg-notify.sh "msg"` — универсальная отправка (HTML, префикс hostname).
- `/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).
**Прочее у `tgclaude`:**
- `/home/tgclaude/.local/lib` — 2.3 ГБ python-пакетов, `chrome-libs` — 3 МБ библиотек для headless-chromium.
- **Ещё один 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` удалён 2026-10-11 — см. 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` — с 2026-10-18 одно задание: `0 * * * * docker exec claude-channels node /root/publisher/publish.mjs` (разбор книги в DulyNotes; час 9/21 по Никосии проверяет сам скрипт, поэтому смена DST и TZ хоста не сдвигают слоты). Синк MyVault снят 2026-10-11 (см. 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`.
- `ua-timer.timer` — Ubuntu Pro. `logrotate`, `man-db`, `dpkg-db-backup`, `e2scrub_all`, `systemd-tmpfiles-clean` — штатное.
- Внутри контейнера `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` нет, трогать не надо.
- `update-notifier-download.timer` / `update-notifier-motd.timer` — десктопное наследие образа, оставлены.
**Отключено 2026-09-08 как наследие шаблона:**
- Таймеры `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-10-11)
До этого регулярных бэкапов fr1 **не было** (только разовый снимок Gitea перед апгрейдом).
- Домашняя `rpi` каждую ночь (02:15 MSK) забирает зашифрованную выгрузку — `stacks/server-backup/`. Ключ `server-backup@rpi` в `~rus/.ssh/authorized_keys` с `restrict,command="/home/rus/bin/backup-export"` (бэкап прежнего файла — `authorized_keys.bak-20261011b`).
- `~/bin/backup-export` читает root-файлы через `docker run --rm --network none -v /:/host:ro debian:stable-slim` — sudo тут по паролю, а `rus` в группе docker. Шифрует `~/bin/age` (статический v1.3.2) открытым ключом; закрытый только у владельца.
- daily: `/etc`, `/srv/gitea` + согласованный `.backup` `gitea.db` (живой файл исключён), `/usr/local`, `~rus` (без backups/openclaw/кэшей), `/root`, crontab. weekly (вс): тома `claude-channels_claude-home`, grafana, alertmanager, pushgateway + `~tgclaude` (без backups/openclaw-deploy/.local).
- ⚠️ Prometheus (том 7.9 ГБ) **не бэкапится** сознательно. ⚠️ Новые важные данные вне перечисленных путей в бэкап не попадут — добавлять в `fr1-backup-export`.
- В образах добавился `debian:stable-slim` (нужен для выгрузки).
## Открытые хвосты (на 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 ГБ).
- Страховочные файлы, оставленные после работ: `~rus/imgbot-repo-pre-merge-20260908-133444.tar.gz` (369 КБ), `~rus/imgbot-legacy-archive-rows-20260908.json` (808 КБ), `~rus/gitea/docker-compose.yml.bak-20260908`.
- ⏳ DNS-запись `astra.ruzzy.dev` (остаток отменённой интеграции с Марусей) — удалить у регистратора. Всё остальное на fr1 демонтировано 2026-10-11.
- ⚠️ Описания **других** серверов не сверялись: `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) обсуждался и **сознательно не заводился** — если
незадокументированные изменения снова начнут накапливаться, начинать
стоит с него.
## Установленные пакеты (сверх базы)
htop, mtr-tiny, tmux, vim, curl, wget, git, unzip, net-tools, dnsutils, ncdu, rsync, fail2ban, unattended-upgrades, sysstat.
- Исправление ложного PushMetricsStale подготовлено в infra 2026-10-11: правило `stacks/monitoring/prometheus/rules/alerts.yml` ограничено job=smart (было smart|backup_.* с порогом 3 ч для всех). backup_servers успешно завершился на rpi в 15:14, итог отправляется раз в сутки после прогона, поэтому алерт в 18:24 был ложным. Свежесть backup_* остаётся под BackupStale/BackupNeverReported с отдельными интервалами. ⚠️ Изменение пока только в репозитории: агенту fr1 требуется проверить правила promtool, установить alerts.yml и reload Prometheus; с rpi административного доступа к fr1 нет. Не перепушивать старые результаты ради исчезновения алерта.