Owner: Operator (Kristerpher) + agent Last updated: 2026-05-03 First execution: 2026-05-02 (CF_ACCESS_SVC_CONSOLE for console-self probe)
You need a CF Access service token when:
console.raxx.app self-probe — needed because the console probes its own /health endpointvault.raxx.app and tickets.raxx.app agent accessDon't use this for human access — that's email-based Access policies, separate flow.
Required:
- CLOUDFLARE_ACCESS_MGMT_TOKEN in Infisical at /MooseQuest/cloudflare/ (alias CF_ACCESS_MGMT). Verify present:
bash
heroku run --app raxx-console-prod --no-tty 'python -c "
from app.services import vault
print(\"OK\" if vault.get_secret_value(\"CLOUDFLARE_ACCESS_MGMT_TOKEN\") else \"MISSING\")
"'
- CLOUDFLARE_ACCOUNT_ID env var on the dyno running the script
Not required (handled in-script): - The new service token's secret never leaves the dyno - The Access app IDs are looked up at runtime
Apps for console.raxx.app (id 0b55d01b-592b-4da4-b170-02d48a9f550d) and console-staging.raxx.app (id fbacf797-9708-40eb-8c70-0ab558e96988) are pre-known from 2026-05-02. For new apps, list them with:
from app.services import vault
import os, requests
mgmt = vault.get_secret_value("CLOUDFLARE_ACCESS_MGMT_TOKEN")
H = {"Authorization": f"Bearer {mgmt}"}
ACCOUNT_ID = os.environ["CLOUDFLARE_ACCOUNT_ID"]
r = requests.get(f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/access/apps",
headers=H, params={"per_page": 50}, timeout=15)
for app in r.json()["result"]:
print(app["id"], app["name"], app.get("domain") or app.get("self_hosted_domains"))
r = requests.post(
f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/access/service_tokens",
headers={"Authorization": f"Bearer {mgmt}", "Content-Type": "application/json"},
json={"name": "<descriptive-name>", "duration": "8760h"}, # 1 year
timeout=15,
)
tok = r.json()["result"]
# tok["client_id"] ends in ".access" — public half
# tok["client_secret"] — visible exactly once; never logged
Critical: create a NEW policy with decision: "non_identity". Do not modify existing user-based policies. Service tokens carry no identity; non_identity is the only decision that matches them.
for app_id in TARGET_APP_IDS:
r = requests.post(
f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/access/apps/{app_id}/policies",
headers={"Authorization": f"Bearer {mgmt}", "Content-Type": "application/json"},
json={
"name": "<service> (service token)",
"decision": "non_identity", # required for service tokens
"include": [{"service_token": {"token_id": tok["id"]}}],
},
timeout=15,
)
assert r.json()["success"], r.json()
If you accidentally create with decision: "allow", the smoke test will 302 to the login page. Service tokens lack identity context that an allow policy demands. Fix:
r = requests.put(
f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/access/apps/{app_id}/policies/{policy_id}",
headers={"Authorization": f"Bearer {mgmt}", "Content-Type": "application/json"},
json={
"name": "<service> (service token)",
"decision": "non_identity",
"include": [{"service_token": {"token_id": tok["id"]}}],
},
timeout=15,
)
The vault expects format client_id:client_secret:
combined = f"{tok['client_id']}:{tok['client_secret']}"
vault.store_secret_version("<NAME>", combined)
Vault path is auto-resolved per _VENDOR_PATH mapping in console/app/services/vault.py — for a CF Access service token, this is /MooseQuest/cloudflare/.
Update _CREDENTIAL_CADENCE_KEY in vault.py if the new token is for a NEW credential (not yet wired). For existing entries (CF_ACCESS_SVC_CONSOLE, CF_ACCESS_SVC_VAULT), no code change needed.
Use the Heroku Platform API (don't shell out to heroku config:set — secrets get echoed). The pattern:
heroku_token = os.environ["HEROKU_API_KEY"]
H = {
"Authorization": f"Bearer {heroku_token}",
"Accept": "application/vnd.heroku+json; version=3",
"Content-Type": "application/json",
}
for app in TARGET_HEROKU_APPS:
r = requests.patch(
f"https://api.heroku.com/apps/{app}/config-vars",
headers=H,
json={"<NAME>": combined},
timeout=20,
)
print(f"{app}: HTTP {r.status_code}")
import time, requests
client_id, client_secret = combined.split(":", 1)
for host in TARGET_HOSTNAMES:
for attempt in range(5):
time.sleep(5)
r = requests.get(f"https://{host}/health",
headers={"CF-Access-Client-Id": client_id,
"CF-Access-Client-Secret": client_secret},
timeout=10, allow_redirects=False)
print(f" attempt {attempt+1}: {host} → HTTP {r.status_code}")
if r.status_code == 200:
break
CF Access policy propagation: 5-30 seconds. First attempt may 302; retry up to 5 times with 5s spacing.
The host sandbox blocks:
- Writing the management token (or new service-token secret) to /tmp/*
- Listing Access apps with the management token (interpreted as scouting beyond the user's stated task)
- Modifying existing CF Access policy decision fields
Run all six steps inside one Heroku one-off dyno (heroku run --app raxx-console-prod --no-tty 'python /dev/stdin' < script.py). The transcript output should be diagnostic only — non-secret IDs, HTTP status codes, success/fail markers.
If the operator wants a from-scratch token provisioning, they say "I authorize
CF_ACCESS_SVC_CONSOLE for the console-self probe, smoke 200 OK on both hosts after the non_identity decision was set/Users/moosequest/repo/TradeMasterAPI/console/app/services/site_probes.py:probe_console_self — consumer of CF_ACCESS_SVC_CONSOLE