4.5 Network
The network decides who can reach our services and what our services can reach. Ours follows three ideas: traffic from the internet enters only through one protected front door, environments have separate networks that never connect, and the database has no public address at all: only services inside the environment's network can reach it.
Target design and current delivery are different
The public edge below is the target architecture, not an inventory of deployed resources. The 7 November repository baseline has Access delivery and private networking, but no general load-balancer/IAP module. P0/P1 preserve that service while preparing common delivery; the staff edge follows in P2. Client exposure remains closed until its boundary is agreed and tested. See the stage baseline.
4.5.1 One Network per Environment¶
The target is one Shared VPC per cloud environment: a private network in its network project that every project hosting our systems in that environment uses. Each application project gets its own subnet in it. Stage retains network project ID moonitoring-net-stage; new environments follow moonitoring-<env>-net. The table preserves existing stage identifiers. Production is not activated.
| Environment | Network | Subnets |
|---|---|---|
| Prod | prod-vpc 10.10.0.0/16 |
sn-platform 10.10.0.0/24, sn-farm 10.10.1.0/24, sn-ml 10.10.2.0/24, sn-grant 10.10.3.0/24 |
| Stage | stage-vpc 10.20.0.0/16 |
stage-platform 10.20.0.0/24, stage-farm 10.20.1.0/24; Grant deferred |
- Regional subnets and workloads use europe-west8 (Milan); VPC networks and the external application load balancer are global resources.
- The ranges never overlap, and the networks are never peered.
- Firewalls deny all inbound traffic; nothing on the internet can open a connection into a network.
- Each network reserves a range for Cloud SQL's private addresses (Private Service Access): the existing stage range is 10.20.128.0/20, named
stage-private-services; prod targets 10.10.240.0/20. This link to Google's own service network is the only peering a network has. Display-name alignment changes neither this allocation nor the stage subnets or instancesql-stage. - Sandbox projects create their own networks if they need one, and never connect them to anything else.
4.5.2 How Traffic Comes In¶
- A global load balancer is the single entry point of prod and of stage, one per environment, built from the same Terraform module (decided 2026-11-05). Every host name of the environment points to it.
- Cloud Armor filters requests at the edge before they reach a service: rate limits and known attack patterns. In prod only: stage runs without it (decided 2026-11-11, below).
- Identity-Aware Proxy (IAP) stands in front of everything staff use, such as the Console: a request passes only after a Google Workspace sign-in. There is no public login page for staff.
- Cloud CDN caches the update files of
releases. - Human external traffic reaches exposed Cloud Run services through the approved edge. Internal services use an approved internal ingress path and IAM; they do not need to traverse the staff IAP frontend. Cloud Run ingress and IAM must be configured together to prevent an external direct-URL bypass.
Stage targets the same edge patterns as prod (decided 2026-11-05): an environment-specific load balancer, URL map and Google-managed certificates, with CDN where the release-files service requires it. This is a design target, not a claim that every component is deployed. Stage differs in who may come in, has no Cloud Armor and initially uses synthetic test data:
-
Stage runs without Cloud Armor, client hosts included (decided 2026-11-11; the stage projects have a Cloud Armor policy quota of 0 that cannot be raised from the console). What protects stage instead: IAP on every backend service, which infra checks on every plan, and the path allowlist of the URL map: a path that no route of an API-only host declares reaches no backend and is redirected to the canonical host. Stage loses the per-IP rate limit and the 404 at the edge for undeclared paths. Prod keeps Cloud Armor, and the edge module refuses to run without it outside stage, because download links and Account are public there. Cloud Armor is tested in stage once Google grants the quota. Release-file buckets never had Cloud Armor: their gate is the short signed URL.
-
Human staff access is behind IAP: Console API requests enter through its BFF; Dev ORMs have dedicated IAP-protected backends. Internal authorized automations follow their service identity contract.
- The paths clients use are behind IAP too, in stage only (decided 2026-11-10; it replaces the list of testers' addresses): those of the farm sync client, of the Organization Dashboard and the download links of
releases(/downloads/v1). The release files themselves come from the CDN with short signed URLs, as in prod (decided 2026-11-11). A person reaches stage with a Google account that IAP allows, then signs in to the product exactly as in prod, with our ownaccesstokens. Prod never has IAP on a client path: customers are not Google accounts of ours. How an app passes IAP is in Signing in through IAP. - The stage testers are one Google group,
stage-testers@moonitoring.com(decided 2026-11-12), with no role in Google Cloud: IAP admits it, next toinfra-admins, onstage-console,stage-accountandstage-dash(applied 2026-11-13) and, since 2026-11-13, also on the system hostsstage-access,stage-farmandstage-releases(infra change in review). Being let in by IAP grants nothing in the product: a tester still signs in with our own tokens, and using the Status section of the Console also needs a staff account inaccess. - The first stage trial is for internal testers only (product owner, 2026-11-11): IAP lets in only
@moonitoring.comGoogle accounts, the members of the stage testers' group, so the OAuth consent of IAP stays internal to our Workspace and no external Google account is configured. The trial includes the Status section of the Console. Opening stage to external testers is a later decision that changes the OAuth configuration of IAP. - Cost the selected frontend, policies, certificates, traffic and any CDN separately before activation. The earlier €20–25 estimate is not a current quote or a measured stage bill.
Development runs on each developer's computer with Docker (Running Locally), so it has no network, load balancer or SQL instance in the cloud.
Every environment has the same routes on its load balancer:
| Host | Routes to | Who comes in |
|---|---|---|
<system>.moonitoring.com, stage-<system>.moonitoring.com |
the backend of the system: every domain is a path, /<domain>/v1, and /ready, the readiness endpoint the probes of status call (Status). access exposes only /tokens/v1 and /organizations/v1 here (decided 2026-11-11); its staff, audit and webhooks domains are reached only through the servers of the Console and of the Dashboard |
clients with our tokens; in stage additionally through IAP as decided on 2026-11-10, for infra-admins and the stage testers' group (since 2026-11-13) |
status.moonitoring.com, stage-status.moonitoring.com (the status page, the one exception to the row above) |
prod: not this load balancer but Cloudflare Pages, with /api/v1/ routed to status-watchdog (Status); stage: / to the page's Cloud Run service status-server, /api/v1 to status-backend |
prod: anyone; stage: through IAP, for infra-admins and the stage testers' group, on both backends |
console.moonitoring.com, stage-console.moonitoring.com |
/ and /api/<system>/: console-server, serving files and proxying API calls; /devorm/<system>/: the system's separate Dev ORM backend |
staff through IAP, with separate application capabilities; API paths are not routed directly from the load balancer to each system |
dash, moogrant and their stage- names |
/ and /api/<system>/: the app's server, serving the pages and forwarding API calls with the person's token from its session (Signing In from an App, decided 2026-11-11) |
users signed in through the Account app; in stage additionally through IAP as decided on 2026-11-10 |
account, stage-account |
the app's own server, which forwards /api/access/ to access |
users with our tokens; in stage additionally through IAP as decided on 2026-11-10 |
release-files.moonitoring.com, stage-release-files.moonitoring.com (the release files host) |
Cloud CDN on the bucket of releases, with short signed URLs, in prod and in stage alike (decided 2026-11-11), so the update check's chunk_base_url is never empty |
installed clients holding a signed URL that releases minted; no IAP in either environment, the short signed URL is the protection |
releases.moonitoring.com/downloads/v1/, stage-releases.moonitoring.com/downloads/v1/ |
the backend of releases, which answers each download link with a redirect to a signed URL of the release files host |
prod: anyone, without a token, rate limited by Cloud Armor (decided 2026-11-11); stage: through IAP, like every client path |
stage-status serves the status page, not the backend of status (decided 2026-11-13). The page reads /api/v1/summary.json from its own host, so the load balancer sends /api/v1 to status-backend and every other path to status-server; the staff API of status stays reachable only through the Console. In stage a check after each deploy confirms that the page answers: it goes through stage-status.moonitoring.com with an identity IAP admits and loads the page and the summary. There is no continuous uptime check behind IAP.
Stage serves release files from the CDN with short signed URLs, like prod (decided 2026-11-11). Without a CDN in stage the update check had no chunk_base_url and no client could install. IAP is incompatible with Cloud CDN, so the files are not behind IAP: only releases, reached through IAP in stage, mints the signed URLs, they expire within minutes, and the bucket itself stays private. The download links' redirect endpoint downloads/v1 stays behind IAP in stage, like every client path. This is a design to build, not a deployed capability.
Staff ingress contract¶
The existing Console server is the BFF (backend for frontend). The browser calls its own Console origin; console-server validates the IAP assertion on each request, associates the session with that person and exchanges the assertion at Access for a short-lived staff token. The token stays on the server. Normal API forwarding uses the staff token in Authorization and the Console service account's Google token in X-Serverless-Authorization. Untrusted browser headers cannot select a different identity. Console implementation
The CI comes in the same way, with no key (decided 2026-11-13): its Google service account passes IAP on the Console's host with a Google ID token and calls /api/releases/ with the header the Console's app sends, x-moonitoring-console. console-server exchanges that assertion like a person's, and access issues the staff token of the staff service account linked to that Google service account (Service Accounts That Sign In Through IAP). The Builds API of releases stays off the host of releases, so it is never public in prod.
Access validates the IAP assertion at the token-exchange boundary using the audience of Console's actual IAP backend. Its ordinary API methods validate their application tokens and permissions; they do not acquire a second IAP audience simply because Console calls them. A Dev ORM has the audience of its own IAP backend. Native staff applications need a separate verified flow; the web BFF does not certify them. Google: verify IAP identity · Google: service-to-service authentication
The client/internal boundary must be demonstrated before opening client ingress. Cloud Run IAM invocation is a service-level check. If a runtime accepts clients without IAM, that does not create separate IAM protection for its staff or internal methods. Choose and test separate exposure profiles/runtimes or an explicit verified caller boundary; a subnet, host name or freely supplied header is insufficient. The first staff test can use Console → private Access while product client ingress stays closed. Stage client paths go behind IAP as decided on 2026-11-10 (next section); until that edge is deployed and tested, client ingress stays closed.
Signing in through IAP from native and client apps¶
An app passes IAP with a Google ID token, and proves who the user is with our own token; the two never mix (decided 2026-11-10). This is the flow of the desktop Console in every environment and of every client app in stage. In stage, the first desktop client (console-desktop-stage) is admitted by the IAP of stage-console and passed a live test on 2026-11-12; the other apps' clients and the stage client backends below are not deployed yet.
| Credential | Says | Where it travels | Who checks it |
|---|---|---|---|
| Google ID token, or the IAP cookie in a browser | This person may reach this environment | Proxy-Authorization: Bearer from a native app; the cookie from a browser |
IAP, at the edge |
MooNitoring token from access |
Who this is in the product and what they may do | Authorization: Bearer |
Every system |
IAP passes Authorization through untouched when its own credential is in Proxy-Authorization. Being let in by IAP grants nothing in the product: every system keeps verifying its own tokens.
A native app signs the person in to Google itself, in the system browser, as the standard for native apps asks (RFC 8252) and as Google documents for IAP (programmatic authentication):
- The app opens the system browser at Google's sign-in, with its desktop OAuth client, a PKCE challenge, a
stateand anonce, and a redirect to a random port on127.0.0.1. - It exchanges the code it receives for an ID token and a refresh token, checks the ID token, keeps it in memory, and stores the refresh token only in the operating system's credential store.
-
Every request carries the ID token in
Proxy-Authorization. The app renews it before it expires, without the browser; a401from IAP means signing in again, and signing out revokes the refresh token. -
The desktop Console calls the Console's host exactly as the browser does.
console-serverreceives the same IAP assertion and exchanges it ataccess; the desktop app never holds a staff token. - Client apps in stage send both headers: the ID token for IAP and the product's own token. A person who signs in to a client app with their MooNitoring account does it after the Google sign-in, in the browser (Signing In from an App).
- The sync client's background service, in stage, cannot open a browser at every start: the tester signs in once during set-up and the service keeps the refresh token in the machine's credential store. Prod has no IAP on this path, so nothing of the kind exists on a farm's PC.
- Web apps need no code to sign in: IAP's cookie does it. When the IAP session expires, a
fetchreceives401; the app opens/?gcp-iap-mode=DO_SESSION_REFRESHand retries (IAP sessions). IAP's cookie belongs to one host, so each web app calls its APIs through its own host, as the Console does.
The flow is written once: the crate moonitoring-edge-session of moonitoring-common-rs for native apps, and a session-refresh helper in @moonitoring/nuxt for web apps. An app's build profile says what stands in front of its environment, nothing or IAP, so the application code is the same in stage and prod. The installer of releases is the one exception (decided 2026-11-13): its single base file serves every environment, so what stands in front of the environment, and with which desktop OAuth client, comes from the parameters releases writes into each installer file (Installer and Client Library).
Its limits decide where it may be used:
- Only Google accounts can sign in this way; Identity Platform users cannot. It gates staff apps everywhere and every app in stage, never a customer path in prod.
- IAP uses the Google-managed OAuth client; no custom IAP client and no IAP secret outside the Terraform state (decided 2026-11-12, confirmed by a live test on
stage-consolethe same day: a token ofconsole-desktop-stageis refused before it is allowed and passes after, a token for another audience is refused). - Each native app has its own desktop OAuth client per environment (decided 2026-11-11), named
<app>-desktop-<env>(e.g.console-desktop-stage), with Internal consent (only@moonitoring.comaccounts) and the scopesopenid email. Its client ID, which is not secret, goes in the allowlistprogrammatic_clientsof the IAP resources that app calls, in Terraform. Terraform cannot create desktop clients, so an infra-admin creates them by hand following the runbookdocs/edge-desktop-oauth-clients.mdofmoonitoring-infra, which also keeps the register of client IDs. Behind IAP the backend sees the audience of the IAP resource, not which client signed the person in, so a client shared by several apps could not tell them apart. - A desktop client's secret ships inside the app and is not a secret: PKCE protects the exchange. It still never goes in Terraform, state, logs or a repository: infra-admins keep it in their password manager, and the app's build reads it from there or from a Secret Manager container whose value is added by hand. The installer's client of an environment behind IAP (
installer-desktop-stage) is read byreleasesinstead, its ID from its configuration and its secret from that Secret Manager container mounted as a file, and written into the parameters of each installer file of that environment. - Refresh tokens do not work on a resource with an access-level policy; keep such policies off the resources native apps call.
The public landing pages and the static news site at news.moonitoring.com stay on Cloudflare, outside Google Cloud (the backend of news runs in Google Cloud like every other system), with Cloudflare Zero Trust in front of what must not be public. We use each provider where it is strongest; what keeps customer data runs in Google Cloud.
4.5.3 How Traffic Goes Out¶
- Cloud SQL has only a private address inside the environment's network; an organization policy forbids public addresses on any instance. Services connect with TLS and sign in with their own identity (IAM database authentication), so no database password exists.
- A fixed-IP Cloud NAT path is a target where vendor allowlists require it. Stage also needs approved access to authentication/JWKS and Google endpoints. Verify egress for each profile; routing all traffic through the VPC without an available exit can break those calls. Do not create NAT merely from a naming convention or assume stage never needs external requests.
- Calls to Google services such as Cloud Storage and Pub/Sub stay on Google's network (Private Google Access).
4.5.4 Where Data Lives¶
The target primary location for customer data is Europe, with regional resources in Milan. Any additional region, replica or residency exception requires an explicit decision. A future cell model is a growth direction, not an existing Terraform module certified by the current stage implementation.