Cloud Console Own Projects

This page covers how people get into the console and what each of them may do in a project. It also covers how keys and secrets are kept, and the checks every outgoing call passes.

Apps use project API keys, not sign-in. What an app can do with one is on For apps.

The way in

By invitation only

A session lasts

12 h idle, 7 days at most

In each project

Admin, or 8 permissions

Project secrets

Sealed, shown once

Who gets in

There is no public sign-up and no allow list. An admin invites each person. After that, people sign in with Google or with a passkey. One local account, the break-glass admin, signs in with a password and a code.

An invitation

  1. An admin invites an emailWith a role. A member also gets their permissions per project. Only admins may invite.
    Mail is off: there are no invitations (503).
  2. The console mails a link<console URL>/invite?token=…. The token is 32 random bytes, and only its SHA-256 is kept. The link is valid for 7 days and works once. Admins can list, resend and cancel invitations. A resent link replaces the old one, which stops working.
    An old, used or cancelled link no longer works.
  3. The person signs in with Google from the linkThe token travels in the sealed cookie with the OAuth state. The Google account must have exactly the invited, verified email.
    Another Google account is refused. The invitation stays open for the right one.
  4. The person is madeWith the invited role and permissions. Every step of an invitation is written to the audit log.
    In. From now on, Google lets this person in by their Google user id.

Sign in with Google

  • This is the main way in. It uses the OpenID Connect authorization-code flow with PKCE, a state in a sealed, short-lived cookie, and a nonce.
  • The ID token's issuer, audience, expiry, nonce and email_verified are checked against Google's published keys.
  • Only Google's user id (sub) is kept, never Google's tokens. Google's own two-step protects the account.
  • Google lets in only people who are already here and not disabled. Anyone else is refused as not invited.

The break-glass admin

  • This is the one local account, named by the ADMIN_EMAIL setting. It is a full admin and, for now, the owner's main account. No admin may demote, disable or remove it.
  • It is the only account with a password, and Google never reaches it. Its sign-in sits behind a separate link.
  • The password has at least 16 characters. It is hashed with PBKDF2-SHA-256: a salt per user, 100,000 iterations, over the password peppered with PASS_SALT.
  • Two-step is required. Until an authenticator is enrolled, the right password gets no session, only an enrolment. A code from the new authenticator saves its secret (sealed), shows ten backup codes once, and starts the session.
  • The password and the code are sent together, so no half-signed-in state is kept. Each code works once.
  • Five wrong tries from one address lock that address out for 15 minutes. Fifty from all addresses together lock the account for 15 minutes. A locked account answers exactly as a wrong password does.

Forgot password

For the break-glass admin only.

  • A request answers the same for any address. Only after answering does the console look the address up and mail the link. Neither the answer nor its timing tells whose address it is.
  • The link holds 32 random bytes, of which only the SHA-256 is kept. It is valid for 30 minutes and good for one try. While it is unused and unexpired, another request mails nothing.
  • At most 5 new links an hour for the account, and 10 requests per address in 15 minutes.
  • The new password has 16 characters or more. Once an authenticator is enrolled, setting it also needs a current code or an unused backup code. An email alone never takes the account.
  • A reset signs out every session, removes the account's passkeys, clears the lockout and is audited. It also mails a notice that the password was changed.

Passkeys

  • Any signed-in person may add up to 10 passkeys and sign in with one instead of Google. They use WebAuthn on WebCrypto, with no library.
  • User verification is required, so a passkey sign-in counts as both factors. ES256 and RS256 keys are taken. With attestation set to none, the console trusts the key, not its maker.
  • Adding or removing a passkey needs a session started in the last 15 minutes. Otherwise the answer is 403 reauth.
  • A passkey works only within 30 days of a sign-in with Google or the password. So suspending someone's Google account still ends their access within 30 days.
  • The challenge is 32 random bytes, good for 2 minutes and once. It travels sealed in the __Host-passkey cookie, bound to its use.
  • Checks: the challenge, the console's exact origin, the RP ID, user presence and verification, and at sign-in the signature and the user handle.
  • A signature counter that goes back removes the passkey and mails its owner.
  • Every add and remove is mailed. Signing out everywhere keeps passkeys; a password reset removes them. The break-glass admin keeps its password and authenticator as the backup.

Sessions and CSRF

PartHow it works
Session tokenRandom. Only its HMAC is stored.
CookieA __Host- cookie, HttpOnly, Secure and SameSite=Strict.
How long12 hours idle, 7 days at most.
CSRF tokenThe HMAC of the session token, so nothing more is stored. Pages read it from GET /api/me and send it as X-CSRF-Token with every change (POST, PUT, PATCH and DELETE).
OriginA change sent from another site's page (its Origin) is refused with 403.
Signing outSign out, or sign out on every device. Disabling or removing someone signs them out everywhere. A demoted admin is signed out.
AnswersJSON, never cached (cache-control: no-store), with the security headers.

A change from a console page

Each step runs only if the one before it passed. Beside each step is what happens when that step refuses.

  1. The originA change must come from the console's own pages.
    403 for another site's page.
  2. The sessionThe session cookie, still within its idle and total time.
    401: sign in first.
  3. The CSRF tokenX-CSRF-Token must belong to this session.
    403.
  4. The roleAn admin-only route needs the admin role.
    403.
  5. The person's change budgetThe change counts one of the person's 120 changes this hour.
    429, and nothing is written.
  6. The permission in the projectprojectFor(), and so can(), runs first on every project route. Every lookup is scoped by the project, so an id or name from another project is never reached.
    404 when the person may not read the project; 403 for a missing permission.
  7. The changeThe route runs, and writes its audit line in the same database batch.
    Done.

Roles and permissions

Admin

  • Everything, on every project.
  • Only admins may invite people, manage users (role, permissions, disable, remove), make projects, set a project's daily quota and read the audit log.
  • An admin may make a member an admin, or the other way round. Both are audited.
  • No admin may change their own role, or disable or remove themself. No admin may do any of that to the break-glass admin.

Member

  • Per project, any combination of the eight permissions below, ticked one by one. Any permission includes read.
  • A project a member has no permission on does not exist for them (404). A route they may see but not use answers 403.
  • A grant on all projects (project: "*") covers every project, including ones made later. It adds up with a project's own grant.
PermissionAllowsIn the presets
readSee the project, its queues, tasks, jobs, logs, secret names and service accounts.Viewer, Editor, Manager
createCreate queues, tasks and jobs.Editor, Manager
updateEdit queues and jobs. The project's alerts and quiet hours use it too.Editor, Manager
deleteDelete queues, tasks and jobs, and purge a queue.Manager
pausePause and resume queues and jobs, and switch the project's maintenance on and off.Editor, Manager
runForce a job to run, run a task now, and retry a failed task.Editor, Manager
keysManage the project's API keys.Manager
secretsManage the project's secrets and service accounts, and where each may be sent.Manager

API keys

Secrets and service accounts

Project secrets

Allowed origins

The six auth options for calls

They are the same for tasks and scheduler jobs. A missing auth means none.

OptionWhat the console sendsWhat the target checksWhat it holds
1. NoneNothingNothing (public endpoints)Nothing
2. HMACX-Console-Timestamp, and X-Console-Signature: v1=<hex HMAC-SHA256> over timestamp.METHOD.url.bodyIt works out the HMAC again with the shared secret, and rejects a timestamp older than 5 minutesA signing secret, made by the console and shown once
3. Basic authAuthorization: Basic base64(user:password)Its usual Basic authUsername and password (sealed); https only
4. Bearer tokenAuthorization: Bearer <token>, a fixed token pasted inIts own token checkThe token (sealed); https only
5. OIDC tokenAuthorization: Bearer <JWT>, signed ES256 by the console, valid 5 minutesThe signature against the console's public keys, the issuer, the audience, the expiry and the service account. No shared secret.A service account and an audience
6. OAuth tokenAuthorization: Bearer <access token>, fetched by the console from the provider (client credentials)The provider's own token checkToken URL (https), client ID, client secret (sealed), scope; optional audience or resource

OIDC tokens and the published signing keys

Safety of outgoing calls

Tasks, jobs and uptime checks all make their calls through one function, callTarget. The URL rules run when a task or job is saved, as a field error. They run again at each attempt.

  1. The URLOnly http:// or https://, at most 2,048 characters. No user:password@ and no #fragment.
    400 when saved, at url.
  2. The host namelocalhost, *.localhost, *.local, *.internal, *.home.arpa and the console's own host are refused. So is an IP literal in a private, loopback, link-local, metadata, CGNAT, reserved, documentation or multicast range, or an IPv6 form of one. Examples: 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ::1 and fc00::/7. In IPv6, only global unicast (2000::/3) is called.
    400 when saved. At an attempt, it fails as blocked.
  3. DNS, before each attemptThe host is looked up with DNS over HTTPS, for both A and AAAA records. If any address it gives is refused, the attempt is too.
    The attempt fails as blocked.
  4. The credentialThe auth option's secret or service account must allow the URL's origin, and must not be expired or disabled.
    The attempt fails as auth, and nothing is sent.
  5. The callRedirects are not followed, so a 3xx is a failure. Each attempt has a deadline: 3 minutes by default, and at most 15 minutes.
    No answer in time: the attempt fails as timeout.
  6. The answerAt most 1 MB is read and 1 KB is kept, with credentials redacted first. Only a 2xx succeeds.
    2xx: done. Any other result is retried by the queue's or the job's settings.

A failing target never makes the console raise alerts about itself in a loop. Failures are rows and log lines only.

Header rules

  • Names are letters, digits, - and _, at most 100 characters.
  • Values hold no CR, LF or other control character. A tab is allowed.
  • At most 50 headers, of 8 KB each.
  • No one can set Authorization; secrets come only through an auth option, by name. Nor Host, Content-Length, Transfer-Encoding, Connection, Keep-Alive, TE, Trailer, Upgrade, Expect, Proxy-*, Sec-*, CF-* or X-Console-*.

Size caps

  • A request body: 64 KB, or 256 KB to create a task (larger: 413). It is read as it arrives and refused once it passes the cap.
  • A task's body: 100 KB.
  • A URL: 2,048 characters. A query parameter: 200 characters.
  • Of a target's answer: 1 MB read, 1 KB kept for 7 days.

The pages

default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
require-trusted-types-for 'script';
trusted-types service-worker

Per-person budgets and rate limits

Reads and changes are capped per person per hour, across every project. API keys and sign-in have limits of their own. Past a limit the answer is 429, and nothing is done.

ForWhatLimit
Each personReads of queues and jobs, counted in the rows a page may read50,000 rows an hour
Each personThe object requests behind those reads1,000 an hour
Each personChanges (POST, PUT, PATCH, DELETE), admin routes included120 an hour. Four are free: sign out, sign out everywhere, remove a passkey, revoke a key
Each personA file of tasksOne change per 10 tasks, so at most 1,200 tasks an hour
Each personSettings exports and imports20 an hour
Each projectTest alerts10 an hour
Each projectTask creations, job runs and retries5,000 a UTC day by default; an admin sets it
Each API key/v1 requests600 a minute
Each API keyTask reads, deletes and repeated names600 an hour
Each addressBreak-glass sign-in and enrolment10 tries in 15 minutes
Each addressGoogle's callback; passkey sign-in20 in 15 minutes each
Each addressForgot password; reset10 in 15 minutes each
Each addressThe other open routes: /health, /.well-known/*, GET /auth/options and GET /auth/google120 a minute together

An address is the caller's IPv4 address, or the /64 of its IPv6 address.

Audit log

Every change a person makes writes an audit line in the same database batch as the change. Some changes are held by a queue's or a job's object: a task's create, run, retry or delete, a purge, and a job's run. Their line is written right after the object answers.

Each line holds the time, who acted, the project, the action, its target and a detail of what changed. Who acted is a person's email, key:<name> for an API key, or system. The detail never holds a secret. Lines are kept for 400 days, and only admins may read them.

AreaWhat is recorded
Sign-inSign-ins (auth.sign_in, with method: "passkey" for a passkey), lockouts (auth.locked), reset links (auth.reset_link) and password resets.
Passkeyspasskey.add, passkey.rename, passkey.remove, and passkey.clone_suspected when a counter goes back.
PeopleEach step of an invitation, role changes both ways, access changes, and disabling and removing people.
Keys and secretskey.create, key.rotate, a key's revoke, secret.create, secret.rotate, secret.update and service_account.update.
Queues, tasks, jobsqueue.*, task.*, tasks.create (one line for a file of tasks), tasks.retry_failed, tasks.delete_failed and job.*.
Project settingsalerts.update, alerts.test, quota.set, maintenance.on and maintenance.off (a person's or a key's), quiet_hours.update, summary.update, settings.export and settings.import.
The systemsigning_key.create, signing_key.replace and project.seed.
Not recordedTasks created or deleted through /v1. They are the apps' traffic, and the run logs and the quota count keep them.

Settings and keys

Three settings protect what the console keeps. They are Worker secrets, pushed by the owner's deploy script from its allow-list. wrangler.toml and the repo hold none of them, and tests use made-up values. Only their names and purposes are given here.

NameWhat it doesIts formIf it is lost or changed
SEAL_KEYSeals every sealed column but the authenticator's: signing keys, project secrets, Google Chat webhooks, cached OAuth tokens, and the headers and bodies of jobs and waiting tasks. It also seals the sign-in and invitation cookies, and keys every HMAC (sessions, CSRF, API keys, backup codes). Two keys are derived from it with HKDF.32 random bytes, base64Every session, API key and backup code stops working. Project secrets and webhooks are entered again, and OAuth tokens are fetched again. Jobs and waiting tasks fail until they are saved again. The signing key replaces itself.
MFA_KEYSeals only the two-step secrets: the break-glass admin's authenticator secret and the pending enrolment cookie. It has its own HKDF info, so neither key opens the other's values.A random string of at least 32 charactersEvery authenticator is enrolled again.
PASS_SALTPeppers the break-glass password: PBKDF2 runs over HMAC-SHA-256(PASS_SALT, password).A random string of at least 16 charactersThe stored password no longer matches. The admin sets it again with forgot password.

Cloud Console, as it is on main. 22 Oct 2026.