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

Установка из deb/rpm-пакетов

Рекомендуемый способ для прода: пакеты кладут исполняемый файл, systemd-юнит, env-файл и пример конфига, корректно ведут себя при обновлении (рабочий конфиг не перетирается). Этот гайд проходит путь «с нуля до connected агента» — установка пакетов, провижининг Vault, выпуск серверного TLS-материала Keeper-а, заполнение конфигов, bootstrap первого оператора и онбординг первого агента.

Все примеры используют обобщённые имена: keeper.example.com — FQDN Keeper-а, host-01.example.com — управляемый хост, archon-alice — первый оператор.

Keeper слушает несколько listener-ов. Значения ниже — прод-дефолты из примера конфига (вы можете их изменить):

ПортListenerПротоколКто ходит
9442listen.grpc.bootstrapserver-only TLSагент на фазе soul init (bootstrap-токен + CSR)
8443listen.grpc.event_streammTLSагент на фазе soul run (долгоживущий EventStream)
8080listen.openapiHTTPоператоры (Operator API), health-check /readyz, web-UI /ui, вьювер спеки /docs
8081listen.mcpHTTPMCP-клиенты
9090listen.metricsHTTPPrometheus scrape (/metrics)

На управляемых хостах наружу не нужно открывать входящие порты — агент сам инициирует соединение к Keeper-у. Локально агент слушает только listener метрик.

Firewall-правила:

  • На keeper-хостах — открыть входящие 9442 и 8443 для подсети управляемых хостов; 8080 / 8081 — для операторской/MCP-сети; 9090 — для Prometheus.
  • С keeper-хостов наружу — доступ к PostgreSQL / Redis / Vault и к git-хостингу (резолв сервисов и плагинов).
  • С хостов агентов наружу — доступ к keeper-ам на 9442 и 8443.

Три пакета:

ПакетКуда ставитьЧто несёт
soul-stack-keeperцентральный узел (1+ инстанс)keeper + systemd-юнит + env + пример конфига
soul-stack-soulкаждый управляемый хостsoul + systemd-юнит + env + пример конфига
soul-stack-soul-lintрабочая станция оператора / CIтолько soul-lint (CLI, без демона и конфига)
Окно терминала
sudo dpkg -i soul-stack-keeper_<version>_amd64.deb # Debian/Ubuntu
sudo rpm -i soul-stack-keeper-<version>.x86_64.rpm # RHEL-семейство

Пакет раскладывает:

ПутьЧтоЗаметка
/usr/local/bin/keeperисполняемый файл, 0755
/etc/systemd/system/keeper.servicesystemd-юнитType=exec, User=soul-stack, hardening (ProtectSystem=strict, единственный writable /var/lib/keeper)
/etc/keeper/keeper.envenv-файл, `confignoreplace`
/etc/keeper/keeper.yml.exampleпример конфига, 0640рабочий конфиг создаёт оператор копированием (шаг 5)
Окно терминала
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-stack
sudo 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-stack
sudo install -d -o soul-stack -g soul-stack /etc/soul /var/lib/soul-stack

soul-stack-soul-lint — просто CLI без демона, ставится одним dpkg -i / rpm -i и не требует настройки.

Эти шаги оператор выполняет на своём проде-Vault (под токеном/политикой с правами на mount-ы). В проде используется persistent backend, auto-unseal и least-privilege policy для самого Keeper-а; провижининг ниже — разовая admin-операция, отдельная от рантайм-доступа Keeper-а.

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)"

PKI выпускает mTLS-сертификаты агентов. Включить engine, сгенерировать корень и завести роль (имена mount/роли — пример, подставьте свои):

Окно терминала
# 1. Включить PKI-engine и поднять max-lease-ttl
vault secrets enable -path=pki pki
vault 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=720h

В проде 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_id
vault read auth/approle/role/keeper-prod/role-id
# secret_id — СЕКРЕТ, положить в файл mode 0400 (шаг 5)
vault write -f auth/approle/role/keeper-prod/secret-id

role_id — идентификатор роли, не секрет (хранится открыто в keeper.yml). secret_id — секрет, в конфиге plaintext-ом не хранится: источник — локальный файл secret_id_file (mode 0400) или env secret_id_env. AppRole-credentials намеренно не читаются из Vault (chicken-egg: именно ими Keeper логинится в Vault).

Это самое аккуратное место онбординга — здесь сходятся две цепочки доверия.

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.crtCA для валидации клиентских сертификатов агентов на mTLS event_stream

Серверный 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-ответа разложить три поля в файлы (certificateserver.crt, private_keyserver.key, issuing_caca.crt) и выставить права:

Окно терминала
sudo install -d -o soul-stack -g soul-stack -m 0750 /etc/keeper/tls
sudo install -o soul-stack -g soul-stack -m 0640 server.crt /etc/keeper/tls/server.crt
sudo install -o soul-stack -g soul-stack -m 0600 server.key /etc/keeper/tls/server.key
sudo install -o soul-stack -g soul-stack -m 0640 ca.crt /etc/keeper/tls/ca.crt

Ротация leaf-а — повтор этой процедуры + рестарт Keeper-а; CA-корень при этом не меняется, поэтому уже онбордженные агенты не затрагиваются.

Скопировать пример в рабочий путь и заполнить:

Окно терминала
sudo cp /etc/keeper/keeper.yml.example /etc/keeper/keeper.yml
sudo chown soul-stack:soul-stack /etc/keeper/keeper.yml
sudo 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/postgres
redis:
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/null
sudo chown soul-stack:soul-stack /etc/keeper/vault-secret-id
sudo chmod 0400 /etc/keeper/vault-secret-id

Включить и запустить:

Окно терминала
sudo systemctl daemon-reload
sudo systemctl enable --now keeper

Проверить:

Окно терминала
systemctl status keeper
journalctl -u keeper -n 100 --no-pager
curl -fsS http://127.0.0.1:8080/readyz && echo OK

/readyz отвечает 200, когда инстанс готов принимать трафик (доступны PostgreSQL и Redis).

Реестр операторов в свежей БД пуст — все 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)

Онбординг двусторонний: оператор регистрирует хост и получает одноразовый bootstrap-токен; на хосте soul init обменивает токен + CSR на mTLS-идентичность. Идентификатор агента (SID) равен FQDN хоста; приватный ключ генерируется на хосте и никогда его не покидает.

На стороне 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.

Перед 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/seed
sudo 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.

Окно терминала
sudo cp /etc/soul/soul.yml.example /etc/soul/soul.yml
sudo chown soul-stack:soul-stack /etc/soul/soul.yml
sudo 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` положит идентичность

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 падает (защита от случайного перевыпуска).

Окно терминала
sudo systemctl daemon-reload
sudo systemctl enable --now soul
systemctl status soul
journalctl -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: connected

Пакеты обновляются обычным 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-а.
СимптомВероятная причинаЧто проверить
soul init: connection refusedKeeper не слушает bootstrap-порт / firewall режет 9442Keeper запущен; открыт входящий 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)
Агент стартует, но не переходит в connectedEventStream-фаза не проходит (mTLS на 8443)открыт входящий 8443; порт совпадает с keeper.endpoints[].event_stream_port; серверный cert и идентичность агента от одного PKI-корня (шаг 3)