Перейти к содержимому

Решение проблем

Практический разбор типовых ситуаций «что-то работает не так»: симптом → причина → проверка и решение. Раздел про эксплуатацию уже запущенной инсталляции; первое спотыкание при подъёме demo-сетапа — в Quick Start, а install-time проблемы онбординга (порты, CA, первый агент) — в таблицах Установка из пакетов → Troubleshooting и Онбординг душ → Troubleshooting.

Перед разбором конкретной ситуации полезно помнить два инварианта, объясняющих большинство «странностей»:

  • Авторитетное состояние — в PostgreSQL, presence и координация — в Redis. Статус агента в реестре (connected/disconnected) — это снимок, который Keeper сводит с фактом фоном; снимок отстаёт от факта на секунды. Минимальный обязательный контур кластера — PostgreSQL + Redis; Vault обязателен для PKI/секретов.
  • Идентичность доказывается, а не сообщается. Подключение агента к Keeper-у держится на общем PKI-корне (mTLS), а доступ оператора — на короткоживущем токене. Большинство отказов «не доверяет» и «доступ запрещён» — про эти два механизма.
СимптомВероятная причинаКуда смотреть
Агент не переходит в connectedсервис не запущен / mTLS не сходится / порт недостижим / SID ≠ FQDN / снимок отстаёт§ Агент не connected
Агент не доверяет серверу Keeper-асерверный cert Keeper-а и SoulSeed выпущены из разных PKI-корней§ TLS/mTLS не сходится
Инкарнация в error_lockedпрогон упал, состояние заблокировано для защиты§ Инкарнация в error_locked
Bootstrap-токен истёк или потерянTTL вышел / токен показывается один раз§ Bootstrap-токен истёк или потерян
Прогон падает на рендере шаблонанеобъявленная переменная / неверный маркер интерполяции / strict-mode§ Ошибки рендера и шаблонов
Операции падают на резолве секретаVault недостижим или путь/поле в KV неверны§ Vault недостижим и PKI
Доступ запрещён (401/403)токен истёк / у оператора нет permission§ Доступ запрещён

Симптом. Хост зарегистрирован, soul init прошёл, но в реестре (GET /v1/souls) он не в статусе connected — остаётся pending или уходит в disconnected.

Идите по списку проверок сверху вниз — он отсортирован от самого частого к редкому.

soul init только получает идентичность (SoulSeed) и завершается — он не держит соединение. Постоянный стрим к Keeper-у держит отдельная команда soul run (в проде — systemd-сервис).

  • Проверьте, что сервис агента активен (systemctl status soul) и в логах нет ошибок старта.
  • Если init прошёл, а демон не запущен — хост так и останется не connected.

2. Подождать пару секунд — статус это снимок

Заголовок раздела «2. Подождать пару секунд — статус это снимок»

Статус в реестре (souls.status) — ленивый снимок для Operator API, а не источник истины о presence. Авторитет «агент онлайн» — живой стрим (lease в Redis); снимок Keeper сводит с фактом фоном и догоняет его с лагом в несколько секунд.

  • Сразу после soul run опросите GET /v1/souls/{sid} ещё раз через несколько секунд — переход pending → connected не мгновенный.
  • Это by-design, не зависание. Если через минуту статуса всё ещё нет — переходите к следующим проверкам.

Рабочий стрим (soul run) идёт по mTLS на EventStream-порт. Стрим установится только если серверный сертификат Keeper-а и SoulSeed агента выпущены из одного PKI-корня — иначе агент не доверяет серверу (и наоборот). Подробный разбор — § TLS/mTLS не сходится.

Агент сам устанавливает исходящее соединение к Keeper-у (входящих портов на управляемом хосте нет). Стрим не поднимется, если порт EventStream недостижим:

  • С хоста агента должен быть доступен endpoints[].host на endpoints[].event_stream_port (mTLS-фаза soul run) — отдельный порт от bootstrap.
  • Проверьте firewall/NAT между хостом и Keeper-ом (или его балансировщиком) на этом порту.
  • host/event_stream_port в soul.yml должны указывать на реальный listener Keeper-а — раскладка блока keeper.endpoints описана в Конфигурация → soul.yml.

SID агента — это FQDN хоста, и он зашит в SoulSeed-сертификат. Если SID, под которым хост зарегистрирован, не совпадает с FQDN из сертификата — Keeper не сопоставит стрим с записью реестра.

  • SID в soul.yml и SID при регистрации (POST /v1/souls) должны совпадать с реальным FQDN.
  • PKI-роль выпускает SoulSeed только на имена из своего allowed-домена — используйте FQDN из этого домена, а не короткое hostname.

Симптом. Агент не подключается, в логах — ошибка верификации сертификата («агент не доверяет серверу», certificate validation failed, unknown authority).

Причина. Для mTLS обе стороны должны доверять одному удостоверяющему центру. Если серверный сертификат Keeper-а подписан другим корнем, чем SoulSeed-сертификаты хостов, — взаимное доверие ломается и стрим не устанавливается.

Проверка и решение.

  • Серверный сертификат Keeper-а и сертификаты агентов должны происходить из одного PKI-корня (PKI Vault). Нельзя взять серверный cert Keeper-а «откуда попало» (например, публичный ACME-сертификат для веб-домена) — он должен быть из того же корня, что и SoulSeed хостов.
  • На хосте агента доверенный CA задаётся явно (поле CA в блоке keeper.tls в soul.yml) и должен указывать на корень того же PKI.
  • FQDN Keeper-а из endpoints[].host должен входить в SAN серверного сертификата — иначе верификация имени не пройдёт даже при верном корне.

Полная модель транспорта и PKI-корня — Безопасность → Транспорт; как этот корень выпускается и где живёт приватный ключ CA — Безопасность → Секреты.

Симптом. GET /v1/incarnations/{name} показывает status: error_locked. Новый прогон на этой инкарнации не запускается.

Что это значит. Прогон сценария завершился ошибкой, и изменения не закоммичены в состояние инкарнации. Состояние блокируется намеренно: это защита от того, чтобы продолжить работу с частично применённым или неизвестным состоянием. БД хранит прежнее (консистентное) состояние, а инкарнация ждёт вмешательства оператора.

Как посмотреть причину.

  • Откройте отчёт о прогоне и историю — GET /v1/incarnations/{name}/history (snapshots состояния по каждому изменению). Там видно, на каком шаге и почему прогон упал.
  • Логи Keeper-а и агента по этому прогону связаны общими идентификаторами (id прогона, trace-id) — этого достаточно, чтобы дойти до конкретного упавшего шага.

Как разблокировать.

  1. Устраните причину провала — это может быть ошибка в сценарии/Destiny, недоступный хост, неверный input, упавший внешний шаг.
  2. Запустите прогон повторно после фикса. Снятие блокировки и повторный запуск — операции Operator API над инкарнацией.
  3. Состояние перейдёт в ready только при успешном завершении прогона; до этого момента прежнее состояние остаётся нетронутым.

Симптом. soul init падает с bootstrap token invalid / expired / used, либо токен потерян и его негде взять.

Причина. Bootstrap-токен:

  • одноразовый — сжигается при первом успешном онбординге;
  • короткоживущий — TTL по умолчанию 24 часа;
  • показывается один раз — Keeper хранит только его хеш, plain-токен после выписки восстановить нельзя.

Решение — перевыпустить токен. Для уже зарегистрированного хоста, который ещё не прошёл онбординг:

Окно терминала
curl -s -X POST https://keeper.example.com/v1/souls/host-01.example.com/issue-token \
-H "Authorization: Bearer $TOKEN"
  • Новый plain-токен возвращается в ответе один раз — сразу сохраните его для доставки на хост.
  • Действует инвариант «максимум один активный токен на хост»: если предыдущий токен ещё активен, запрос без флага вернёт 409 (защита от плодящихся валидных токенов). Чтобы заменить активный токен новым, используйте флаг force (CLI/обёртка) или force: true (MCP) — старый помечается использованным, выпускается новый.
  • Доставьте новый токен на хост любым удобным способом и повторите soul init.

Если хост уже успешно онбордился (SoulSeed на диске есть), а токен «потерян» — он больше не нужен: онбординг пройден, идентичность лежит на хосте. Перевыпуск нужен только для повторного онбординга.

Полный поток онбординга и доставки токена — Онбординг душ. Для агента в push-режиме (по SSH) bootstrap-токен не используется вовсе.

Симптом. Прогон падает на фазе рендера — до применения к хосту. Ошибка указывает на необъявленную переменную, неизвестную функцию или ошибку синтаксиса выражения.

Причина. В Soul Stack два движка шаблонизации с строгой границей по файлам, и оба работают в strict-mode (никакого молчаливого «пустой строки вместо неизвестного» — отсутствующее имя это ошибка, а не пустое значение):

  • CEL — все выражения в YAML. Top-level expression-ключи (when:, where:, changed_when:, failed_when:, until:) — вся строка целиком есть CEL-выражение, без обёртки. Интерполяция внутри строк (params:, vars:, литералы) — через маркер ${ … }.
  • Go text/template — рендер файлов-шаблонов templates/<path>.tmpl. Запускается единственным шагом core.file.rendered, тоже в strict-mode.

Типичные ошибки и решение.

Что в ошибкеПричинаРешение
undeclared reference / неизвестное имяпеременной нет в контексте (опечатка, не объявлена в input:/vars:, не тот аксессор)объявите вход в input: Destiny или значение в vars:; сверьте имя; факты хоста доступны как soulprint.self.<path> (голая soulprint.<path> — ошибка)
${...} попало в файл как естьмаркер интерполяции ${ … } работает в YAML-выражениях, а не внутри .tmpl-файловв .tmpl используйте синтаксис Go text/template ({{ .var }}), а не ${ … }
строка не распозналась как выражениеперепутаны контексты: в expression-ключе обёрнули в ${ }, или в строковом контексте забыли маркерв when:/where:/… пишите голый CEL без обёртки; в строках — только через ${ … }
неизвестная функция в шаблоне файлафункция вне разрешённого набора (исключены читающие FS/сеть/окружение, выполняющие команды, генерирующие крипто)используйте функцию из разрешённого набора либо вынесите вычисление в CEL-фазу

Ловите ошибки офлайн — soul-lint до применения. Линтер soul-lint проверяет артефакты (Destiny/Scenario/шаблоны) офлайн, без доступа к Keeper-у и хостам — это правильное место поймать ошибку рендера до прогона. Запускайте его локально и в CI как обязательный шаг. Подкоманды, exit-коды и пример CI-шага — Компоненты → soul-lint. Сам синтаксис Destiny и рендер файлов — DSL → Destiny.

Симптом. Старт нового Keeper-инстанса падает, либо прогоны начинают валиться на фазе рендера с ошибкой чтения секрета; онбординг новых агентов не проходит.

Причина. Vault обслуживает две критичные функции: секреты (DSN PostgreSQL, ключ подписи операторских токенов, значения для vault(...) в выражениях и vault:-ref в конфиге) и PKI (выпуск SoulSeed-сертификатов, общий корень доверия для mTLS).

Минимальный обязательный контур кластера — PostgreSQL + Redis; Vault обязателен именно для PKI и секретов. Без Vault страдают операции, которым нужны секрет или новый сертификат.

Что продолжает работать при недоступном Vault:

  • уже установленные стримы агентов (соединение поднято, зарезолвленные секреты закэшированы);
  • фоновая чистка и координация Keeper-а;
  • операции Operator API, не требующие секретов из Vault.

Что падает:

  • старт нового Keeper-инстанса — он резолвит DSN PostgreSQL из Vault на старте (fail-fast);
  • любой прогон с секретом — шаг, тянущий vault(...) в CEL или vault:-ref в шаблоне, не отрендерится;
  • онбординг нового агента — PKI недоступен, SoulSeed не выпустить.

Проверка и решение.

  • Проверьте доступность Vault и его статус (unsealed). Симптом «cannot read postgres dsn» на старте Keeper-а — почти всегда про Vault: либо он недоступен, либо нужный KV-путь/поле отсутствуют.
  • Сверьте vault:-ref в конфиге Keeper-а: путь и имя поля в KV должны существовать.
  • После восстановления Vault существующие сессии переживают сбой — Keeper переустановит токен; при быстром возврате Vault операторы могут не заметить перебоя.
  • Как Vault интегрирован, как резолвятся секреты и где живёт корень PKI — Безопасность → Секреты.

Симптом. Вызов Operator API возвращает 401 или 403.

Причина и решение. Это два разных случая:

  • 401 — токен не принят. Операторский токен (JWT) короткоживущий, и немедленного отзыва по содержимому токена нет: истёкший токен просто перестаёт приниматься (это и есть основная защита — отозванный оператор перестаёт действовать с истечением TTL). Решение — получить свежий токен. Перевыпуск операторского токена — операция Operator API; разбор реестра операторов и ролей — Управление операторами.
  • 403 — токена приняли, но прав нет. У оператора нет нужного permission для операции (модель default-deny). Проверьте роль оператора и её permissions. Грамматика прав и scope — Операторы → Роли и доступ.