diff --git a/.github/workflows/bdd-tests.yml b/.github/workflows/bdd-tests.yml
index f2298bc36..b650d646a 100644
--- a/.github/workflows/bdd-tests.yml
+++ b/.github/workflows/bdd-tests.yml
@@ -219,12 +219,13 @@ jobs:
packages: read
strategy:
fail-fast: false
- # GitHub-hosted runners share CPU under heavy matrix fan-out, and
- # the timing-sensitive scenarios (sleep-based retain/lifetime
- # waits, SCRAM passthrough reconnect) lose their margin when 20+
- # BDD jobs run in parallel. Cap concurrency so each suite gets a
- # less contested runner.
- max-parallel: 4
+ # Six concurrent suites is the cap this matrix was tuned for.
+ # GitHub-hosted runners share CPU under heavy fan-out; with 20+
+ # BDD jobs in parallel, the sleep-based lifecycle tests and the
+ # SCRAM passthrough reconnect tests lost timing margin. This cap
+ # keeps those suites stable while reducing total wall time versus
+ # the previous limit of four.
+ max-parallel: 6
matrix:
suite:
- { name: "Go", cargo: "test --test bdd -- --tags @go" }
@@ -249,6 +250,7 @@ jobs:
- { name: "Server TLS", cargo: "test --test bdd -- --tags @server-tls" }
- { name: "TLS migration (vendored OpenSSL)", cargo: "test --features tls-migration --test bdd -- --tags @tls-migration" }
- { name: "Startup parameters", cargo: "test --test bdd -- --tags @startup-parameters" }
+ - { name: "Web UI", cargo: "test --test bdd -- --tags @web-ui" }
steps:
- name: Checkout repository
uses: actions/checkout@v4
diff --git a/.github/workflows/dashboard-validation.yml b/.github/workflows/dashboard-validation.yml
index 0d95ad127..c7e90dadd 100644
--- a/.github/workflows/dashboard-validation.yml
+++ b/.github/workflows/dashboard-validation.yml
@@ -5,6 +5,7 @@ on:
branches: [master]
paths:
- "grafana/**"
+ - "monitoring/prometheus-rules/**"
- "scripts/dashboard-*"
- "scripts/docker-smoke.sh"
- "src/web/metrics/**"
@@ -17,6 +18,7 @@ on:
pull_request:
paths:
- "grafana/**"
+ - "monitoring/prometheus-rules/**"
- "scripts/dashboard-*"
- "scripts/docker-smoke.sh"
- "src/web/metrics/**"
diff --git a/Cargo.lock b/Cargo.lock
index d8b726c3f..31ebfaa47 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2118,6 +2118,8 @@ dependencies = [
"bytes",
"fallible-iterator",
"postgres-protocol",
+ "serde",
+ "serde_json",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 73bdb6c75..efe811b98 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -68,7 +68,10 @@ iota = { version = "0.2.3" }
pin-project-lite = "0.2.16"
pam-client = { version = "0.5.0", optional = true }
postgres = "0.19.10"
-tokio-postgres = "0.7"
+# `with-serde_json-1` lets `Row::try_get` decode `json`/`jsonb`
+# columns directly into `serde_json::Value`, which allows auth_query
+# startup_parameters to come from a native JSON column.
+tokio-postgres = { version = "0.7", features = ["with-serde_json-1"] }
postgres-native-tls = "0.5.1"
flate2 = "1.0.28"
sd-notify = "0.4"
diff --git a/documentation/en/src/changelog.md b/documentation/en/src/changelog.md
index 89eaa50e6..fc0711631 100644
--- a/documentation/en/src/changelog.md
+++ b/documentation/en/src/changelog.md
@@ -2,7 +2,18 @@
### 3.9.1
-Web admin console refresh.
+Web admin console refresh and a follow-up pass on `startup_parameters`.
+
+Upgrade notes for operators monitoring 3.9.0:
+
+- The pg_doorman-side budget rejection now returns `SQLSTATE 53400`
+ (`configuration_limit_exceeded`) instead of `54000`. Alert rules
+ and log filters keyed on `54000` need to switch.
+- `PgDoormanStartupParameterPgRejection` is now `severity: warning`
+ (was `critical` in 3.9.0). Cascade-overflow stays `critical`. Review
+ the Alertmanager / on-call routing if you key on severity to page.
+
+#### Web admin console
- Light theme by default. Three-position theme toggle (Light / System / Dark)
in the sidebar footer; choice persists in localStorage.
@@ -39,6 +50,29 @@ Backend: `web/access_log.rs` demotes authenticated 2xx reads to debug.
Docs: `guides/web-ui.md` rewritten for the new pages and shortcuts.
+#### startup_parameters follow-up
+
+- If the resolved `startup_parameters` set exceeds the startup packet
+ budget, backend startup now fails with `SQLSTATE 53400`. A
+ deterministic `general + pool` overflow is rejected at config load.
+- The final `ParameterStatus` messages sent to the client no longer
+ overwrite operator-managed GUC names, so the client-visible values
+ match the backend checkout state.
+- `auth_query` now rebuilds a dynamic pool after a successful MD5
+ refetch, rejects the stale-overlay race in `create_dynamic_pool`, and
+ accepts native `json`/`jsonb` startup_parameter columns without a
+ `::text` cast.
+- `/api/config` and `/api/pools` show literal startup_parameter values
+ only to `Admin`; SSO readers get the masked view. `/api/config` also
+ marks `general.host`, `general.port`, `web.host`, and `web.port` as
+ restart-required.
+- Prometheus rules now cover PostgreSQL-side rejection, budget overflow,
+ malformed auth_query columns, dedicated-mode drops, and rejected SSO
+ credentials sent over insecure transport.
+- Each pool now precomputes the merged startup map, budget decision, and
+ canonical operator-key set. Backend checkout reuses those cached
+ values instead of cloning and recalculating the map each time.
+
### 3.9.0
Per-pool PostgreSQL startup parameters. pg_doorman can now add
diff --git a/documentation/en/src/guides/web-ui.md b/documentation/en/src/guides/web-ui.md
index 15b39ec21..5ad4fe2e3 100644
--- a/documentation/en/src/guides/web-ui.md
+++ b/documentation/en/src/guides/web-ui.md
@@ -130,7 +130,7 @@ proxy:
| `sso_groups_claim` | Name of the JWT claim that carries the user's group memberships. Read together with `sso_admin_groups`. | `"groups"` |
| `sso_admin_groups` | Group names that promote an SSO user to `Admin`. Empty keeps every SSO login at the read-only `Sso` role. | `[]` |
| `sso_require_https` | Reject Bearer/cookie/query SSO credentials presented over plain HTTP. The listener treats a request as secure only when the TCP peer is in `trusted_proxies` and `X-Forwarded-Proto: https` is forwarded. Defaults to off so SSO keeps working through a TLS-terminating proxy that reaches pg_doorman over a private HTTP leg. | `false` |
-| `trusted_proxies` | CIDR ranges trusted to set `X-Forwarded-For` / `Forwarded` / `X-Forwarded-Proto`. Empty trusts only the listener's own peer. See [Access log](#access-log). | `[]` |
+| `trusted_proxies` | CIDR ranges trusted to set `X-Forwarded-For` / `Forwarded` / `X-Forwarded-Proto`. With an empty list, pg_doorman ignores forwarded headers and uses the TCP peer address. If `sso_require_https = true` is behind a TLS-terminating proxy, add that proxy CIDR so `X-Forwarded-Proto: https` is accepted. See [Access log](#access-log). | `[]` |
### Promoting SSO users to Admin via group claim
diff --git a/documentation/en/src/tutorials/startup-parameters.md b/documentation/en/src/tutorials/startup-parameters.md
index 1a3f0709b..dbf2c819b 100644
--- a/documentation/en/src/tutorials/startup-parameters.md
+++ b/documentation/en/src/tutorials/startup-parameters.md
@@ -52,15 +52,27 @@ FROM pg_authid
WHERE rolname = $1;
```
-The column must serialize as `text`. If the SQL returns `json` or
-`jsonb`, add an explicit `::text` cast. pg_doorman reads the column
-as `text` and logs a warning for that fetched row when the type does
-not match.
+The column may be `text`, `json`, or `jsonb`; pg_doorman dispatches by
+the column type without requiring a cast. The content must be a JSON
+object whose values are strings. Other PostgreSQL types (or a custom
+domain on top of `jsonb`) log a warning and the per-user overlay is
+ignored.
Dedicated `auth_query` mode (`server_user` set) ignores the per-user
column and logs once per (pool, username): one shared backend serves
many users, so a per-user override cannot apply.
+Changes to a per-user `startup_parameters` row apply to **new** backend
+connections, but only after pg_doorman re-reads the row. The
+`auth_query` cache holds positive entries for `auth_query.cache_ttl`
+(default one hour) and on a refresh detects the overlay change and
+drops the dynamic pool so the next login rebuilds it against the new
+values. Until the cache entry expires, reconnecting clients still see
+the old overlay. To force an immediate rollout: lower `cache_ttl` and
+reload the config, restart pg_doorman, or wait for the TTL to elapse.
+Backends that are already checked out keep the values captured when
+their pool was created.
+
## What pg_doorman does with the values
pg_doorman adds the resolved parameter set to the PostgreSQL
@@ -92,21 +104,24 @@ At config load:
- Keys must match PG GUC naming `^[A-Za-z_][A-Za-z0-9_.]*$`. Namespaced
names like `auto_explain.log_min_duration` are accepted; arbitrary
punctuation is not.
-- Reserved keys (`user`, `database`, `replication`, `options`, and
- anything starting with `_pq_.`) are refused. pg_doorman manages
- them itself or PG treats them specially in the StartupMessage.
+- Reserved keys (`user`, `database`, `replication`, `options`, `role`,
+ `session_authorization`, and anything starting with `_pq_.`) are
+ refused. pg_doorman manages them itself or PG treats them specially in
+ the StartupMessage.
- Values must not contain null bytes.
- Each level (general or per-pool) must fit within the startup-parameter
budget: `MAX_STARTUP_PACKET_LENGTH` (10 000 bytes) minus 512 bytes
reserved for pg_doorman-managed keys.
-Before each backend spawn pg_doorman checks the resolved parameter set
-against the same cap. Two layers that fit on their own can overflow once
-`auth_query` adds a third layer. If only the `auth_query` layer pushes
-the set over the cap, pg_doorman drops that layer and keeps the
-general/pool baseline. If the baseline itself or the full startup packet
-does not fit, pg_doorman skips all configured parameters for that
-spawn and logs the byte counts.
+Before each backend start, pg_doorman checks the resolved parameter set
+against the same cap. Layers that fit individually can exceed the limit
+after merging: `general + pool` can already be too large, and an
+`auth_query` row can push a valid baseline over the limit. Any overflow
+now returns a PostgreSQL-style error (`SQLSTATE 53400`) to the client
+instead of sending a partial or empty `StartupMessage`. The warning log
+records the byte counts, and
+`pg_doorman_startup_parameters_dropped_total` increments for each
+rejected backend start.
## What happens when PG rejects a parameter
diff --git a/documentation/ru/src/authentication/auth-query.md b/documentation/ru/src/authentication/auth-query.md
index 338d112c1..a4820d55b 100644
--- a/documentation/ru/src/authentication/auth-query.md
+++ b/documentation/ru/src/authentication/auth-query.md
@@ -30,10 +30,11 @@ pools:
Запрос должен возвращать колонку с именем `passwd` или `password`,
содержащую хеш MD5 или SCRAM. Дополнительные колонки игнорируются, кроме
-необязательной `startup_parameters`. В passthrough-режиме pg_doorman
-читает её как JSON-объект в `text` с пользовательскими параметрами
-запуска PostgreSQL. Dedicated-режим игнорирует её и пишет
-предупреждение.
+необязательной `startup_parameters`. В сквозном режиме pg_doorman
+читает эту колонку как `text`, `json` или `jsonb`; значение должно быть
+JSON-объектом с параметрами запуска PostgreSQL для конкретного
+пользователя. В выделенном режиме эта колонка игнорируется, а в лог
+пишется предупреждение.
`user` и `password` — это учётные данные, под которыми pg_doorman выполняет lookup-запрос. У них должно быть право читать колонку с учётными данными. Либо выдайте доступ к специально созданному представлению (рекомендуется), либо используйте пользователя из группы `pg_read_server_files`.
diff --git a/documentation/ru/src/guides/web-ui.md b/documentation/ru/src/guides/web-ui.md
index 909767841..e83c06d6a 100644
--- a/documentation/ru/src/guides/web-ui.md
+++ b/documentation/ru/src/guides/web-ui.md
@@ -130,7 +130,7 @@ SSO опциональный. По умолчанию (`[web].sso_enabled = fals
| `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`). Пустой список — доверять только непосредственному TCP-peer. См. [Журнал доступа](#журнал-доступа). | `[]` |
+| `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 с группами
diff --git a/documentation/ru/src/reference/general.md b/documentation/ru/src/reference/general.md
index badb9ba8f..926faa35c 100644
--- a/documentation/ru/src/reference/general.md
+++ b/documentation/ru/src/reference/general.md
@@ -684,9 +684,11 @@ hostnossl all all 192.168.1.0/24 trust
протокольные ключи (`user`, `database`, `replication`, `options`,
`_pq_.*`), имена GUC, нулевые байты и размер этого уровня. Перед каждым
запуском бэкенда объединённый набор параметров снова проверяется по
-лимиту `MAX_STARTUP_PACKET_LENGTH` PostgreSQL; если он не помещается,
-pg_doorman пропускает операторские параметры для этого запуска и пишет
-предупреждение.
+лимиту `MAX_STARTUP_PACKET_LENGTH` PostgreSQL (10 000 байт). Каскад и
+проверка пакета отклоняют запуск бэкенда с SQLSTATE 53400
+(`configuration_limit_exceeded`); если каскад выходит за бюджет только
+из-за overlay из `auth_query`, pg_doorman сбрасывает этот overlay и
+продолжает с baseline.
Если PostgreSQL отвергает параметр при запуске бэкенда, pg_doorman
возвращает клиенту `ErrorResponse` PostgreSQL без изменений: повторной
diff --git a/documentation/ru/src/tutorials/startup-parameters.md b/documentation/ru/src/tutorials/startup-parameters.md
index 7c1fdd0ae..27fb0d969 100644
--- a/documentation/ru/src/tutorials/startup-parameters.md
+++ b/documentation/ru/src/tutorials/startup-parameters.md
@@ -42,10 +42,11 @@ work_mem = "64MB"
настроек PostgreSQL по умолчанию. Уже открытые бэкенды не меняются:
новые значения вступают в силу по мере ротации соединений.
-В passthrough-режиме `auth_query`, когда `server_user` не задан, запрос
+В сквозном режиме `auth_query`, когда `server_user` не задан, запрос
аутентификации может вернуть необязательную колонку `startup_parameters`
-типа `text` с JSON-объектом. Значения из этой колонки переопределяют
-`general` и настройки пула только для конкретного пользователя.
+типа `text`, `json` или `jsonb` с JSON-объектом. Значения из этой
+колонки переопределяют `general` и настройки пула только для
+конкретного пользователя.
```sql
SELECT
@@ -58,15 +59,28 @@ FROM pg_authid
WHERE rolname = $1;
```
-Колонка должна возвращаться как `text`. Если SQL отдаёт `json` или
-`jsonb`, добавьте явное приведение типа `::text`. pg_doorman читает её
-именно как `text` и пишет предупреждение для полученной строки, если
-тип не совпал.
+pg_doorman выбирает декодер по типу колонки, поэтому приведение `::text`
+не требуется. Содержимое должно быть JSON-объектом, а значения —
+строками. Для других типов PostgreSQL, в том числе доменов поверх
+`jsonb`, pg_doorman пишет предупреждение и игнорирует пользовательские
+параметры из этой строки.
-Dedicated-режим `auth_query`, когда `server_user` задан, игнорирует эту
+Выделенный режим `auth_query`, когда `server_user` задан, игнорирует эту
колонку и один раз пишет предупреждение на пару `(пул, пользователь)`.
В этом режиме один серверный пул обслуживает разных пользователей,
-поэтому per-user значения применить нельзя.
+поэтому пользовательские значения применить нельзя.
+
+Изменения в пользовательской строке `startup_parameters` применяются
+только к **новым** серверным подключениям и только после того, как
+pg_doorman перечитает строку. Кеш `auth_query` хранит положительные
+записи в течение `auth_query.cache_ttl` (по умолчанию час); при
+обновлении кеша pg_doorman замечает изменение overlay и сбрасывает
+динамический пул, чтобы следующий логин пересоздал его с новыми
+значениями. Пока запись кеша не истекла, новые подключения клиентов
+получают прежний overlay. Чтобы выкатить изменения немедленно: уменьшить
+`cache_ttl` и сделать reload конфигурации, перезапустить pg_doorman или
+дождаться истечения TTL. Бэкенды, которые уже выданы клиентам,
+продолжают работать со значениями, сохранёнными при создании пула.
## Что pg_doorman делает со значениями
@@ -99,31 +113,34 @@ checkout=> SET plan_cache_mode = 'auto'; RESET ALL; SHOW plan_cache_mode;
`^[A-Za-z_][A-Za-z0-9_.]*$`. Составные имена вроде
`auto_explain.log_min_duration` допустимы; произвольная пунктуация
нет.
-- Зарезервированные ключи (`user`, `database`, `replication`, `options`
- и всё, что начинается с `_pq_.`) отклоняются. pg_doorman управляет
- ими сам, либо PostgreSQL обрабатывает их в `StartupMessage` особым
- образом.
+- Зарезервированные ключи (`user`, `database`, `replication`, `options`,
+ `role`, `session_authorization` и всё, что начинается с `_pq_.`)
+ отклоняются. pg_doorman управляет ими сам, либо PostgreSQL
+ обрабатывает их в `StartupMessage` особым образом.
- Значения не должны содержать нулевой байт.
- Каждый уровень (`general` или `pool`) должен помещаться в лимит для
операторских параметров: `MAX_STARTUP_PACKET_LENGTH` (10000 байт)
минус 512 байт, зарезервированных под служебные ключи pg_doorman.
-Перед запуском каждого бэкенда pg_doorman заново проверяет объединённый
-набор параметров по тому же лимиту. Два уровня, которые помещались по
-отдельности, могут вместе выйти за лимит, особенно когда `auth_query`
-добавляет третий слой. Если лимит превышает только слой `auth_query`,
-pg_doorman отбрасывает этот слой и сохраняет baseline из `general` и
-пула. Если не помещается сам baseline или полный startup-пакет,
-pg_doorman пропускает все операторские параметры для этого
-запуска и пишет размеры в лог.
+Перед запуском каждого бэкенда pg_doorman снова проверяет объединённый
+набор параметров по тому же лимиту. Слои, которые помещаются по
+отдельности, могут превысить лимит после объединения: `general + pool`
+может быть слишком большим сам по себе, а строка `auth_query` может
+переполнить уже допустимый базовый набор. Любое превышение теперь
+возвращается клиенту как PostgreSQL-ошибка с `SQLSTATE 53400`; пустой
+или урезанный `StartupMessage` не отправляется. В предупреждении
+записываются размеры в байтах, а
+`pg_doorman_startup_parameters_dropped_total` увеличивается на каждой
+отклонённой попытке запуска бэкенда.
## Что происходит, если PG отвергает параметр
Если PostgreSQL отвергает заданный оператором параметр при запуске
бэкенда, pg_doorman возвращает клиенту `ErrorResponse` PostgreSQL без
-изменений. Клиент видит тот же sqlstate (`22023`, `42704`, `42501`,
-`55P02` или любой другой код из стартового семейства) и то же сообщение,
-что увидел бы при прямом подключении к PostgreSQL.
+изменений. Клиент видит тот же `SQLSTATE` (`22023`, `42704`, `42501`,
+`55P02` или любой другой код, который PostgreSQL вернул при отклонении
+`StartupMessage`) и то же сообщение, что увидел бы при прямом
+подключении к PostgreSQL.
pg_doorman не пробует повторить подключение без отклонённого параметра
и не отключает этот ключ автоматически для пула. Следующее подключение
@@ -153,8 +170,14 @@ admin> SHOW STARTUP_PARAMETERS;
параметра, заданного оператором. Имя параметра и пользователя
пишутся в строку лога уровня `warn`; в лейблы они не включены, чтобы
динамические `auth_query`-пулы не раздували количество серий.
-
-Разумная отправная точка для алерта: если
+- `pg_doorman_startup_parameters_dropped_total{pool, reason}` считает
+ случаи, когда pg_doorman отклонил `startup_parameters` до отправки
+ `StartupMessage`: превышение лимита, неподдерживаемый тип или
+ неверный JSON из `auth_query`, недопустимые ключи или значения, а
+ также пользовательские значения, проигнорированные в выделенном
+ режиме.
+
+Практичное условие для алерта: если
`pg_doorman_backend_startup_parameter_errors_total` растёт по одному и
тому же пулу несколько минут подряд, новые подключения к этому пулу
падают на одном и том же GUC. Конфигурацию нужно исправить до возврата
@@ -177,8 +200,8 @@ admin> SHOW STARTUP_PARAMETERS;
- [Общие настройки](../reference/general.md): `startup_parameters`.
- [Настройки пула](../reference/pool.md):
`pools.