diff --git a/docs.json b/docs.json index 986eeac..035d66f 100644 --- a/docs.json +++ b/docs.json @@ -75,6 +75,7 @@ "pages": [ "infrastructure/cloud-compose", "infrastructure/self-hosted-operations", + "infrastructure/current-release-status", "infrastructure/release-compatibility" ] }, diff --git a/infrastructure/cloud-compose.mdx b/infrastructure/cloud-compose.mdx index 1420100..3631c1e 100644 --- a/infrastructure/cloud-compose.mdx +++ b/infrastructure/cloud-compose.mdx @@ -12,7 +12,7 @@ Use the [cloud-compose reference documentation](https://cc.libops.io/) for the e -This page is an architecture contract, not a release announcement. Examples use `RELEASE_OR_FULL_SHA` deliberately. Do not configure a provider entrypoint, foundation module, package-version map, or Direct VPC option until the exact cloud-compose tag or commit you selected contains it and its release CI is green. +This page is an architecture contract, not a release announcement. Examples use `RELEASE_OR_FULL_SHA` deliberately. Do not configure a provider entrypoint, foundation module, package-version map, or Direct VPC option until the exact cloud-compose tag or commit you selected contains it and its release CI is green. Check the dated [Current Release Status](/infrastructure/current-release-status) before interpreting a planned integration as released. ## Deployment flow @@ -83,6 +83,8 @@ A module ref alone does not make a deployment reproducible. Record and pin: Core `sitectl` and its plugins have independent releases. In releases with per-package pins, `runtime.sitectl.version` is only the legacy fallback for packages without a package-specific value, and `latest` remains mutable. A release with only one shared version cannot represent an independently released plugin set. On a host with several projects, all projects share one installed package set, so select a set compatible with every application plugin on that host. +A package-version map selects versions; it does not retain their artifacts. The current LibOps package publisher rebuilds repository metadata from current release artifacts and can prune an older package. For a durable production rollback, preserve a repository snapshot or the matching GitHub release packages and `checksums.txt` in controlled storage, then test installation from that retained source. + A branch or tag in `runtime.compose` intentionally follows future repository changes. A full commit is detached and verified before deployment, and cloud-compose records the deployed head for later comparison. A commit identifies repository bytes; downstream branch protection and commit-signing policy still determine whether those bytes were trusted. ## Host-control trust boundary @@ -127,6 +129,10 @@ If the selected release does not contain that foundation module, follow its release-specific IAM instructions. Do not copy an unpublished foundation module from a development branch into an otherwise pinned production stack. + +In the [July 13, 2026 release-status snapshot](/infrastructure/current-release-status), PPB `0.5.0` exists as a source/GitHub release but its versioned container is not published. The power-management paragraphs below define the contract a selected release must satisfy; they do not make `0.5.0` a consumable image or make the managed shared-router/private-PPB path available. + + The optional Cloud Run power-management ingress reaches the VM's private application port through [Direct VPC egress](https://cloud.google.com/run/docs/configuring/vpc-direct-vpc); it does @@ -144,6 +150,14 @@ VPC ingress feature. Cloud Run services do not support Direct VPC ingress. Public requests enter Cloud Run through its configured service/edge ingress; Direct VPC egress is only the private hop from that service to the VM. +Cloud-compose configures this Cloud Run attachment with +`vpc_direct_egress = "PRIVATE_RANGES_ONLY"`. Only requests to private +destination ranges use the VPC path. Other outbound traffic follows normal +Cloud Run egress and does not traverse this VPC or its Cloud NAT. Changing the +service to `ALL_TRAFFIC` is not a tuning-only change: it creates a different +routing, NAT, cost, and cold-start contract and requires a separate design +review and hosted smoke test. + When power management is enabled, the subnet must be IPv4 `/26` or larger, use RFC1918, RFC6598 (`100.64.0.0/10`), or Class E (`240.0.0.0/4`) space, and keep the Cloud Run default MTU of `1460`. Cloud Run receives ephemeral addresses @@ -187,11 +201,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. +The target LibOps managed platform's depth-zero setting is a separate, +unreleased contract. Its authenticated shared router must replace any incoming +canonical client-IP header, and the private PPB must read that single value at +depth zero rather than trust the incoming `X-Forwarded-For` chain. See [Security +and Operations](/platform/security-operations) for that target 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 diff --git a/infrastructure/current-release-status.mdx b/infrastructure/current-release-status.mdx new file mode 100644 index 0000000..aab4365 --- /dev/null +++ b/infrastructure/current-release-status.mdx @@ -0,0 +1,56 @@ +--- +title: "Current Release Status" +description: "A dated availability record for the independently released artifacts in the LibOps application and infrastructure stack" +--- + + +There is not yet a published, platform-wide known-good release set for the managed shared-router and private-PPB request path. The architecture pages describe the target contract, but they are not evidence that this integration is generally available. Do not change production DNS or infrastructure for that path until a later status record identifies every immutable artifact and its green end-to-end release gate. + + +This snapshot was reviewed on **July 13, 2026 at 23:00 UTC**. It is a release record, not a moving "latest" lookup. A later tag does not silently update the compatibility claims on this page. + +## Status meanings + +| Status | Meaning | +| --- | --- | +| Released dependency | The named independent release exists. Consumers must still verify its release notes, immutable identity, and compatibility with their complete deployment. | +| Released docs set | The exact core and plugin commits were used to generate and validate the published command documentation. | +| Unrecorded | Independently usable artifacts exist, but LibOps has not published an aggregate compatibility record proving one complete cross-repository set. | +| Preview | Code or architecture work exists, but the production integration and rollout gate are incomplete. | +| Blocked | A required release, provenance, CI, or end-to-end gate is not complete. Do not operate the feature as released. | + +## Published foundations + +| Surface | Dated reference | Status | What this record proves | +| --- | --- | --- | --- | +| Core `sitectl` and application plugins | [Generated documentation dependency manifest](https://github.com/libops/sitectl-docs/blob/de317cf1af3ade6097fbe37470a78790807ad713/scripts/snippet-dependencies.json) | Released docs set | The documentation was generated from the full core and plugin commits in that manifest. Package versions remain independent. | +| cloud-compose | [`0.10.3`](https://github.com/libops/cloud-compose/releases/tag/0.10.3) | Released dependency | This tag exists as a stable self-hosted baseline. Only fields and migrations present in that tag are available to its consumers. | +| Terraform Cloud Run v2 module | [`0.7.0`](https://github.com/libops/terraform-cloudrun-v2/releases/tag/0.7.0) | Released dependency | The reusable module release exists. That does not prove that cloud-compose or the managed control plane has promoted every module capability. | +| Buildkit images and Compose templates | The exact template checkout and each `tag@sha256:digest` it records | Unrecorded | Templates record immutable direct-image identities, but this snapshot does not contain a generated aggregate of every template commit, Buildkit digest, and hosted smoke-test result. | + +The table deliberately does not invent an image digest, template commit, signature identity, or CI result that has not been captured by a release manifest. For self-hosted deployments, build a deployment-specific record from the exact template commit and immutable references you test. + +## Managed integrations not released in this snapshot + +| Integration | Status | Gate that remains | +| --- | --- | --- | +| Versioned PPB support image | Blocked | A [`0.5.0` source/GitHub release](https://github.com/libops/ppb/releases/tag/0.5.0) exists, but its versioned container is **not published**. There is no released PPB `tag@sha256:digest` to consume or verify against the expected publisher identity. The source tag is not a substitute for that container artifact. | +| Cloudflare and load balancer to shared Cloud Run router to private per-site PPB | Blocked / preview | Publish and verify the exact router and PPB image digests and expected keyless publisher identities; merge and release the API/router integration; pin the dependencies in the managed Terraform roots; pass the private-origin, client-IP, authorization-preservation, timeout, Direct VPC, and rollback canaries. | +| 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. | +| Platform-wide image, template, CLI, plugin, and infrastructure compatibility manifest | Blocked | Generate the aggregate record from release automation and attach hosted CI evidence for the exact references. Until then, each operator owns a deployment-specific record. | + +## What a promoted managed set must record + +A future status entry can change a managed integration to released only when it names: + +- every Buildkit and support-image tag plus immutable digest; +- the expected signing repository, workflow identity, and successful verification result for privileged managed images; +- every template release or full commit; +- core `sitectl` and each plugin's independent package version; +- the cloud-compose release or full commit, reusable Terraform-module releases, and provider lockfiles; +- the API, worker, router, and PPB release identities; +- the exact hosted smoke, upgrade, migration, canary, and rollback evidence. + + + Follow the dependency order and keep a deployment-specific compatibility and rollback record. + diff --git a/infrastructure/release-compatibility.mdx b/infrastructure/release-compatibility.mdx index 7b645b8..08d9d55 100644 --- a/infrastructure/release-compatibility.mdx +++ b/infrastructure/release-compatibility.mdx @@ -7,6 +7,10 @@ LibOps applications span several independently released repositories. Treating t Architecture documentation can describe a contract before every dependent repository has published it. That does not make a tag, package, module, image, or digest available. A compatibility set is consumable only after every selected reference resolves from its release channel and the corresponding hosted CI is green. + +The dated [Current Release Status](/infrastructure/current-release-status) records which foundations are independently released and which managed integrations remain preview or blocked. It intentionally does not fill missing digests or CI evidence with assumptions. + + ## Release dependency order Publish and consume changes in this order: @@ -69,6 +73,8 @@ Cloud-compose releases that expose `runtime.sitectl.package_versions` can pin ea Production should not follow a package manager's `latest` channel without a reviewed compatibility record. +The current Linux package publisher rebuilds APT and RPM metadata from the current release artifacts and does not retain older versions in the package repository. A per-package version map is therefore reproducible only while every named package remains downloadable. For a long-lived production rollback, retain a repository snapshot or archive the matching GitHub release packages and `checksums.txt` in controlled storage. A version string without a retained artifact is not a recovery plan. + ## Compatibility record Keep one release record for the complete deployed set. At minimum, record: @@ -147,6 +153,7 @@ Keep enough information to reconstruct the last known-good release: - image tags and digests; - Composer lockfile; - core and per-plugin package versions; +- retained native-package or GitHub release assets plus their published checksums, when the package repository does not preserve that version; - cloud-compose module source and provider lockfile; - Terraform state version; - application-consistent data backup and required secrets; @@ -164,6 +171,7 @@ Rollback is a tested procedure, not a list of old version numbers. Exercise it i - Treating a successful Terraform plan as application migration validation. - Rolling an image back across an irreversible schema migration. - Migrating Terraform module addresses by allowing resource recreation. +- Assuming a native-package pin remains installable after the package repository has pruned that release. diff --git a/platform/adoption-model.mdx b/platform/adoption-model.mdx index 6c5b501..85c7ad9 100644 --- a/platform/adoption-model.mdx +++ b/platform/adoption-model.mdx @@ -149,7 +149,11 @@ 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 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. + +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. ## Responsibility changes by adoption layer diff --git a/platform/automation-backplane.mdx b/platform/automation-backplane.mdx index a56fd79..5c916b6 100644 --- a/platform/automation-backplane.mdx +++ b/platform/automation-backplane.mdx @@ -7,6 +7,10 @@ LibOps uses GitHub as an important collaboration surface, but the platform shoul LibOps is building a webhook-driven automation backplane for events that need to trigger work inside the platform: dependency scanning, CI runner orchestration, deployment follow-up, and repository maintenance. + +The separate request-serving API and `api-worker` Cloud Run deployment described below is a **preview target**, not a released managed topology in the [July 13, 2026 status snapshot](/infrastructure/current-release-status). Process separation in source code or a Compose development stack does not prove that the worker service, identity migration, database path, rollout, and rollback have been promoted in production. + + ## GitHub events in LibOps The automation backplane listens for GitHub events such as: @@ -37,7 +41,7 @@ The service is intentionally event-driven. GitHub is the collaboration surface, LibOps keeps customer-facing API requests separate from retrying background work. The API request path validates the caller, commits the requested state change, records any required outbox event in MariaDB, and returns. Control-plane workers then drain durable queues for reconciliation, webhook fanout, Slack task replies, and Terraform scheduling. -API-local maintenance work follows the same rule. Organization synchronization and email verification cleanup run under the dedicated `api-worker` process, selected by `LIBOPS_API_WORKER_JOBS`, rather than inside the request-serving API process. The API worker exposes its own `/health` and `/ready` endpoints so it can be scaled, rolled out, and restarted independently. +The target API-local maintenance contract follows the same rule. Organization synchronization and email verification cleanup run under the dedicated `api-worker` process, selected by `LIBOPS_API_WORKER_JOBS`, rather than inside the request-serving API process. The API worker exposes its own `/health` and `/ready` endpoints so a released worker deployment can scale, roll out, and restart independently. This gives the platform three operational boundaries: @@ -45,7 +49,7 @@ This gives the platform three operational boundaries: - **Durable work before acknowledgement:** mutating API paths should not report success until both the resource change and the outbox/event record are committed. - **Last-known-good runtime state:** existing sites continue serving from their last applied configuration if the API or control plane is temporarily unavailable. A control-plane outage should block new changes, not running site traffic. -The request-serving API and `api-worker` deploy as separate Cloud Run services. The API service uses `/health` for liveness and `/ready` as the startup gate. The worker runs with at least one instance and always-allocated CPU because it owns polling loops; moving those loops back into the API process would reintroduce request-path coupling. +In the target managed deployment, the request-serving API and `api-worker` will deploy as separate Cloud Run services. The API service will use `/health` for liveness and `/ready` as the startup gate. The worker requires at least one instance and always-allocated CPU because it owns polling loops; moving those loops back into the API process would reintroduce request-path coupling. Do not use these settings as a rollout instruction until the status record identifies the released worker artifact and migration runbook. Vault OIDC discovery and JWKS validation follow the same last-known-good rule. Successful live JWKS refreshes write a local cache containing the issuer, JWKS URL, keys, and cache time. If Vault or JWKS discovery is briefly unavailable during startup, the API can start from that cache for the configured trust window and reports `degraded_last_known_good` in `/ready`. After the trust window expires, readiness fails and cached keys are no longer used. diff --git a/platform/custom-domains.mdx b/platform/custom-domains.mdx index 357963f..b642fe5 100644 --- a/platform/custom-domains.mdx +++ b/platform/custom-domains.mdx @@ -1,9 +1,13 @@ --- title: "Custom Domains" -description: "Add a public hostname to a LibOps-managed site" +description: "Prepare a public hostname after managed ingress is enabled for a LibOps-managed site" --- -LibOps sites can serve traffic for customer-owned domains through the shared Cloudflare ingress path. You add the hostname to the site in LibOps, then point the hostname at the LibOps SaaS fallback hostname. +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. + + +The shared router and private site-origin path is not generally available in the [July 13, 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. + 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. @@ -19,6 +23,7 @@ You need: - 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. 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. @@ -34,11 +39,11 @@ Root domains such as `example.edu` require DNS-provider-specific CNAME flattenin 6. Enter the hostname, for example `collections.example.edu`. 7. Confirm the target site and save. -After saving, LibOps records the domain against the site as active. The shared ingress router can route the hostname as soon as DNS sends traffic to the LibOps fallback hostname. +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. ## Add DNS records -Point the public hostname at the LibOps fallback hostname: +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: ```text Type: CNAME @@ -55,9 +60,10 @@ Technical operators can create, inspect, and remove the same domain resources th ## Check Routing -The domain is ready when: +The domain is ready only when: - 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. diff --git a/platform/security-operations.mdx b/platform/security-operations.mdx index 772fc10..570dc6a 100644 --- a/platform/security-operations.mdx +++ b/platform/security-operations.mdx @@ -9,6 +9,10 @@ Security ownership changes with the adoption layer. Cloudflare edge policy, LibO 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 13, 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. + + ## Managed-platform defaults @@ -23,9 +27,9 @@ For the full self-hosted checklist, including Terraform state, cloud IAM, secret -## Managed ingress request path +## Managed ingress target request path -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: +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: ```text Visitor @@ -46,21 +50,21 @@ Private IAM-authenticated per-site PPB 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. +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. -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. +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. 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. +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 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 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. -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. +The router must authenticate to each private PPB with a Google ID token for one fixed custom HTTPS audience. It must send 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 target router-to-PPB hop. The client-IP allowlist is an additional wake-up policy, not application authentication. -### Boundary ownership +### Target boundary ownership | Layer | Owns | Must preserve | | --- | --- | --- | @@ -73,19 +77,21 @@ The router authenticates to each private PPB with a Google ID token for one fixe ## Private VM delivery on Google Cloud -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. +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. 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. + +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. -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. +Direct VPC revision addresses are ephemeral, so the target VM firewall cannot safely allow individual addresses or use a Cloud Run service identity as the ingress source. It must allow the complete regional subnet CIDR on only the required application or controller ports and target 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`. +The target managed project allocates 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. +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. In the target design, 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. +The target managed exception uses bounded retries inside that first request. Its contract allocates PPB 45 seconds to power on the VM and 35 seconds to establish the backend connection. PPB must not replay an application request after a connection succeeds. If a controlled failure occurs before dispatch, it may 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. Every outer deadline leaves room for the inner operation to finish: @@ -96,7 +102,7 @@ Router Cloud Run request 110 < 120 seconds Cloudflare non-Enterprise read ceiling 120 seconds ``` -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. +The target chain is summarized as `45 + 35 < 100 < 110 < 120`, with the separate five-second route lookup also fitting inside the 110-second router request. Its Terraform contract must reject 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 diff --git a/templates/app-support.mdx b/templates/app-support.mdx index 61416df..f94456b 100644 --- a/templates/app-support.mdx +++ b/templates/app-support.mdx @@ -96,6 +96,28 @@ Do not edit generated configuration inside a running container. The next render Continuous confd rendering is an explicit image option, not a reason to make arbitrary files mutable. Changes still need a declared input and a restart or reload behavior that the image supports. +### Ingress upload and timeout contract + +The shared `sitectl` ingress component owns two operator-facing tuning values. Its defaults are component defaults applied when the component is set; they are not a promise that every raw image has identical Dockerfile defaults. + +| `sitectl set ingress` option | Accepted input | Component default | +| --- | --- | --- | +| `--max-upload-size` | Digits with an optional case-insensitive `K`, `M`, or `G` suffix, such as `1048576`, `128M`, or `2G`. Decimals and a `T` suffix are rejected. | `128M` | +| `--upload-timeout` | One integer and one unit: `ms`, `s`, `m`, or `h`, such as `500ms`, `300s`, `10m`, or `1h`. A unitless or compound value such as `300` or `1m30s` is rejected. | `300s` | + +For a PHP/nginx application plugin, a successful component change writes the following component-owned values into the tracked application service: + +| Input | Tracked Compose result | Rendered behavior | +| --- | --- | --- | +| `--max-upload-size` | `PHP_UPLOAD_MAX_FILESIZE`, `PHP_POST_MAX_SIZE`, `NGINX_CLIENT_MAX_BODY_SIZE` | PHP upload and POST limits plus nginx request-body limit | +| `--upload-timeout` | `NGINX_CLIENT_BODY_TIMEOUT`, `NGINX_FASTCGI_READ_TIMEOUT`, `NGINX_FASTCGI_SEND_TIMEOUT` | nginx client-body and FastCGI read/send timeouts, using the supplied duration unchanged | +| `--upload-timeout` | `PHP_MAX_INPUT_TIME`, `PHP_MAX_EXECUTION_TIME`, `PHP_REQUEST_TERMINATE_TIMEOUT` | PHP and PHP-FPM timeout values in whole seconds; a subsecond remainder rounds up | +| `--upload-timeout` | Traefik `--entryPoints..transport.respondingTimeouts.readTimeout` for the configured HTTP and HTTPS entrypoints | Edge request-read timeout, using the supplied duration unchanged | + +Drupal, Islandora, OJS, Omeka Classic, Omeka S, and WordPress use that PHP/nginx mapping. ArchivesSpace does not: its plugin deliberately removes the shared PHP/nginx variables because its JVM application image does not consume them. For ArchivesSpace, `--upload-timeout` updates Traefik only, and `--max-upload-size` does not establish a backend upload limit. An ArchivesSpace backend limit or timeout needs an explicit application-plugin and image mapping before it can be described as end to end. + +Container environment values override image `ENV` defaults, and confd renders the supported nginx and PHP files from those values during startup. A later `sitectl set` or component reconciliation can replace manual edits to component-owned Compose fields, so make durable changes through the component and commit its diff. + ## Secrets and database initialization The Compose secret boundary follows least privilege: