Секреты и ротация¶
Где в установке Taimen лежат секреты, с какими правами, как следить за сроками и как менять каждый вид секрета — плановая ротация и замена при компрометации. Статья для инженера эксплуатации и ответственного за безопасность.
Принципы¶
- Секреты живут только в
.envи каталогеsecrets/клона суперпроекта (оба в.gitignore) и в credential-файлах рабочих мест и runner-хоста. - Права —
0600на файл,0700на каталог. Клиентcontrol-planeотказывается читать~/.config/iam/credentials.json, если файл доступен кому-то кроме владельца (iam_credentials_file_permissions). - Скрипты платформы секреты не печатают:
make secrets,deploy/bootstrap.pyпишут их сразу в файлы с0600и выводят только идентификаторы и публичные префиксы PAT. - Секрет, который прошёл через чат, тикет, скриншот или историю shell, считается скомпрометированным и перевыпускается.
Инвентарь¶
Хост платформы: .env¶
| Переменная | Кто использует | Как заменить |
|---|---|---|
CP_POSTGRES_PASSWORD, IAM_POSTGRES_PASSWORD, MEMORY_POSTGRES_PASSWORD, PLATFORM_POSTGRES_PASSWORD, ENT_POSTGRES_PASSWORD, POL_POSTGRES_PASSWORD |
Базы и сервисы | ALTER ROLE в базе, затем .env, затем пересоздание сервиса (см. ниже) |
KEYCLOAK_DB_PASSWORD |
Роль keycloak в platform-db |
Так же, как пароли БД |
REDIS_PASSWORD |
platform-redis, platform-api |
.env и пересоздание обоих |
CP_BOOTSTRAP_TOKEN |
POST /api/v1/bootstrap Control Plane |
.env и up -d control-plane-api |
IAM_BOOTSTRAP_TOKEN |
Административные операции IAM (X-IAM-Bootstrap-Token), воркер policy-service |
.env и пересоздание iam-service (и policy-service, policy-worker, если подняты) |
ENT_BOOTSTRAP_TOKEN, POL_BOOTSTRAP_TOKEN |
Экспериментальные сервисы | .env и пересоздание сервиса |
MEMORY_API_KEY |
Статический ключ памяти: memory-service, ядро до перехода на service account, демо |
.env и пересоздание всех потребителей |
KEYCLOAK_ADMIN_PASSWORD |
Первичный администратор Keycloak | См. «Keycloak» |
KEYCLOAK_CLIENT_SECRET |
Confidential-клиент platform-api в realm |
См. «Keycloak» |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY |
Root-учётка MinIO и platform-api |
Согласованно в minio и platform-api |
CP_S3_ACCESS_KEY_ID, CP_S3_SECRET_ACCESS_KEY |
Пользователь Control Plane в хранилище объектов (только свой бакет), его заводит minio-bootstrap |
.env, повторный запуск minio-bootstrap, затем пересоздание control-plane-api, control-plane-worker, context-adapter; для внешнего S3 — сначала у провайдера (см. Хранилище объектов) |
AITUNNEL_API_KEY |
Ключ OpenAI-совместимого LLM-провайдера для памяти и исполнителей | У провайдера, затем .env и пересоздание потребителей |
SUPPORT_BOT_PAT |
Демо-бот | Как PAT агента |
make secrets заполняет случайными значениями все перечисленные пароли,
bootstrap-токены, MEMORY_API_KEY, пароль и секрет Keycloak и ключи MinIO,
если они пусты.
Секретов со значением по умолчанию в compose.yml нет: пароли БД и
bootstrap-токены всех профилей, включая экспериментальные, объявлены
обязательными (${VAR:?…}) и генерируются make secrets. Если ключа нет в
.env (файл создан по старому .env.example или строка закомментирована),
make secrets дописывает его.
Хост платформы: secrets/¶
| Файл | Что | Кто пишет | Кто читает |
|---|---|---|---|
iam-signing.pem |
Приватный ключ подписи access token (RSA 3072) | make secrets |
iam-service (uid 10001, docker secret) |
entitlement-signing.pem |
Ключ подписи entitlement-service | make secrets |
entitlement-service |
harness-pat |
PAT оператора (read/write/admin) | bootstrap, шаг 4 | Переносится на рабочее место оператора |
control-plane-iam.env |
CP_IAM_CLIENT_ID, CP_IAM_CLIENT_SECRET — service account ядра |
bootstrap, шаг 2a | docker compose (env_file трёх процессов ядра) |
memory-service-iam.env |
Service account памяти к policy-service | bootstrap, шаг 7 | docker compose (env_file) |
policy-service-cp.env |
Service account проекции policy-service | bootstrap, шаг 7 | docker compose (env_file) |
Права: всё — 0600. Файлы, которые монтируются в контейнер (ключи подписи),
на Linux должны принадлежать uid 10001:
sudo chown 10001:10001 secrets/iam-signing.pem secrets/entitlement-signing.pem
sudo chmod 600 secrets/*.pem
Ослаблять права до 644 вместо chown нельзя: это приватные ключи и токены.
Env-файлы (*.env) читает docker compose на хосте, их владелец — тот, кто
запускает compose.
Runner-хост и рабочие места¶
| Где | Секрет | Комментарий |
|---|---|---|
| Runner | PAT агента | Файл-секрет контейнера или ~/.config/iam/credentials.json пользователя runner |
| Runner | CLAUDE_CODE_OAUTH_TOKEN |
Токен подписки кодового агента; принадлежит человеку, а не агенту. Выпускается claude setup-token на машине с браузером |
| Runner | Токен forge (для push веток задач) | Минимальные права: запись только в репозитории задач, чтение соседей |
| Оператор | ~/.config/iam/credentials.json |
JSON-объект: ключ — адрес IAM, tenant и principal через вертикальную черту, значение с полем token; режим 0600 |
Узлы fleet¶
Секреты агентов, размещённых fleet, лежат на узле в каталоге secretsDir —
по файлу на секрет, имя файла совпадает с именем в placement.secrets описания
агента (см. Узлы и fleet). PAT агентов в этом
каталоге нет: их выпускает fleet-controller и доставляет на узел запечатанными
ключом узла.
Файл в secretsDir |
Кому | Что |
|---|---|---|
claude-oauth-token |
Кодовые агенты claude-code |
Токен подписки; принадлежит человеку |
github-token |
Кодовые агенты и источник наблюдений git-connector |
Токен forge: у кодовых агентов — публикация веток задач, у коннектора — чтение репозиториев и прогонов CI (ci.run_observed) |
Каталог 0700, файлы 0600, владелец — uid образа исполнителя (10001).
Новый файл узел предложит контроллеру со следующим отчётом, перезапуск узла не
нужен.
Сроки жизни credentials¶
| Credential | Срок | Параметр |
|---|---|---|
| Access token IAM | 300 с | IAM_TOKEN_TTL_SECONDS (по умолчанию) |
| PAT, срок по умолчанию | 30 дней | IAM_PAT_DEFAULT_TTL_SECONDS=2592000 |
| PAT, максимальный срок | 365 дней | IAM_PAT_MAX_TTL_SECONDS=31536000; больше — 422 expiry_too_long |
PAT, выпущенный deploy/bootstrap.py |
180 дней | Аргумент --pat-ttl (секунды) |
| Свежесть входа человека для выпуска PAT | 300 с | IAM_PAT_MAX_AUTHENTICATION_AGE_SECONDS |
| Service account (client credentials) | Без срока | Отзыв — …/service-accounts/{client_id}:revoke |
Параметры IAM в compose.yml не пробрасываются: чтобы изменить значения по
умолчанию, добавьте их в environment сервиса iam-service через
compose.override.yml.
Ротация не продлевает PAT
POST …/platform-access-tokens/{id}:rotate меняет только секрет: новый
токен наследует audiences, потолок scope и expiresAt
предшественника. Это инструмент для утечки, а не для продления. Чтобы
продлить доступ, выпускается новый PAT, а старый отзывается.
Как следить за сроками¶
source <(grep -E '^(IAM_BOOTSTRAP_TOKEN|IAM_TENANT_ID)=' .env)
curl -s "http://127.0.0.1:18010/api/v1/tenants/$IAM_TENANT_ID/platform-access-tokens" \
-H "X-IAM-Bootstrap-Token: $IAM_BOOTSTRAP_TOKEN" \
| python3 -c 'import json,sys; [print(t["expiresAt"][:10], t["publicPrefix"], t["name"]) for t in sorted(json.load(sys.stdin), key=lambda t: t["expiresAt"])]'
Ответ содержит только активные токены (includeRevoked=true — вместе с
отозванными), с полями publicPrefix, name, principalId, expiresAt,
lastUsedAt. Заведите напоминание за две недели до ближайшего expiresAt.
Процедуры¶
Во всех примерах ниже переменные окружения:
IAM=http://127.0.0.1:18010
T=<tenant-id> # IAM tenant
H="X-IAM-Bootstrap-Token: $IAM_BOOTSTRAP_TOKEN"
Перевыпуск PAT агента (плановый)¶
У агента (principal вида agent) нет человеческого входа, свежий
authentication context для него не требуется.
# 1. Новый PAT; Idempotency-Key обязателен (без него — 400 idempotency_key_required)
curl -s -X POST "$IAM/api/v1/tenants/$T/principals/<agent-principal-id>/platform-access-tokens" \
-H "$H" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "runner-2026-q3", "audiences": ["control-plane"],
"scopeCeiling": ["control-plane:read", "control-plane:write"],
"expiresInSeconds": 15552000}' > /tmp/pat.json
# поле token — секрет, показывается ровно один раз
- Установите токен на runner-хост (файл-секрет контейнера или
credentials.jsonпользователяrunner,0600), удалите/tmp/pat.json. - Перезапустите исполнителя и убедитесь, что он получил задачу или хотя бы
открыл сессию (в логах нет
invalid_token). - Отзовите прежний PAT:
curl -s -X POST "$IAM/api/v1/tenants/$T/platform-access-tokens/<old-credential-id>:revoke?reason=superseded" \
-H "$H" # 204
scopeCeiling должен быть подмножеством allowedScopes указанных audiences,
иначе 422 invalid_scope_ceiling. PAT выпускается только principal вида
human или agent; для service_account — 422 principal_kind_not_allowed
(у сервисов свой поток client credentials).
Перевыпуск PAT человека¶
Человеку IAM выпускает PAT только при свежем (не старше 300 с) authentication context. Запись контекста и выпуск выполняются одна за другой:
curl -s -X POST "$IAM/api/v1/tenants/$T/principals/<human-principal-id>/authentication-contexts" \
-H "$H" -H "Content-Type: application/json" \
-d '{"issuer": "https://platform.example.com/iam", "acr": "bootstrap", "amr": ["bootstrap-script"]}'
curl -s -X POST "$IAM/api/v1/tenants/$T/principals/<human-principal-id>/platform-access-tokens" \
-H "$H" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "operator-2026-q3", "audiences": ["control-plane"],
"scopeCeiling": ["control-plane:read", "control-plane:write", "control-plane:admin"],
"expiresInSeconds": 15552000}'
Если между вызовами прошло больше 300 с — 403 authentication_context_expired;
если контекста нет вовсе — 403 authentication_context_required.
deploy/bootstrap.py делает то же самое для оператора, если файла
secrets/harness-pat нет: удалите (переименуйте) файл и запустите bootstrap
повторно, затем отзовите прежний PAT.
Замена PAT при утечке¶
-
Немедленно перекройте доступ в Control Plane: отзовите binding identity. Это закрывает вход сразу, не дожидаясь истечения уже выданных access token (до 300 с):
-
Отзовите PAT в IAM (
:revoke) — либо владелец отзывает свой токен сам, без bootstrap-полномочий: -
Выпустите новый PAT (процедуры выше), восстановите binding повторным
POST /api/v1/principals/<principal-id>/iam-bindingsс теми же правами. - Проверьте audit IAM и журнал Control Plane за период утечки.
Подробный сценарий — в Аварийных процедурах.
Ротация секрета service account¶
Service accounts ядра и сервисов платформы (notification-iam.env, fleet-iam.env) перевыпускает bootstrap: если
env-файла нет, он выпускает новый service account, пишет файл и отзывает
прежний.
mv secrets/control-plane-iam.env secrets/control-plane-iam.env.old
python3 deploy/bootstrap.py --env .env --name <env>
docker compose up -d control-plane-api control-plane-worker context-adapter
shred -u secrets/control-plane-iam.env.old
Service accounts памяти и проекции policy-service (memory-service-iam.env,
policy-service-cp.env) bootstrap создаёт только если файла нет и прежний не
отзывает — после перевыпуска отзовите старый вручную:
Ротация ключа подписи IAM¶
IAM публикует в /.well-known/jwks.json один ключ — текущий. Сервисы
проверяют подпись по JWKS с кэшем и обновляют его при встрече неизвестного
kid.
-
Сгенерируйте новый ключ и дайте ему новый
kid: -
В
.env:IAM_SIGNING_KEY_FILE=./secrets/iam-signing.new.pem,IAM_SIGNING_KEY_ID=<новый kid>. docker compose up -d iam-service.- Проверьте
curl -s $IAM/.well-known/jwks.json— новыйkid.
Что происходит: PAT и секреты service accounts от ключа подписи не зависят и
остаются действительными. Access token, подписанные старым ключом (живут
до 300 с), перестают проходить проверку, как только сервис обновит JWKS;
клиенты получают 401 и заново обменивают PAT. Выполняйте ротацию в период
низкой активности. Старый файл ключа храните до конца окна как точку отката.
Компрометация ключа подписи
Утечка iam-signing.pem позволяет подделать access token любого
principal. Ротируйте ключ немедленно и перезапустите сервисы,
проверяющие токены (control-plane-api, memory-service и другие),
чтобы они сбросили кэш JWKS со старым ключом.
Пароли баз данных¶
POSTGRES_PASSWORD применяется образом PostgreSQL только при инициализации
пустого тома. Изменение .env на существующей базе пароль не меняет —
сервис просто перестанет подключаться. Порядок:
# 1. Сменить пароль роли в базе
docker compose exec control-plane-db psql -U control_plane -d control_plane \
-c "ALTER ROLE control_plane PASSWORD '<новый пароль>'"
# 2. Записать тот же пароль в .env (CP_POSTGRES_PASSWORD)
# 3. Пересоздать потребителей
docker compose up -d control-plane-api control-plane-worker context-adapter
Для роли keycloak в platform-db — то же через суперпользователя platform:
ALTER ROLE keycloak PASSWORD '…', затем KEYCLOAK_DB_PASSWORD и
docker compose up -d keycloak.
Keycloak¶
| Секрет | Особенность | Порядок замены |
|---|---|---|
KEYCLOAK_ADMIN_PASSWORD |
Переменная создаёт временного администратора только при первом старте, когда администратора нет | Смените пароль администратора в консоли Keycloak или через Admin API; .env поддерживайте в согласии для скриптов |
KEYCLOAK_CLIENT_SECRET |
Подставляется в шаблон realm (realm-render) и попадает в Keycloak только при первом импорте realm |
Перегенерируйте секрет клиента platform-api в realm, запишите его в .env, docker compose up -d platform-api |
Пароли пользователей realm меняются через Admin API или консоль; см. Вход людей — Keycloak.
Токен подписки кодового агента и токен forge¶
- Токен подписки выпускается человеком на машине с браузером
(
claude setup-token), на runner-хосте войти интерактивно нельзя. Замена: новый токен → файл-секрет или env-файл исполнителя → перезапуск. - Отзыв токена подписки выполняется на стороне поставщика. После отзыва исполнитель продолжает брать задачи и валить их — остановите его.
- Токен forge заменяется так же; проверка — успешная публикация ветки
следующей задачи (в артефакте
commitполеpublished: true).
Рекомендуемый календарь¶
| Периодичность | Действие |
|---|---|
| Еженедельно | Список PAT с expiresAt ближе 30 дней |
| За 2 недели до истечения | Перевыпуск PAT исполнителей и операторов |
| Раз в квартал | Ротация MEMORY_API_KEY, ключа LLM-провайдера, секретов service accounts |
| Раз в год | Ротация ключа подписи IAM и паролей БД |
| Сразу | Любой секрет, прошедший через переписку или логи |