sudo: Access is on, and everyone at g1t.sh is staff
The Access app for sudo.g1t.sh has its g1t staff policy (the owner, and emails at g1t.sh), and the worker checks tokens against its AUD tag and team domain. STAFF_EMAILS takes @domain entries, matched on the whole domain, so a lookalike such as xg1t.sh is not staff.
4 files+38−100/4 viewed
| 35 | 35 | - Application name: `sudo`. | |
| 36 | 36 | - Session duration: short, such as 8 hours. | |
| 37 | 37 | - Public hostname: `sudo.g1t.sh` (path empty, so it covers everything). | |
| 38 | − | 2. **Add a policy:** action *Allow*, include *Emails* → the owner's address | |
| 39 | − | (the same addresses as `STAFF_EMAILS`). Add more staff here *and* in | |
| 40 | − | `STAFF_EMAILS`; either one alone is not enough. | |
| 38 | + | 2. **Add a policy** (`g1t staff`): action *Allow*, include *Emails* → the | |
| 39 | + | owner's address, and *Emails ending in* → `g1t.sh` for everyone with a | |
| 40 | + | g1t address (the same entries as `STAFF_EMAILS`, where a domain is | |
| 41 | + | written `@g1t.sh`). Add more staff here *and* in `STAFF_EMAILS`; either | |
| 42 | + | one alone is not enough. | |
| 41 | 43 | 3. Save, then open the application's **Overview** (or *Basic information*) | |
| 42 | 44 | and copy the **Application Audience (AUD) tag**. | |
| 43 | 45 | 4. Find the **team domain** under **Zero Trust → Settings → Custom pages** | |
| 48 | 50 | "vars": { | |
| 49 | 51 | "ACCESS_TEAM_DOMAIN": "<team>.cloudflareaccess.com", | |
| 50 | 52 | "ACCESS_AUD": "<the AUD tag>", | |
| 51 | − | "STAFF_EMAILS": "syntaqx@gmail.com" | |
| 53 | + | "STAFF_EMAILS": "syntaqx@gmail.com, @g1t.sh" | |
| 52 | 54 | } | |
| 53 | 55 | ``` | |
| 54 | 56 |
| 6 | 6 | authorize, | |
| 7 | 7 | clearKeyCache, | |
| 8 | 8 | isSameOrigin, | |
| 9 | + | isStaff, | |
| 10 | + | parseStaff, | |
| 9 | 11 | readSettings, | |
| 10 | 12 | verifyAccessJwt, | |
| 11 | 13 | } from "./access.ts"; | |
| 204 | 206 | assert.equal(isSameOrigin(post({ origin: "https://sudo.g1t.sh", "sec-fetch-site": "cross-site" })), false); | |
| 205 | 207 | assert.equal(isSameOrigin(post({})), false); | |
| 206 | 208 | }); | |
| 209 | + | ||
| 210 | + | test("everyone at a staff domain is staff, and nobody at a lookalike", () => { | |
| 211 | + | const staff = parseStaff("syntaqx@gmail.com, @g1t.sh"); | |
| 212 | + | assert.deepEqual(staff, ["syntaqx@gmail.com", "@g1t.sh"]); | |
| 213 | + | assert.equal(isStaff("syntaqx@gmail.com", staff), true); | |
| 214 | + | assert.equal(isStaff("ada@g1t.sh", staff), true); | |
| 215 | + | assert.equal(isStaff("ada@xg1t.sh", staff), false); | |
| 216 | + | assert.equal(isStaff("ada@g1t.sh.evil.com", staff), false); | |
| 217 | + | assert.equal(isStaff("someone@gmail.com", staff), false); | |
| 218 | + | assert.equal(isStaff("@g1t.sh", staff), false); | |
| 219 | + | assert.equal(isStaff("a@b@g1t.sh", staff), false); | |
| 220 | + | }); |
| 26 | 26 | teamDomain: string; | |
| 27 | 27 | /** The Access application's Audience (AUD) tag. */ | |
| 28 | 28 | aud: string; | |
| 29 | − | /** Lowercased staff emails. */ | |
| 29 | + | /** Lowercased staff emails, and `@domain` for everyone at a domain. */ | |
| 30 | 30 | staff: string[]; | |
| 31 | 31 | }; | |
| 32 | 32 | ||
| 62 | 62 | return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.cloudflareaccess\.com$/.test(host) ? host : null; | |
| 63 | 63 | } | |
| 64 | 64 | ||
| 65 | + | /** | |
| 66 | + | * The staff list: emails, and `@g1t.sh` for everyone with an address at | |
| 67 | + | * that domain. Access proves the address belongs to whoever signed in. | |
| 68 | + | */ | |
| 65 | 69 | export function parseStaff(raw: string): string[] { | |
| 66 | 70 | return raw | |
| 67 | 71 | .split(/[,\s]+/) | |
| 68 | 72 | .map((email) => email.trim().toLowerCase()) | |
| 69 | − | .filter((email) => /^[^@\s]+@[^@\s]+$/.test(email)); | |
| 73 | + | .filter((email) => /^[^@\s]*@[a-z0-9.-]+\.[a-z]{2,}$/.test(email)); | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | /** Whether a signed-in email is on the staff list: exactly, or by its whole domain. */ | |
| 77 | + | export function isStaff(email: string, staff: string[]): boolean { | |
| 78 | + | const at = email.lastIndexOf("@"); | |
| 79 | + | if (at <= 0 || email.indexOf("@") !== at) return false; | |
| 80 | + | const domain = email.slice(at); | |
| 81 | + | return staff.some((entry) => (entry.startsWith("@") ? entry === domain : entry === email)); | |
| 70 | 82 | } | |
| 71 | 83 | ||
| 72 | 84 | export type AccessClaims = { | |
| 234 | 246 | if (!verified.ok) return verified; | |
| 235 | 247 | const email = typeof verified.claims.email === "string" ? verified.claims.email.trim().toLowerCase() : ""; | |
| 236 | 248 | if (!email) return { ok: false, reason: "token has no email" }; | |
| 237 | − | if (!settings.staff.includes(email)) return { ok: false, reason: "not staff", email }; | |
| 249 | + | if (!isStaff(email, settings.staff)) return { ok: false, reason: "not staff", email }; | |
| 238 | 250 | return { ok: true, email }; | |
| 239 | 251 | } | |
| 240 | 252 |
| 16 | 16 | "services": [{ "binding": "BILLING", "service": "g1t-billing" }], | |
| 17 | 17 | "vars": { | |
| 18 | 18 | // The Zero Trust team domain, such as `g1t.cloudflareaccess.com`. | |
| 19 | − | "ACCESS_TEAM_DOMAIN": "", | |
| 19 | + | "ACCESS_TEAM_DOMAIN": "syntaqx.cloudflareaccess.com", | |
| 20 | 20 | // The Access application's Audience (AUD) tag. | |
| 21 | − | "ACCESS_AUD": "", | |
| 21 | + | "ACCESS_AUD": "5479fcf84130fc7ae68c207ae69a81b2aa17d97714443c6f1dddd058c530b8cf", | |
| 22 | 22 | // Who may use sudo, comma separated. Access lets them in; this | |
| 23 | 23 | // decides again, in case the Access policy is ever widened. | |
| 24 | − | "STAFF_EMAILS": "syntaqx@gmail.com" | |
| 24 | + | "STAFF_EMAILS": "syntaqx@gmail.com, @g1t.sh" | |
| 25 | 25 | }, | |
| 26 | 26 | "observability": { "enabled": true }, | |
| 27 | 27 | "upload_source_maps": true |