GCP, from this site's side
Status, from /shipped/: proposed Terraform module secrets-env and the dev environment root · proposed Storage Security Rules deployed per environment · proposed Sign in and out with Google and email/password against the chosen environment · proposed Setup checklist: every per-project resource as a row, with Fix where fixable client-side
The site is static files on GitHub Pages. Everything that remembers anything across devices is in one Google Cloud project per environment: a login service and a bucket. This page is that project seen from the project's own side: what is in it, who set it up, what each request from a browser looks like when it arrives, and what Google can and cannot do with what it holds. All of it is proposed: no project exists yet, and the bootstrap needs a human (needs.md).
What the project is for
Two jobs, and a third that is only tooling. Identity Platform signs users in and hands the browser a token that names them. A Cloud Storage bucket, behind Security Rules, stores each user's encrypted keyring where only that user's token can reach it. Terraform, run by GitHub Actions, creates both and can change or delete them. There is no Cloud Function, no proxy, no API of our own, and the design forbids adding one: a server that touched plaintext would be a party that could be compelled or compromised, and the whole point is that no such party exists.
There is one project per environment (dev, main, prod), each with its own bucket and its own users, chosen at runtime by the browser's configuration (Environments). A customer can run their own project in their own organisation and billing, point the public site at it, and get the same site with their own data.
The same diagram as text
Your browser (this sit Identity Platform The bucket, behind Sec GitHub Actions: Terraf
│ │ Terraform: enable email and Google sign-in, authorised domains
│ │ secrets.sgit.ai and localhost │
│ │◀──────────────────────────────────────────────│
│ │ │ │
│ │ │ Terraform: the bucket (versioned, CORS for
│ │ │ secrets.sgit.ai), and the rules release
│ │ │ from infra/rules/storage.rules
│ │ │◀──────────────────────│
│ │ │ │
│ sign in, in a popup: email and password, or Google │
│──────────────────────▶│ │ │
│ │ │ │
│ an ID token: uid, email, expiry │ │
│◀──────────────────────│ │ │
│ │ │ │
│ GET or PUT users/{uid}/keyring.json with the token; PUT carries x-goog-if-generation-match
│──────────────────────────────────────────────▶│ │
│ │ │ │
│ │ │ ┆ rules: the caller's uid must equal {uid}
│ │ │ ┆ in the path; objects have a size limit;
│ │ │ ┆ nothing else is readable
│ │ │ │
│ ciphertext and the generation number │ │
│◀──────────────────────────────────────────────│ │
│ │ │ │
│ │ ┆ sees: who signed in, when, from where; can disable an account or
│ │ ┆ sign in as anyone, and gets ciphertext for it
│ │ │ │
│ │ │ ┆ sees: object names, sizes, times; can
│ │ │ ┆ delete or roll back; never a plaintext
│ │ │ ┆ byte │
│ │ │ │- Identity Platform
- Google's login service, used from the browser through the vendored Firebase Auth SDK, in a popup. Email and password, or Google sign-in. The result is an ID token the page holds in memory and sends with every bucket request. The login decides which prefix of the bucket the browser may touch and nothing else: an administrator of Identity Platform can disable an account or sign in as anyone, and what they get for it is ciphertext. Identity Platform's own passkey feature is not used; the unlock passkey is a plain WebAuthn credential registered by this page.
- The uid
- The login's stable identifier for a user. It names the user's prefix in the bucket,
users/{uid}/, and it appears in the rules. It is not a secret and it is not the WebAuthn user handle, which is 32 random bytes of its own, so the authenticator learns nothing about the account. - The bucket
- A Cloud Storage for Firebase bucket, one per environment, with uniform access, versioning on, ten noncurrent versions kept and a thirty-day soft-delete, and CORS for
https://secrets.sgit.aionly. The browser reads and writes objects in it directly over HTTPS with the login's token; a write carriesx-goog-if-generation-matchso two devices cannot overwrite each other blindly. What it holds per user:keyring.json(ciphertext, a public salt, wrapped keys) andmeta.json(public identifiers). What it sees: names, sizes, times. What it can do: delete, roll back. What it cannot do: read a plaintext byte. - The Security Rules
- A rules file,
infra/rules/storage.rules, deployed in front of the bucket. A signed-in user may read and write only under their own uid; object sizes are capped; nothing else is readable by anyone through the API. The rules are the whole access control of the MVP, which is why the admin pages show the deployed rules against the file in this repository and why the probe pagestorage.html(proposed) tries to read another uid's path and expects a 403. - Terraform
- The description of everything above, in
infra/terraform/: one module, one root per environment. It enables the services, links the project to Firebase, registers the web app whose public identifiers go intoconfig/environments.json, configures the login, makes the bucket, and releases the rules. GitHub Actions runs it: plan on a pull request, apply on push todevfor the dev environment. It can reconfigure or destroy; it has no path to a plaintext byte, because none exists in the project. - Workload Identity Federation
- How the pipeline becomes someone in GCP without a stored key. A pool and a provider in the project trust GitHub's own token for exactly this repository and these branches, and let it act as the Terraform service account. No service account key exists anywhere: not in the repository, not in a GitHub secret, not on a laptop. The first project and the pool themselves are made once, by a person, with the bootstrap script, because the pipeline that depends on them cannot create them.
What Google sees, and what it cannot
| Google can | Google cannot |
|---|---|
| See who signed in, when, from where; the email on the account | See a plaintext entry, a key, the recovery code, the PRF bytes |
| Sign in as any user and fetch their objects | Open those objects: the PRF secret is in the user's authenticator, bound to this origin |
| Delete a keyring, roll it back to an older version, keep a copy | Alter it undetected: a changed byte fails authentication; a transplanted body fails on its AAD |
| See object sizes and times, and so roughly how much a user keeps and how often it changes | See how many entries, of what kind, or their names |
This is the first row of the table on the security page, from the other side. The row that matters more is the last one there: whoever serves the JavaScript can get everything, which is why that page is mostly about this repository and not about Google.
The admin pages
There is no admin role in the app. /admin/ signs you in to Google with your own account (OAuth, scope cloud-platform, token in memory only) and calls GCP's REST APIs as you: if your account has IAM on the project, the checklist shows each resource as present, enabled, configured, or not, with a Fix where one is possible from a browser. An admin page can list users, deploy rules and change CORS. It cannot open a keyring, because a keyring is ciphertext and an admin's token unlocks nothing.
A customer's own project
You have a GCP organisation and a billing account. Run the bootstrap for one project, add your domain to the authorised domains, run Terraform, and paste the printed configuration into the site's Environment page, or into your fork's config/environments.json. The target is under thirty minutes with no support, and the admin checklist verifies each step. Your users then sign in to your login and store ciphertext in your bucket, from the same public site or your copy of it.
Read next
Environments: how the browser chooses a project. Passkeys: the thing Google never holds. The bootstrap, as commands.