Files
bare/docs/deploy.md
T
mayatnikovandClaude Opus 5 db45978b16 Этап 6: закалка — лимиты ADR-021, аудит модели угроз, сверка документов
Лимиты: все четыре правила ADR-021 — регистрация 5/час на IP, вход 10/10 мин
на IP и ник, сообщения 30/мин, прочие изменяющие 60/мин; 429 с Retry-After;
X-Real-IP читается только с loopback, иначе адрес соединения — иначе заголовок
отменял бы лимит на IP; карты вёдер ограничены поколениями.

Аудит нашёл то, что пропустили пять раундов ревью:
ADR-056: nginx вёл access_log с IP и полными путями вопреки обещанию deploy.md.
Ники и социальный граф ложились в /var/log/nginx рядом с чистым журналом bare.
ADR-058: «выйти на других устройствах» не обрывал уже открытый SSE — отозванная
сессия продолжала получать сообщения.
ADR-059: промежуточный ключ комнаты был невосстановим. Участник, пропустивший
офлайн два rekey подряд, навсегда не расшифровал бы сообщения среднего ключа —
вопреки обещанию storage.md о повторной попытке после получения keyId.
ADR-063: ACK уходил по одному на конверт, а не пачкой. Получатель в оживлённой
комнате выедал общее ведро подтверждениями и упирался в 429 на всех изменяющих
запросах, включая выход из комнаты: 116 отказов за прогон стало нулём.
ADR-055, 057, 060, 061, 062: ключи вёдер и границы, +dirty у bare version,
403 unknown_device не хоронит сообщение, усечение имени в подсказке ввода,
kdf как оракул после повышения цели KDF.

Модель угроз пополнена тем, что действительно видит оператор: push-подписки
лежат в базе открытым текстом, и вместе с VAPID-ключом с той же машины это
произвольное уведомление на экране блокировки.

README приведён к v1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-23 06:22:52 +03:00

7.2 KiB
Raw Permalink Blame History

Деплой

Цель — ssh xmatic (Ubuntu 22.04, x86_64), домен bare.xmatic.team, A-запись на IP сервера. Решения — ADR-022.

Сборка

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bare ./cmd/bare

Версия бинаря — vcs.revision из debug.ReadBuildInfo(), печатается по bare version; у сборки из изменённого рабочего дерева (vcs.modified) к ревизии дописывается +dirty — сверка со сборкой из тега не должна проходить молча (ADR-057). /healthz отвечает только ok.

Первичная настройка сервера (один раз)

sudo useradd --system --home /var/lib/bare --shell /usr/sbin/nologin bare
sudo mkdir -p /opt/bare /var/lib/bare /etc/bare
sudo chown bare:bare /var/lib/bare
sudo chmod 0700 /var/lib/bare /etc/bare

Права закрыты намеренно (ADR-032): в базе лежат argon2id(authKey) и ключевые блобы, машина общая.

/etc/bare/env (владелец root, режим 0600):

BARE_ADDR=127.0.0.1:8411
BARE_DB=/var/lib/bare/bare.db
BARE_ORIGIN=https://bare.xmatic.team
BARE_VAPID_PUBLIC=<из bare vapid>
BARE_VAPID_PRIVATE=<из bare vapid>
BARE_VAPID_SUBJECT=mailto:admin@xmatic.team
BARE_INVITE_CODE=<пусто или код>

bare vapid печатает пару ключей; выполняется локально один раз, результат вписывается в файл.

/etc/systemd/system/bare.service:

[Unit]
Description=Bare chat
After=network-online.target
Wants=network-online.target

[Service]
User=bare
Group=bare
EnvironmentFile=/etc/bare/env
ExecStart=/opt/bare/bare serve
Restart=on-failure
RestartSec=2
StateDirectory=bare
StateDirectoryMode=0700
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/bare

[Install]
WantedBy=multi-user.target

/etc/nginx/sites-available/bare.xmatic.team (затем симлинк в sites-enabled):

server {
    listen 80;
    listen [::]:80;
    server_name bare.xmatic.team;
    access_log off;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name bare.xmatic.team;

    ssl_certificate     /etc/letsencrypt/live/bare.xmatic.team/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bare.xmatic.team/privkey.pem;
    add_header Strict-Transport-Security "max-age=31536000" always;

    # журнал запросов ведёт только bare, и ведёт без ника, IP и query (ADR-056)
    access_log off;

    client_max_body_size 64k;

    location /api/events {
        proxy_pass http://127.0.0.1:8411;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        gzip off;
        proxy_read_timeout 1h;
    }

    location / {
        proxy_pass http://127.0.0.1:8411;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Connection "";
    }
}

Сертификат: сначала временный конфиг только с блоком :80 (без return, с root для ACME) или certbot --nginx -d bare.xmatic.team — на машине certbot уже обслуживает соседние сайты, использовать тот же способ, что у них (ls /etc/letsencrypt/renewal/ показывает, какой плагин).

sudo nginx -t && sudo systemctl reload nginx
sudo systemctl daemon-reload && sudo systemctl enable --now bare

Обновление — scripts/deploy.sh

Перед сборкой: если менялись index.html, app.css, js/*, manifest.json или иконки — сменить VERSION в web/sw.js (ADR-023). Без этого установленные приложения получат новую оболочку только вторым открытием, по ETag.

#!/bin/sh
set -eu
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /tmp/bare ./cmd/bare
scp /tmp/bare xmatic:/tmp/bare
ssh xmatic 'sudo install -m 0755 -o root -g root /tmp/bare /opt/bare/bare && sudo systemctl restart bare && sleep 1 && curl -fsS http://127.0.0.1:8411/healthz'

Сверка подлинности: sha256sum /opt/bare/bare на сервере равен хешу сборки из тега на той же версии Go с теми же флагами.

Проверка после деплоя

  • curl -I https://bare.xmatic.team/ — 200, заголовки CSP и nosniff.
  • curl -N https://bare.xmatic.team/api/events — 401 (без cookie), без буферизации.
  • curl -s https://bare.xmatic.team/sw.js | grep VERSION — версия та, что в репозитории.
  • journalctl -u bare -f — старт, применённые миграции, нет ошибок.

Бэкап

sqlite3 /var/lib/bare/bare.db "VACUUM INTO '/var/lib/bare/backup.db'" или копия файла при остановленном сервисе. В базе только шифротексты и метаданные — бэкап не содержит переписки. Копия наследует режим 0600 (ADR-032); при восстановлении в другое место права надо выставить руками.

Логи

Сервер пишет в stdout: время, метод, путь, статус, длительность; для маршрутов /api/ вместо пути пишется шаблон (/api/users/{nick}), чтобы ник не попадал в журнал, а если отказ случился до маршрутизации (Origin, предел тела) и шаблона ещё нет — просто /api/; ника в журнале нет вовсе, включая отказы по лимитам (ADR-055); IP не пишется. Причины ответов 500 internal (ADR-027) пишутся отдельной строкой, без данных запроса. Отправитель пушей пишет класс отказа — «таймаут», «имя не разрешилось», «отправка не удалась» — без адреса подписки и идентификатора устройства (ADR-047). journald хранит по своим правилам.

nginx журнал запросов не ведёт: access_log off в обоих server-блоках (ADR-056). Без этой строки он унаследовал бы access.log формата combined из /etc/nginx/nginx.conf — с адресом клиента и полным URI, то есть с ником и социальным графом. error_log остаётся: это журнал сбоев, а не запросов, и при отказе он записывает адрес клиента.