Модель безопасности¶
Статья описывает, как в Taimen устроены identity, credentials и авторизация: кто выпускает токены, что в них лежит, как сервис решает, разрешено ли действие, и как отзывается доступ. Она адресована инженерам безопасности и тем, кто подключает к платформе новые сервисы, агентов и харнессы.
Три вопроса — три ответчика¶
Решение о любом запросе разложено на независимые вопросы, у каждого свой владелец:
| Вопрос | Кто отвечает | Чем |
|---|---|---|
| Кто это? | IAM Service | подписанный RS256 access token одного audience |
| Может ли он вообще пользоваться продуктом? | Entitlement Service (опционально, профиль entitlement) |
решение по лицензии; по умолчанию выключено (CP_ENTITLEMENT_ENABLED=false) |
| Можно ли ему это действие над этим ресурсом? | сервис-владелец ресурса (Control Plane, Memory Service, …) | собственные права principal и доменные инварианты |
Токен IAM не несёт доменных прав. Право создать задачу принадлежит Control Plane, а не провайдеру identity: IAM лишь ограничивает токен scope'ами, а сервис пересекает их со своими правами.
Порядок проверок в общем Policy Enforcement Point (platform-auth-sdk) фиксирован
и не настраивается:
flowchart LR
T[Токен] --> I[identity<br/>подпись, iss, aud, exp]
I --> R[revocation<br/>binding, principal]
R --> E[entitlement<br/>если включён]
E --> P[policy<br/>если включён PDP]
P --> D[доменные проверки<br/>сервиса]
D --> OK[разрешено]
Каждый следующий шаг дороже предыдущего и имеет смысл только после него. Отказ на любом шаге — стабильный код для клиента и точная причина в аудите.
IAM: субъекты и credentials¶
IAM владеет tenants, principals и их credentials:
| Вид principal в IAM | Кто | Credential |
|---|---|---|
human |
человек | PAT (выпуск только после свежей аутентификации) или федерация через внешний IdP |
agent |
автономный исполнитель | PAT, выпускаемый bootstrap-операцией |
service_account |
сервис | clientId + clientSecret |
workload |
нагрузка без интерактивного входа | в Control Plane отображается в service |
Platform Access Token (PAT)¶
PAT — долгоживущий секрет human или agent. Главное свойство: он предъявляется только IAM и только в теле запроса. Ни один resource service PAT не видит.
- Выпуск:
POST /api/v1/tenants/{t}/principals/{p}/platform-access-tokens(bootstrap-заголовокX-IAM-Bootstrap-Token, обязательныйIdempotency-Key), с перечнемaudiences, потолкомscopeCeilingи срокомexpiresInSeconds. - Для человека выпуск требует свежего authentication context — не старше
IAM_PAT_MAX_AUTHENTICATION_AGE_SECONDS(300 с); иначеauthentication_context_required/authentication_context_expired. - Для агента снимок происхождения честно фиксирует bootstrap-операцию
(
agent_bootstrap), и в выданных access token у агента нетauth_timeиacr— по этому признаку и поprincipal_typeего сессии отличаются от человеческих. - Service account PAT получить не может:
422 principal_kind_not_allowed. - Срок: по умолчанию 30 дней (
IAM_PAT_DEFAULT_TTL_SECONDS), максимум 365 дней (IAM_PAT_MAX_TTL_SECONDS). Ротация (:rotate) меняет секрет, но не продлевает окно жизни — для продления нужен новый выпуск.
Обмен PAT на access token¶
curl -s -X POST http://127.0.0.1:18010/api/v1/platform-access-tokens:exchange \
-H 'Content-Type: application/json' \
-d '{"token": "'"$PAT"'", "audience": "control-plane",
"scopes": ["control-plane:read", "control-plane:write"]}'
{
"accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"tokenType": "Bearer",
"expiresIn": 300,
"audience": "control-plane",
"scope": ["control-plane:read", "control-plane:write"],
"sessionId": "<session-id>"
}
Правила обмена:
audienceдолжен быть в списке audiences PAT и активен в tenant — иначе403 audience_not_allowed.- Запрошенные
scopesдолжны входить в пересечение потолка PAT иallowedScopesaudience — иначе403 scope_not_allowed. - Пустой список
scopesозначает «весь потолок» (пересечённый с тем, что разрешено audience). - Scope пишется с префиксом audience:
control-plane:read, а неread.
Service accounts¶
Сервисы (ядро Control Plane при походе в память, notification-service, воркер policy-service) получают токены по client credentials:
curl -s -X POST http://127.0.0.1:18010/api/v1/tokens/exchange \
-H 'Content-Type: application/json' \
-d '{"clientId": "<client-id>", "clientSecret": "<client-secret>",
"audience": "memory-service", "scopes": ["memory:read"]}'
Service account создаётся с набором audiences и scopeCeiling
(POST /api/v1/tenants/{t}/service-accounts), отзывается
POST …/service-accounts/{clientId}:revoke. Отдельного изменения потолка у
service account нет: при расширении потолка выпускается новый, прежний
отзывается. Bootstrap делает это автоматически. См.
Service accounts.
Access token¶
Access token — JWT RS256 (typ: at+jwt, kid из IAM_SIGNING_KEY_ID),
TTL по умолчанию 300 с (IAM_TOKEN_TTL_SECONDS):
| Claim | Содержимое |
|---|---|
iss |
issuer IAM — ${TAIMEN_PUBLIC_URL}/iam |
sub |
IAM principal id |
tenant_id |
IAM tenant id |
aud |
ровно один audience |
scope |
выданные scopes |
scope_ceiling |
потолок credential |
principal_type |
вид principal (human, agent, service_account, …) |
credential_id |
id credential, по которому выпущен токен |
session_id |
id сессии обмена |
auth_time, acr |
только у человека с подтверждённым входом |
iat, nbf, exp, jti |
время жизни и уникальность |
Публичные ключи — GET /.well-known/jwks.json IAM. Сервисы читают JWKS по
внутреннему адресу (http://iam-service:8010/.well-known/jwks.json), а
issuer сверяют с публичным: проверка подписи не зависит от внешнего прокси и
TLS.
Audiences и scopes¶
Каждый сервис — отдельный audience со своим реестром допустимых scopes. Bootstrap регистрирует и приводит к реестру:
| Audience | Scopes |
|---|---|
control-plane |
control-plane:read, control-plane:write, control-plane:admin |
memory-service |
memory:read, memory:write, memory:pii, memory:tenants, memory:on-behalf, memory:service |
entitlement-service |
entitlement:check-on-behalf |
policy-service |
policy:check, policy:check-on-behalf, policy:admin |
Один токен — один сервис: токен для Control Plane не принимается памятью и
наоборот (сервис требует точного совпадения aud).
Control Plane: bindings и права¶
Control Plane — resource server: токены проверяет, но не выпускает.
flowchart TB
TOK[access token<br/>iss, sub, tenant_id, scope] --> B{binding<br/>issuer + sub}
B -- "нет / tenant не совпал /<br/>binding или principal не active" --> X[401 — единый ответ,<br/>причина только в аудите]
B -- ok --> PERM[permissions binding]
PERM --> N[∩ scope токена]
N --> AZ[доменная авторизация<br/>CP_AUTHZ_MODE]
- Binding. Внешняя identity отображается на локальный principal строкой
iam_principal_bindingsпо паре(issuer, iam_principal_id). Неизвестная identity, чужой tenant, отключённый binding или неактивный principal дают один и тот же ответ401— код ответа не раскрывает чужой каталог; точная причина (binding_not_found,tenant_mismatch,binding_disabled,principal_not_active) остаётся в аудите. -
Сужение scope. Права binding пересекаются с потолком токена по простому правилу:
control-plane:admin— права binding без сужения;- право
adminбез admin-scope не действует никогда; - права вида
*.readтребуютcontrol-plane:read; - все остальные —
control-plane:write.
Scope только сужает, никогда не расширяет: binding с правом записи при токене «только чтение» писать не сможет. 3. Доменная авторизация. Режим
CP_AUTHZ_MODE: -local(по умолчанию) — права из binding и ролей Control Plane; -shadow— решение по-прежнему локальное, но параллельно спрашивается policy-service, расхождения пишутся в журнал; -policy— решение принимает policy-service (профильpolicy, experimental).
Права агентов¶
Bootstrap выдаёт агентам по умолчанию sessions.open, tasks.read,
tasks.write, tasks.claim, events.read, artifacts.read,
artifacts.write, projects.read, task_types.read и отказывается
выдавать агенту admin или approvals.decide: решение по approval — всегда
человек. Полный перечень прав — Права и scopes.
Legacy API-ключи¶
Control Plane исторически поддерживает статические ключи cp_<prefix>_<secret>.
В поставке они выключены: CP_LEGACY_API_KEYS_ENABLED=false, предъявленный
ключ даёт invalid_credentials. Ключ администратора, который возвращает
первичный bootstrap Control Plane, bootstrap-скрипт сразу отзывает.
Аварийный вход
Если IAM недоступен, владелец хоста выпускает аварийный ключ: командой
в контейнере control-plane-api, для активного человека, с правами
admin, сроком не больше 4 часов и причиной в журнале (CP-ADR-0065).
Такой ключ принимается и при закрытом окне legacy-ключей; граница
доверия — shell на хосте. Выключается CP_BREAK_GLASS_ENABLED=false. См.
Аварийные процедуры.
Memory Service¶
Память принимает два вида credential параллельно:
- статический ключ
MEMORY_API_KEY(CB_SERVER_API_KEY) — полный доступ ко всем namespace; в поставке нужен до bootstrap и демо-сервисам; - токены IAM audience
memory-service(MEMORY_IAM_ENABLED=true):memory:read/memory:write— чтение и запись,memory:pii— полный доступ к персональным данным,memory:service— регистрация доменных пакетов видов,memory:tenants— память всех tenant (только service account ядра). Токен даёт доступ к namespacetenant:<tenant_id>и его поддереву.
Control Plane ходит в память service account'ом из
secrets/control-plane-iam.env (режим CP_CONTEXT_AUTH=auto переключается на
него сам, как только файл появился и ядро перезапущено); до этого — статическим
ключом. Любой дефект токена — 401, недоступный JWKS — 503 (fail closed).
Отзыв доступа¶
| Что отозвать | Как | Когда перестанет работать |
|---|---|---|
| PAT | POST /api/v1/tenants/{t}/platform-access-tokens/{id}:revoke (bootstrap) или POST /api/v1/platform-access-tokens:revoke-self |
новые обмены — сразу; уже выданные access token — до истечения (≤ TTL, 300 с) |
| Service account | POST /api/v1/tenants/{t}/service-accounts/{clientId}:revoke |
то же |
| Principal IAM целиком | POST /api/v1/tenants/{t}/principals/{p}:disable |
то же |
| Доступ к Control Plane | отозвать binding (POST /api/v1/iam-bindings/{binding_id}:revoke) или отключить локальный principal |
в пределах кэша binding: CP_IAM_BINDING_CACHE_TTL_SECONDS (30 с); отрицательный ответ живёт не дольше CP_IAM_BINDING_STALE_AFTER_SECONDS (120 с) |
Короткий TTL access token ограничивает окно, но не закрывает его: закрывает локальная revocation policy сервиса. В Control Plane это проекция binding и principal: если источник не смог ответить, доступ не выдаётся (fail closed).
Порядок заведения нового principal
Сначала создайте binding в Control Plane, потом делайте первый запрос
токеном. Отрицательный ответ «binding не найден» кэшируется процессом
control-plane-api не дольше CP_IAM_BINDING_STALE_AFTER_SECONDS.
Создание binding через API (POST /api/v1/principals/{id}/iam-bindings)
сбрасывает кэш этой identity сразу; если же binding появился в обход API,
новый доступ заработает только после истечения этого окна.
Секреты платформы¶
| Секрет | Где | Назначение |
|---|---|---|
| Ключ подписи IAM | secrets/iam-signing.pem (RSA 3072, 0600), монтируется docker-секретом |
подпись всех access token |
IAM_BOOTSTRAP_TOKEN |
.env |
административные операции IAM заголовком X-IAM-Bootstrap-Token |
CP_BOOTSTRAP_TOKEN |
.env |
однократный POST /api/v1/bootstrap Control Plane (Authorization: Bearer); после первого tenant повтор даёт 409 already_bootstrapped |
MEMORY_API_KEY |
.env |
статический ключ памяти |
| PAT оператора | secrets/harness-pat (0600) |
вход человека |
| Client credentials сервисов | secrets/*-iam.env (0600) |
service accounts ядра и опциональных сервисов |
| Пароли БД, Keycloak, MinIO | .env |
инфраструктура |
.env, secrets/ и deploy/state/ исключены из git. Секреты не печатаются
bootstrap-скриптом и не попадают в журнал событий: Control Plane отвергает
текст, похожий на секрет, в полях задач и документах Work Graph
(secret_material_rejected). Локальное хранилище PAT клиента
(~/.config/iam/credentials.json) должно иметь права 600, иначе клиент
откажется его читать (iam_credentials_file_permissions).
Ротация и хранение — Секреты и ротация.
Граница доверия харнесса¶
Локальные проверки харнесса (MCP-сервер, runner) — защита клиента, а не enforcement boundary: человек с доступом к машине может обойти их. Сильная граница — на сервере: claim, fencing token, права binding и gate-approvals проверяются Control Plane при каждой записи. У runner-демона периметр задают непривилегированный пользователь ОС и ограничения systemd, а не режим разрешений кодового агента. См. Identity агента.