# Setting up GitHub Action secrets — least-privilege edition The CHAINSTATE deploy workflow needs three secrets in your GitHub repository. This document gives you the *minimum-scope* version of each — a leaked secret here cannot touch anything outside this one project. All three are added the same way: > **GitHub repo** → **Settings** → **Secrets and variables** → **Actions** → **New repository secret** Use the exact secret name from the table; the workflow looks them up by these names verbatim. --- ## Before you create anything · two one-time dashboard steps These are not secrets — they're account-level setup that has to exist once, ever. ### A · Pick your `workers.dev` subdomain (one time, ever) 1. Go to https://dash.cloudflare.com → **Workers & Pages** 2. If this is your first Worker, Cloudflare prompts you to pick a subdomain (e.g. `redciprianpater`). 3. After this, your CHAINSTATE worker URL will be: `https://chainstate-worker..workers.dev` You will never repeat this step. It's a one-time account choice. ### B · Create the three KV namespaces locally (one time, ever) The CF API token below cannot create namespaces for you — you must create them locally first, then commit their IDs into `wrangler.toml`. On your laptop: ```bash # install wrangler if you haven't: npm install --global wrangler # log in (opens a browser, one-time): wrangler login # create the three namespaces: wrangler kv:namespace create CHAINSTATE_NODES wrangler kv:namespace create CHAINSTATE_CACHE wrangler kv:namespace create CHAINSTATE_CONSENSUS ``` Each command prints something like: ``` 🌀 Creating namespace with title "chainstate-worker-CHAINSTATE_NODES" ✨ Success! Add the following to your configuration file in your kv_namespaces array: { binding = "CHAINSTATE_NODES", id = "a1b2c3d4e5f6789012345678901234ab" } ``` Take the `id` from each and paste into the matching block in `wrangler.toml`, replacing the `REPLACE_ME_AFTER_kv_namespace_create_*` placeholders. Commit and push. --- ## Summary table | Secret name | Source | Scope | |------------------|-----------------------------------------------------------------------------------------------|------------------------------------------------| | `CF_API_TOKEN` | https://dash.cloudflare.com/profile/api-tokens · **Custom Token**, *not* the template | Only Workers Scripts + KV (no zones, no D1, no R2) | | `CF_ACCOUNT_ID` | https://dash.cloudflare.com — right sidebar on any zone overview | (Not secret in the cryptographic sense; just an identifier) | | `HF_TOKEN` | https://huggingface.co/settings/tokens · **Fine-grained**, scoped to one Space | Write to `CPater/chainstate` only | --- ## 1 · `CF_API_TOKEN` — least-privilege Custom Token The "Edit Cloudflare Workers" template grants more than you need (it includes zone routes, tail logs, R2 read, etc). For CHAINSTATE you only need two account-level permissions. Build a **Custom Token** instead. 1. Sign in at https://dash.cloudflare.com → **My Profile** → **API Tokens** 2. Click **Create Token**, then scroll to the bottom and click **"Get started" under "Create Custom Token"** (do NOT use the template). 3. Token name: `chainstate-deploy` 4. **Permissions** section — add exactly these two rows and nothing else: | Scope | Resource | Action | |----------|-----------------------|----------| | Account | Workers Scripts | **Edit** | | Account | Workers KV Storage | **Edit** | 5. **Account Resources**: - Choose **"Include"** → **"Specific account"** → select **the single account** that owns your `workers.dev` subdomain. Do **not** select "All accounts." 6. **Zone Resources**: - Leave **empty** (or set to "All zones from an account" if Cloudflare insists). You do not bind a custom domain in `wrangler.toml`, so zone permissions are unnecessary. 7. **Client IP Address Filtering** (optional but recommended): - Restrict to GitHub Actions IP ranges if you want belt-and-braces. The list is published at https://api.github.com/meta (the `actions` array). Many people skip this because the list changes; a 1-year TTL plus quick revocation is usually enough. 8. **TTL**: set to **1 year** so the token is force-rotated annually. 9. Click **Continue to summary** → **Create Token** → **copy immediately** (CF only shows it once). 10. Add to GitHub: name = `CF_API_TOKEN`, value = the token. **What this token can do**: - Upload, modify, or delete Worker scripts in your account - Read, write, or delete KV namespace entries **What this token cannot do**: - Touch DNS, Pages, R2, D1, queues, durable objects, zones, certificates, accounts, billing, or any other Cloudflare product - Affect any of your other (non-Workers) services ### Even-tighter variant: dedicated CF account If you want zero blast radius, create a *separate* Cloudflare account just for CHAINSTATE. Cloudflare lets you create multiple accounts on the same email (use sub-addressing like `you+chainstate@example.com`). Add the Custom Token within that account, and the token cannot reach any of your other Cloudflare work even by account compromise. --- ## 2 · `CF_ACCOUNT_ID` — just an identifier This is the 32-hex Cloudflare account ID. It is not a secret in the cryptographic sense — it doesn't grant any access on its own. But it has to match the account the `CF_API_TOKEN` was created under, so it goes alongside it. 1. https://dash.cloudflare.com → click into any zone (any domain you own; or **Workers & Pages** if you have no domains) 2. Right sidebar: **Account ID** — 32 lowercase hex chars 3. Copy and add to GitHub: name = `CF_ACCOUNT_ID`, value = the hex string --- ## 3 · `HF_TOKEN` — fine-grained, scoped to one Space A "Write" role token grants write access to every repo and Space on your HuggingFace account. Use a **fine-grained token** scoped to just `CPater/chainstate`: 1. Sign in at https://huggingface.co 2. **Settings** → **Access Tokens** (or https://huggingface.co/settings/tokens) 3. Click **+ Create new token** 4. **Token type**: select **"Fine-grained"** (do NOT select Read or Write). 5. **Token name**: `chainstate-deploy` 6. **Token expiration**: set to **1 year** to force rotation. 7. Scroll to **"Repositories permissions"**: - Click **"Add a repository"** - Repository: `CPater/chainstate` - Type: **Space** (not Model, not Dataset) - Permission: **Write contents** (this is the only one you need; do not check "Manage repository settings") 8. **Leave every other section unchecked**: - User permissions: none - Organization permissions: none - Inference endpoints: none - Billing: none 9. Click **Create token** → copy the `hf_...` value immediately. 10. Add to GitHub: name = `HF_TOKEN`, value = the `hf_...` token. **What this token can do**: - Write files into `CPater/chainstate` Space repo **What this token cannot do**: - Touch any of your other Spaces, models, or datasets - Read or write your account profile - Bill anything on your account - Read private repos you own - Use inference endpoints --- ## Verifying the secrets are wired correctly After all three are added, push a no-op commit to `main`: ```bash git commit --allow-empty -m "trigger first deploy" git push origin main ``` In the GitHub Actions tab you should see two jobs: - **`cf-worker`** — succeeds when wrangler deploys the Worker. The job log prints the Worker URL near the end (`Worker is uploaded… https://chainstate-worker..workers.dev`). If you see "permission denied on KV namespace", the IDs in `wrangler.toml` are wrong — re-create with `wrangler kv:namespace list` and update. - **`hf-space`** — succeeds when `huggingface_hub.upload_folder` finishes. Last line is `HF Space updated ✓`. If you see "Token is unauthorized", the token is missing the repo binding — fix in step 3.7 above. Then test the Worker directly: ```bash curl https://chainstate-worker..workers.dev/status curl -X POST https://chainstate-worker..workers.dev/query \ -H 'Content-Type: application/json' \ -d '{"query": "∫∂x → ?", "swarmSize": 20, "consensusDepth": 3}' ``` Verify the HF Space at `https://cpater-chainstate.static.hf.space/`. The Query/Terminal/SCAN pages will use the Worker once you wire its URL into the Space — easiest way is to add this script tag to `index.html` (somewhere before the closing ``): ```html ``` Or set it temporarily via the browser console: ```js window.__CHAINSTATE_WORKER = "https://chainstate-worker..workers.dev" ``` --- ## Triggering deploys The workflow ships on **three** triggers: 1. **Push to `main`** — every commit on the main branch deploys. Useful for fast iteration. 2. **GitHub Release** — publish a release (e.g. `v0.1.0`) and the workflow runs. Useful for tagging stable deploys. 3. **Manual dispatch** — go to GitHub repo → **Actions** → **deploy** → **Run workflow**. Useful when you want to retry without a commit. To publish a release: ```bash git tag v0.1.0 git push origin v0.1.0 ``` Then in GitHub: **Releases** → **Draft new release** → choose `v0.1.0` → **Publish release**. The workflow fires automatically. --- ## Rotating secrets CF tokens have no built-in expiry by default but we set a 1-year TTL above. HF fine-grained tokens also have an explicit expiry. To rotate: 1. Create a new token following the steps above 2. **GitHub repo** → **Settings → Secrets → Actions** → click the existing secret → **Update** 3. Paste the new value, save 4. Delete the old token at the source (CF dashboard / HF tokens page) There is no need to re-run any workflow; the next push or release will use the new value. --- ## Security notes - These secrets are **repository-scoped** in GitHub — they're available to workflow jobs in this one repo only. They are not exposed to forks or PRs from forks. - The values are encrypted at rest in GitHub and decrypted only inside the runner VM. Any line containing the literal value is masked as `***` in logs. - **Anyone with push access to `main`** can effectively use these secrets via a malicious workflow change. Protect `main` with required reviews / branch protection if you have collaborators. - Both Cloudflare and HuggingFace let you revoke a token immediately if it leaks; you should not rely on git history scrubbing if a token was committed accidentally — **revoke first**, then clean. - If you ever suspect a token is compromised: revoke at source (CF or HF dashboard), then generate a new one and update the GitHub secret. The workflow uses the new value on the very next run.