A status endpoint that's coarse in public, detailed in private
A public status page is useful, and also a gift to attackers: per-service up/down, latency, internal hostnames and IPs are a live map of your infrastructure. We wanted the health signal without the reconnaissance. Here is the pattern.
The problem
Our headless front end shows a small status strip. It fetched a Drupal /api/system-status endpoint that returned per-service detail to everyone — Postgres, Redis, Solr, the mesh, monitoring, storage, down to an internal LAN IP. An anonymous caller got a complete service inventory and outage map.
The shape
Two audiences, one endpoint:
- Anonymous gets a coarse roll-up:
{ status: "ok" | "degraded", time }. Enough to render a green or red dot. No service names, no latency, no addresses. - An authorized caller gets the full per-service breakdown.
The question is how the front end proves it is authorized without shipping a credential to the browser.
Server-to-server, not browser-to-server
The browser never sees the secret. The front end's own server renders the strip: it fetches Drupal from the server, attaches a shared secret in a header, and passes only the coarse result down to the client. The secret lives in the server's environment, never in a bundle.
// front-end server -> Drupal (secret stays server-side)
fetch(statusUrl, { headers: { "x-status-secret": process.env.STATUS_SECRET } })Drupal unlocks the detailed response when the header matches — compared in constant time — or when the caller holds the admin permission. Everyone else gets the roll-up.
$detailed = $user->hasPermission('administer site configuration')
|| hash_equals($configured, $provided);Details that matter
- Constant-time compare. Use
hash_equals()(orcrypto.timingSafeEqual), never==, so the secret cannot be recovered by timing. - Keep the detailed response private. Mark it no-store and vary on the secret header, so a shared cache never serves detail to an anonymous request.
- Deploy-order-safe. Until the secret is present on both sides, the endpoint returns the coarse roll-up. A header Drupal does not yet accept, or a secret the front end does not yet send, both degrade to public-safe — so the backend, front-end, and infrastructure changes can ship in any order.
- Fail closed. No secret configured means detail is off for everyone, not on.
The result
The public internet sees one honest bit — up or degraded. Operators and the front-end server see everything. The infrastructure map never leaves the trust boundary, and the health signal still works.