From d10efadaba6ea995f7fde2afa1c65fdc549781ef Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 29 May 2026 11:18:28 +0200 Subject: [PATCH] fix(ferretdb): create the Mongo user via DocumentDB to fix SCRAM salt length A plain CREATE ROLE ... PASSWORD stores PostgreSQL's native 16-byte SCRAM salt. DocumentDB has no check_password_hook, so that salt is served verbatim to Mongo clients, which reject it at SASL step2 with 'invalid salt length of 16'. Provision the user through documentdb_api.create_user / update_user instead: DocumentDB builds the SCRAM-SHA-256 verifier with documentdb.scramDefaultSaltLen (28 bytes) via its own scram_build_secret. create_user only accepts a read-only role or the clusterAdmin + readWriteAnyDatabase pair, so use the latter for R/W. - CREATE EXTENSION documentdb now runs before user provisioning so the API functions exist. - Spec built with jq (password JSON-escaped) and passed in a $DDB$ dollar-quoted SQL literal; idempotent via update_user when the role already exists. - Set password_encryption = 'scram-sha-256' explicitly in postgresql.conf. --- ferretdb/README.md | 2 +- ferretdb/install.sh | 48 +++++++++++++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/ferretdb/README.md b/ferretdb/README.md index 3e82e31..c4d15c4 100644 --- a/ferretdb/README.md +++ b/ferretdb/README.md @@ -142,7 +142,7 @@ PostgreSQL role, and the data are left untouched. - **OS**: latest Debian LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale - **Storage engine**: PostgreSQL `17` (PGDG) + the DocumentDB extension (`pg_documentdb`, `pg_cron`), loopback-only on `127.0.0.1:5432` - **Proxy**: official `ferretdb` deb from `github.com/FerretDB/FerretDB`, systemd unit, listening on `0.0.0.0:27017` -- **Auth**: MongoDB client credentials map to PostgreSQL roles; the app role is a superuser so the single homelab user can manage collections and Mongo users (`documentdb.enableUserCrud`) +- **Auth**: the app user is provisioned through `documentdb_api.create_user` (roles `clusterAdmin` + `readWriteAnyDatabase`), **not** a plain `CREATE ROLE`. DocumentDB builds the SCRAM-SHA-256 verifier with its own 28-byte salt (`documentdb.scramDefaultSaltLen`); a native PostgreSQL role would store a 16-byte salt that MongoDB clients reject (`invalid salt length of 16 in sasl step2`). FerretDB connects to PostgreSQL as the same user. - **Network**: FerretDB exposed on `0.0.0.0:27017`; Tailscale runs in the LXC for tailnet reachability (no `serve`) - **Config**: `/etc/postgresql/17/main/conf.d/documentdb.conf` (extension settings) and `/etc/systemd/system/ferretdb.service.d/override.conf` (`FERRETDB_POSTGRESQL_URL`, `FERRETDB_LISTEN_ADDR`) - **Logs**: PostgreSQL via its stock `logrotate`; FerretDB via journald, capped at `SystemMaxUse=200M` diff --git a/ferretdb/install.sh b/ferretdb/install.sh index b01d013..7cd1164 100755 --- a/ferretdb/install.sh +++ b/ferretdb/install.sh @@ -467,6 +467,10 @@ documentdb.enableBypassDocumentValidation = true documentdb.enableUserCrud = true documentdb.maxUserLimit = 100 +# Ensure any password we set is hashed with SCRAM-SHA-256 (PG default, set +# explicitly so it is active before roles are provisioned). +password_encryption = 'scram-sha-256' + # Postgres stays loopback-only; FerretDB (same LXC) is the network front door. listen_addresses = '127.0.0.1' EOF @@ -474,22 +478,36 @@ EOF log_info "Restarting PostgreSQL..." systemctl restart postgresql - log_info "Bootstrapping role '${FERRETDB_USER}' and documentdb extension..." - # The single homelab app role is a superuser so it can manage collections and - # MongoDB users (documentdb.enableUserCrud). FerretDB connects as this role - # and LibreChat authenticates as the same user/password. - local pw_escaped="${FERRETDB_PASSWORD//\'/\'\'}" - su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null </dev/null <<'SQL' CREATE EXTENSION IF NOT EXISTS documentdb CASCADE; +SQL + + # Provision the user THROUGH DocumentDB — never a plain CREATE ROLE ... PASSWORD. + # DocumentDB builds the SCRAM-SHA-256 verifier with its own salt length + # (documentdb.scramDefaultSaltLen = 28 bytes) via documentdb_api.create_user / + # update_user. A native PostgreSQL role instead stores a 16-byte salt, which + # MongoDB clients reject at SASL step2 with "invalid salt length of 16". + # create_user only accepts a read-only role, or the clusterAdmin + + # readWriteAnyDatabase pair we use here for full read/write (FerretDB/DocumentDB + # commands/users.c::ValidateAndObtainUserRole). The spec is built with jq so the + # password is JSON-escaped, then embedded in a $DDB$-dollar-quoted SQL literal. + log_info "Provisioning MongoDB user '${FERRETDB_USER}' via DocumentDB (28-byte SCRAM salt)..." + local role_exists cmd spec + role_exists=$(su -s /bin/sh postgres -c \ + "psql -tAX -d postgres -c \"SELECT 1 FROM pg_roles WHERE rolname = '${FERRETDB_USER}'\"" 2>/dev/null || true) + if [[ "$role_exists" == "1" ]]; then + log_info "Role '${FERRETDB_USER}' already exists — resetting its password via DocumentDB." + cmd="update_user" + spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \ + '{updateUser:$u, pwd:$p}') + else + cmd="create_user" + spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \ + '{createUser:$u, pwd:$p, roles:[{role:"clusterAdmin",db:"admin"},{role:"readWriteAnyDatabase",db:"admin"}]}') + fi + su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null <