
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 забирает всё это у продуктов. Человек входит один раз и с одним и тем же аккаунтом попадает в консоль шлюза, платформу агентов, студию 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, обмен токенов |
Вход, шаг за шагом
- Его начинает продукт. Его сервер отправляет браузер на
/oidc/authorizeсstate,nonceи PKCE challenge. Доступен только поток authorization code, и PKCE (S256) обязателен для каждого клиента, включая конфиденциальные. - pacod-id проверяет запрос. Область определяется по
Host; клиент должен принадлежать ей, а redirect URI должен точно совпадать с зарегистрированным. Пока это не подтверждено, ошибка показывает страницу и никогда не перенаправляет. - Единый вход или сценарий. Живой сеанс области сразу получает код. Иначе pacod-id сохраняет
запрос как сценарий входа на 15 минут, привязывает его к браузеру cookie и отправляет браузер на
свою страницу
/login. - Человек входит — паролем или, если область это поддерживает, через Google. Каждая попытка занимает своё место в ограничителях в PostgreSQL ещё до хеширования пароля, а неизвестный адрес получает тот же ответ, что и неверный пароль. Если адрес нужно сначала подтвердить или пароль пора сменить, сценарий ведёт туда.
- Сеанс и код. Вход создаёт новый SSO-сеанс (12 часов или 7 дней с «не выходить») и код,
который живёт 60 секунд, действует один раз и привязан к клиенту, redirect URI и PKCE
challenge. Браузер возвращается в продукт с
code,stateиiss. - Продукт обменивает его на
/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/). Векторы и есть спецификация, поэтому решение одинаково, где бы его ни
принимали.
Лента авторизации
Каждый продукт держит свою копию привязок и следит за изменениями:
- При запуске он загружает снимок: свой каталог, организации и собственные scopes, привязки
своих ролей и ролей
id.*, отключённые и удалённые аккаунты и сеансы, отозванные за последние 15 минут, — всё на один порядковый номер,seq. - Затем он через long poll запрашивает изменения после этого
seq, ожидая до 30 секунд за вызов. Ответ приходит, как только что-то меняется: PostgreSQLLISTENбудит ждущий запрос. - Изменения хранятся 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.
Что почитать
- Runbook: установка и эксплуатация.
- HTTP API: все эндпоинты и события ленты.
- Модель угроз: меры защиты и оставшиеся риски.
- Замеры: задержка эндпоинта токенов и ленты.
- Результаты проверки на соответствие и тестовые векторы движка правил.
Быстрый старт
Разработка и тестирование
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, которые передаёт при каждом запуске.