--- id: RMASTER-ADR-0008 title: "Private-by-default Exposure" status: accepted revision: "accepted-1" owner: railiance-master last_reviewed: "2026-08-15" review_interval: 6m --- # ADR-0008: Private-by-default Exposure Date: 2026-08-15 Status: Accepted ## Context ADR-0006 says a topology binding is not permission to run a workload in production. It does not say who may reach a listener. A working deploy, a `hosts_rail` / `binds_rapp` line, or an Ingress object has been enough to put something on the public internet. Family readiness vocabularies are deliberately not unified (`schemas/README.md`). Reef `lifecycle_state` has no `production-approved`. Rapp `readiness_state` has no `production-approved` either. Exposure cannot be derived from those enums. Live public surfaces already exist on `reef-railiance`. They must be named as grants, not silently grandfathered and not taken down by this decision. Ratified 2026-08-15 under `RMASTER-WP-0023-T01`. ## Decision New reefs, rails, and rapps are unreachable from the public internet until they are production-safe **and** explicitly granted. The field name is `exposure`. Missing field means `private`. | Posture | Meaning | Who may reach it | | --- | --- | --- | | `private` | No extra listener we control | in-cluster DNS, same-reef services | | `operator` | Same listener as `private`, plus a named ops-bridge / SSH tunnel | operator and approved agents | | `public` | Internet listener we publish | anyone the Ingress / DNS / UFW surface allows | `operator` is an access annotation, not a different packet posture. It does not open a host port or a public Ingress. Default for a new reef, rail, or rapp is `private`. Use `operator` only for a named admin or API surface that must be reachable before admission (k3s API, OpenBao UI). Do not prefer `operator` as the debug default. `public` requires both an ADR-0006 binding of `production-approved` and an explicit grant. A deploy, a binding line, or an Ingress object is not a grant. Do not key `public` off rapp `readiness_state` and do not add `production-approved` to the rapp enum for this purpose. A reef public surface (host port or public DNS we publish) is a **substrate grant**, not reef `lifecycle_state`. A rapp cannot be `public` on a reef that has not granted a public surface. Conflicting declarations fail closed. The shared vocabulary is one enum. It is not one schema shape and not one admission check: | Family | What the field means | What makes `public` legal | | --- | --- | --- | | `rapp-*` | intended consumer-facing listener | binding `production-approved` + grant | | `rail-*` | which listener classes the rail may emit | rail may emit public Ingress only when a bound grant exists | | `reef-*` | host ports and public DNS we publish | substrate grant | A grant is accepted residual risk, not a self-serve wish. Required fields: hostname or port, reason, `approved_on`, residual-risk owner. `6443` / the k3s API is not a grantable public surface. This contract covers listeners we control: host ports, Ingress, Service types, and public DNS we publish. Provider-native internet APIs (Scaleway S3 and other provider-delegated endpoints) are outside this enum. Ops-bridge is the normal path to a shielded thing. The detailed per-family reading lives in [`docs/exposure-posture-contract.md`](../exposure-posture-contract.md). ## Consequences - ADR-0006 still answers "may this binding run in production?" This ADR answers "who may reach the listener?" Do not merge the axes. - The three readiness enums stay distinct on purpose. - Family schemas grow an additive `exposure` field. Rapp `data_classification: public` is a different field and must not be reused as the posture name. - Implementation stays in the owning repos. This ADR does not install NetworkPolicy, UFW, Ingress, or tunnels. - Existing public surfaces on `reef-railiance` remain up until named as grants. This ADR is not a shutdown plan. - CoulombCore host inventory and Q7 / Goss reaction stay outside this decision.