Установка из deb/rpm-пакетов
Рекомендуемый способ для прода: пакеты кладут исполняемый файл, systemd-юнит, env-файл и пример конфига, корректно ведут себя при обновлении (рабочий конфиг не перетирается). Этот гайд проходит путь «с нуля до connected агента» — установка пакетов, провижининг Vault, выпуск серверного TLS-материала Keeper-а, заполнение конфигов, bootstrap первого оператора и онбординг первого агента.
Все примеры используют обобщённые имена: keeper.example.com — FQDN Keeper-а, host-01.example.com — управляемый хост, archon-alice — первый оператор.
Keeper слушает несколько listener-ов. Значения ниже — прод-дефолты из примера конфига (вы можете их изменить):
| Порт | Listener | Протокол | Кто ходит |
|---|---|---|---|
9442 | listen.grpc.bootstrap | server-only TLS | агент на фазе soul init (bootstrap-токен + CSR) |
8443 | listen.grpc.event_stream | mTLS | агент на фазе soul run (долгоживущий EventStream) |
8080 | listen.openapi | HTTP | операторы (Operator API), health-check /readyz, web-UI /ui, вьювер спеки /docs |
8081 | listen.mcp | HTTP | MCP-клиенты |
9090 | listen.metrics | HTTP | Prometheus scrape (/metrics) |
На управляемых хостах наружу не нужно открывать входящие порты — агент сам инициирует соединение к Keeper-у. Локально агент слушает только listener метрик.
Firewall-правила:
- На keeper-хостах — открыть входящие
9442и8443для подсети управляемых хостов;8080/8081— для операторской/MCP-сети;9090— для Prometheus. - С keeper-хостов наружу — доступ к PostgreSQL / Redis / Vault и к git-хостингу (резолв сервисов и плагинов).
- С хостов агентов наружу — доступ к keeper-ам на
9442и8443.
1. Установка пакетов
Заголовок раздела «1. Установка пакетов»Три пакета:
| Пакет | Куда ставить | Что несёт |
|---|---|---|
soul-stack-keeper | центральный узел (1+ инстанс) | keeper + systemd-юнит + env + пример конфига |
soul-stack-soul | каждый управляемый хост | soul + systemd-юнит + env + пример конфига |
soul-stack-soul-lint | рабочая станция оператора / CI | только soul-lint (CLI, без демона и конфига) |
Keeper (на центральном узле)
Заголовок раздела «Keeper (на центральном узле)»sudo dpkg -i soul-stack-keeper_<version>_amd64.deb # Debian/Ubuntusudo rpm -i soul-stack-keeper-<version>.x86_64.rpm # RHEL-семействоПакет раскладывает:
| Путь | Что | Заметка |
|---|---|---|
/usr/local/bin/keeper | исполняемый файл, 0755 | — |
/etc/systemd/system/keeper.service | systemd-юнит | Type=exec, User=soul-stack, hardening (ProtectSystem=strict, единственный writable /var/lib/keeper) |
/etc/keeper/keeper.env | env-файл, `config | noreplace` |
/etc/keeper/keeper.yml.example | пример конфига, 0640 | рабочий конфиг создаёт оператор копированием (шаг 5) |
Soul (на каждом управляемом хосте)
Заголовок раздела «Soul (на каждом управляемом хосте)»sudo dpkg -i soul-stack-soul_<version>_amd64.debРаскладка симметрична Keeper-у: /usr/local/bin/soul, /etc/systemd/system/soul.service, /etc/soul/soul.env (SOUL_CONFIG=/etc/soul/soul.yml), /etc/soul/soul.yml.example.
Системный пользователь и каталоги
Заголовок раздела «Системный пользователь и каталоги»Оба юнита работают под системным пользователем soul-stack. Создание пользователя и каталогов состояния — за оператором (юниты ожидают их готовыми). На каждом хосте один раз:
На keeper-хосте:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin soul-stacksudo install -d -o soul-stack -g soul-stack /etc/keeper /var/lib/keeperНа soul-хосте:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin soul-stacksudo install -d -o soul-stack -g soul-stack /etc/soul /var/lib/soul-stacksoul-stack-soul-lint — просто CLI без демона, ставится одним dpkg -i / rpm -i и не требует настройки.
2. Провижининг Vault
Заголовок раздела «2. Провижининг Vault»Эти шаги оператор выполняет на своём проде-Vault (под токеном/политикой с правами на mount-ы). В проде используется persistent backend, auto-unseal и least-privilege policy для самого Keeper-а; провижининг ниже — разовая admin-операция, отдельная от рантайм-доступа Keeper-а.
2.1. KV-секреты
Заголовок раздела «2.1. KV-секреты»Keeper резолвит DSN PostgreSQL, пароль Redis и ключ подписи операторских JWT через vault:-ref-ы из конфига. Записать их в KV (mount secret/). Подходит как KV v2 (рекомендуется — versioning и metadata), так и KV v1 — Keeper определяет версию mount-а автоматически, отдельно её настраивать не нужно:
# DSN внешнего PostgreSQL (поле `dsn`)vault kv put secret/keeper/postgres \ dsn="postgres://keeper:<password>@postgres.example.com:5432/keeper?sslmode=require"
# Пароль внешнего Redis (поле, на которое указывает redis.password_ref)vault kv put secret/keeper/redis \ password="<redis-password>"
# Ключ подписи операторских JWT — 32 байта рандома, base64.# Сгенерировать ОДИН раз и зафиксировать: смена ключа инвалидирует все живые JWT.vault kv put secret/keeper/jwt-signing-key \ signing_key="$(openssl rand -base64 32)"2.2. PKI: engine + корень + роль выпуска
Заголовок раздела «2.2. PKI: engine + корень + роль выпуска»PKI выпускает mTLS-сертификаты агентов. Включить engine, сгенерировать корень и завести роль (имена mount/роли — пример, подставьте свои):
# 1. Включить PKI-engine и поднять max-lease-ttlvault secrets enable -path=pki pkivault secrets tune -max-lease-ttl=87600h pki
# 2. Сгенерировать корневой сертификат — общий якорь доверия для всех агентов# И серверного cert-а Keeper-а (см. шаг 3).vault write pki/root/generate/internal \ common_name="soul-stack" ttl=87600h
# 3. Роль soul-seed: домены/SAN, разрешённые для выпускаемых сертификатов.# allowed_domains — под FQDN-схему ваших хостов.vault write pki/roles/soul-seed \ allowed_domains="example.com" \ allow_subdomains=true \ max_ttl=720h2.3. AppRole для рантайм-доступа Keeper-а
Заголовок раздела «2.3. AppRole для рантайм-доступа Keeper-а»В проде Keeper аутентифицируется в Vault через AppRole (не root-токен). Создать роль с привязкой least-privilege policy:
vault policy write keeper-prod keeper-vault-policy.hcl
vault write auth/approle/role/keeper-prod \ token_policies=keeper-prod \ secret_id_ttl=720h token_ttl=1h token_max_ttl=24h
# role_id — НЕ секрет, пойдёт в keeper.yml::vault.auth.role_idvault read auth/approle/role/keeper-prod/role-id
# secret_id — СЕКРЕТ, положить в файл mode 0400 (шаг 5)vault write -f auth/approle/role/keeper-prod/secret-idrole_id — идентификатор роли, не секрет (хранится открыто в keeper.yml). secret_id — секрет, в конфиге plaintext-ом не хранится: источник — локальный файл secret_id_file (mode 0400) или env secret_id_env. AppRole-credentials намеренно не читаются из Vault (chicken-egg: именно ими Keeper логинится в Vault).
3. TLS-материал Keeper-а
Заголовок раздела «3. TLS-материал Keeper-а»Это самое аккуратное место онбординга — здесь сходятся две цепочки доверия.
Что нужно положить
Заголовок раздела «Что нужно положить»Keeper слушает оба gRPC-listener-а (bootstrap 9442 и event_stream 8443) с серверным сертификатом:
| Файл | Роль |
|---|---|
/etc/keeper/tls/server.crt | серверный leaf-cert Keeper-а (предъявляется на bootstrap + event_stream) |
/etc/keeper/tls/server.key | приватный ключ leaf-а |
/etc/keeper/tls/ca.crt | CA для валидации клиентских сертификатов агентов на mTLS event_stream |
Один PKI-корень для всего
Заголовок раздела «Один PKI-корень для всего»Серверный cert Keeper-а обязан цепляться к тому же PKI-корню, что и сертификаты агентов. Иначе на mTLS-стриме агент не доверяет серверному cert-у Keeper-а, а Keeper — клиентскому cert-у агента. Поэтому серверный leaf Keeper-а выпускается из той же роли pki/issue/soul-seed, что и сертификаты агентов.
Процедура выпуска
Заголовок раздела «Процедура выпуска»vault write -format=json pki/issue/soul-seed \ common_name="keeper.example.com" \ alt_names="keeper.example.com" \ ttl=720h > keeper-issue.jsonИз JSON-ответа разложить три поля в файлы (certificate → server.crt, private_key → server.key, issuing_ca → ca.crt) и выставить права:
sudo install -d -o soul-stack -g soul-stack -m 0750 /etc/keeper/tlssudo install -o soul-stack -g soul-stack -m 0640 server.crt /etc/keeper/tls/server.crtsudo install -o soul-stack -g soul-stack -m 0600 server.key /etc/keeper/tls/server.keysudo install -o soul-stack -g soul-stack -m 0640 ca.crt /etc/keeper/tls/ca.crtРотация leaf-а — повтор этой процедуры + рестарт Keeper-а; CA-корень при этом не меняется, поэтому уже онбордженные агенты не затрагиваются.
4. Конфиг keeper.yml
Заголовок раздела «4. Конфиг keeper.yml»Скопировать пример в рабочий путь и заполнить:
sudo cp /etc/keeper/keeper.yml.example /etc/keeper/keeper.ymlsudo chown soul-stack:soul-stack /etc/keeper/keeper.ymlsudo chmod 0640 /etc/keeper/keeper.ymlОбязательные к проверке/правке блоки:
# Идентичность инстанса — уникальна в кластере (несколько keeper-ов = разные kid)kid: keeper-01
listen: grpc: bootstrap: addr: "0.0.0.0:9442" tls: { cert: /etc/keeper/tls/server.crt, key: /etc/keeper/tls/server.key } event_stream: addr: "0.0.0.0:8443" # прод-порт EventStream (в dev — 9443) tls: { cert: /etc/keeper/tls/server.crt, key: /etc/keeper/tls/server.key, ca: /etc/keeper/tls/ca.crt } openapi: { addr: "0.0.0.0:8080" } mcp: { addr: "0.0.0.0:8081" } metrics: { addr: "0.0.0.0:9090" }
# Внешние хранилища — через vault:-ref (значения положены на шаге 2.1)postgres: dsn_ref: vault:secret/keeper/postgresredis: addr: "redis.example.com:6379" password_ref: vault:secret/keeper/redis
# Vault — AppRole (шаг 2.3) + PKI-mount (шаг 2.2)vault: addr: "https://vault.example.com:8200" auth: method: approle role_id: keeper-prod # role_id из шага 2.3 (не секрет) secret_id_file: /etc/keeper/vault-secret-id # secret_id, файл mode 0400 pki_mount: "pki"
# JWT операторов (signing-key положен на шаге 2.1)auth: jwt: signing_key_ref: vault:secret/keeper/jwt-signing-key issuer: keeper-01 ttl_default: 24h ttl_bootstrap: 720h # 30 днейПоложить secret_id (из шага 2.3) в файл, на который указывает secret_id_file:
echo -n "<secret_id>" | sudo tee /etc/keeper/vault-secret-id >/dev/nullsudo chown soul-stack:soul-stack /etc/keeper/vault-secret-idsudo chmod 0400 /etc/keeper/vault-secret-id5. Запуск Keeper
Заголовок раздела «5. Запуск Keeper»Включить и запустить:
sudo systemctl daemon-reloadsudo systemctl enable --now keeperПроверить:
systemctl status keeperjournalctl -u keeper -n 100 --no-pagercurl -fsS http://127.0.0.1:8080/readyz && echo OK/readyz отвечает 200, когда инстанс готов принимать трафик (доступны PostgreSQL и Redis).
6. Bootstrap первого оператора (Archon)
Заголовок раздела «6. Bootstrap первого оператора (Archon)»Реестр операторов в свежей БД пуст — все API вернут 403, пока не создан первый оператор. Bootstrap — административная подкоманда самого исполняемого файла keeper (не отдельный режим), запускается один раз на одном инстансе:
sudo -u soul-stack keeper init \ --archon=archon-alice \ --config=/etc/keeper/keeper.yml \ --credential-out=/etc/keeper/archon-alice.jwtПод PG advisory lock проверяется, что реестр операторов пуст; создаётся первый оператор с ролью cluster-admin (permissions ["*"]); выпускается JWT (TTL = auth.jwt.ttl_bootstrap, по умолчанию 30 дней) и пишется в --credential-out с mode 0400.
Сохранить токен в переменную для следующих шагов:
TOKEN=$(sudo cat /etc/keeper/archon-alice.jwt)7. Онбординг агента (Soul)
Заголовок раздела «7. Онбординг агента (Soul)»Онбординг двусторонний: оператор регистрирует хост и получает одноразовый bootstrap-токен; на хосте soul init обменивает токен + CSR на mTLS-идентичность. Идентификатор агента (SID) равен FQDN хоста; приватный ключ генерируется на хосте и никогда его не покидает.
7.1. Зарегистрировать хост
Заголовок раздела «7.1. Зарегистрировать хост»На стороне Keeper-а (через Operator API; SID = FQDN будущего хоста):
curl -s -X POST http://keeper.example.com:8080/v1/souls \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"sid": "host-01.example.com", "transport": "agent", "covens": ["demo"]}'В ответе — bootstrap_token (возвращается один раз, TTL по умолчанию 24 часа). Запись агента появляется в статусе pending. Метка covens: ["demo"] — стабильный тег хоста для таргетинга сценариев. Потерянный токен не восстановить — только перевыпустить через POST /v1/souls/{sid}/issue-token.
7.2. Trust-материал на хосте
Заголовок раздела «7.2. Trust-материал на хосте»Перед soul init агент ещё не имеет mTLS-идентичности, но bootstrap-фаза (9442) идёт по server-only TLS — агент обязан проверить серверный cert Keeper-а против CA-файла из soul.yml::keeper.tls.ca. Доверие устанавливается не TOFU, а явной предзагрузкой CA. Положите на хост PKI-корень (тот же CA, что и серверный cert Keeper-а, шаг 3):
sudo install -d -o soul-stack -g soul-stack -m 0750 /var/lib/soul-stack/seedsudo install -o soul-stack -g soul-stack -m 0644 ca.crt /var/lib/soul-stack/seed/ca.crtПосле успешного soul init Keeper возвращает PKI-цепочку, которую агент сохраняет и использует для верификации сервера на EventStream-фазе. Предзагруженный ca.crt нужен только до первого soul init.
7.3. Конфиг soul.yml
Заголовок раздела «7.3. Конфиг soul.yml»sudo cp /etc/soul/soul.yml.example /etc/soul/soul.ymlsudo chown soul-stack:soul-stack /etc/soul/soul.ymlsudo chmod 0640 /etc/soul/soul.ymlМинимум, что правится:
keeper: endpoints: - host: keeper.example.com # FQDN из SAN серверного cert-а (шаг 3) event_stream_port: 8443 # прод-порт EventStream (в dev — 9443) bootstrap_port: 9442 # server-only TLS, фаза `soul init` priority: 1 tls: ca: /var/lib/soul-stack/seed/ca.crt # предзагружен на шаге 7.2
paths: modules: /var/lib/soul-stack/modules seed: /var/lib/soul-stack/seed # сюда `soul init` положит идентичность7.4. soul init — обмен токена на идентичность
Заголовок раздела «7.4. soul init — обмен токена на идентичность»Bootstrap-токен передаётся через env-переменную (предпочтительно — не светит в ps/history) или из stdin:
SOUL_BOOTSTRAP_TOKEN='<bootstrap_token из 7.1>' \ sudo -u soul-stack -E soul init --config=/etc/soul/soul.ymlКоманда определяет SID (= FQDN), генерирует приватный ключ + CSR (ключ никогда не покидает хост), подключается к bootstrap-listener-у Keeper-а, предъявляет токен + CSR, получает подписанную mTLS-идентичность и раскладывает её в paths.seed. Если идентичность уже есть — init падает (защита от случайного перевыпуска).
7.5. Запуск демона и проверка
Заголовок раздела «7.5. Запуск демона и проверка»sudo systemctl daemon-reloadsudo systemctl enable --now soulsystemctl status souljournalctl -u soul -n 100 --no-pagerАгент инициирует EventStream-стрим к Keeper-у (mTLS, порт 8443). Проверить, что хост перешёл в connected, со стороны Keeper-а:
curl -s http://keeper.example.com:8080/v1/souls/host-01.example.com \ -H "Authorization: Bearer $TOKEN"# в ответе status: connected8. Обновление
Заголовок раздела «8. Обновление»Пакеты обновляются обычным dpkg -i / rpm -U новой версии. Что важно:
- Рабочие конфиги не перетираются.
*.yml.exampleприходит из пакета, но ваш/etc/keeper/keeper.ymlи/etc/soul/soul.ymlсоздавали вы сами — upgrade их не трогает. Env-файлы помеченыconfig|noreplace. После апгрейда сверьте свой конфиг с новым*.yml.exampleна предмет новых ключей. - DDL-миграции схемы БД Keeper-а применяются идемпотентно при старте Keeper-а. Перед upgrade — backup PostgreSQL.
- Миграции состояния инкарнаций (
state_schema) — отдельная оператор-инициированная операция через Operator API (POST /v1/incarnations/{name}/upgrade), forward-only, не запускается автоматически при рестарте Keeper-а.
Troubleshooting
Заголовок раздела «Troubleshooting»| Симптом | Вероятная причина | Что проверить |
|---|---|---|
soul init: connection refused | Keeper не слушает bootstrap-порт / firewall режет 9442 | Keeper запущен; открыт входящий 9442 с soul-хоста; host/bootstrap_port в soul.yml верны |
soul init: certificate validation failed | предзагруженный CA не от того PKI-корня; FQDN keeper-а не в SAN серверного cert-а | keeper.tls.ca = issuing_ca PKI (шаг 7.2); FQDN из endpoints[].host в SAN (шаг 3) |
soul init: keeper.tls.ca is empty | не указан/не предзагружен CA-файл | заполнить keeper.tls.ca и положить файл (шаг 7.2) |
soul init: bootstrap token invalid / expired / used | токен сожжён, истёк (TTL 24h) или SID не совпал | перевыпустить токен POST /v1/souls/{sid}/issue-token; сверить SID = FQDN |
| Keeper: operators registry is empty; refusing to start | первый запуск без bootstrap-а | выполнить keeper init (шаг 6) |
Агент стартует, но не переходит в connected | EventStream-фаза не проходит (mTLS на 8443) | открыт входящий 8443; порт совпадает с keeper.endpoints[].event_stream_port; серверный cert и идентичность агента от одного PKI-корня (шаг 3) |