tools/pgsql_user_sync/README.md
This operator-managed sample periodically copies eligible PostgreSQL role
verifiers into ProxySQL's pgsql_users table. It never reads plaintext role
passwords. ProxySQL continues to authenticate from its runtime user table;
the scheduler interval plus one successful run is the expected propagation
delay.
The sample owns only rows marked in attributes with
{"proxysql_pgsql_user_sync":{"profile":"..."}}. It does not synchronize
routing, TLS, connection limits, or other ProxySQL policy.
Install the dependencies in a dedicated environment (or into the Python installation used by Scheduler):
python3 -m venv /opt/proxysql-pgsql-user-sync
/opt/proxysql-pgsql-user-sync/bin/pip install -r requirements.txt
install -m 0755 proxysql_pgsql_user_sync.py /usr/share/proxysql/tools/pgsql_user_sync/
Copy proxysql_pgsql_user_sync.ini.example to a regular file, replace both
obvious password placeholders, and restrict it before entering real secrets:
install -o proxysql -g proxysql -m 0600 proxysql_pgsql_user_sync.ini.example /etc/proxysql/pgsql-user-sync.ini
Set [proxysql] to ProxySQL's PostgreSQL Admin interface (normally port
6132). The synchronizer uses PostgreSQL's simple-query protocol there.
The synchronizer rejects group-write/execute and other-user permissions, and
accepts only files owned by root or by the account running it. The primary
example is service-owned 0600 so the ProxySQL service account can read it.
Alternatively, keep the file root-owned and use chown root:proxysql with mode
0640 (a dedicated proxysql group); that is also accepted. Database passwords
stay in this file and never appear in Scheduler arguments or normal logs.
As a trusted PostgreSQL administrator, connect to the configured database and
run create_source_function.sql with a runtime-only reader password:
psql --set=ON_ERROR_STOP=1 --set=proxysql_auth_reader_password='choose-a-secret' \\
--file=create_source_function.sql postgres
The script is rerunnable: it reapplies the reader password, creates a
SECURITY DEFINER function with SET search_path = pg_catalog, and explicitly
revokes defaults before granting only database CONNECT, schema USAGE, and
function EXECUTE.
The proxysql_auth_managed NOLOGIN role is the import allow-list. Grant and
revoke membership deliberately, for example:
GRANT proxysql_auth_managed TO app_login;
REVOKE proxysql_auth_managed FROM app_login;
The function uses a direct allow-list membership check and additionally
requires a login-capable, non-superuser role with a non-null verifier and no
expired rolvaliduntil. Replication roles and service accounts are not
imported unless an operator explicitly places them on the allow-list;
superusers are always excluded.
Always use absolute paths when invoking the script:
/opt/proxysql-pgsql-user-sync/bin/python /usr/share/proxysql/tools/pgsql_user_sync/proxysql_pgsql_user_sync.py \
--config /etc/proxysql/pgsql-user-sync.ini --dry-run --verbose
Remove --dry-run to apply the plan. Useful non-secret overrides are
--default-hostgroup N, --missing-role-action disable|keep,
--save-to-disk/--no-save-to-disk, and --verbose. --dry-run fetches and
validates both snapshots but performs no writes, runtime load, or disk save.
An exclusive lock makes overlapping scheduler invocations a successful skip.
The default lock is in ProxySQL's private data directory; if you override it,
use a service-owned directory. Symlinked configuration and lock files are
rejected.
ProxySQL starts scheduler programs with execve and an empty environment, so
the filename, script, interpreter, and configuration path must all be
absolute. The script's lock file handles overlap. Adapt the ID and paths to
your installation, then run the two load/save statements shown below:
INSERT INTO scheduler
(id, active, interval_ms, filename, arg1, arg2, arg3, comment)
VALUES
(9100, 1, 10000,
'/opt/proxysql-pgsql-user-sync/bin/python',
'/usr/share/proxysql/tools/pgsql_user_sync/proxysql_pgsql_user_sync.py',
'--config',
'/etc/proxysql/pgsql-user-sync.ini',
'Synchronize PostgreSQL role verifiers');
LOAD SCHEDULER TO RUNTIME;
SAVE SCHEDULER TO DISK;
missing_role_action = disable (the default) sets active = 0 for managed
rows absent from the validated source snapshot. keep leaves them alone.
Neither mode deletes rows, and a role that becomes eligible later is updated
and reactivated.
An existing unmanaged username is a conflict and aborts before writes by
default. Set adopt_existing_users = true only after reviewing the row: the
next run marks it for this profile and preserves its other ProxySQL policy.
A row owned by another profile is always a conflict; ownership is never
silently transferred.
The synchronizer builds and validates the complete source snapshot before
writing. It uses the Admin interface's autocommit operations, so the main
table and runtime load are non-transactional: a mid-run write failure can
leave main.pgsql_users partially changed while runtime retains its last
known-good state. No global LOAD PGSQL USERS TO RUNTIME is issued until all
planned writes succeed. The next run compares both tables and repairs the
main/runtime divergence.
LOAD PGSQL USERS TO RUNTIME is global. To avoid overwriting unrelated
runtime state, a detected unmanaged main/runtime divergence aborts before any
write or load. A runtime-load failure is retried on the next run. A disk
save occurs only after a successful load; if it fails, the already-loaded
runtime remains active and the outcome is reported as partial. Set
save_to_disk = false or use --no-save-to-disk when disk persistence is
managed separately.
Successful and skipped runs emit one concise summary containing profile, duration, and discovered/created/updated/reactivated/disabled/unchanged/ conflicted counts. Errors are nonzero except lock contention, which is an expected successful skip. Logs never contain passwords, verifiers, password- bearing connection strings, or complete configuration objects. Verbose mode may name users and action types, but still never credential material.
Choose exactly one of these mutually exclusive models:
pgsql_users cluster propagation.pgsql_users.Do not combine independent writers with cluster propagation: competing loads can cause checksum/epoch churn and race one another.