Skip to content

feat: KEEP-438 add Cloudflare Access header support#63

Merged
suisuss merged 2 commits intomainfrom
feat/KEEP-438-cf-access-headers
May 7, 2026
Merged

feat: KEEP-438 add Cloudflare Access header support#63
suisuss merged 2 commits intomainfrom
feat/KEEP-438-cf-access-headers

Conversation

@suisuss
Copy link
Copy Markdown
Contributor

@suisuss suisuss commented May 7, 2026

Summary

  • Adds Cloudflare Access env-var pass-through so kh can talk to PR previews (app-pr-<N>.keeperhub.com) and staging behind Cloudflare Zero Trust.
  • Reads CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET (service-token pair, sent as CF-Access-Client-Id / CF-Access-Client-Secret) and CF_AUTHORIZATION (cookie minted by cloudflared access login, sent as Cookie: CF_Authorization=...). Env wins over hosts.yml, matching the existing KH_API_KEY > hosts.yml precedence.
  • Fixes three call paths that built their own *http.Client and bypassed the shared khhttp.Client header injection: internal/auth/token.go (fetchSessionInfo, fetchAPIKeyInfo, fetchOrgDetails) and cmd/doctor/doctor.go. Without this, kh auth status and kh doctor would 403 at CF Access even with creds configured.
  • Cookie merge appends to any existing Cookie: value in hosts.yml.headers (RFC 6265 ; separator) instead of clobbering it.

Notes

  • The per-host headers: map in hosts.yml already supported arbitrary headers via the shared client. This PR is just the env-var convenience layer plus the bypass-path fixes.
  • internal/auth/device.go (browser login flow) loops over hosts.yml headers but does not pick up env vars. Left as follow-up since service tokens are CI-only and the affected envs are reached with API keys, not the device flow.
  • The pre-existing API-key validator in fetchAPIKeyInfo accepts any HTTP 200 (so a CF Access HTML login page falsely validates). Independent issue, flagged but not fixed here.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test ./... passes (unit tests for MergeCloudflareAccessEnv cover no-env / service-token / partial-creds-ignored / cookie / cookie-appends-to-existing / env-precedence / service-token+cookie combined / no-mutation)
  • End-to-end against app-staging.keeperhub.com with a real CF_AUTHORIZATION cookie:
    • Without env: kh chain list -> decoding chains response: invalid character '<' (request lands on the CF Access HTML login page)
    • With env: real chain table returned
  • kh doctor against the same host: chains check goes from "reachable" (HTML 200) to "19 chains available" (real JSON) once cookie is set
  • End-to-end against app-pr-1162.keeperhub.com using hosts.yml only (no env): kh chain list, kh workflow list, kh workflow create, kh workflow get, kh auth status, kh doctor all working through CF Access

Usage

One-time setup per gated environment

Either path works; pick one.

A) hosts.yml (recommended for everyday work)

# ~/.config/kh/hosts.yml
hosts:
    app-pr-1234.keeperhub.com:
        token: kh_<api-key-from-the-PR-env-UI>
        headers:
            CF-Access-Client-Id: <id>.access
            CF-Access-Client-Secret: <secret>
            # OR (for user-JWT auth via cloudflared):
            # Cookie: CF_Authorization=<jwt>

Then just run kh --host app-pr-1234.keeperhub.com workflow list (or export KH_HOST=app-pr-1234.keeperhub.com).

B) Environment variables (recommended for CI / one-offs)

# Service-token pair (both must be set; partial values ignored)
export CF_ACCESS_CLIENT_ID=<id>.access
export CF_ACCESS_CLIENT_SECRET=<secret>

# OR a cookie from cloudflared
export CF_AUTHORIZATION=$(cloudflared access token --app=https://app-pr-1234.keeperhub.com)

kh --host app-pr-1234.keeperhub.com workflow list

Env vars override hosts.yml on key collision; non-CF headers in hosts.yml are preserved.

Cookie auth via cloudflared (when service tokens aren't authorized for the env)

# One-time, opens a browser tab; JWT is cached at ~/.cloudflared/<host>-<aud>-token (24h validity)
cloudflared access login https://app-pr-1234.keeperhub.com

# Read the cached JWT into env or hosts.yml
CF_AUTHORIZATION=$(cloudflared access token --app=https://app-pr-1234.keeperhub.com) kh ...

When kh against the env starts returning invalid character '<' (response is the CF Access HTML login page), the JWT has expired - re-run cloudflared access login.

Precedence

Same model as KH_API_KEY > hosts.yml:

  1. Env vars (CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET, CF_AUTHORIZATION) win on key collision.
  2. hosts.yml headers: entries that don't collide are preserved.
  3. Authorization: Bearer <kh_token> is set independently via token: / KH_API_KEY and is never overwritten by header merging.

Caveats

  • A service token must be authorized by the specific app's CF Access policy; a token scoped only to production will be rejected (service_token_status: false in the redirect JWT meta) by per-PR CF Access apps even though the headers reach CF.
  • cloudflared access token returns the cached JWT unconditionally and does not auto-refresh; once expired, you must re-run access login.
  • Cookie env merge appends to any existing Cookie: in hosts.yml.headers rather than overwriting, so users mixing CF auth with other cookies don't lose them.

suisuss added 2 commits May 7, 2026 14:23
…ments

Lets the CLI talk to PR previews and staging hosts behind Cloudflare Zero
Trust. Reads CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET (service tokens)
and CF_AUTHORIZATION (cookie minted by `cloudflared access login`) from
the environment, merged on top of any per-host headers in hosts.yml. Env
wins over hosts.yml, matching the KH_API_KEY > hosts.yml precedence model.

Also fixes the auth-token validation and doctor health-check paths, which
built their own *http.Client and bypassed the shared khhttp.Client header
injection -- so without these changes any kh command that lands on those
paths would 403 at CF Access even with creds configured.
…ad of clobbering

Concatenate `CF_Authorization=<jwt>` onto any pre-existing `Cookie:` value
in hosts.yml.headers using the `;` separator from RFC 6265, instead of
overwriting it. Adds tests for the append path and for the combined
service-token + cookie env case.

Caught in PR review.
@suisuss suisuss merged commit c5f1326 into main May 7, 2026
4 checks passed
@suisuss suisuss deleted the feat/KEEP-438-cf-access-headers branch May 7, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant