From 64f30138cad24f1ea9659b767d26af03ccb2fb1c Mon Sep 17 00:00:00 2001 From: libops-agent <115990865+libops-agent@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:09:12 +0000 Subject: [PATCH] [minor] Document Google-native publishing domains --- infrastructure/cloud-compose.mdx | 6 +- infrastructure/current-release-status.mdx | 2 +- infrastructure/release-compatibility.mdx | 50 +++--- platform/adoption-model.mdx | 2 +- platform/custom-domains.mdx | 168 +++++++++++++++------ platform/getting-site-ready-to-publish.mdx | 5 +- platform/managed-platform.mdx | 2 +- platform/open-source-adoption.mdx | 2 +- platform/security-operations.mdx | 33 ++-- platform/site-manager-guide.mdx | 2 +- 10 files changed, 168 insertions(+), 104 deletions(-) diff --git a/infrastructure/cloud-compose.mdx b/infrastructure/cloud-compose.mdx index 77a7eff..4dc212e 100644 --- a/infrastructure/cloud-compose.mdx +++ b/infrastructure/cloud-compose.mdx @@ -240,9 +240,9 @@ power-on budget and a 300-second backend-dial budget inside the 600-second Cloud Run request ceiling. Its hosted GCP gate sends one request, verifies the VM reaches `RUNNING`, follows a changed ephemeral external address after a stop/start, then performs host-level and application checks. Those direct-path -budgets do not fit behind non-Enterprise Cloudflare's 120-second read limit; -the managed router uses a separate pre-dispatch retry contract documented in -[Security and Operations](/platform/security-operations). +budgets are intentionally longer than the managed platform's bounded router +request; the managed router uses a separate pre-dispatch retry contract +documented in [Security and Operations](/platform/security-operations). In releases that expose the hardened power-management contract, enabling it requires explicit original-client CIDRs in diff --git a/infrastructure/current-release-status.mdx b/infrastructure/current-release-status.mdx index 6e353bc..af7669b 100644 --- a/infrastructure/current-release-status.mdx +++ b/infrastructure/current-release-status.mdx @@ -38,7 +38,7 @@ The table deliberately does not invent an image digest, template commit, signatu | Integration | Status | Gate that remains | | --- | --- | --- | -| Cloudflare and load balancer to shared Cloud Run router to private per-site PPB | Blocked / preview | PPB `0.5.1` is available, but the exact router image, API/router integration, managed Terraform pins, private-origin, client-IP, authorization-preservation, split timeout, Direct VPC, and rollback canaries still need promotion as one managed set. | +| Cloud DNS and Certificate Manager through the global load balancer, Cloud Armor, shared Cloud Run router, and private per-site PPB | Blocked / preview | PPB `0.5.1` is available, but the exact edge-controller and router images, DNS delegation, certificate-map lifecycle, API/router integration, managed Terraform pins, private-origin, client-IP, authorization-preservation, split timeout, Direct VPC, and hosted canaries still need promotion as one managed set. Cloud CDN remains disabled. | | Organization Vault three-image runtime | Blocked / preview | The verified-publisher desired state is merged, but the public GAR writer migration has not passed a successful central Terraform apply and protected-workflow publication canary; the Vault Init GAR upload still fails closed. Apply and verify that central state, publish and verify the independently versioned `vault-server`, `vault-init`, and `vault-proxy` manifests, release the sitectl-admin digest resolver and exact tag-commit/signature gate, pin all three immutable GAR manifests, and pass the API, Terraform, initialization, recovery, and rollback gates. A source release, local candidate, or reserved version is not an aggregate runtime release. | | Canonical API image set and production VM resolver | Blocked / preview | Merge the hosted post-CI publisher and `sitectl admin terraform api-compose-images`; publish the exact protected-main run to GHCR plus the appropriate private or public GAR repository; verify every digest's reusable-workflow identity, caller repository/ref/SHA, and caller-workflow annotation; and prove fresh VM bootstrap plus in-place refresh with the four verified private-GAR Compose images, the checkout detached at the publication commit, and legacy boot-disk discovery that fails on ambiguity. | | Separate request-serving API and `api-worker` Cloud Run services | Preview | Release the managed worker deployment and prove identity bootstrap, database connectivity, readiness, rollout, rollback, and removal of any temporary migration privilege. A process boundary in source or Compose is not proof of this Cloud Run topology. | diff --git a/infrastructure/release-compatibility.mdx b/infrastructure/release-compatibility.mdx index f2e33f5..9097daa 100644 --- a/infrastructure/release-compatibility.mdx +++ b/infrastructure/release-compatibility.mdx @@ -125,56 +125,42 @@ copy. Images consumed by LibOps GCP runtimes also receive a GAR copy: `api` (also tagged as `dash`), `api-init`, `api-vault-agent`, and `control-plane` use the private API-project repository; `terraform-runner`, `gcp-vm-ip-controller`, `controller-ingress`, and `site-controller` use shared -public GAR; and `cloudflare-ingress-router` uses the separate +public GAR; and `site-router` uses the separate shared-infrastructure GAR repository. The independently owned `vault-server` image is not built or published by the API workflow. -The Terraform runner has a separate provider-mirror build path so one large -provider build does not hide which provider failed or exhaust one native -runner. It gives every pinned Terraform provider its own lane: +The Terraform runner consumes official provider packages through Terraform's +authenticated Registry mirror command. Its dedicated mirror root pins the +providers required by the embedded stacks: | Provider lane | Contents | | --- | --- | | `google` | Google provider | | `google-beta` | Google Beta provider | | `vault` | Vault provider | -| `cloudflare` | Cloudflare provider | | `time` | Time provider | -On a pull request, each lane is built and scanned separately for native -`linux/amd64` and `linux/arm64`. The jobs do not receive package, GAR, or OIDC -publisher authority. They upload one-day, run-ID-scoped mirror artifacts, and -the matching architecture's Terraform-runner job downloads only artifacts -from that workflow run. It validates the exact allowlisted files, executable -modes, ELF architectures, Go toolchain, and security-sensitive dependency -versions before assembling the runner. The completed runner then proves that -each embedded stack can initialize and validate without downloading providers. -These artifacts are an internal handoff between credential-free PR jobs, not -published packages or a release channel. +The committed provider lockfile contains the upstream authenticated checksums +for `linux/amd64` and `linux/arm64`. Pull-request jobs build the complete +Terraform runner for both native architectures without package, GAR, or OIDC +publisher authority, scan it, and prove every embedded stack initializes and +validates with network provider installation disabled. The workflow does not +compile providers, publish intermediate provider images, or replace upstream +binaries to chase dependency scanner findings. Provider fixes arrive through +reviewed upstream releases and lockfile updates. After required CI succeeds for the exact current `main` commit, the protected -publisher builds the same five multi-platform lanes. Their intermediate -images are GHCR-only, scanned and keylessly signed build products named -`terraform-providers-google`, `terraform-providers-google-beta`, and -the corresponding `terraform-providers-vault`, -`terraform-providers-cloudflare`, and `terraform-providers-time` packages. The -workflow uses the stable -`sha-<40-character-commit>-run--provider` selector for those -five intermediates. Before building the final runner, it resolves each -intermediate to an immutable digest, requires exactly the expected amd64 and -arm64 manifests and distinct digests, verifies each keyless signature against -the pinned shared publisher identity, and confirms that the selected commit is -still current `main`. Only those verified digest references enter the final -Terraform-runner build. +publisher builds the same final multi-platform runner directly from the +reviewed Dockerfile and lockfile. There is no provider-image handoff between +jobs or registries. The deployable images, including the final `terraform-runner`, use the `sha-<40-character-commit>-run--attempt-` selector. The runner is scanned, signed, and published to both GHCR and shared public GAR. Deployment resolves every deployable image name independently and -passes only digest references; it does not deploy or pin the intermediate -provider images. These signatures bind the reviewed publisher identity to the -published digests. They are not SLSA provenance or an attestation of the full -build-material graph. +passes only digest references. These signatures bind the reviewed publisher +identity to the published digests. They are not SLSA provenance or an +attestation of the full build-material graph. Before this API publication path can be promoted, the admin plugin must resolve every selected API bundle image and the independently released Vault server to diff --git a/platform/adoption-model.mdx b/platform/adoption-model.mdx index fa12f25..dbc83dd 100644 --- a/platform/adoption-model.mdx +++ b/platform/adoption-model.mdx @@ -162,7 +162,7 @@ must not manage a resource owned by another root. The shared-router/private-PPB integration is a target architecture and is not generally released in the [current status snapshot](/infrastructure/current-release-status). The ownership boundary remains valid, but the linked router identity, canonical client-IP, and timeout details must not be treated as the current production topology until their release gate is complete. -In that target path, controller and site delivery use private network traffic but remain authenticated. Shared project state owns the Cloud Run service identity, network-use permissions, and regional network capacity; runtime state owns instance-scoped power grants and VM firewall rules. The public site path still enters through the declared Cloudflare and load-balancer edge because Direct VPC provides Cloud Run **egress**, not service ingress. See [Security and Operations](/platform/security-operations) for the target router-to-PPB identity, canonical client-IP, subnet, startup, and timeout contracts. +In that target path, controller and site delivery use private network traffic but remain authenticated. Shared project state owns the Cloud Run service identity, network-use permissions, and regional network capacity; runtime state owns instance-scoped power grants and VM firewall rules. The public site path still enters through Cloud DNS, the global external Application Load Balancer, Cloud Armor, and the shared router because Direct VPC provides Cloud Run **egress**, not service ingress. See [Security and Operations](/platform/security-operations) for the target router-to-PPB identity, canonical client-IP, subnet, startup, and timeout contracts. ## Responsibility changes by adoption layer diff --git a/platform/custom-domains.mdx b/platform/custom-domains.mdx index 77921f2..9f4dad4 100644 --- a/platform/custom-domains.mdx +++ b/platform/custom-domains.mdx @@ -1,83 +1,161 @@ --- title: "Custom Domains" -description: "Prepare a public hostname after managed ingress is enabled for a LibOps-managed site" +description: "Verify and publish a customer-owned hostname through the LibOps Google Cloud edge" --- -The target LibOps managed-ingress service can serve customer-owned domains through a shared Cloudflare path. You add the hostname to the site in LibOps, then point it at the LibOps SaaS fallback hostname only after LibOps confirms that the managed ingress release is enabled for that site. +LibOps publishes customer-owned hostnames through one Google Cloud edge. Cloud +DNS provides managed `libops.site` names, Google Certificate Manager issues and +renews public certificates, the global Application Load Balancer terminates +TLS, Cloud Armor applies edge controls, and the LibOps site router selects the +exact active site. + +Adding a domain starts an asynchronous, server-owned workflow. A new domain is +always pending. You cannot set it active, choose an origin, or provide +certificate and edge resource fields. -The shared router and private site-origin path is not generally available in the [July 17, 2026 release-status snapshot](/infrastructure/current-release-status). Creating a domain record in the dashboard or API does not prove that end-to-end routing is active. Do not change production DNS until LibOps support confirms the current ingress path and fallback hostname for your site. +The Google-native shared router and private site-origin path is a target +managed-platform contract until the [current release status](/infrastructure/current-release-status) +names the complete promoted artifact set. Do not change production DNS based +only on a pending domain row. Use the DNS instructions returned for your exact +domain after LibOps reports the route ready. -Do not point customer DNS directly at a site VM, Cloud Run service, or load balancer IP. The public hostname should enter through Cloudflare so edge rules, TLS, rate limits, and the LibOps site router stay in the request path. - -Adding a domain does not make the site public by itself. LibOps sites are IP-allowlisted by default, so only IPs covered by firewall rules can reach them. Before announcing a production hostname, follow [Getting a Site Ready to Publish](/platform/getting-site-ready-to-publish) and add a public production firewall rule when appropriate. +Adding a domain does not make the site public by itself. LibOps sites are +IP-allowlisted by default. Before announcing a production hostname, follow +[Getting a Site Ready to Publish](/platform/getting-site-ready-to-publish) and +add intentional public firewall rules. +Cloud CDN is disabled for this launch. The managed edge provides TLS, +load-balancing, routing, Cloud Armor, and Google infrastructure DDoS +protection; it does not cache application responses or provide offline origin +availability. + ## Before you start You need: -- A LibOps site where you have permission to manage site settings. -- Access to the domain's DNS provider. -- A subdomain such as `collections.example.edu` or `journal.example.org`. -- A publishing plan for production firewall access. -- Confirmation from LibOps support that managed ingress is enabled for this site and that `customers.libops.site` is the correct current DNS target. +- a LibOps site where you can manage settings; +- access to the authoritative DNS for the customer hostname; +- a subdomain such as `collections.example.edu` or `journal.example.org`; and +- a plan for the site's public firewall access. - -Root domains such as `example.edu` require DNS-provider-specific CNAME flattening or ALIAS/ANAME support. Use a subdomain unless LibOps support has confirmed the root-domain plan. - +Subdomains provide the simplest traffic record. A DNS apex such as +`example.edu` cannot use a standards-compliant CNAME. LibOps returns the shared +A and AAAA values for an apex only after the route is ready. + +## Add the domain + +In the dashboard: -## Add a domain in the dashboard +1. Open the organization and project that own the site. +2. Open the site and its **Domains** section. +3. Select **Add domain**. +4. Enter the exact hostname and confirm the target site. +5. Save. -1. Open the LibOps dashboard. -2. Go to the organization and project that contain the site. -3. Open the site. -4. Open the site's Domains or Ingress section. -5. Select **Add domain**. -6. Enter the hostname, for example `collections.example.edu`. -7. Confirm the target site and save. +LibOps normalizes and reserves the hostname, creates a pending binding, and +returns the first DNS instruction. Creating the row does not publish traffic. -After saving, LibOps records the requested domain against the site. An active database status alone does not prove that the edge and origin path has been promoted or can route traffic. Wait for managed-ingress confirmation before changing DNS. +Technical operators can use `sitectl-libops` for the same +create/list/get/check/retry/delete lifecycle. See the +[sitectl domains reference](https://sitectl.libops.io/plugins/libops/domains). -## Add DNS records +## 1. Prove ownership -After LibOps confirms the site's ingress path, point the public hostname at the supplied fallback hostname. For the target architecture documented here, that value is: +LibOps returns a one-time TXT instruction similar to: + +```text +Type: TXT +Name: _libops-verify.collections.example.edu +Value: +TTL: 300 or provider default +``` + +Copy the name and value exactly. LibOps stores only a digest and never +redisplays the proof. If it is lost or expires, rotate it through the domain +workflow. + +Use **Check again** after the TXT record is authoritative. The check observes +DNS and wakes reconciliation; it does not force the domain into a later state. + +## 2. Authorize certificate renewal + +After ownership succeeds, LibOps creates a Google Certificate Manager DNS +authorization and returns Google's exact CNAME instruction: + +```text +Type: CNAME +Name: +Value: +TTL: 300 or provider default +``` + +This validation CNAME must remain in DNS for certificate renewal. If the zone +has restrictive CAA records, allow Google Trust Services with `pki.goog`. + +LibOps waits for the validation record, managed certificate, certificate-map +entry, and exact site route. Missing DNS remains a waiting state. A retry only +resumes a retryable failed reconciliation; it cannot skip validation. + +## 3. Publish traffic + +LibOps returns the traffic record only after TLS and routing are ready. For a +subdomain it is an organization-specific target such as: ```text Type: CNAME Name: collections.example.edu -Value: customers.libops.site +Value: edge.foo.libops.site TTL: 300 or provider default ``` -If the domain is hosted in another Cloudflare zone, create the CNAME as **DNS only**, not proxied. +Always use the value returned for the domain. Do not substitute a VM address, +a Cloud Run `run.app` hostname, a remembered shared hostname, or an address +copied from another organization. + +For an apex, use the exact A and AAAA values returned by LibOps. Do not create +a CNAME at a standards-compliant apex. + +LibOps marks the domain active only after public DNS resolves to the managed +edge and TLS serves the expected hostname. + +## Verify publication -## Manage a domain with sitectl +The domain is ready when: -Technical operators can create, inspect, and remove the same domain resources through the `sitectl-libops` plugin. Current flags and output behavior live in the [sitectl domains reference](https://sitectl.libops.io/plugins/libops/domains). +- LibOps reports `ACTIVE`, not merely pending or route-ready; +- public DNS matches the traffic instruction returned for this binding; +- TLS is valid for the exact customer hostname; +- firewall rules permit the intended visitors; +- the request reaches the intended site; and +- application canonical URL and trusted-host settings use the public hostname. -## Check Routing +For a public production site this normally includes `0.0.0.0/0` and, when IPv6 +is intended, `::/0`. DNS and certificate observations can lag after an +authoritative record changes. Use **Check again** rather than creating a second +domain binding. -The domain is ready only when: +## Delete a domain -- LibOps reports the domain as active. -- LibOps confirms that the site's managed ingress and private origin are promoted and healthy. -- Public DNS resolves the hostname to the LibOps fallback hostname. -- TLS is available at the edge. -- Firewall rules allow the intended visitors. For a public production site, this usually includes `0.0.0.0/0` for IPv4 traffic. -- Requests for the customer hostname are routed to the correct LibOps site. -- The original `Host` header is preserved so the application can identify the public hostname. +Deletion is asynchronous and fail-closed. LibOps blocks routing first, removes +the exact certificate-map entry, certificate, and authorization, and retains +the row while cleanup is in progress. The domain view then identifies the +traffic, validation, and proof records that can be removed. -DNS changes can take time to propagate. If the domain still does not resolve after the status is active, check the authoritative DNS records first. +Do not attach the same hostname to another site until deletion and the hostname +quarantine finish. A later attachment requires a new ownership proof. ## Common issues -| Issue | What to check | +| State or symptom | What to check | | --- | --- | -| Domain is not active | Confirm the domain was created without an explicit pending, suspended, or blocked status. | -| Browser sees the wrong site | Confirm the hostname is attached to the intended LibOps site. | -| Domain resolves but does not load | Confirm the CNAME points to `customers.libops.site`, not a VM IP or Cloud Run hostname. | -| Domain resolves but visitors are blocked | Confirm firewall rules allow the visitor IPs. Public production sites need an intentional public rule such as `0.0.0.0/0`. | -| Cloudflare dashboard shows another proxy layer | If your DNS provider is Cloudflare, make the customer CNAME DNS-only. | +| Waiting for ownership | Query the authoritative TXT name and compare it with the current one-time instruction. | +| Waiting for certificate DNS | Confirm the exact Google CNAME name and target and leave it in place. | +| Certificate issuance failed | Check CAA and allow `pki.goog`; then retry only if LibOps marks the error retryable. | +| Route ready, traffic not active | Publish the exact CNAME or A/AAAA values returned for this binding and wait for authoritative DNS. | +| Browser sees the wrong site | Confirm the hostname is attached to the intended site and that DNS does not contain an older conflicting record. | +| Visitors are blocked | Confirm the production site's firewall rules intentionally allow their IPv4 and, when enabled, IPv6 ranges. | +| Domain is deleting | Wait for server-owned cleanup; do not recreate it or remove validation records before the UI says they are safe to remove. | diff --git a/platform/getting-site-ready-to-publish.mdx b/platform/getting-site-ready-to-publish.mdx index 8261fb5..f8fa3f3 100644 --- a/platform/getting-site-ready-to-publish.mdx +++ b/platform/getting-site-ready-to-publish.mdx @@ -11,7 +11,7 @@ Use this checklist when moving a production site from private review to public a 1. **Confirm the production site**: Make sure you are working with the production site ID, not a development or staging site. 2. **Deploy reviewed code and configuration**: Verify the production deployment has current repository changes, required secrets, and application settings. -3. **Add the custom domain**: Attach the hostname in LibOps and point DNS at `customers.libops.site`. See [Custom Domains](/platform/custom-domains). +3. **Add and verify the custom domain**: Attach the hostname in LibOps, complete the ownership TXT and Google validation CNAME steps, then publish only the traffic record returned after the route is ready. See [Custom Domains](/platform/custom-domains). 4. **Open public firewall access**: Add a site firewall rule for any IPv4 address. Add IPv6 too if your production site should accept IPv6 traffic. 5. **Verify from outside your allowed networks**: Test the public hostname from a network that is not already covered by office or VPN firewall rules. @@ -37,7 +37,8 @@ In the dashboard: ## Final Checks - LibOps shows the expected domain on the production site. -- Public DNS resolves the hostname to `customers.libops.site`. +- LibOps reports the domain `ACTIVE`, and public DNS matches the traffic instruction returned for that exact binding. +- The Google validation CNAME remains in authoritative DNS for certificate renewal. - The production site's firewall rules include `0.0.0.0/0` for public IPv4 traffic. - The site loads from an IP address that is not on your office or VPN allowlist. - Application-level settings, such as canonical URL or trusted host configuration, use the public hostname. diff --git a/platform/managed-platform.mdx b/platform/managed-platform.mdx index 87a335b..5e55065 100644 --- a/platform/managed-platform.mdx +++ b/platform/managed-platform.mdx @@ -5,7 +5,7 @@ description: "How LibOps provides secure compute, single-tenant hosting, and rep LibOps gives institutions a managed platform for operating open-source applications without building a custom cloud operations practice for every project. -The platform combines Google Cloud infrastructure, single-tenant virtual machines, Cloudflare edge security, Vault-backed secret management, GitHub review, and Docker Compose templates. +The platform combines Google Cloud DNS, load balancing, Certificate Manager and Cloud Armor with single-tenant virtual machines, Vault-backed secret management, GitHub review, and Docker Compose templates. **Who this page is for:** institutional leaders, IT directors, and procurement evaluators. For day-to-day operators see the [Site Manager Guide](/platform/site-manager-guide). For developers see the [Engineer Guide](/platform/engineer-guide). diff --git a/platform/open-source-adoption.mdx b/platform/open-source-adoption.mdx index 8a325d3..d1564a4 100644 --- a/platform/open-source-adoption.mdx +++ b/platform/open-source-adoption.mdx @@ -35,7 +35,7 @@ LibOps lets institutions adopt sophisticated open-source applications without fi Dedicated compute boundaries for every workload. - Cloudflare-backed ingress and traffic management. + Google Cloud DNS, managed TLS, load-balancer ingress, and Cloud Armor policy. Vault-backed storage for sensitive configuration. diff --git a/platform/security-operations.mdx b/platform/security-operations.mdx index 45d5a56..8466050 100644 --- a/platform/security-operations.mdx +++ b/platform/security-operations.mdx @@ -3,21 +3,21 @@ title: "Security and Operations" description: "Managed-platform security defaults and the responsibilities retained by self-hosted operators" --- -Security ownership changes with the adoption layer. Cloudflare edge policy, LibOps Vault resources, platform membership, and managed runtime operations apply to LibOps-managed sites. They are not automatically added to a Compose template or a cloud-compose deployment in an institution's account. +Security ownership changes with the adoption layer. Google Cloud edge policy, LibOps Vault resources, platform membership, and managed runtime operations apply to LibOps-managed sites. They are not automatically added to a Compose template or a cloud-compose deployment in an institution's account. For the full self-hosted checklist, including Terraform state, cloud IAM, secret delivery, backups, and restore testing, see [Self-Hosted Operations](/infrastructure/self-hosted-operations). -The shared Cloud Run router and private per-site PPB path below is a **target managed-platform contract**, not a generally available release in the [July 17, 2026 release-status snapshot](/infrastructure/current-release-status). It must not be used as a production migration or DNS runbook until a later status record names the promoted image digests, publisher identities, infrastructure and API releases, and passing end-to-end canaries. +The shared Cloud Run router and private per-site PPB path below is a **target managed-platform contract**, not a generally available release in the [July 17, 2026 release-status snapshot](/infrastructure/current-release-status). It must not be used as a production DNS runbook until a later status record names the promoted image digests, publisher identities, infrastructure and API releases, and passing end-to-end canaries. ## Managed-platform defaults - Managed public sites use LibOps-managed ingress and Cloudflare policy for encrypted traffic, routing, and edge controls. + Managed public sites use Cloud DNS, Google-managed TLS, a global external Application Load Balancer, and Cloud Armor for encrypted traffic, routing, and edge controls. Managed platform secrets are stored and scoped as platform resources rather than committed to repositories or copied into unmanaged environment files. @@ -29,16 +29,16 @@ The shared Cloud Run router and private per-site PPB path below is a **target ma ## Managed ingress target request path -The target design places Cloudflare in front of managed public sites. It has one declared public edge, a source-restricted router boundary, and an IAM-authenticated site origin: +The target design has one Google-managed public edge, a load-balancer-only router boundary, and an IAM-authenticated site origin: ```text Visitor | v -Cloudflare edge +Cloud DNS hostname | v -Global external HTTPS load balancer + Cloud Armor +Global external Application Load Balancer + Cloud Armor + Certificate Manager | v Shared Cloud Run router @@ -50,7 +50,7 @@ Private IAM-authenticated per-site PPB VM private IP and application port ``` -In the target contract, Cloud Armor must admit only Cloudflare's current published IPv4 and IPv6 proxy ranges and deny other sources. The shared router must accept traffic only from the load balancer, and its default `run.app` URL must be disabled so it cannot become an alternate public edge. +In the target contract, Cloud Armor applies the reviewed public customer policy at the load balancer. Customer and model backends use separate policies. Preconfigured WAF and rate-limit rules begin in preview and are enforced individually after false-positive review. The shared router accepts traffic only from the load balancer, and its default `run.app` URL is disabled so it cannot become an alternate public edge. Cloud CDN remains disabled. The target per-site power and proxy backend (PPB) has a different URL decision. Its `run.app` hostname must remain addressable because the shared router uses that hostname as the origin, but it must not be public: Cloud Run invocation is granted only to the router service account. The VM application port remains private and is reached only through the runtime subnet firewall rule. @@ -58,7 +58,7 @@ Self-hosted operators must supply their own DNS, certificate lifecycle, trusted- ### Client IP and request identity -In the target path, Cloudflare supplies `CF-Connecting-IP`. Because the load balancer has already restricted the source to Cloudflare proxy networks, the router may treat that header as eligible input, but it must fail closed unless there is exactly one non-comma, syntactically valid IP value. It must not fall back to `True-Client-IP`, `CF-Connecting-IPv6`, or a caller-supplied `X-Forwarded-For` value. +The load balancer overwrites `X-Libops-Edge-Client-IP` with its authenticated client address and `X-Libops-Edge-SNI` with the TLS SNI hostname. The router accepts exactly one syntactically valid value for each and rejects a normalized SNI/Host mismatch. It ignores caller-supplied vendor headers and the untrusted prefix of `X-Forwarded-For`. The router must overwrite `X-Libops-Client-IP` with that validated address and rebuild the forwarded client headers. Each site PPB must read the single canonical `X-Libops-Client-IP` value at depth `0` and check it against that site's wake-up allowlist. An incoming hostile `X-Forwarded-For` prefix or forged `X-Libops-Client-IP` must not move the selected address. This target depth-zero contract is specific to the router-created canonical header; it is not the direct public PPB `X-Forwarded-For` contract documented for self-hosted cloud-compose. @@ -68,16 +68,16 @@ The router must authenticate to each private PPB with a Google ID token for one | Layer | Owns | Must preserve | | --- | --- | --- | -| Cloudflare | Customer hostname edge, TLS, and platform edge policy | Send traffic to the shared origin and supply the visitor IP header | -| Shared edge Terraform | Global load balancer, Cloud Armor Cloudflare-range policy, router identity, regional router services, and timeout contract | Keep the router load-balancer-only and its image digest pinned | -| Shared router | Hostname lookup, exact Cloudflare visitor-IP validation, canonical header replacement, and origin ID token | Reject ambiguous client identity, preserve application authorization, and route only to validated Cloud Run origins | +| Cloud DNS, Certificate Manager, and load balancer | Authoritative managed names, public TLS, SNI selection, stable addresses, and the declared public edge | Preserve exact DNS ownership, certificate-map readiness, and load-balancer-overwritten identity headers | +| Shared edge Terraform | Global load balancer, Certificate Manager map, Cloud Armor policy, router identity, regional router services, and timeout contract | Keep the router load-balancer-only, Cloud CDN disabled, and image digests pinned | +| Shared router | Exact active-host lookup, SNI/Host validation, canonical header replacement, and origin ID token | Reject ambiguous client identity, preserve application authorization, and route only to validated Cloud Run origins | | Project foundation | Regional network and subnets, Cloud Run service identity, and network-use permissions | Keep Direct VPC capacity and IAM available before runtimes attach | | Runtime state | Per-site PPB and identity, router invoker grant, custom audience, VM start permission, and app-port firewall rule | Scope power permission to the runtime VM and destination traffic to its service account and declared ports | | Compose repository and application | Application ingress settings, trusted-proxy behavior, sessions, and application authorization | Treat forwarded identity as deployment configuration and retain application-level access controls | ## Private VM delivery on Google Cloud -In the target path, the site PPB reaches the application's private VM address through [Direct VPC egress](https://cloud.google.com/run/docs/configuring/vpc-direct-vpc) without a Serverless VPC Access connector. A separate Direct VPC attachment carries authenticated controller requests to the private rollout endpoint. Cloud Run services and jobs do not support Direct VPC ingress: external traffic still enters the declared Cloudflare, load-balancer, and Cloud Run path. Worker-pool ingress is a separate capability that this design does not use. +In the target path, the site PPB reaches the application's private VM address through [Direct VPC egress](https://cloud.google.com/run/docs/configuring/vpc-direct-vpc) without a Serverless VPC Access connector. A separate Direct VPC attachment carries authenticated controller requests to the private rollout endpoint. Cloud Run services and jobs do not support Direct VPC ingress: external traffic still enters through Cloud DNS, the global load balancer, Cloud Armor, and the shared Cloud Run router. Worker-pool ingress is a separate capability that this design does not use. The target deployment sets Cloud Run egress to `PRIVATE_RANGES_ONLY`. Only traffic to private destination ranges uses the VPC attachment; other outbound traffic follows normal Cloud Run egress and does not traverse this VPC or its Cloud NAT. Changing the service to `ALL_TRAFFIC` creates a different routing, NAT, cost, and startup-latency contract and requires a separate review and hosted test. @@ -181,20 +181,19 @@ Cloud Run request ceiling 600 seconds PPB does not replay an application request after a connection succeeds. The hosted direct-path smoke test sends exactly one request, proves that the VM reaches `RUNNING`, follows any changed ephemeral VM address, and verifies the application through the host after the response. -The non-Enterprise Cloudflare path cannot contain that 540-second worst case. Its separate outer budgets are: +The managed router deliberately uses a shorter pre-dispatch budget than the direct PPB path: ```text Bounded route lookup 5 seconds Router origin request 100 seconds Router Cloud Run request 110 seconds -Cloudflare non-Enterprise read limit 120 seconds -5 + 100 < 110 < 120 +5 + 100 < 110 ``` -The router's bounded route lookup must also fit before its origin deadline. On a long cold start, the managed path must return a controlled, retryable failure before application dispatch while wake-up work can continue; it must not claim that the first Cloudflare request always reaches the application. A response such as `503` with `Retry-After` lets the client decide whether retrying its operation is safe. The router and PPB must never replay a request whose application dispatch might have started. Clients must also tolerate occasional connection resets during Cloud Run or network maintenance. +The router's bounded route lookup must fit before its origin deadline. On a long cold start, the managed path returns a controlled, retryable failure before application dispatch while wake-up work can continue; it does not claim that the first request always reaches the application. A response such as `503` with `Retry-After` lets the client decide whether retrying its operation is safe. The router and PPB never replay a request whose application dispatch might have started. Clients must also tolerate occasional connection resets during Cloud Run or network maintenance. -Terraform must reject equality or inversion within each timeout contract. Do not raise the managed outer value above 120 unless the Cloudflare zone has an Enterprise timeout and every router deadline, failure contract, and hosted test is changed together. +Terraform rejects equality or inversion within each timeout contract. Changing the managed budget requires coordinated router, Cloud Run, load-balancer, failure-contract, and hosted-test review. ## Vault-backed secrets diff --git a/platform/site-manager-guide.mdx b/platform/site-manager-guide.mdx index 4781a6a..8335693 100644 --- a/platform/site-manager-guide.mdx +++ b/platform/site-manager-guide.mdx @@ -64,7 +64,7 @@ When you're ready to take a production site public, use the Dashboard to: Confirm the production environment reflects all approved changes. - Add the custom domain and verify the application's hostname settings. + Add the custom domain, complete the DNS proof and certificate instructions, and publish the returned traffic record only after LibOps reports the route ready. Adjust the firewall rules to allow public traffic only after the site is approved for launch.