Files
wakey/docs/origin-auth.md
T

6.4 KiB

Origin Authentication With Caddy

Cloudflare Access protects the operator surface at the edge. An origin reachable directly must also verify Access tokens: a nonempty assertion header is not authentication. A read-only probe through mails.ldlda.com, preserving Wakey's TLS hostname and HTTP host, returned 403 without a header and 200 with a forged header on the same origin IP. Do not retain the old header-presence matcher.

Configure CC

Add this to the deployed CC configuration, using your protected application's settings, not the bypass application's audience:

bind = "127.0.0.1:23501"

[access]
issuer = "https://ldlda.cloudflareaccess.com"
audience = "0c8ca0bce40a48ddc5bc065cea8390eaddd75a84458f147bbe249fcc57c3d781"
# Optional; defaults to the issuer's /cdn-cgi/access/certs endpoint.
# jwks_url = "https://ldlda.cloudflareaccess.com/cdn-cgi/access/certs"

Issuer and key URLs must use HTTPS without user credentials, query or fragment. The issuer is an origin without a trailing slash; it must exactly match the JWT issuer. The audience is not a secret. URLs come only from trusted configuration, never from an incoming token. init-config --from-config preserves [access].

CC requires a loopback bind when [access] is configured. With no [access], the verifier returns 401; existing local development routes remain unchanged. Changes to Access configuration require a daemon restart, not a state reload.

Configure Caddy

Use the Caddy template, replacing the domain and every upstream port with your deployment values. Its protected handler calls CC's GET /internal/auth/verify through forward_auth, then proxies the original request only on success. No additional service or custom Caddy module is needed.

Protect /, /ui* and /api/v1/control/*. This includes operator WebSocket handshakes; established WebSocket frames are not individually authenticated and token expiry does not automatically disconnect an existing WebSocket.

Keep /healthz, /api/v1/agents/enroll, /api/v1/agent/ws and /api/v1/agent/terminals/* outside Access verification. They retain Wakey's own agent/scoped-relay authentication. Cloudflare's bypass application should cover these paths; terminal child paths must be included. If using HTTP ACME challenges, ensure /.well-known/acme-challenge/* is exempt at Cloudflare; Caddy handles its own active challenges, not CC.

Do not proxy /internal/auth/verify publicly and do not put forward_auth in front of the verifier itself. Do not replace verification with a static header that Caddy adds to every request. Caddy owns enforcement: direct access to the CC port would bypass it, so loopback binding and host access controls matter.

Verification And Cache

The verifier accepts only RS256, verifies signatures, and requires matching issuer/audience plus expiry. A future nbf also rejects the token. No clock-skew leeway is applied; keep the host clock synchronized. Duplicate assertion headers and assertions over 16 KiB are rejected. No tokens, claims or upstream bodies are logged.

Responses have no identity payload:

Status Meaning
204 Valid protected-application token
401 Missing/invalid token, unknown key after successful key retrieval, or unconfigured verifier
503 Required signing keys cannot be obtained safely

CC reuses one HTTP client and stores parsed keys with their HTTP cache policy. http-cache-semantics determines freshness from the issuer's Cache-Control, Age, Date and Expires, rather than a built-in key TTL. For example, public, max-age=14400, must-revalidate, no-transform permits four hours of freshness minus the response's existing age; must-revalidate forbids stale reuse, not every fresh reuse. No heuristic lifetime is invented when freshness is unspecified. A no-cache response requires revalidation before reuse; no-store retains neither keys nor policy after serving the fetching request.

Fresh matching keys verify locally. Stale policies produce conditional requests using ETag or Last-Modified when available. A matching 304 renews the policy and reuses parsed keys without downloading/parsing a body; mismatched or unsolicited 304 responses fail closed. An unknown key ID forces conditional revalidation even when the stored response remains fresh, to discover rotation.

Fetches are single-flight and limited to once per minute, including failures. Rotation immediately after a fetch may therefore take up to a minute to become available. If the issuer requires revalidation on every use (or allows no storage), subsequent callers inside that cooldown receive 503 rather than reuse keys contrary to the HTTP policy. Successful 200 refresh replaces the entire set, including removing all keys for a valid empty set. Failed refresh does not extend freshness: matching keys may be used only while the stored policy still permits reuse. There is no indefinitely stale fallback, including on outages.

Fetches have a five-second timeout, no redirects, a 256 KiB body limit and at most 64 keys. These limits are internal safeguards, not new config knobs.

Rollout

  1. Install the new CC binary, add [access], and restart CC. Check the loopback verifier returns 401 for an absent or forged assertion.
  2. Update Caddy from the template and validate its configuration before reload. The repo template uses port 6767; this deployment uses 23501.
  3. Confirm a real Access login can use the UI/control APIs and operator terminal WebSocket. Confirm agent connections and enrollment still work.
  4. Repeat a direct-origin request with Wakey's Host/SNI. Both absent and forged assertions must return 401, not the UI. Verify the internal endpoint itself returns 404 through the public virtual host.

Do not roll back to header-only acceptance on a verification failure. A UI build or a CC restart alone does not update the deployed Caddy policy.

References: Cloudflare token validation and Caddy forward_auth. HTTP policy is provided by http-cache-semantics; only the single-resource storage, bounded fetch and anti-hammering policy are application-owned. No general-purpose disk or Moka response cache is needed.