Runbook — секреты и ротация
Audience: operator. Все секреты хранятся как age-зашифрованный шифротекст в git; расшифровка возможна только приватным ключом, который не в git.
⚠ В этом документе — только где лежит секрет и как его крутить. Никаких значений. Никогда не вставлять токен в URL remote и никогда не печатать его в лог/чат.
Модель хранения
- Репозиторий секретов: приватный
kontinuum/secrets(http://gitlab.local.lan/kontinuum/secrets.git). Содержит только файлы*.age(шифротекст age). Клон на воркере:worker:~/secrets-repo. - Корневой приватный ключ:
worker:~/.git-key(age identity). НЕ в git. Это out-of-band корень доверия: кто его имеет — читает все секреты. - Публичный ключ:
worker:~/.git-key.pub— им шифруют новые секреты (приватник для добавления не нужен).
Что лежит в хранилище
Клон репозитория секретов на воркере:
worker:~/kontinuum-secrets(исторически упоминался~/secrets-repo— авторитетен фактический клон; репозиторий один:kontinuum/secrets).
Файл (*.age) | Содержит | Потребитель |
|---|---|---|
git-credentials.age | GitLab PAT (см. ниже) | git на воркере |
release-key-v2.jks.age | активный Android release keystore (0.5.1+) | подпись APK |
release-key-v2.creds.age | alias + storepass/keypass для release-key-v2 (сильный, маскируемый) — это и есть текущие CI-vars ANDROID_KEY_ALIAS/ANDROID_KEYSTORE_PASSWORD/ANDROID_KEY_PASSWORD | CI-подпись APK |
release-key.jks.age | архив — keystore старого ключа (cert d71bd8d6, alias key-alias, слабый 7-симв пароль). Подписал только 0.5.0 | воспроизводимость/анализ 0.5.0; в CI больше не используется |
release-key.creds.age | alias + storepass/keypass архивного старого keystore (0.5.0 only) | воспроизводимость 0.5.0 |
updates-deploy-ssh-key.age | CI-var UPDATES_DEPLOY_SSH_KEY (file-type): SSH deploy-ключ публикации APK на updates.kontinuum.cloud | CI release-publish |
release-publish-token.age | CI-var RELEASE_PUBLISH_TOKEN | CI release-publish |
hygiene-pat.age | CI-var HYGIENE_PAT | CI hygiene/cleanup job |
submodule-sync-token.age | CI-var SUBMODULE_SYNC_TOKEN | CI рекурсивный submodule-checkout |
cf-service-token.age | CF Access service-token | автоматический доступ к GitLab API/CI |
stalwart.env.age | env почтового сервера Stalwart | mail-сервис |
(Список может расти — авторитетен ls worker:~/kontinuum-secrets/*.age.)
Операции (всё на воркере)
Прочитать секрет:
cd ~/secrets-repo
age -d -i ~/.git-key release-key-v2.jks.age > /tmp/release-key.jks # пример (активный ключ)
# ... использовать, затем удалить расшифрованную копию:
shred -u /tmp/release-key.jksДобавить / обновить секрет (приватник не нужен — шифруем публичным):
cd ~/secrets-repo
age -r "$(cat ~/.git-key.pub)" -o name.age <plaintext-файл-или-stdin>
git add name.age && git commit -m "secrets: add/rotate name" && git pushGitLab PAT (личный токен)
- Живёт только в
worker:~/.git-credentials(форматhttp://oauth2:<TOKEN>@gitlab.local.lan). Git использует его через credential helper — remotes остаются без токена в URL. - Ротация: выпустить новый PAT в GitLab → отредактировать один файл
~/.git-credentials(заменить токен) → также обновитьgit-credentials.ageв хранилище. Больше нигде токен не дублируется.
Никогда
- Не встраивать токен в URL remote (
git remote set-url …token…). - Не печатать токен (
echo,cat ~/.git-credentials,psс cloudflared). - Для извлечения PAT в скрипте —
sed-выемка в переменную, не в stdout.
CI-секреты
Секреты, нужные пайплайну, продублированы как GitLab CI-переменные (masked) в настройках проекта/группы. То есть один логический секрет может жить в двух местах: age-хранилище (источник истины для людей/воркера) и masked CI-var (для раннера). При ротации обновлять оба.
Корневой ключ — критично
worker:~/.git-key — единственная копия корня. Если воркер умрёт и off-machine копии нет — все in-git секреты потеряны (шифротекст нерасшифруем).
Обязательство
~/.git-key обязан быть забэкаплен off-machine (менеджер паролей). Это не «желательно» — это условие выживания всего секрет-стора. Проверка: операционно подтверждать наличие копии в менеджере паролей. См. также runbook бэкапов.
CI ↔ age карта (проект 23, аудит #75 + миграция v2 #118 C — 2026-06-24)
Каждый CI-секрет проекта 23 теперь имеет age-бэкап в kontinuum/secrets. Один логический секрет живёт в двух местах: age-хранилище (источник истины для людей/воркера) и CI-var (для раннера). При ротации обновлять оба.
| CI-var | masked | protected | type | age-файл |
|---|---|---|---|---|
ANDROID_KEYSTORE_B64 | нет (слишком длинный — лимит маскирования GitLab) | да | env | release-key-v2.jks.age (cert 954bcd2e) |
ANDROID_KEYSTORE_PASSWORD | да (v2: 32 симв ≥ 8 → маскируется) | да | env | release-key-v2.creds.age |
ANDROID_KEY_PASSWORD | да (v2: 32 симв ≥ 8 → маскируется) | да | env | release-key-v2.creds.age |
ANDROID_KEY_ALIAS | нет (не секрет; = kontinuum-release-v2) | да | env | release-key-v2.creds.age |
SUBMODULE_SYNC_TOKEN | да | да | env | submodule-sync-token.age |
HYGIENE_PAT | да | да | env | hygiene-pat.age |
UPDATES_DEPLOY_SSH_KEY | нет (file-vars маскировать нельзя; не эхоятся) | да | file | updates-deploy-ssh-key.age |
RELEASE_PUBLISH_TOKEN | да | да | env | release-publish-token.age |
✅ Закрыто: keystore-пароли маскируются (#75 через #118 C)
Ранее ANDROID_KEYSTORE_PASSWORD/ANDROID_KEY_PASSWORD были по 7 символов (< 8 → GitLab отвергал masked=true). После миграции CI на release-key-v2 (2026-06-24, #118 решение C) оба пароля — по 32 символа → GitLab принял masked=true. Гэп #75 закрыт: оба пароля теперь маскируются в логах.
✅ Разрешено: CI подписывает ключом v2 (#118 решение C)
CI (проект 23) теперь подписывает release-key-v2 (cert 954bcd2e…, alias kontinuum-release-v2, сильный 32-симв пароль). ANDROID_KEYSTORE_B64/ANDROID_*_PASSWORD/ANDROID_KEY_ALIAS обновлены на v2 через API (2026-06-24, sudo=cto) и проверены: реальный release-APK, переподписанный текущими CI-vars, верифицируется как Signer #1 certificate SHA-256: 954bcd2e…, не старый d71bd8d6. Старый release-key.jks — архив (подписал только 0.5.0).
⚠ Continuity намеренно сломана на 0.5.0→0.5.1 (#118 C)
v2 — другой signing-cert, чем 0.5.0 (d71bd8d6). Android обновляет APK только при совпадении signing-cert, поэтому 0.5.1 НЕ установится поверх 0.5.0 как апдейт. Это сознательное решение founder (#118, вариант C): база юзеров ≈ 0 (только founder, pre-launch), простота ухода на сильный ключ важнее continuity.
Founder ОБЯЗАН переустановить: удалить 0.5.0 и поставить 0.5.1 с нуля (авто-апдейт с 0.5.0 на 0.5.1 не сработает). Делать это один раз, когда 0.5.1 будет реально собран и опубликован. Последующие релизы (0.5.1→0.5.2…) снова идут авто-апдейтом — все на v2.
Известные пробелы (не покрыто age-бэкапом)
- bill .env — на воркере не найден; стек bill спящий/выключен. Забэкапить при провижене bill.
Порядок восстановления (worker погиб)
Предпосылка: out-of-band корень (~/.git-key + GitLab-логин) восстановлен из менеджера паролей founder.
- GitLab-доступ. Войти в GitLab (логин из менеджера паролей). Выпустить/ восстановить PAT → положить в
worker:~/.git-credentials(http://oauth2:<TOKEN>@gitlab.local.lan). Без токена в URL remote. - Корневой age-ключ. Восстановить
worker:~/.git-key(0600) и~/.git-key.pubиз менеджера паролей. Проверка:age -d -i ~/.git-key ~/kontinuum-secrets/cf-service-token.age >/dev/null. - Клон хранилища.
git clone … kontinuum/secrets.git ~/kontinuum-secrets. - Подпись релизов. Расшифровать
release-key-v2.jks.age+release-key-v2.creds.age→ восстановить CI-varsANDROID_KEYSTORE_B64(base64 jks, protected, masked=false),ANDROID_KEYSTORE_PASSWORD+ANDROID_KEY_PASSWORD(masked=true),ANDROID_KEY_ALIAS(kontinuum-release-v2) через API проекта 23. Проверка: cert восстановленного keystore =954bcd2e…. (Старыйrelease-key.*.age— только для воспроизведения архивного 0.5.0, в CI не нужен.) - CI-токены. Восстановить
SUBMODULE_SYNC_TOKEN,HYGIENE_PAT,RELEASE_PUBLISH_TOKEN(env, masked) иUPDATES_DEPLOY_SSH_KEY(variable_type=file) из соответствующих.ageчерез API. - Инфра-сервисы.
cf-service-token.age→ CF Access;stalwart.env.age→/etc/stalwart/на head. - Проверка. Прогнать release-пайплайн (сборка+подпись+публикация APK).
Founder TODO (out-of-band корень)
Подтвердить, что в менеджере паролей лежат обе копии корня: (1) приватный age-ключ ~/.git-key (целиком), (2) GitLab-логин/восстановление. Без них шифротекст в git нерасшифруем — это условие выживания всего стора.