# Production hardening

> A checklist for running QueryProxy on a reachable network, from the application key and HTTPS to read-only connections and what the defaults already cover.

- Documentation: Guides
- Updated: 2026-10-31
- Source: https://queryproxy.com/docs/production-hardening/
- Language: en-US
- Author: Muhammet Şafak

---
QueryProxy stores the credentials to every database it fronts, so treat the
instance itself as sensitive. Work through this list before exposing it to a
network.

## Recommended production setup

- For teams or users who only read, register a separate connection with a
  database user that has `SELECT` privileges only. Keep write access on a
  different connection, since approved writes run through the connection they
  were submitted on. On SQL Server, which has no read-only session, give the
  connection a separate **read username and password** with `SELECT` only.
- Point that read-only connection at a replica where you can.
- Restrict the network path between QueryProxy and the target database.
- Require TLS on every network connection and verify the server certificate
  (`verify-ca` or `verify-full` on PostgreSQL, "Verify the server certificate"
  on MySQL and MariaDB, `TrustServerCertificate` off on SQL Server).
  TLS is not enforced on connections created before v0.3.0 until you edit them;
  `queryproxy:doctor` lists them.

The SQL guard is not a last line of defence. Reads run in a read-only session on
the database side, but the privileges of the database user remain the last line,
so build the defence with them as well.

## Checklist

- **Set `APP_KEY` explicitly** and keep it stable. It encrypts connection
  credentials and chat secrets; changing it makes every stored credential
  unreadable. Generate one with
  `docker run --rm queryproxy/queryproxy php artisan key:generate --show` and
  pass it as `-e APP_KEY=...` (every container of the same instance must share
  it). When rotating, move the old key into `APP_PREVIOUS_KEYS` so existing
  ciphertext still decrypts. Without an explicit `APP_KEY`, the container
  generates one on first boot and persists it to the data volume.
- **Serve over HTTPS** behind a TLS-terminating reverse proxy (the container
  serves plain HTTP on `:7432`). Then set `SESSION_SECURE_COOKIE=true`.
- **Set `TRUSTED_PROXIES` to your proxy's IP or CIDR** whenever a proxy sits in
  front of QueryProxy. No proxy is trusted by default, so until you set it the
  app reads the connecting peer as the client: `X-Forwarded-Proto` is ignored
  (HTTPS is not detected, so no HSTS header) and audit entries record the
  proxy's address. Never set it to `*`: that lets any client forge
  `X-Forwarded-For`, which both fakes the audit trail's client IP and hands out
  unlimited login and 2FA attempts, since those throttles are keyed per IP. See
  [Configuration](/docs/configuration/#behind-a-reverse-proxy).
- **Supply secrets as files** where your platform provides them: set
  `APP_KEY_FILE`, `DB_PASSWORD_FILE` and the other `*_FILE` variables instead of
  putting values in the environment. See
  [Configuration](/docs/configuration/#secrets-from-files).
- **Keep demo seeding off** (`QUERYPROXY_SEED_DEMO=false`, the default) on any
  reachable instance.
- **Require 2FA** for privileged roles with `QUERYPROXY_REQUIRE_2FA=admins`
  (or `dba` / `all`). Users are funnelled to enrolment on next login.
- **If you use single sign-on, enforce MFA at the identity provider.** Users who
  sign in through SSO are outside `QUERYPROXY_REQUIRE_2FA`; QueryProxy cannot see
  which second factor the provider used. Connect only a provider that verifies
  e-mail addresses (QueryProxy links and creates accounts only when the provider
  asserts a verified address: `email_verified`, or `xms_edov` for Entra ID), and consider `QUERYPROXY_LOCAL_LOGIN=admins_only` so
  that a password stays available to system administrators only, for break-glass
  access. Signing out of QueryProxy does not sign the user out of the provider. See
  [Single sign-on](/docs/sso-oidc/).
- **Treat API tokens as credentials.** Give each token only the scopes it needs,
  and keep `approvals:write` for the few tokens that must decide requests: a leaked
  reviewer token can approve other users' requests. Lower
  `QUERYPROXY_API_TOKEN_MAX_DAYS` (default `90`) to the longest lifetime you accept,
  and revoke tokens of people who change roles. A token cannot approve its owner's
  own request or take an override. Token creation and revocation are audited. See
  [REST API and tokens](/docs/api/).
- **On Kubernetes, turn the NetworkPolicy on** (`networkPolicy.enabled`) and list
  your ingress controller in `networkPolicy.ingressFrom`, so only it reaches port
  7432 (your network plugin must enforce NetworkPolicy). List the egress you need with
  `networkPolicy.egress.restrict=true`. Use your own Secret
  (`secrets.existingSecret`) rather than the chart-managed one. See
  [Kubernetes, Helm and high availability](/docs/kubernetes/).
- **Prefer time-bound grants** for access that is not permanent: a DBA can grant a
  connection for a period and at the `read` level. See
  [Approval workflow](/docs/approval-workflow/#time-bound-grants-and-access-requests).
- **Keep a copy of the audit log outside the database.** Turn on a SIEM sink
  (`QUERYPROXY_SIEM_SINKS`) so that someone with database access cannot rewrite
  history unseen. See [Audit chain and SIEM export](/docs/audit-export/).
- **Protect `/metrics`** with a token or an address allowlist, or leave it off
  (the default).
- **Run the doctor** after every deploy: `php artisan queryproxy:doctor
  --strict`. See [Troubleshooting](/docs/troubleshooting/).
- **Back up the data and the `APP_KEY` separately.** See
  [Backup and restore](/docs/backup-restore/).
- **Restrict connection targets** with `QUERYPROXY_CONNECTION_HOST_DENYLIST` /
  `QUERYPROXY_SQLITE_ALLOWED_DIR` if DBAs should not reach arbitrary hosts.

## What is on by default

No configuration is needed for these:

- Security response headers, including a Content-Security-Policy, and a rate
  limit on the Livewire endpoint.
- Encrypted connection credentials at rest and TOTP two-factor authentication
  (optional until you require it).
- Comment-normalized SQL guards with an executor-enforced row ceiling.
- Replay-protected, identity-bound chat callbacks, and self-approval prevention.
- A tamper-evident audit log (hash chain and database triggers), with the SQL
  shown redacted to Auditors and in chat, and login/webhook rate limiting.
- SSRF-guarded outbound webhooks.
- API tokens that always expire, are stored as a hash and stop working when their
  owner is suspended or deleted.
- Masked-at-write, fail-closed result storage.

## Next steps

- All environment variables: [Configuration](/docs/configuration/).
- Who and what QueryProxy defends against: [Threat model](/docs/threat-model/).
- Non-Docker installs: [Manual installation](/docs/manual-installation/).
- Chat callbacks: [Slack integration](/docs/slack-integration/) and
  [Teams integration](/docs/teams-integration/).
