# 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-10
- 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.
- Point that read-only connection at a replica where you can.
- Restrict the network path between QueryProxy and the target database.
- On PostgreSQL, check the transport yourself: the connection uses
  `sslmode=prefer` today, and a TLS setting arrives in v0.3.0.

The SQL guard is not a last line of defence. Until read-only execution on the
database side arrives (v0.3.0), build the defence with database privileges 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).
- **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 funneled to enrollment on next login.
- **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.
- Immutable audit logs and login/webhook rate limiting.
- SSRF-guarded outbound webhooks.
- Masked-at-write, fail-closed result storage.

## Next steps

- All environment variables: [Configuration](/docs/configuration/).
- Non-Docker installs: [Manual installation](/docs/manual-installation/).
- Chat callbacks: [Slack integration](/docs/slack-integration/) and
  [Teams integration](/docs/teams-integration/).
