Migrating to trusted documents
GraphQL only A plain HTTP API has no operations to register; its paths are the allowlist.
With trusted documents, your service runs only the operations you registered, and refuses every other one, however valid. An attacker cannot craft a new query, and you know exactly what runs against your API. Switching it on in one step breaks every client that sends an operation you forgot. This guide gets there in stages, so the switch happens against evidence. How persisted queries work is on persisted queries.
The running example is a web app built with graphql-codegen, which writes
dist/persisted-documents.json, and a mobile app released every two weeks.
What you need
Section titled “What you need”- Clients that send the Apollo-style
extensions.persistedQueryhash; see how a client sends one. - A manifest from your client build, and a CI job that can run
gpilot pq push; see deploy from CI.
1. Push the manifest before every client release
Section titled “1. Push the manifest before every client release”Register the operations of each client build before that build reaches users:
gpilot pq push ./dist/persisted-documents.json --dry-run # parse and verify onlygpilot pq push ./dist/persisted-documents.jsonA push only adds, so the operations of a release still in the field keep working. Every entry is checked: its id must be the SHA-256 of its document, and a mismatch is reported with the one configuration line that fixes it. Formats and flags are on registering your operations.
2. Run in audit mode
Section titled “2. Run in audit mode”Let everything run, and record what is not registered:
[persisted_query]preset = "trusted_migrating"That preset resolves hashes, lets clients register operations through the APQ handshake, and
logs every operation the registry does not hold as PERSISTED_QUERY_UNTRUSTED_OPERATION, with its
hash and operation name. Nothing is refused yet. If clients should not register anything while
you watch, turn the handshake off beside the preset:
[persisted_query]preset = "trusted_migrating"apq = falseDeploy with gpilot deploy.
3. Collect what is missing
Section titled “3. Collect what is missing”Two places show what trusted would refuse:
- The edge codes. Each request that ran an unregistered operation carries
PERSISTED_QUERY_UNTRUSTED_OPERATIONunder Edge codes in its Overview tab in Insights. - The APQ list.
gpilot apq ls, or the APQ tab on the portal’s Queries page, lists the operations clients registered themselves that your manifest does not declare. Read one withgpilot apq show <hash>.
For each one, find the client code that sends it and add it to that client’s manifest, or decide it should stop running. Keep watching until a full release cycle passes with nothing new; for the mobile app, that means until the oldest version still in use has sent everything it sends.
4. Fold it in and clean up
Section titled “4. Fold it in and clean up”Push the updated manifests, then remove what clients registered on their own:
gpilot pq push ./dist/persisted-documents.jsongpilot apq rm --allapq rm revokes those entries and purges everything cached for them.
5. Switch to trusted
Section titled “5. Switch to trusted”[persisted_query]preset = "trusted"trusted resolves hashes, refuses the handshake and refuses anything the registry does not hold.
Deploy.
Check that it works
Section titled “Check that it works”A registered operation still answers:
curl -sS "https://$GRAPHPILOT_HOST/graphql" -H 'content-type: application/json' \ -d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"<a hash from your manifest>"}}}'A query nobody registered is refused with 403 and FORBIDDEN:
curl -sS -o/dev/null -w '%{http_code}\n' "https://$GRAPHPILOT_HOST/graphql" \ -H 'content-type: application/json' -d '{"query":"{ __typename }"}'The refused request’s log line is PERSISTED_QUERY_REFUSED. Every answer the edge can give is on
codes.
Pitfalls
Section titled “Pitfalls”- Cached responses outlive the switch. The allowlist is checked on a miss, not on a hit, so
revoke with
gpilot pq rmorgpilot apq rm, which purge, what must stop at once; see choosing a setup. only_registeredandapqtogether are refused at deploy time. An allowlist anyone can add to is not one.trustedalready hasapqoff; do not turn it back on.- Ad-hoc queries stop too. Your own tooling, a GraphiQL session and a partner’s scripts are refused unless their operations are registered. Put them on a separate service if they need to stay open.
- A client released before its push sends hashes the registry cannot resolve, and under
trustedthat is a403. Push in the same pipeline, before the release. - Subscriptions are not covered. Operations sent over a WebSocket travel inside the tunnel, where the edge never reads them; see subscriptions and WebSockets.
- A client profile can require hashes. A client profile with
persisted_query = "required"refuses the APQ handshake too, so its operations have to be pushed; see telling your clients apart.
Related
Section titled “Related”- Persisted queries: configuration, presets and limits
- Queries in the portal
gpilot pqandgpilot apq- Hardening a public API