Веб-консоль
В pg_doorman встроена операторская веб-консоль. Она работает на том же HTTP-сервере, что отдаёт Prometheus-метрики; собранный фронтенд лежит внутри бинарника. Запуск консоли не добавляет внешних зависимостей: один процесс, один бинарь, один TCP-порт.
Включение
Консоль настраивается в секции [web]. Старое имя секции [prometheus]
тоже принимается как алиас.
[web]
enabled = true
host = "0.0.0.0"
port = 9127
# Операторская консоль (по умолчанию выключена)
ui = true
ui_anonymous = false
log_tap_max_entries = 8192
При web.ui = true и general.admin_password, равном пустой строке или
литералу "admin", консоль на старте переходит в режим «только
метрики». HTTP-сервер продолжает отдавать /metrics, но административные эндпоинты
иначе оказались бы открыты любому. Задайте настоящий пароль до того, как
включать ui = true. Срабатывание этой проверки видно в логе по строке
web.ui = true ignored: admin_password is default/empty.
| Параметр | Описание | По умолчанию |
|---|---|---|
enabled | Запускать ли HTTP-сервер. /metrics работает независимо от ui. | false |
host | Адрес, на котором слушает HTTP-сервер. | "0.0.0.0" |
port | Порт HTTP-сервера. | 9127 |
ui | Отдавать SPA по / и публичные API-эндпоинты. | false |
ui_anonymous | При true публичные API-эндпоинты принимают запросы без авторизации. См. Роли доступа. | false |
log_tap_max_entries | Размер кольцевого буфера в памяти для /api/logs. 0 отключает эндпоинт. | 8192 |
URL-карта
| URL | Требуемая роль | Назначение |
|---|---|---|
/, /pools, любой путь вне API | нет | Оболочка приложения. Отдаётся анонимно даже при ui_anonymous = false, чтобы прямая ссылка не открывала системный диалог Basic-авторизации браузера до того, как появится форма входа React. |
/assets/* | нет | Хэшированные JS, CSS, шрифты и SVG. Cache-Control: public, max-age=31536000, immutable. |
/metrics | нет | Prometheus exposition format. От ui не зависит. |
GET /api/auth/config | нет | Сообщает SPA, подключён ли SSO и какая роль у текущего запроса. |
GET /api/version, /api/overview, /api/pools, /api/clients, /api/servers, /api/connections, /api/stats, /api/databases, /api/users, /api/auth_query, /api/config, /api/log_level, /api/pool_coordinator, /api/pool_scaling, /api/sockets, /api/prepared, /api/interner, /api/top/clients, /api/top/prepared, /api/apps, /api/events | Anonymous, когда ui_anonymous = true, иначе Sso | JSON только для чтения, повторяет формат SHOW <admin-команда>. |
GET /api/logs, /api/prepared/text/{hash}, /api/interner/top, /api/top/queries | Sso | Эндпоинты только для чтения с персональными данными. /api/logs подключает буфер логов на первом запросе и отключает его через 2 минуты простоя. /api/top/queries возвращает первые ~120 символов SQL-текста из кеша. Эти данные не вынесены в публичную поверхность, потому что превью могут содержать литералы и идентификаторы клиентов. |
POST /api/admin/{reload,pause,resume,reconnect} | Admin | Управляющие операции администратора. Семантика та же, что и у admin-протокола через psql. |
Роли доступа
Сервер на каждом запросе вычисляет одну из трёх ролей. Проверка работает на стороне сервера; SPA дублирует её на клиенте только для того, чтобы не показывать действия, недоступные текущему оператору.
| Роль | Как запрос её получает | Что роль даёт |
|---|---|---|
Anonymous | Учётных данных нет, [web].ui_anonymous = true. | Публичные /api/* только для чтения из таблицы выше плюс /metrics. На пути с персональными данными и /api/admin/* возвращается 401. |
Sso | Валидный JWT в Authorization: Bearer, в cookie sso_access_token= или в query ?token=, который не попадает в группу администраторов. | Все эндпоинты чтения, включая пути с персональными данными. На POST /api/admin/* отдаётся 403. |
Admin | Либо корректная пара Basic из [general].admin_username / admin_password, либо валидный JWT, у которого значение [web].sso_groups_claim пересекается с [web].sso_admin_groups. | Полный доступ, включая POST /api/admin/{reload,pause,resume,reconnect}. |
Когда в одном запросе есть и Basic, и SSO-токен, приоритет у Basic.
Корректный admin-пароль даёт Admin независимо от состояния SSO.
Неверный Basic-пароль не блокирует SSO-ветку: SSO-источники всё равно
проверяются, и валидный JWT даёт роль Sso (или Admin, если совпала
группа администраторов). Это покрывает типичный случай: в localStorage лежит
просроченный JWT рядом с рабочим Basic-паролем.
Basic-пароль сравнивается за постоянное время, чтобы по длительности
сравнения нельзя было угадывать символы. JWT проверяются по публичному
ключу из [web].sso_public_key_file; разобранный ключ кэшируется на
время жизни процесса и перечитывается на RELOAD.
fetch-обёртка SPA шлёт Accept: application/json, и сервер на ней
отдаёт чистый 401 без WWW-Authenticate: Basic. Без этого браузер
закешировал бы то, что оператор ввёл в системном диалоге Basic, и
подставлял этот пароль поверх формы входа React. Инструменты с
Accept: */* (curl, gh) получают challenge как обычно.
401 Unauthorized отдаётся, когда учётных данных не пришло или ни
один вариант не прошёл парсинг и валидацию. 403 Forbidden — когда
данные валидны, но роли не хватает для пути; тело —
{"error":"forbidden","message":"admin role required"}. SPA на 401
повторно открывает форму входа, на 403 показывает неблокирующий
баннер «admin role required», не уводя на форму входа.
Настройка SSO
SSO опциональный. По умолчанию ([web].sso_enabled = false) сервер
обслуживает только роли Anonymous и Admin через Basic. Чтобы подключить
внешний SSO-прокси:
-
Получите от SSO-провайдера публичный RSA-ключ, которым он подписывает JWT, и сохраните его в PEM-файле (например,
/etc/pg_doorman/sso-public.pem). Для oauth2-proxy ключ извлекается из приватного:openssl rsa -in private.pem -pubout -out public.pem. Для Keycloak — см. Keycloak ниже. -
Добавьте SSO-поля в
[web]:[web] enabled = true ui = true host = "127.0.0.1" port = 9127 ui_anonymous = false sso_enabled = true sso_proxy_url = "https://sso.example.com/oauth2/start" sso_public_key_file = "/etc/pg_doorman/sso-public.pem" sso_audience = ["pg_doorman"] sso_allowed_users = ["*"] -
Перечитайте конфиг:
kill -SIGHUP <pid>илиpsql -h <host> -p 6432 -U admin -d pgbouncer -c 'RELOAD'. -
Проверьте:
curl http://<host>:9127/api/auth/configдолжен вернуть"sso_enabled":trueи заданныйsso_proxy_url.
| Поле | Назначение | По умолчанию |
|---|---|---|
sso_enabled | Включает SSO-ветку. Без неё JWT не валидируются. | false |
sso_proxy_url | URL, на который SPA уводит браузер по кнопке «Sign in via SSO». Бэкенд этот URL сам не вызывает. | null |
sso_public_key_file | Путь к PEM-файлу с публичным RSA-ключом. Читается на старте и при RELOAD. | null |
sso_audience | Допустимые значения claim aud. Токен принимается, если совпадает хотя бы одно. Обязательное поле при sso_enabled = true. | [] |
sso_allowed_users | Allowlist по claim preferred_username (или sub). ["*"] принимает любого. Иначе пропускаются только перечисленные имена. | ["*"] |
sso_groups_claim | Имя JWT-claim, в котором лежат группы пользователя. Читается вместе с sso_admin_groups. | "groups" |
sso_admin_groups | Группы, которые поднимают SSO-пользователя до Admin. Пустой список оставляет каждый SSO-логин на роли Sso только для чтения. | [] |
sso_require_https | Отклонять Bearer/cookie/query SSO-credentials, пришедшие по plain HTTP. Запрос считается защищённым только если TCP-peer входит в trusted_proxies и прокси прислал X-Forwarded-Proto: https. По умолчанию выключено, чтобы SSO продолжал работать в схеме «TLS терминирует прокси → pg_doorman слушает HTTP во внутренней сети». | false |
trusted_proxies | CIDR доверенных обратных прокси для X-Forwarded-For, Forwarded и X-Forwarded-Proto. При пустом списке pg_doorman игнорирует эти заголовки и берёт адрес прямого TCP-пира. Если sso_require_https = true работает за прокси, который завершает TLS, добавьте CIDR этого прокси, чтобы доверять X-Forwarded-Proto: https. См. Журнал доступа. | [] |
Поднятие SSO-пользователя до Admin через claim с группами
По умолчанию SSO-логин получает роль Sso — доступ только для чтения к логам
и SQL-текстам, но без POST /api/admin/*. Чтобы операторы могли
запускать управляющие операции администратора через SSO без раздачи Basic-пароля,
настройте sso_groups_claim и sso_admin_groups:
[web]
sso_enabled = true
sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
sso_audience = ["pg_doorman"]
sso_groups_claim = "groups"
sso_admin_groups = ["pg-doorman-admins"]
Когда в валидном JWT приходит "groups": [..., "pg-doorman-admins"],
запрос получает роль Admin. В access-логе это выглядит как
auth_role=admin auth_source=sso, и SSO-админы по-прежнему отличимы от
Basic-админов. /api/auth/config отдаёт
sso_admin_groups_configured = true, и SPA убирает из формы входа
обещание «SSO grants read-only access».
Keycloak
Keycloak подписывает каждый JWT RSA-ключом realm'а. Публичную часть этого ключа нужно один раз выгрузить в PEM-файл, который читает pg_doorman.
Без UI — через JWKS-эндпоинт realm'а:
REALM=https://kc.example.com/realms/operators
curl -s "$REALM/protocol/openid-connect/certs" \
| jq -r '.keys[] | select(.alg=="RS256") | "-----BEGIN CERTIFICATE-----\n" + .x5c[0] + "\n-----END CERTIFICATE-----"' \
| openssl x509 -pubkey -noout \
> /etc/pg_doorman/sso-public.pem
Через админ-консоль: Realm settings → Keys → строка с
Algorithm = RS256 и Use = SIG → Public key → скопированное
base64-тело завернуть в PEM-файл с заголовками
-----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY-----.
Секция [web] под Keycloak выглядит так:
[web]
sso_enabled = true
sso_proxy_url = "https://kc.example.com/realms/operators/protocol/openid-connect/auth"
sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
sso_audience = ["pg_doorman"] # client_id, заданный в Keycloak
sso_groups_claim = "groups" # значение по умолчанию для маппера «groups»
sso_admin_groups = ["pg-doorman-admins"]
Чтобы Admin через group claim работал, добавьте клиенту маппер
Group Membership (Clients → нужный client → Mappers). Без
этого маппера Keycloak выдаёт токены без groups, и каждый оператор
остаётся в роли Sso.
После ротации ключа realm'а заново выгрузите PEM и сделайте
RELOAD — pg_doorman подхватит новый ключ без рестарта.
Когда SSO-конфигурация сломана
Опечатка в SSO-секции не должна выводить операторскую консоль из строя.
При sso_enabled = true, но не загружаемом рантайме (нет PEM-файла,
пустой audience, нечитаемый PEM) сервер пишет причину в лог на уровне
error, оставляет SSO выключенным на этот запуск и обслуживает только
Basic и Anonymous. Та же причина видна в двух точках, чтобы оператор
заметил поломку, а не тихий откат на Basic:
/api/auth/config.sso_config_errorсодержит человекочитаемое сообщение. SPA показывает баннер с этим текстом в форме входа.- Метрика
pg_doorman_web_sso_config_errorравна1, пока SSO запрошен, но не загружен. В паре сpg_doorman_web_sso_enabledподходит для alert-правила.
Логин из браузера
При первом заходе SPA получает /api/auth/config и показывает форму
входа. Если в ответе пришёл sso_proxy_url, рядом с Basic-формой
появляется кнопка Sign in via SSO; иначе — только Basic.
Клик по Sign in via SSO уводит браузер на
${sso_proxy_url}?redirect_to=<текущий URL>. Внешний proxy выполняет
OAuth/OIDC-обмен и возвращает браузер обратно с ?token=<jwt>. SPA
сохраняет токен в localStorage, чистит URL от параметра и шлёт
Authorization: Bearer <jwt> на каждом следующем запросе.
В нижней части боковой панели отображается имя текущего пользователя: admin
для Basic или sso: <preferred_username> для SSO. Кнопка Sign out
очищает в localStorage оба ключа (pgdoorman.admin-auth и
pgdoorman.sso-token) и заново открывает форму входа.
Тихое обновление токена запускается раз в 60 секунд. Когда до exp остаётся
меньше 90 секунд, SPA открывает скрытый iframe с URL
${origin}/?sso_silent=1. Внутри iframe запускается минимальный
SilentCallback без обычных polling-эффектов. Он
через window.postMessage отдаёт новый токен parent-окну. Если
тихое обновление не сработало:
- при наличии Basic-данных SPA удаляет SSO-токен без редиректа, и дальнейшие запросы идут под Basic;
- иначе SPA уходит на полный редирект через SSO-proxy.
Срок жизни JWT задавайте не меньше 5 минут — более короткие токены успевают истечь до тихого обновления.
SPA cookie не шлёт (credentials: "omit" на каждом fetch). Путь с
cookie sso_access_token существует для сайдкаров, curl и oauth2-proxy
вариантов, которые кладут токен в cookie на общем домене.
Basic-пароль по умолчанию живёт только в памяти React и пропадает после
полной перезагрузки страницы. Галочка Remember me on this device в форме входа
сохраняет его в localStorage, поэтому консоль открывается без повторного ввода.
Очистка хранилища сайта в браузере удаляет и Basic, и SSO-запись.
Журнал доступа
После каждого ответа (200/401/403/404/5xx, включая запросы к /metrics)
консоль пишет одну logfmt-строку в канал pg_doorman::web::access:
INFO pg_doorman::web::access method=GET path=/api/admin/reload query=false status=200 bytes=42 latency_ms=12 peer=10.0.1.5:42312 auth_role=admin auth_source=basic auth_user=admin
Поля:
method,path— HTTP-метод и URL-путь. Тела запросов и ответов в лог не пишутся.query=true|false— была ли в запросе строка параметров. Сама строка не логируется, чтобы JWT в?token=не попадал в журнал.status,bytes,latency_ms— статус ответа, размер тела и полная задержка ответа.peer— адрес инициатора запроса. По умолчанию это непосредственный TCP-peer. Если он попадает в[web].trusted_proxies, сервер разбираетX-Forwarded-For(илиForwarded, RFC 7239), идёт по цепочке справа налево, пропускает доверенные адреса и берёт первый недоверенный. Недоверенный клиент не может подделать поле — если TCP-peer не в списке доверенных, заголовки прокси игнорируются.auth_role—admin,sso,anonymousилиrejected.auth_source—basic,ssoили-.auth_user— имя пользователя из учётных данных или-для анонимов и отклонённых запросов.
Уровни:
info— все управляющие действия (POST /api/admin/*), все чтения персональных данных (/api/logs,/api/prepared/text/*,/api/interner/top,/api/top/queries), все эндпоинты аутентификации и SSO (/api/auth/*,/api/sso/*) и все ответы с кодом, отличным от 2xx.debug— любые остальные успешные чтения (2xx), анонимные или под авторизованной ролью. SPA опрашивает/api/overview,/api/pools,/api/clients,/api/processраз в 1,5–3 секунды; при старом правиле «любой 2xx с авторизацией = info» оператор на странице Logs видел собственные polling-запросы. Рутинные чтения логируются наdebug, аRUST_LOG=infoостаётся для управляющих действий, auth-трафика и ошибок.
Отдельный канал pg_doorman::web::access позволяет фильтровать поток
журнала доступа независимо от остальных логов. В выпадающем фильтре на
странице Logs этот канал включается или исключается одним кликом.
Реальный IP клиента за обратным прокси
По умолчанию peer фиксирует TCP-адрес, который соединился с
сервером. За обратным прокси это адрес самого прокси. Чтобы видеть
реальный IP клиента, добавьте CIDR прокси в [web].trusted_proxies:
[web]
trusted_proxies = ["10.0.0.0/8", "192.168.0.0/16"]
Распознаются и X-Forwarded-For, и Forwarded. Несколько доверенных
переходов в цепочке пропускаются. X-Forwarded-For, пришедший от
недоверенного клиента, игнорируется, поэтому через эту настройку
произвольный вызывающий не может управлять полем access-лога.
Метрики
| Метрика | Тип | Лейблы | Назначение |
|---|---|---|---|
pg_doorman_web_sso_enabled | gauge | — | 1, когда SSO загружен успешно, иначе 0. |
pg_doorman_web_sso_config_error | gauge | — | 1, когда sso_enabled = true, но рантайм не поднялся. |
pg_doorman_web_auth_attempts_total | counter | role, source | Попытки авторизации в разрезе итоговой роли (admin/sso/anonymous/rejected) и источника (basic/sso/none). |
pg_doorman_web_requests_total | counter | status_class, role | Запросы к веб-консоли в разрезе HTTP-статуса (1xx–5xx) и роли. |
pg_doorman_web_sso_validation_errors_total | counter | reason | Отказы валидации JWT по причине: signature, expired, audience, no_username, allowlist. |
Устойчивый рост signature означает, что SSO-прокси ротировал ключ, а
sso_public_key_file остался старый. Рост allowlist — кто-то снаружи
sso_allowed_users упорно пытается войти. Рост 4xx для роли sso
обычно указывает на сломанный прокси перед pg_doorman.
Диагностика
401 на JWT, который должен быть валиден. Проверьте, что aud
совпадает хотя бы с одним значением из sso_audience и exp ещё не
истёк. PEM проверяется через openssl rsa -pubin -in <pem> -text -noout.
Счётчик pg_doorman_web_sso_validation_errors_total{reason} показывает,
какая именно проверка не прошла.
403 на JWT, который должен быть валиден. Путь требует роли
Admin (например, POST /api/admin/reload). Войдите по Basic
admin-паролю или добавьте группу пользователя в
[web].sso_admin_groups и перечитайте конфиг.
SPA не показывает Sign in via SSO. /api/auth/config не возвращает
sso_proxy_url. Либо [web].sso_enabled = false, либо sso_proxy_url
не задан, либо рантайм не поднялся (ищите sso_config_error в том же
ответе).
Тихое обновление токена не срабатывает. SSO-прокси должен возвращать свежий
токен без полного экрана логина, когда iframe приходит с активной
сессией. У oauth2-proxy это включается флагом --silent-refresh=true.
JWT в cookie игнорируется. Cookie должна попасть на pg_doorman с
того же домена, и aud обязан входить в sso_audience. SPA сама
cookie не шлёт; cookie-аутентификация рассчитана на curl, сайдкары и
oauth2-proxy-варианты, которые проставляют токен в cookie на общем
домене.
Страницы
В боковой панели восемь пунктов. Экран инцидента открывается из
Обзора. Страницы с SQL-текстом и логами требуют роли Sso или
Admin; анонимные пользователи их не видят.
Обзор (/overview)
Страница по умолчанию. Показывает состояние всего процесса, основные метрики, очереди, насыщение пулов, частые SQLSTATE и свёрнутый блок ресурсов. Если пул ушёл на резервный бэкенд через Patroni, сверху появляется баннер со списком затронутых пулов.
Пулы (/pools)
Таблица всех пулов user@database: размер, активные соединения,
ожидание, p95, ошибки, насыщение и флаг резервного бэкенда. Строка
открывает Детали пула.
Детали пула (/pools/:poolId)
Подробности одного пула: режим, лимиты, текущие соединения, TLS,
резервный бэкенд, SQLSTATE, параметры запуска PostgreSQL и причины
сработавших порогов. Здесь же находятся действия PAUSE, RESUME,
RECONNECT и общий RELOAD.
Клиенты (/clients)
Таблица клиентов с фильтрами в URL:
/clients?pool=shop_checkout&state=waiting&user=app
Фильтры: pool, database, user, state, application_name, peer-адрес.
Сортировка: запросы, ошибки, возраст соединения и возраст текущего
запроса. Вместе со страницей Серверы помогает связать клиента с
PostgreSQL pid.
Серверы (/servers)
Таблица бэкенд-соединений из SHOW SERVERS: server_id,
process_id, база, пользователь, приложение, состояние, возраст
активного запроса, счётчики запросов и ошибок, трафик и TLS.
Используйте server_id из строки клиента, чтобы найти pid в
pg_stat_activity.
Приложения (/apps)
Одна строка на каждый application_name: активные клиенты, qps, tps,
суммарные запросы, транзакции, ошибки и err / 1k q.
Кеши (/caches)
Две вкладки: кеш подготовленных запросов по пулам и кеш SQL-текста в
процессе. Обе могут показывать SQL, поэтому доступны только Sso и
Admin.
Логи (/logs)
Лента LogTap с фильтрами в URL:
/logs?level=ERROR&q=53300
Пауза останавливает только отображение; серверный буфер продолжает
заполняться. При [web].log_tap_max_entries = 0 страница показывает,
что поток логов выключен. Доступ: Sso / Admin.
Конфиг и состояние (/config)
Зеркало команд SHOW CONFIG, SHOW DATABASES, SHOW USERS,
SHOW AUTH_QUERY, SHOW LOG_LEVEL, SHOW STARTUP_PARAMETERS,
SHOW SOCKETS, SHOW POOL_SCALING, SHOW POOL_COORDINATOR.
Страница помогает проверить текущую конфигурацию и увидеть, какие
ключи применяются на RELOAD, а какие требуют рестарта. Кнопка
Reload config доступна только Admin.
Экран инцидента (/wall)
Версия Обзора для большого экрана: карта насыщения пулов, крупные
метрики и последние управляющие действия. По Esc возвращает на
/overview.
Управляющие действия
Четыре управляющие команды доступны в SPA:
| Действие | Область | Где найти | Подтверждение |
|---|---|---|---|
RELOAD | все пулы | Конфиг и состояние · Детали пула | RELOAD |
PAUSE | один user@database | Детали пула | имя базы |
RESUME | один user@database | Детали пула, когда пул паузирован | имя базы |
RECONNECT | один user@database | Детали пула | имя базы |
Семантика та же, что в admin-протоколе через psql. PAUSE прекращает
выдачу бэкендов в нужном пуле; уже идущие транзакции продолжают
выполняться. RESUME снова разрешает выдачу. RECONNECT закрывает
idle-бэкенды и отказывает активным при возврате. RELOAD перечитывает
pg_doorman.toml; размер пула уменьшается по мере освобождения
соединений.
Подтверждение вводом защищает от случайного RELOAD или PAUSE не
того пула. Результат показывается уведомлением, а само действие
пишется в access-лог и список последних событий администратора.
Горячие клавиши
Сочетания работают вне текстовых полей.
| Сочетание | Действие |
|---|---|
| ⌘ K / Ctrl K | Поиск по страницам и пулам. |
| ? | Список горячих клавиш. |
| Esc | Закрыть подсказку или окно. На /wall возвращает на Обзор. |
Тема
Внизу боковой панели есть переключатель Light / System /
Dark. По умолчанию используется Light. Выбор хранится в
localStorage.
Встроенная справка
Рядом с заголовками метрик и секций есть значок (i). Подсказка объясняет, что означает число, откуда оно взято, как считается и какие пороги считаются нормой.
Сборка из исходников
Собранный веб-интерфейс лежит в git по пути frontend/dist/, чтобы
RPM-, DEB- и Docker-сборки не зависели от Node.js.
Разработчикам, правящим веб-интерфейс, нужно пересобирать его перед
коммитом:
cd frontend
npm ci
npm run install-hooks # одноразово: ставит pre-commit hook для синхронизации dist
npm run lint
npm run typecheck
npm run build
npm run install-hooks опционален. CI его не требует: проверка
.github/workflows/frontend.yml запускает npm run check-dist и
блокирует слияние, если исходники меняли без пересборки dist/. Она
же запускает lint и typecheck на каждом PR, который трогает
frontend/.
Развёртывание
/metrics доступен без авторизации на том же HTTP-сервере, что и
консоль. Так задумано: иначе сломались бы существующие настройки
Prometheus. Авторизация на /api/* не распространяется на
/metrics — метрики раскрывают имена пулов, пользователей и БД,
давление на пул, состояние auth_query и форму нагрузки. Либо держите
секцию [web] на приватном host:port, доступном только системе
сбора метрик, либо ставьте перед HTTP-сервером прокси, который добавляет
авторизацию на /metrics отдельно.