Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions infrastructure/cloud-compose.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ larger right-edge suffix appended by trusted intermediaries before its larger
depth is accepted. Treat this as a wake-up filter, not as a replacement for
application authentication or a controlled load balancer/Cloud Armor policy.

The LibOps managed platform's depth-zero setting is a separate contract. Its
authenticated shared router replaces any incoming canonical client-IP header,
and the private PPB reads that single value at depth zero; it does not trust the
incoming `X-Forwarded-For` chain. See [Security and
Operations](/platform/security-operations) for that managed request path.

Google recommends an egress-aware startup probe for ordinary Direct VPC
services. The power-button proxy cannot use application reachability as its
startup gate because the application VM is intentionally allowed to be off;
Expand Down
18 changes: 15 additions & 3 deletions infrastructure/release-compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ Publish and consume changes in this order:
| 3 | Core `sitectl` | CLI and plugin SDK contract tests pass; package artifacts are published |
| 4 | Application plugins | Each plugin is built against the required core; rollout and component tests pass |
| 5 | Dependent plugins | Included plugins are published first; Drupal precedes ISLE when ISLE requires the newer Drupal contract |
| 6 | Reusable Terraform dependencies and support images | Required module commits are merged; support images such as PPB are released by immutable digest; validation, mock and image tests, and release CI pass |
| 6 | Reusable Terraform dependencies and support images | Required module commits are merged; support images such as PPB are released by immutable digest and signed by the expected publisher identity; validation, mock and image tests, and release CI pass |
| 7 | `cloud-compose` | The referenced Terraform modules, support-image digests, sitectl package set, and templates are available; provider, bootstrap, and hosted smoke tests pass |
| 8 | Managed API and control plane | Its pinned infrastructure contract and support images resolve; Go, Terraform contract, mock-plan, and hosted managed-path checks pass |
| 8 | Managed API and control plane | Its pinned infrastructure contract and support images resolve; managed images come from CI-passed commits and have verified signatures; Go, Terraform contract, mock-plan, and hosted managed-path checks pass |
| 9 | Public documentation | Every referenced image, package, template, Terraform dependency, cloud-compose tag, and managed integration is live; generated command references are pinned to the released commit set; docs validation and link checks pass |

A layer may be unchanged in a release. The ordering still matters whenever its contract changes.
Expand All @@ -30,6 +30,18 @@ dependency. When it adopts a new core SDK helper, update and test it only after
that core release is live, and publish its contributor documentation after the
scaffold branch is merged.

## Managed artifact promotion gates

A digest fixes image bytes, but it does not prove which repository or workflow produced them. Managed request-path images add a provenance gate to the dependency order:

1. Pull-request CI builds, tests, and scans the proposed image without production publisher credentials.
2. A publisher runs only after the repository's required CI succeeds for the exact commit on the protected primary branch. It publishes that commit's image, records its digest, and signs the digest with short-lived workload identity.
3. Before changing Terraform, the release operator verifies the signature against the expected repository and workflow identity, not merely any valid keyless signature.
4. Terraform pins the verified digest. A readable tag may accompany it, but a tag is not the deployed identity.
5. The dependent module or control plane runs validation and its hosted end-to-end path with those exact references before promotion.

For the managed site path, publish and verify the PPB support image before cloud-compose or the vendored managed runtime points at it. Publish and verify the shared router image before shared edge Terraform pins it. The managed API and control plane move only after both infrastructure dependency sets are available and green. A failed smoke test is fixed at the producing layer and republished; it is not bypassed by pointing consumers at a moving tag.

## Images and templates

For bundled applications, Buildkit must publish the application-versioned image before a template points `BASE_IMAGE` at that version. Direct Compose images use a tag plus digest; the digest update should come from the artifact actually published under the reviewed tag. A digest keeps the bytes immutable even when no upstream release tag exists, but prefer a semantic release tag over `main` or `latest` when one is available.
Expand Down Expand Up @@ -69,7 +81,7 @@ Keep one release record for the complete deployed set. At minimum, record:
| Plugin to template | Template repository/ref and the plugin release whose lifecycle and component tests cover it |
| cloud-compose to sitectl | One version per installed package, when the selected module supports it |
| cloud-compose to reusable Terraform/support artifacts | Module release or full commit, provider lockfile, exact dependency commits, and support-image digests |
| Managed control plane to workload | API/control-plane release, pinned infrastructure contract, site repository commit, and recorded deployment operation |
| Managed control plane to workload | API/control-plane release, verified image digests and publisher identities, pinned infrastructure contract, site repository commit, and recorded deployment operation |

A readable tag explains intent; the digest or full commit fixes bytes. Record both when the artifact format supports both.

Expand Down
6 changes: 1 addition & 5 deletions platform/adoption-model.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,7 @@ Managed Terraform keeps long-lived ownership narrow:

Creation follows foundation, organization, Vault configuration, project, then edge; deletion reverses that order. A site does not receive an independent Terraform state, and one state must not manage a resource owned by another root.

Controller delivery is private network traffic but still authenticated. A Cloud Run controller can send to a VM through Direct VPC **egress**. A site-facing Cloud Run power proxy uses the same egress pattern to reach the VM's private application port. Cloud Run services do not gain Direct VPC ingress from this configuration: public traffic still enters the declared Cloud Run/edge ingress, and VPC firewall rules govern the egress hop to the VM.

Direct VPC addresses are ephemeral. The VM ingress rule therefore allows the complete selected subnet CIDR on only the required port and targets the runtime service account. The project foundation owns the Cloud Run service identity, required network-use role, and reusable custom-role definitions; per-runtime state owns the instance-scoped role bindings and firewall rule. The Cloud Run service, subnet, and VM must use compatible regions, the subnet needs adequate address capacity, and the default MTU remains `1460`.

Ordinary Direct VPC services should use an egress-aware startup probe with a bounded retry window. The power proxy is the deliberate exception: the application VM may be off, so application reachability cannot be its startup gate. Its bounded first-request power-on and backend dial loop is the readiness boundary. Clients must still tolerate occasional connection resets during Cloud Run or network maintenance.
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 exact router-to-PPB identity, canonical client-IP, subnet, startup, and timeout contracts.

## Responsibility changes by adoption layer

Expand Down
73 changes: 66 additions & 7 deletions platform/security-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,80 @@ For the full self-hosted checklist, including Terraform state, cloud IAM, secret
</Card>
</CardGroup>

## Managed ingress
## Managed ingress request path

LibOps places Cloudflare in front of managed public sites. Edge policy helps keep common abuse and unwanted request volume away from application hosts while centralizing TLS and routing operations.
LibOps places Cloudflare in front of managed public sites. The managed path has one declared public edge, a source-restricted router boundary, and an IAM-authenticated site origin:

Self-hosted operators must supply their own DNS, certificate lifecycle, trusted-proxy configuration, firewall policy, and abuse controls. Enabling Traefik in a template does not provide the surrounding edge service.
```text
Visitor
|
v
Cloudflare edge
|
v
Global external HTTPS load balancer + Cloud Armor
|
v
Shared Cloud Run router
| Google ID token + canonical client IP
v
Private IAM-authenticated per-site PPB
| Direct VPC egress
v
VM private IP and application port
```

Cloud Armor admits only Cloudflare's current published IPv4 and IPv6 proxy ranges and denies other sources. The shared router accepts traffic only from the load balancer. Its default `run.app` URL is disabled so it cannot become an alternate public edge.

The per-site power and proxy backend (PPB) has a different URL decision. Its `run.app` hostname remains addressable because the shared router uses that hostname as the origin, but it is not public: Cloud Run invocation is granted only to the router service account. The VM application port is private and is reached only through the runtime subnet firewall rule.

Self-hosted operators must supply their own DNS, certificate lifecycle, trusted-proxy configuration, firewall policy, and abuse controls. Enabling Traefik in a template does not provide this surrounding edge service.

### Client IP and request identity

Cloudflare supplies `CF-Connecting-IP`. Because the load balancer has already restricted the source to Cloudflare proxy networks, the router can treat that header as eligible input, but it still fails closed unless there is exactly one non-comma, syntactically valid IP value. It does not fall back to `True-Client-IP`, `CF-Connecting-IPv6`, or a caller-supplied `X-Forwarded-For` value.

The router overwrites `X-Libops-Client-IP` with that validated address and rebuilds the forwarded client headers. Each site PPB reads the single canonical `X-Libops-Client-IP` value at depth `0` and checks it against that site's wake-up allowlist. An incoming hostile `X-Forwarded-For` prefix or forged `X-Libops-Client-IP` therefore cannot move the selected address. This 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.

The router authenticates to each private PPB with a Google ID token for one fixed custom HTTPS audience. It sends that credential in `X-Serverless-Authorization`, leaving the request's application `Authorization` header intact for the site. Cloud Run IAM and the custom audience authenticate the router-to-PPB hop. The client-IP allowlist is an additional wake-up policy, not application authentication.

### Boundary ownership

| 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 |
| 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

For the managed GCP design, a site-facing Cloud Run service can reach the application's private VM address through Direct VPC **egress** without a Serverless VPC Access connector. The same pattern carries authenticated controller requests to the private rollout endpoint. This does not enable Direct VPC ingress for Cloud Run services: external traffic still enters the declared Cloud Run and edge ingress paths.
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. The same pattern carries authenticated controller requests to the private rollout endpoint. Cloud Run services do not support Direct VPC ingress: external traffic still enters the declared Cloudflare, load-balancer, and Cloud Run path.

Direct VPC revision addresses are ephemeral, so the VM firewall cannot safely allow individual addresses or use a Cloud Run service identity as the ingress source. It allows the complete regional subnet CIDR on only the required application or controller ports and targets the runtime VM service account. Every workload attached to that subnet is consequently inside the source trust zone.

The managed project creates a `/20` subnet in each selected region. A self-hosted deployment must use at least `/26`, but that minimum may be too small once existing allocations, Cloud Run's `/28` allocation blocks, roughly two addresses per steady-state service instance, and overlapping revisions are counted. The service, VM, and subnet must use compatible regions, and the network MTU remains Cloud Run's default `1460`.

Cloud Run can retain revision addresses for up to 20 minutes after scale-down. After detaching or deleting the last Cloud Run user, allow one to two hours before deleting the subnet; reserved serverless addresses cannot be removed manually.

### Cold starts and timeout ownership

Direct VPC attachment can delay new connections for a minute or more. An ordinary Cloud Run service should use a bounded, retrying startup probe against a representative egress destination before accepting traffic. PPB cannot use the sleeping application VM as that probe target: PPB must accept the first request in order to start the VM, so requiring the VM to be reachable at startup would deadlock the wake path.

The managed exception uses bounded retries inside that first request. PPB has 45 seconds to power on the VM and 35 seconds to establish the backend connection. It does not replay an application request after a connection succeeds. If a controlled failure occurs before dispatch, it can return `503` with `Retry-After`; the client decides whether its operation is safe to retry. Clients must also tolerate occasional connection resets during Cloud Run or network maintenance.

Cloud Run revision addresses are ephemeral, so the VM firewall cannot safely allow individual addresses or use a Cloud Run service identity as an ingress source. It allows the complete selected subnet CIDR on only the required application or controller port and targets the runtime VM service account. Treat every workload on that subnet as part of the source trust zone. The project foundation owns the Cloud Run service agent and network-use permissions; runtime state narrows start and suspend permissions to the selected VM.
Every outer deadline leaves room for the inner operation to finish:

The Cloud Run service and subnet must be in compatible regions, the subnet must have capacity for `/28` allocation blocks and overlapping revisions, and the network must use Cloud Run's default MTU of `1460`. A `/26` is the minimum, not a sizing recommendation for a busy shared subnet.
```text
PPB power on + backend dial 45 + 35 < 100 seconds
Router lookup + origin proxy 5 + 100 < 110 seconds
Router Cloud Run request 110 < 120 seconds
Cloudflare non-Enterprise read ceiling 120 seconds
```

Ordinary Direct VPC services should delay readiness until a bounded egress-aware startup probe succeeds. The power proxy cannot probe the application as a startup condition because its purpose is to accept the first request while the VM is off. Its bounded power-on and backend connection retry is the exception. Clients and services must still handle occasional connection resets during network maintenance.
The chain is often summarized as `45 + 35 < 100 < 110 < 120`, with the separate five-second route lookup also fitting inside the 110-second router request. Terraform rejects equality or inversion at any boundary. Do not raise the outer value above 120 unless the Cloudflare zone has an Enterprise timeout and every inner deadline and test is changed as one contract.

## Vault-backed secrets

Expand Down