Два окна страницы Accounts в pacod-id на id.demo.pacod.io, светлое и тёмное, наложенные друг на друга, у каждого заголовок pacod-gw · AI Gateway, слева ссылки аккаунта и администрирования, поиск по всем аккаунтам, строка admin@pacod.demo с кнопкой Send reset link и форма Create an account.
Экран аккаунтов в администрировании pacod-id днём и ночью: найти любой аккаунт, увидеть его последний вход, отправить ссылку для сброса пароля или создать новый. Страницы носят бренд своей области, а единственная область демо для всех продуктов оформлена в бренде шлюза, отсюда и знак pacod-gw.

pacod-id — провайдер OpenID Connect, на котором работают продукты PACOD: один общий пул аккаунтов, готовые страницы входа и аккаунта, организации и привязки ролей. Его токены говорят, кто человек, а не что ему можно: каждый продукт следит за лентой привязок ролей и сам решает о доступе, с одним движком правил на Rust, TypeScript и Python.

Идентификация и доступ

  • Лицензия: Apache-2.0
  • Версия: 0.1.0 · Предварительная
  • Языки: Rust, TypeScript, Python
  • Технологии: Rust, axum, PostgreSQL, OpenID Connect, SvelteKit, Docker, Helm
  • Открыть на GitHub · Создать issue

Возможности

Провайдер OpenID Connect

Поток authorization code с PKCE для каждого клиента, клиенты из конфигурации и без экрана согласия, проверенный локально набором тестов OpenID Foundation.

Один пул аккаунтов

Люди, сервисные аккаунты, организации, приглашения и привязки ролей, общие для всех продуктов; sub человека везде один и тот же.

Лента авторизации

Продукты загружают снимок привязок и ждут изменений через long poll, так что отозванная роль или завершённый сеанс доходят до них без запроса на каждое обращение.

Один движок правил, три порта

Манифесты, scopes, allowed, has_role, touches и can_grant на Rust, TypeScript и Python, и все три проходят одни и те же тестовые векторы YAML.

Ключи и токены

RS256 или ES256 на ring, ключи подписи запечатаны ключом шифрования ключей и меняются каждые 90 дней, refresh-токены ротируются и распознают повторное использование.

Готовые страницы

Страницы входа, регистрации, аккаунта и администрирования в бренде области, на четырёх языках, в светлой и тёмной теме, под строгой Content-Security-Policy.

Готовые клиенты

Crate для Rust, пакет для Python с зависимостями FastAPI и вход backend-for-frontend для SvelteKit 2 и 3.

Готов к запуску

Статический образ на distroless, стек для разработки с почтовым приёмником и mock-версией Google, а также Helm-чарт.

Диаграмма платформы с выделенным pacod-id и приглушёнными остальными проектами; золотые пунктирные линии связывают его с pacod-ap, pacod-cms, pacod-kb и pacod-gw.
Место pacod-id. pacod-ap, pacod-cms, pacod-kb и консоль pacod-gw впускают людей через него и берут у него их роли (золотые точки). Приглушено остальное: вызовы LLM через pacod-gw (зелёный), люди, пользующиеся продуктами (серый), и pacod-benchmark, измеряющий шлюз (коралловый).

Скриншоты

Светлая карточка входа на id.demo.pacod.io со знаком GW, словами Sign in и Welcome back, полями адреса и пароля, флажком Keep me signed in, зелёной кнопкой Sign in и ссылкой Forgot your password; под ней выбор языка и темы и слова PACOD-GW · AI GATEWAY.
Страница входа, куда каждый продукт отправляет своих людей, на четырёх языках и в светлой, тёмной или системной теме. Она носит бренд своей области: в демо одна область для всех продуктов, в бренде шлюза, и нет ссылки на регистрацию, потому что аккаунты там появляются по приглашению.
Светлая страница Organisations and roles с таблицей Your roles из семи строк, каждая Everywhere и с датой Oct 27, 2026; роли admin, superuser, admin, platform admin, platform admin, admin и admin для продуктов id, pacod, pacod, gw, cms, kb и bench.
Организации и роли, какими человек видит свои. Администратор демо имеет собственную роль администратора pacod-id и роль администратора в каждом продукте (pacod — это pacod-ap, bench — pacod-benchmark), каждую на уровне платформы, показанном как Everywhere. Каждый продукт читает эти назначения из потока.
Тёмная страница Audit log с заголовком pacod-gw · AI Gateway, с фильтрами Action, By и Concerning над списком записей login_succeeded, у каждой время, идентификатор аккаунта, адрес клиента и затронутый аккаунт.
Журнал аудита в тёмной теме: входы и неудачные попытки, выходы, изменения аккаунтов, ролей, приглашений, токенов и ключей, у каждого — кто, о ком и с какого адреса. Он хранится 400 дней, фильтруется по действию, исполнителю и объекту и доступен владельцам id.admin и id.auditor.
Светлая страница Password and security с заголовком pacod-gw · AI Gateway, с формой Change your password для текущего и нового пароля и кнопкой Save и панелью Delete your account, где для подтверждения нужно ввести адрес admin@pacod.demo.
Пароль и безопасность — одна из страниц аккаунта, которые есть у каждого. Новый пароль завершает сеансы на других устройствах; удаление аккаунта, после входа в последние десять минут, убирает его из pacod-id и сообщает каждому продукту стереть то, что он хранит о человеке.

Зачем он нужен

Каждый продукт, который впускает людей, в итоге заводит свои пароли, письма об аккаунтах, страницы администрирования и таблицы ролей, а у каждого человека оказывается по аккаунту на продукт. pacod-id забирает всё это у продуктов. Человек входит один раз и с одним и тем же аккаунтом попадает в консоль шлюза, платформу агентов, студию CMS, базу знаний и бенчмарки. Машины тоже входят — как сервисные аккаунты.

Это провайдер OpenID Connect для ваших собственных приложений. Каждый клиент описан в его конфигурации: динамической регистрации и экрана согласия нет. Пул аккаунтов один, и sub человека одинаков в каждой области. От продукта к продукту различаются вид входа и то, кому что там разрешено, — и pacod-id записывает и то и другое.

Области, бренды и клиенты

Область (realm) — это один хост issuer со своим брендом, cookie входа, языками и политиками: минимальная длина пароля, длительность сеанса, может ли регистрироваться кто угодно и какие роли управляют аккаунтами. pacod-id выбирает область по Host запроса, поэтому две области могут делить один пул аккаунтов и при этом выглядеть как два разных продукта.

Бренд области — один из пяти встроенных: gateway, cms и kb для продуктов PACOD, а также shop и bazaar для вымышленного продукта, на котором построены документация и тесты. Он задаёт знак, надпись и цвета страниц и оформление писем. Бренд принадлежит области, а не клиенту: все приложения одной области выглядят одинаково. pacod-demo запускает одну область, pacod, для всех продуктов и даёт ей бренд шлюза. Поэтому на его страницах входа и аккаунта стоит знак pacod-gw, из какого бы продукта ни пришёл человек.

Клиент — это одно приложение одного из трёх видов:

Вид Типичное применение Гранты
confidential Backend-for-frontend продукта, который хранит секрет authorization_code, refresh_token
public Нативное или мобильное приложение; его коды защищает только PKCE authorization_code, refresh_token
service Сервер продукта, который обращается к pacod-id client_credentials, обмен токенов

Вход, шаг за шагом

  1. Его начинает продукт. Его сервер отправляет браузер на /oidc/authorize с state, nonce и PKCE challenge. Доступен только поток authorization code, и PKCE (S256) обязателен для каждого клиента, включая конфиденциальные.
  2. pacod-id проверяет запрос. Область определяется по Host; клиент должен принадлежать ей, а redirect URI должен точно совпадать с зарегистрированным. Пока это не подтверждено, ошибка показывает страницу и никогда не перенаправляет.
  3. Единый вход или сценарий. Живой сеанс области сразу получает код. Иначе pacod-id сохраняет запрос как сценарий входа на 15 минут, привязывает его к браузеру cookie и отправляет браузер на свою страницу /login.
  4. Человек входит — паролем или, если область это поддерживает, через Google. Каждая попытка занимает своё место в ограничителях в PostgreSQL ещё до хеширования пароля, а неизвестный адрес получает тот же ответ, что и неверный пароль. Если адрес нужно сначала подтвердить или пароль пора сменить, сценарий ведёт туда.
  5. Сеанс и код. Вход создаёт новый SSO-сеанс (12 часов или 7 дней с «не выходить») и код, который живёт 60 секунд, действует один раз и привязан к клиенту, redirect URI и PKCE challenge. Браузер возвращается в продукт с code, state и iss.
  6. Продукт обменивает его на /oidc/token, обычно на внутреннем слушателе, и получает access-токен, id_token и refresh-токен. Их issuer — по-прежнему публичный URL области.

Токены

  • Access-токены — это JWT (RFC 9068), RS256 или ES256 в зависимости от клиента, по умолчанию действуют 5 минут (от 2 до 15). В них сказано, кто, в каком сеансе и с каким scope, но ролей в них нет.
  • id_token живёт 5 минут и содержит адрес и имя, если их запрашивают scopes.
  • Refresh-токены непрозрачны и меняются при каждом использовании. Токен, предъявленный повторно в течение 30 секунд, получает того же преемника, так что две вкладки могут обновиться одновременно. Предъявление более старого завершает все сеансы аккаунта, сообщает об этом всем продуктам и отправляет владельцу письмо.
  • Сервисные токены (pidt_…) принадлежат сервисным аккаунтам, показываются один раз и хранятся как HMAC.
  • Обмен токенов (RFC 8693) позволяет сервису продукта действовать от имени человека в пределах его прав, а act называет сервис. Делегированный доступ заканчивается вместе с сеансом человека.

Когда сеанс завершается, каждый клиент, у которого был в нём доступ и есть URI back-channel logout, получает токен выхода, доставка которого повторяется в течение десяти минут.

Роли не попадают в токены

Роли намеренно не кладутся в access-токены: отозванная роль должна перестать действовать сразу, а не когда истечёт токен. Каждый продукт при каждом запуске передаёт манифест: свои типы scopes, свои роли с их разрешениями и то, кто что может назначать. Scopes образуют дерево: от platform, при необходимости через organisation:<id>, до собственных типов продукта. Вот манифест шлюза в сокращении:

product: gw
scope_types:
  workspace: { parent: organisation }
roles:
  platform_admin:
    at: [platform]
    permissions: ["*"]
    protected: true
  org_admin:
    at: [organisation]
    permissions: [org.manage, org.read, workspace.manage, workspace.read,
                  audit_records.read, audit_log.read, members.manage]
    protected: { bypass: platform.manage }
  viewer:
    at: [platform, organisation, workspace]
    permissions: [org.read, workspace.read]
grant:
  default: members.manage
  platform_admin: platform.manage

Привязка — это роль на одном scope, например gw.viewer на gw.workspace:ws_1, и её разрешения действуют вниз по дереву: роль на уровне организации действует в каждом рабочем пространстве под ней. pacod-id хранит привязки и проверяет каждое изменение. Чтобы назначить роль, нужно разрешение на назначение из манифеста на этом scope (здесь администратор организации назначает роли внутри своей организации, и только администратор платформы делает других администраторами платформы), а роль protected никогда не теряет последнего владельца. Там, где манифест это допускает, администраторы продукта могут заводить свои роли, которые сами по себе не дают разрешений, но учитываются в has_role.

Собственный манифест pacod-id встроен: id.admin может в pacod-id всё и защищена, а id.auditor читает аккаунты и журнал аудита. Область может также назвать роли продуктов, владельцы которых управляют зарегистрированными в ней аккаунтами; демо называет администраторов продуктов. Такой администратор управляет только аккаунтами, все роли которых он мог бы назначить сам, так что администратор продукта не может завладеть администратором идентификации.

Движок правил отвечает, среди прочего, на allowed, has_role, touches и can_grant. Он есть на Rust, TypeScript и Python, и все три проходят одни и те же тестовые векторы YAML (policy/engine-vectors/). Векторы и есть спецификация, поэтому решение одинаково, где бы его ни принимали.

Лента авторизации

Каждый продукт держит свою копию привязок и следит за изменениями:

  1. При запуске он загружает снимок: свой каталог, организации и собственные scopes, привязки своих ролей и ролей id.*, отключённые и удалённые аккаунты и сеансы, отозванные за последние 15 минут, — всё на один порядковый номер, seq.
  2. Затем он через long poll запрашивает изменения после этого seq, ожидая до 30 секунд за вызов. Ответ приходит, как только что-то меняется: PostgreSQL LISTEN будит ждущий запрос.
  3. Изменения хранятся 7 дней. Более старый курсор получает 410, и продукт заново загружает снимок.
{
  "seq": 1843,
  "events": [
    { "seq": 1843, "kind": "binding.removed", "principal": "usr_0191…", "role": "gw.viewer",
      "path": ["platform", "organisation:org_1", "gw.workspace:ws_1"] }
  ]
}

События касаются привязок, аккаунтов (отключение, включение, удаление, изменение), сеансов, сервисных токенов, scopes и каталогов. Запись отвечает номером seq ленты, так что продукт, которому нужно прочитать собственную запись, ждёт, пока его копия не дойдёт до этого номера. Клиентские библиотеки для Rust и Python делают всё это в своём кэше привязок. Access-токены несут sid сеанса, который продукт отклоняет, как только лента сообщает о его отзыве.

По замеру make measure на одной машине разработчика (Docker Desktop на Apple silicon) изменение доходило до ждущего подписчика с p99 около 10 мс при цели меньше секунды. Это доля самого pacod-id: цель продуктов — две секунды от выхода до отказа — зависит от подписчика каждого продукта.

Из чего он состоит

Часть Что это
crates/pacod-id Сервис: провайдер OpenID Connect, аккаунты, организации, привязки ролей, лента авторизации, почта, аудит
crates/pacod-id-authz Движок правил
crates/pacod-id-jose Compact JWS (RS256, ES256) на ring и JWKS
crates/pacod-id-client Проверка токенов, кэш привязок, интроспекция и обмен токенов для продуктов на Rust
python/ Пакет pacod_id: движок правил, проверка токенов, кэш привязок, зависимости FastAPI
packages/* @pacod/id-authz, @pacod/ui, @pacod/id-client, @pacod/id-sveltekit
ui/ Готовые страницы входа, аккаунта и администрирования

Сервис — один бинарный файл на Rust и axum с тремя слушателями:

Слушатель Порт Для кого
public 8070 Браузеры и приложения через обратный прокси с TLS: эндпоинты OpenID Connect и готовые страницы
internal 8071 Только серверы продуктов: токены, лента, интроспекция, отзыв сеансов, admin API
metrics 9170 /healthz, /readyz и /metrics

Готовые страницы — статическая сборка SvelteKit, которую бинарный файл отдаёт сам, подставляя бренд, язык и тему области ещё до первой отрисовки. Там люди находят профиль, пароль, устройства и сеансы, способы входа, организации и роли, а администраторы — аккаунты, сервисные аккаунты и журнал аудита. @pacod/ui содержит токены дизайна, темы брендов и компоненты входа, которые продукты, использующие pacod-id, могут разделять.

Модель безопасности

Основные меры из модели угроз:

  • Пароли: Argon2id на минимуме OWASP (19 MiB, 2 прохода), по умолчанию 12 символов и список из 46 528 распространённых паролей, которые не принимаются. Ограничители входа живут в PostgreSQL и общие для всех экземпляров: на аккаунт, на известный браузер и на IP-адрес.
  • Без проверки существования аккаунта: вход, сброс пароля и регистрация отвечают одинаково, есть ли у адреса аккаунт или нет.
  • Ключи подписи: создаются pacod-id, запечатаны в базе данных ключом шифрования ключей (PID_KEK), меняются каждые 90 дней, а следующий ключ публикуется на день раньше. Экстренная смена — одна команда.
  • Хранимые секреты: refresh-токены и ссылки из писем как SHA-256, секреты клиентов и сервисные токены как HMAC под отдельным pepper. Собственные строки журнала pacod-id никогда не содержат паролей, токенов или ключей, и строки на каждый запрос он не пишет.
  • Сценарии в браузере: PKCE, state, nonce и iss при каждом перенаправлении; точные redirect URI; Content-Security-Policy из хешей скриптов страниц; запись принимается только с собственного origin области.
  • Доступность снаружи: публичный слушатель не обслуживает внутренние и административные маршруты. В полном стеке pacod внутренний слушатель привязан к pacod-id-internal — псевдониму, который есть у pacod-id только во внутренней сети pacod-admin-net, куда подключаются лишь серверы продуктов. NetworkPolicy Helm-чарта пускает на внутренний порт только из указанных вами источников.
  • Образ: статический бинарный файл musl на distroless, без прав root и без оболочки.

Модель угроз перечисляет и то, что остаётся, помечая каждое как открытое или принятое: внутренний слушатель говорит по обычному HTTP, сервис любого продукта может завершить любой сеанс, а организации у продуктов общие.

Эксплуатация

Области и клиенты описаны в одном файле TOML, секреты — никогда. Секрет клиента берётся из переменной, которую называет его secret_env, а ${NAME} подставляет значение из окружения.

[[realm]]
name = "gateway"
issuer = "https://id.example.com"
brand = "gateway"
locales = ["en", "bg", "de", "ru"]
self_signup = false

[[client]]
id = "pacod-gw-console"
realm = "gateway"
kind = "confidential"
secret_env = "PGW_UI_CLIENT_SECRET"
redirect_uris = ["https://console.example.com/auth/callback"]
audience = "pacod-gw"

pacod-id check-config проверяет файл без базы данных. Помимо него сервису нужны PID_DATABASE_URL, PID_KEK и PID_TOKEN_PEPPER, каждая — как переменная, файл или каталог секретов.

  • База данных: PostgreSQL, с собственной ролью, которая не является суперпользователем. Миграции только добавляют и выполняются при каждом запуске под advisory lock.
  • Экземпляры: сколько угодно на одной базе. Каждый отправляет подошедшие письма и выходы и перезагружает ключи и каталоги; один, держащий аренду, меняет ключи и удаляет старые строки.
  • Почта: ни один запрос не ждёт SMTP. Письма попадают в очередь, в бренде и на языках области, и повторяются около восьми часов.
  • Первый администратор: создаётся при запуске из PID_BOOTSTRAP_ADMIN_*, по умолчанию с id.admin.
  • Наблюдение: /readyz проверяет базу данных и ключ подписи; /metrics даёт число и длительность запросов по слушателям. Журналы следуют общему формату pacod — одна строка JSON или текста на событие. Входы, изменения ролей и действия администраторов попадают в журнал аудита, который хранится 400 дней и доступен на страницах администрирования.

Шлюз, который сам впускал людей, может передать их командой pacod-id import gw: пароли, административные токены и ссылки-приглашения продолжают работать, и пока шлюз не завершит переход, он может вернуться к собственному входу, если здесь с тех пор ничего не изменилось.

В стеке pacod

make compose-up запускает стек для разработки: область shop на http://127.0.0.1:26070 с открытой регистрацией, почтовый приёмник и замену Google, всё опубликовано только на 127.0.0.1. Как и каждый проект pacod, pacod-id подключается к общей Docker-сети pacod-net, а его настройка compose состоит из двух файлов. docker-compose.yml содержит только pacod-id; docker-compose.standalone.yml добавляет собственные копии общих компонентов: PostgreSQL (pacod-postgres), pacod-db-init, который создаёт роль pacod_id и её базу, если их нет, почтовый приёмник (pacod-mailpit) и mock-версию Google (id-mock-idp).

Полный стек pacod, pacod-demo, загружает только первый файл. Он запускает один PostgreSQL, один почтовый приёмник, один edge и один pacod-id для всех продуктов, с единственной областью и собственными клиентами продуктов: по клиенту для входа и сервисному клиенту для приложения, шлюза, студии, базы знаний и бенчмарков. В platform/ лежит доля pacod-id в этой конфигурации: его скрипт базы данных, его маршрут в общем Caddy и его задание Prometheus.

Что проверено и чего пока нет

  • make verify запускает форматирование, линтеры, проверку типов и все тесты на Rust, TypeScript и Python. CI также проверяет зависимости, собирает образ, запускает стек для разработки, проверяет цели по задержкам и линтует Helm-чарт.
  • make e2e прогоняет готовые страницы и три тестовых приложения в настоящем браузере, с тремя областями, почтовым приёмником и mock-версией Google, и обходит каждый бренд в светлой и тёмной теме. Он запускается локально, не в CI.
  • Набор тестов на соответствие OpenID Foundation (v5.3.2) запускался локально: планы Config, Basic и RP-Initiated Logout без единого проваленного условия из 2 370 проверенных, а один тест Back-Channel Logout заблокировал самоподписанный сертификат самого набора. Локальные прогоны — не сертификация.
  • pacod-id 0.1.0 ещё не выпущен. Обновления, резервное копирование и восстановление не проверялись.
  • Helm-чарт проходит helm lint и проверку схем, но ещё не устанавливался в кластер. Настоящий SMTP-сервер и настоящий Google не пробовались — только mock-версия из стека для разработки.
  • MFA и passkeys пока нет, а значит, нет и claim acr. Пока их нет, страницы администрирования действуют по одному лишь cookie сеанса, не требуя недавнего входа.
  • Нет claims адреса и телефона; scope profile даёт name и locale.
  • Общего ограничения частоты запросов, кроме ограничителей входа, нет, а образы пока не подписываются и не сопровождаются SBOM.

Что почитать

Быстрый старт

Разработка и тестирование

make test-services
make verify
make e2e

Сборка и запуск образа

make image
make compose-up

Работает с

  • pacod-gw: С PGW_AUTH=oidc консоль впускает людей через pacod-id; ядро передаёт роли gw, регистрирует организации и рабочие пространства и следит за лентой.
  • pacod-ap: Приложение и его консоль администратора входят через pacod-id; API передаёт роли pacod и следит за лентой, так что у приложения и консоли шлюза один общий аккаунт.
  • pacod-cms: Студия впускает людей через pacod-id, где хранятся их роли на уровне организации и сайта.
  • pacod-kb: Интерфейс впускает людей через pacod-id; роли viewer, editor и admin назначаются там.
  • pacod-benchmark: Интерфейс впускает людей через pacod-id, а API проверяет каждого вызывающего по своим ролям viewer, operator и admin, которые передаёт при каждом запуске.