Skip to content

4.8 Identity & Access

Every action in our systems is done by someone, and must be allowed. Two words carry this page:

  • An identity is who acts: a person or a service.
  • A permission is an action that identity may perform, such as reading a bucket or deploying a service. Permissions are grouped into roles.

Google Cloud checks permissions for everything that runs on it; our job is to define the identities and grant each one the roles it needs, and nothing more. Customers are the exception: their permissions live in our own data model and our systems check them.

4.8.1 People

Human cloud access is assigned through dedicated Google Workspace or Cloud Identity groups. Terraform manages the roles granted to each group on cloud resources; authorized directory administrators manage the people in those groups outside Terraform. Joining or leaving an existing group does not require a Terraform deployment.

Shared/stage access — dated evidence, reconciled 7 November 2026

On 5 November Terraform applied impersonation bindings for infra-admins on foundation, plan-stage and apply-stage, plus scoped observer bindings on the four stage projects. On 6 November Jacopo and Lorenzo were added as members of stage-release-admins; the Access delivery configuration grants that group approval of its stage rollout. The earlier statement that both stage groups were empty describes only the 5 November read. Production and PAM remain inactive; directory restrictions and access through groups without alternative personal grants still require their own checks. See the baseline register, which does not claim a new directory read.

Group email Purpose Boundary and current status
infra-admins@moonitoring.com Trusted infrastructure administrators across environments Impersonate sa-tf-foundation and the plan/apply identities of active environments. Shared/stage initially; production only after explicit activation, within the managed-project boundary. No automatic organization-policy grant.
stage-release-admins@moonitoring.com Stage release approvers Approve application stage rollouts separately from PR review; the Access pipeline grant is conditioned on access-stage. Terraform build approval is a separate activation. No Terraform impersonation through this group.
release-admins@moonitoring.com Production release approvers Reserved for production; no live IAM grant or approval workflow.
stage-observers-admins@moonitoring.com Inspect stage resources and operations Direct, scoped metadata, ordinary-log and monitoring reads in the four stage projects. No Terraform impersonation/state, object contents, secret values, SQL login/query/export, API invocation or resource mutation.
observers-admins@moonitoring.com Production observers Reserved for the corresponding production observation scope; no live IAM grant.
stage-infra-admins@moonitoring.com Unused existing group No grants planned. Keep it unchanged; the single infra-admins group replaces a separate stage administrator group.

Jacopo and Lorenzo are the agreed responsible people for these groups. The 5 November directory read confirmed both as OWNER and MEMBER of infra-admins; the 6 November read confirmed both as MEMBER of stage-release-admins. The last cited observer read, on 5 November, showed no members. stage-infra-admins is unused. The production approver's actual email remains release-admins@moonitoring.com: a lookup of prod-release-admins@moonitoring.com found no group, so a naming proposal must not be used as an IAM principal. The historical ownership and membership observations are recorded in the infrastructure access register. Settings that restrict membership changes and prohibit external members still require verification. Directory ownership, group membership and effective cloud permissions are separate checks.

The observer's SQL role is a narrow custom metadata role because the predefined Cloud SQL Viewer includes backup export permission. Ordinary logs and service configuration can contain application-written data; observer access does not redact that content. Studio and interactive SQL access are separate grants and are not part of this rollout. The infrastructure team-access runbook defines the resource grants, rollout checks and current status.

For each group, name an owner and a backup, record approved membership changes, and follow onboarding and offboarding. Directory membership does not synchronize application roles, capabilities, Organization memberships or entitlements.

Organization break-glass access, developer sandboxes, ML access and security-audit access require their own scoped decisions; the groups above do not grant them automatically.

Resource and IAM changes use reviewed Terraform plans. The stage infrastructure uses the reviewed manual plan/apply path; the application release path has its own approved Cloud Build and Cloud Deploy stages. Neither is enabled automatically by merging the documentation. Cloud Build's approver role is project-wide in the shared CI project, so different group names alone do not enforce separate stage/prod approval boundaries. The Access Cloud Deploy approver grant instead uses a pipeline binding with a target condition. Production approval activation remains blocked until all relevant scopes are resolved. See Infrastructure as Code.

The single infrastructure administrator group has broad write access through impersonation and can operate outside a release pipeline. Membership overlap and those direct permissions mean review and release approval are process controls, not hard separation between author and approver or approval for every Google Cloud operation. Directory membership changes use the separate audited process above.

Access to Prod

The target production policy is no standing access to prod data. The following Privileged Access Manager workflow remains a production design requirement, not a claim that it is deployed:

Grant Gives Lasts Condition
prod-db-access The Dev ORM of one system on that system's prod database, signing in with the person's own Google identity 1 hour A second person approves
prod-debug Logs and viewer in prod 4 hours Granted at once, an alert is sent

4.8.2 Services

The table below names the ten new identities created and verified on 5 November 2026 in stage phase 1. It is not a list of deployed workloads or SQL logins. The earlier four runtime identities and their SQL checks remain recorded in the 4 November stage acceptance record; that record does not certify access for the new identities or databases.

Every service and job runs as its own service account, named sa-<role>, with only the roles its work needs. An API and its migration job have separate identities; future workers also receive their own. Service accounts have no keys: a service gets short-lived tokens from Google Cloud, and pipelines borrow an identity through impersonation. Where SQL access is enabled, services authenticate through IAM; phase 1 grants no new SQL login or access.

System Runtime project ID Initial API identity Migration identity
access moonitoring-platform-stage sa-accounts sa-access-migrate
releases moonitoring-platform-stage sa-release sa-releases-migrate
ai-gateway moonitoring-platform-stage sa-gateway sa-ai-gateway-migrate
farm moonitoring-farm-stage sa-farm-api sa-farm-migrate
moochat moonitoring-farm-stage sa-moochat sa-moochat-migrate
status moonitoring-platform-stage sa-status sa-status-migrate
news moonitoring-platform-stage sa-news sa-news-migrate

Every other component of a system has its own identity, sa-<system>-<component> (e.g. sa-releases-consumer, sa-releases-relay, sa-releases-sweeper, sa-access-sweeper); the API and migration names of the table stay as they are (decided 2026-11-11). Every push subscription and every Cloud Scheduler call has an identity of its own, which infra creates with the link and which may invoke only its target: sa-ps-<key> for a push, sa-sch-<key> for a schedule, the key at most 23 characters (e.g. sa-sch-acc-relay, sa-ps-acc-audit-farm); the receiver admits exactly those identities and checks as audience its fixed URL https://<service>-<project number>.europe-west8.run.app, and a push subscription lives in the consumer's project (decided 2026-11-11).

Apps' servers have their own identities, with no database (decided 2026-11-11): sa-console (Console server, delivery console), sa-account (Account app, account) and sa-dashboard (Organization Dashboard server, dashboard), each in moonitoring-platform-stage and allowed to invoke only the backends it calls. An app's secrets are named <app>-<purpose>, e.g. account-session-password, dashboard-session-key.

These are the selected names for the new per-component identities, not names inferred from the legacy Terraform inventory. The API names sa-accounts, sa-release and sa-gateway retain the earlier target catalog's spelling; sa-moochat and the sa-<system>-migrate names complete this phase's selection. Their databases are hosted in moonitoring-db-stage, not in the projects holding these identities. The project display-name alignment preserves these project IDs and service-account identities.

The existing four legacy runtime identities and the platform database were preserved by phase 1. That provisioning phase alone activated no SQL IAM users, Cloud SQL Client/Instance User grants, PostgreSQL privileges or workload images for the identities above. Access enablement followed separately; the 6 November SQL checks distinguish runtime from migrator and test cross-database denials. Those early checks preceded the application migration and do not alone certify later table privileges. A service-account name confers no database authority.

The follow-up on 5 November provisioned the news database in moonitoring-db-stage and the two identities below. The subsequent Terraform refresh/plan shows no differences; this is a provisioning result, not SQL-access or application acceptance:

System Runtime project ID API identity Migration identity Status
news moonitoring-platform-stage sa-news sa-news-migrate Provisioned; no workload or verified SQL access

Provisioning a database or identity and verifying SQL access are separate milestones. The first SQL baseline is limited to Access, with positive and negative privilege checks before workload activation. The other new system identities, including News, remain dormant; they receive no SQL access through this initial Access enablement.

Console, further workers (including ingestion, parser, resolver and export components) and Dev ORM identities remain separate future deployment work. The static site of news is hosted on Cloudflare; its Google Cloud backend is separate future deployment work, as shown in Where the Systems Run Today. The ML target identities (sa-ml-pipelines, sa-ml-endpoints) and the shared Terraform/build identities remain outside this phase; see Infrastructure as Code for the latter.

Services call their destinations directly using their own Google identity and scoped IAM invocation grants. Access is not a proxy for every internal request. The destination also applies its application policy: which RPCs a caller may use, which replica it may receive, and which user context is required. Being on the subnet or listed among known service accounts does not grant every operation: a service's list of callers is authentication only, and every RPC checks the caller for its own operation (decided 2026-11-11, gRPC Between Services). The Console's server verifies IAP before using a staff token; see the staff ingress contract. Directory access to the Console, staff capabilities in Access, and SQL privileges are separate controls.

4.8.3 Customers

This is the target application authorization flow. Creating cloud access groups does not implement it.

Organization users sign in through access, which uses Identity Platform underneath and signs the short-lived token that every other system verifies. What a user may do is decided in two steps, both checked by the system that receives the request:

  1. The user's role in the Organization the request concerns must include the capability, for example farm.models.use. A direct role counts when that Organization is the token's active Organization; a delegated role counts only when the token's active Organization is the delegation's delegatee, and the two are never added together (Choosing the Organization).
  2. The Organization must hold the entitlement, for example farm.models.mastitis. Through a delegation, the entitlement checked is the acting Organization's, the token's active Organization (decided 2026-11-11): an insurer or a cooperative pays for what it uses on the farms delegated to it, and the farm's own plan neither limits nor pays for that use.

The data behind both is described in Organizations & Access.

A customer signs in on the sign-in page of the Account app (decided 2026-11-10), with e-mail and password or Continue with Google. The page also offers Forgot password and Create an account; Identity Platform handles all four, sends the reset e-mail and verifies the e-mail of a new account before it can be used, so access never sees or keeps a password. A person who signs in for the first time then registers as the Tokens API describes: name, surname and telephone, and the language of the apps and e-mails (RegistrationRequest.locale).

Signing In from an App

Every app signs a person in through the Account app, with the same flow (decided 2026-11-10 for native apps, 2026-11-11 for web apps). The app never shows a password field and never sees the password; the sign-in page is the Account app's, and access issues the tokens as for every other sign-in. This is the OAuth 2.0 authorization-code flow with PKCE (RFC 7636), for native apps as RFC 8252 describes it, with access as its only server. Only the Account app exchanges an Identity Platform sign-in itself; every other app gets its tokens with a code.

A native app, such as the farm sync client or its installer:

  1. The app listens on a random port of 127.0.0.1, makes a PKCE verifier and a state, and opens the system browser at account.moonitoring.com/authorize with the parameters of RFC 6749 and 7636, response_type=code, client_id, redirect_uri, code_challenge, code_challenge_method=S256 and state, plus device_name (the computer's name) and, when a fresh sign-in is needed, prompt=login.
  2. The person signs in there (Identity Platform behind access, as on the web) and sees which app on which computer asks to use their account, with Allow and It's not me.
  3. On Allow, the Account app asks access for a one-time code bound to the app, the loopback address and the challenge (POST /tokens/v1/authorizations) and sends the browser back to the app's loopback address with the code and the state. The page then says the person can go back to the app.
  4. The app checks the state and exchanges the code and its verifier at POST /tokens/v1/tokens (authorization_code grant) for the person's tokens. Its /callback answers the browser with a redirect to account.moonitoring.com/authorize/done?client_id=…, the page that says the person can go back to the app, as Claude Code does; the loopback itself shows nothing.
  5. The token has no active Organization when the person belongs to more than one (decided 2026-11-10), for this grant and for the identity-platform grant alike: the app asks which one, as Choosing the Organization describes, and a refresh with organization_id switches to it. With one Organization the token carries it.

A web app, such as the Organization Dashboard, signs in the same way through its own server (decided 2026-11-11), the backend for frontend, as the Console's server is for staff (Staff ingress contract):

  1. A person who opens the app without a session is sent by the app's server to account.moonitoring.com/authorize with response_type=code, the app's client_id, its redirect_uri, an S256 code_challenge and a state. The server keeps the verifier, the state and the page that was asked for in a short-lived HttpOnly cookie of its own host.
  2. The person signs in on the Account app, or is already signed in there, and goes straight back: our own web apps skip the Allow page, as Google's own apps do, because they are first-party and ask for nothing more than the sign-in itself. The Account app asks access for the code (POST /tokens/v1/authorizations) and sends the browser to the app's /auth/callback with the code and the state.
  3. The server checks the state, exchanges the code and its verifier at POST /tokens/v1/tokens, and keeps the access and refresh tokens in its own session: a cookie of its host that is HttpOnly, Secure and SameSite=Lax, sealed with a key of the app's server, so any instance can serve it. Then it sends the browser to the page that was asked for.
  4. The page never sees a token. It calls /api/<system>/ on its own host; the server forwards each call with the access token in Authorization and renews it with the refresh token before it expires. A call that changes something is accepted only from the app's own origin.
  5. Switching Organization is a refresh with organization_id, made by the server, as for a native app; the person chooses as Choosing the Organization describes.
  6. Sign-out revokes the refresh token (POST /tokens/v1/tokens/revoke) and ends the session; the browser then signs out of the Account app too, so the next sign-in on that computer asks for the password.

  7. Identity Platform proves the password; access decides who the person is in MooNitoring and what they may do. We never keep passwords. Locally, the Firebase Auth emulator stands in for Identity Platform, and access accepts its tokens only with MOONITORING_ENVIRONMENT=local.

  8. Only registered apps can ask, each with a fixed client id and its own redirects; any other redirect is refused, compared exactly, so a code can only reach the app that asked:

    • the native apps, the farm sync client moonitoring-farm-sync and the installer of releases, moonitoring-installer (decided 2026-11-10): the redirect must be http://127.0.0.1:<port>/callback, so a code can only reach the computer where the sign-in started;
    • the web apps, today the Organization Dashboard moonitoring-dashboard and later MooGrant (decided 2026-11-11), each with an exact allowlist per environment: https://dash.moonitoring.com/auth/callback in prod, https://stage-dash.moonitoring.com/auth/callback in stage, and http://localhost:9200/auth/callback only with MOONITORING_ENVIRONMENT=local.

    The code lasts 60 seconds, works once, and only with the verifier of its challenge, for the client and the redirect it was issued for. - A refresh token belongs to one app and never reaches a page: a native app keeps it only in the operating system's credential store of that Windows user, a web app only in its server's session, and each revokes it at sign-out. - A write that needs an admin capability asks for a sign-in at most five minutes old; a read never does, even under an admin capability (decided 2026-11-11). The token carries the time of the sign-in on the Account app; when it is older, the system answers 403 reauthentication-required and the app sends the person back to the Account app with prompt=login, which asks for the password again: a native app in the browser, a web app with a redirect that comes back to the same page. - The installer signs the person in once for both apps (decided 2026-11-10): with the person's token it installs the client and, when the person chooses a farm, connects the computer and leaves a hand-over bundle, so the sync client starts already connected and does not ask the person to sign in (Installer and Client Library). - A native app waits for the browser at most ten minutes (decided 2026-11-10). Then it stops listening on its loopback port, so a late answer reaches nobody, and offers to start again. - "It's not me" ends the attempt: the Account app sends the browser back to the app's loopback address with error=access_denied and the same state, as OAuth 2.0 does; the app checks the state, stops, and says that nobody was connected. No code is issued. - Allow needs a recent sign-in (decided 2026-11-10): when the person signed in to the Account app more than five minutes before, the Allow page of a native app asks for their password again (Identity Platform reauthentication) before it calls POST /tokens/v1/authorizations, so a browser left signed in on a shared computer cannot connect a new app. A web app has no Allow page, so access asks for this recent sign-in for native apps only. - In stage the person signs in twice: with a Google account for Identity-Aware Proxy (Network), then with their MooNitoring account. Prod has only the second.

Choosing the Organization

After signing in, every app asks which Organization the person works in, when they belong to more than one (decided 2026-11-11): the native apps, the web apps and MooChat alike. Who the person acts as is separate from which farm they look at.

  1. The choice offers only the person's own Organizations: those where they hold a direct membership or a role through a user group, the memberships of GET /organizations/v1/me, each with its name, type and status. A delegated membership is never an identity to choose.
  2. The chosen Organization becomes the token's active Organization, with a refresh that names it (organization_id), and each app remembers the last choice and offers it first the next time. With one Organization nothing is asked.
  3. Its type tells the app what to show: a Farm its own herd, a Cooperative, an Insurer or a Professional the farms that share data with it. MooChat, for example, offers a farmer's tools to a Farm and an insurer's to an Insurer; the design of MooChat comes later.
  4. Acting as a delegatee, the person picks the subject of their work. The app lists the farms that delegated to the active Organization, with the delegated role (delegated_organizations of GET /me), and the person picks one; the app then calls that farm's resources with its id in the path. The token stays the delegatee's, and picking another farm changes no token.
  5. Every system applies the same rule to its copy of memberships: a delegated membership counts only when the token's active Organization is the delegation's delegatee. Acting as their own farm a person never uses delegated rights, and acting as an insurer never the rights they hold in their own farm; rights from the two are never mixed.
  6. The audit names all three: the person, the Organization they acted as and the subject, such as "Mario Rossi, for Assicurazioni X, on Cascina Uno" (PlatformAuditEvent).

4.8.4 Audit

The following is the target centralized audit design, not a claim that the sink, locked retention, pgAudit or application audit pipeline has been deployed. Group-membership changes must still have their own directory audit evidence.

Every change leaves a record that the person who made it cannot erase.

flowchart LR admin[Admin Activity logs] access[Data Access logs] orm[Dev ORM statement log] pgaudit[Cloud SQL pgAudit log] sink[Organization log sink] bucket[(Locked bucket<br/>moonitoring-logging<br/>400 days)] bq[BigQuery linked dataset] alerts[Alerts<br/>PAM grants, IAM, sink changes] admin & access & orm & pgaudit --> sink --> bucket --> bq sink --> alerts
  • An organization log sink copies the audit logs of every project into a bucket in moonitoring-logging whose retention is locked for 400 days: not even an administrator can delete them earlier.
  • Writes made through the Console or the admin APIs are also recorded as PlatformAuditEvent rows: who, what, when and which capability was used. Each system publishes its rows on its own topic, <system>.audit.v1, in its own project (decided 2026-11-11): only that system's backend and relay hold the publisher role on it, and access-consumer has one subscription on each system's topic, so the system is known from the subscription. Pub/Sub carries no identity of the publisher, so a topic shared by every system could not prove where an event came from. A Cloud Storage subscription on each audit topic copies the events to the locked bucket.
  • On prod, Cloud SQL's pgAudit records changes to tables, roles and permissions, and every statement run through the Dev ORM, in the same logs.
  • Data Access logs are enabled only where they matter (secrets, the raw bucket, the Dev ORM), because they are billed by volume.