Лимиты: все четыре правила 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
150 lines
7.2 KiB
Markdown
150 lines
7.2 KiB
Markdown
# Деплой
|
||
|
||
Цель — `ssh xmatic` (Ubuntu 22.04, x86_64), домен `bare.xmatic.team`, A-запись на IP сервера. Решения — ADR-022.
|
||
|
||
## Сборка
|
||
|
||
```sh
|
||
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`.
|
||
|
||
## Первичная настройка сервера (один раз)
|
||
|
||
```sh
|
||
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`:
|
||
|
||
```ini
|
||
[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`):
|
||
|
||
```nginx
|
||
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/` показывает, какой плагин).
|
||
|
||
```sh
|
||
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.
|
||
|
||
```sh
|
||
#!/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` остаётся: это журнал сбоев, а не запросов, и при отказе он записывает адрес клиента.
|