SECURITY.md
Rama main · réplica de lectura del repositorio original.
# Security Policy
QueryAPIGate executes SQL on behalf of whoever can reach it, so please treat it as a sensitive piece of infrastructure.
This page is an operational checklist; for the reasoning behind each control - what's actually being defended
against, how it works, and where it stops - see [documentation/THREAT_MODEL.md](documentation/THREAT_MODEL.md).
## Reporting a vulnerability
Please **do not open a public issue**. Report it privately through GitHub's
["Report a vulnerability"](https://github.com/AnanthaRajuC/QueryAPIGate/security/advisories/new) form on this repository,
or email arcswdev@gmail.com. You can expect an acknowledgement within a few days.
## Supported versions
Security fixes are made against the latest released version.
## Verify a release
From 0.31, every image (`ghcr.io/anantharajuc/queryapigate`, and its `-h2` variant) and the Helm chart
(`oci://ghcr.io/anantharajuc/charts/queryapigate`) is signed keyless with [cosign](https://docs.sigstore.dev/), by
digest, by this repository's release workflow - and carries SLSA provenance, an SBOM and a GitHub attestation
([ADR 0018](documentation/adr/0018-signed-releases.md)). A signature you can trust names exactly this identity:
```bash
cosign verify ghcr.io/anantharajuc/queryapigate:0.31.0 \
--certificate-identity https://github.com/AnanthaRajuC/QueryAPIGate/.github/workflows/docker.yml@refs/tags/v0.31.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/anantharajuc/queryapigate:0.31.0 --owner AnanthaRajuC
```
The same for the chart, `ghcr.io/anantharajuc/charts/queryapigate:0.31.0`. Anything else - another repository,
another workflow, another tag, or no signature - isn't a release of this project. To have a cluster refuse it, and
to read the SBOM: [guide 59](https://github.com/AnanthaRajuC/QueryAPIGate/blob/main/how-to/59-verify-a-release-before-you-run-it.md).
Releases before 0.31 aren't signed.
### SLSA level
Releases meet **[SLSA](https://slsa.dev/spec/v1.0/levels) Build Level 2**: each artifact is built on a GitHub-hosted
runner by a workflow in this repository, and comes with provenance - which workflow, at which commit, from which
tag - that the build platform signs (GitHub artifact attestations, through Sigstore), so it can't be forged without
that workflow. That holds for:
- **the images and the Helm chart**, from 0.31 (`docker.yml`);
- **the sdist and wheel on PyPI**, from 0.35.1 (`release.yml`). Earlier ones carry only PyPI's own attestations
(PEP 740), which say who uploaded them, not how they were built. To check one:
```bash
pip download queryapigate==0.35.1 --no-deps -d .
gh attestation verify queryapigate-0.35.1-py3-none-any.whl --owner AnanthaRajuC
```
**Not Level 3.** Level 3 asks that the steps doing the build can't influence the provenance. Here the attestation is
made in the same job as the build, so a compromised build step - a poisoned dependency of the build, say - could in
principle shape what gets attested. Building and attesting in a reusable workflow, isolated from the caller, is the
documented way to Level 3; it isn't done yet.
## Hardening checklist for deployments
- Set `QUERYAPIGATE_API_KEY`, and put the service behind TLS (a reverse proxy) - the key is sent in a header. It is the
shared, full-access admin key; give anyone or anything that only needs to run queries a scoped key instead
(`POST /api/v1/api-keys`), limited to specific connections and, separately, to read-only access.
- Give the people (and pipelines) who manage the server their own admin tokens, with the narrowest role that fits -
`queryapigate admins create NAME --role owner` for the first - and then keep the shared key sealed as a break-glass
key, or remove it ([guide 43](https://github.com/AnanthaRajuC/QueryAPIGate/blob/main/how-to/43-give-your-team-their-own-admin-access.md)).
- Leave `QUERYAPIGATE_ALLOW_WRITES` unset unless you really need writes, and connect with a database account that only
has the privileges the API should have. The read-only guard - including the single-statement check it relies on -
is defence in depth, not a substitute for grants; it is dialect-aware (MySQL/ClickHouse honour backslash escapes
in string literals by default, PostgreSQL/SQLite/H2 do not) and is exercised by a fuzz test suite
(`tests/test_sql_guard_fuzz.py`), but a regex-based guard can never be a full SQL parser for every server mode.
- Keep database passwords out of the store: use `"password": "${ENV_VAR}"` references, or set
`QUERYAPIGATE_SECRET_KEY` to encrypt them at rest
([guide 22](https://github.com/AnanthaRajuC/QueryAPIGate/blob/main/how-to/22-encrypt-passwords-at-rest.md)).
- Set `QUERYAPIGATE_SECRET_ENV_VARS` to the variables your connections reference, and `QUERYAPIGATE_METRICS_TOKEN`
wherever `/metrics` can be reached from outside
([guide 58](https://github.com/AnanthaRajuC/QueryAPIGate/blob/main/how-to/58-keep-the-servers-own-secrets-and-metrics-in.md)).
- Run only signed releases ([above](#verify-a-release)), by digest.
- Do not enable `QUERYAPIGATE_DEBUG` on a reachable host.
- Set `QUERYAPIGATE_RATE_LIMIT` on anything reachable beyond a trusted network; it also throttles API key guessing.
- Behind a reverse proxy, set `QUERYAPIGATE_TRUST_PROXY` to the number of proxies so limits apply per real client - and
leave it at `0` when clients connect directly, otherwise they can forge `X-Forwarded-For` to evade the limit.
- With the Rust data plane and Redis, give the data plane its own Redis user that may only count rate limits:
`+evalsha +script|load +time +hmget +hset +expire` on `~qag:ratelimit:*`
([guide 64](https://github.com/AnanthaRajuC/QueryAPIGate/blob/main/how-to/64-try-the-rust-data-plane.md)). Anyone
who can write those keys can reset a caller's budget.
- Only enable `QUERYAPIGATE_CORS_ORIGINS` for sites you control, and never `*` without an API key.
- Keep the query time limit (`QUERYAPIGATE_QUERY_TIMEOUT`, 30 seconds by default) and `QUERYAPIGATE_MAX_PAGE_SIZE` so one
expensive request cannot monopolise the service; setting the timeout to `0` removes that protection.
- Prefer bound `:name` parameters over `{name}` text placeholders.
- Note that `GET /api/v1/connections` reveals hosts, ports, users and database names (passwords are masked).
- Plan for backing up and restoring the store - `queryapigate.db` in `QUERYAPIGATE_HOME`, or the PostgreSQL database
`QUERYAPIGATE_DATABASE_URL` names ([backups and restores](documentation/DEPLOYMENT.md#5-persistent-data-backups-and-restores)).