diff --git a/workplans/RMASTER-WP-0023-private-by-default-exposure.md b/workplans/RMASTER-WP-0023-private-by-default-exposure.md new file mode 100644 index 0000000..36d520f --- /dev/null +++ b/workplans/RMASTER-WP-0023-private-by-default-exposure.md @@ -0,0 +1,241 @@ +--- +id: RMASTER-WP-0023 +type: workplan +title: "Private-by-default exposure until production admission" +domain: financials +repo: railiance-master +status: proposed +owner: bernd +topic_slug: railiance +created: "2026-08-15" +updated: "2026-08-15" +related: + - RMASTER-WP-0017 + - RMASTER-WP-0021 + - RAIL-HO-WP-0009 +related_repos: + - railiance-master + - rail-kubernetes + - railiance-cluster + - reef-railiance + - railiance-infra + - railiance-enablement +--- + +# RMASTER-WP-0023 — private-by-default exposure until production admission + +## Goal + +Make every new reef, rail, and rapp **unreachable from the public internet +until it is production-safe**, by one family rule that the implementation +repos can enforce. + +"Production-safe" already has a name: ADR-0006 admission +(`declared` → `installed` → `verified` → `production-approved`). What is +missing is the **default-private exposure rule** and a field the schemas can +check. Topology and a working deploy must not imply a public listener. + +## Why this is here + +`railiance-master` owns framework contracts that span `reef-*`, `rail-*`, +and `rapp-*`. Shielding "whatever we spin up" is that kind of rule. + +It does **not** belong as the system owner in: + +- `railiance-infra` — host UFW. Necessary for a new reef with no 80/443; + useless for a rapp that shares `railiance01` ingress. RAIL-HO-WP-0009 + closed the hand-opened k3s API; do not make S1 the admission desk for + rapps. +- `railiance-cluster` / `rail-kubernetes` — Ingress, NetworkPolicy, Service + types. This is where most packets will actually be blocked. They cannot + define what "production-safe" means for a rapp or for a non-k8s reef + (`reef-storage`). +- each `rapp-*` — must declare intended exposure; must not punch its own + public hole. +- `railiance-enablement` — should ship private-by-default templates; does + not own the rule. +- `reef-railiance` alone — too local; the next reef must inherit the same + rule. + +## What already exists + +- ADR-0006 and `docs/reef-production-readiness-contract.md` — admission + states; network isolation and extra gates for internet-facing or + high-value workloads. +- Family schemas (`schemas/{reef,rail,rapp}.schema.json`) — no + `exposure` / `network_posture` field. +- Readiness vocabularies are **deliberately not unified** + (`schemas/README.md`): + - rail: `declared | installed | verified | production-approved | deprecated` + - rapp: `draft | declared | deployed | verified | retired` + - reef: `lifecycle_state` `planned | active | retired` + - bindings: ADR-0006 states on the reef +- RAIL-HO-WP-0009 — operator access to a shielded k3s API is ops-bridge + (`k3s-api-railiance01` on `:16444`). That is the intended pattern for + anything not yet public. +- Live public exceptions already exist on `reef-railiance` (Forgejo, + Coulomb Social, Host Europe Nydus on 2224). They must be named as + grants, not silently grandfathered. + +## Decision to ratify (T01) + +Proposed default, for review: + +| Posture | Meaning | Who may reach it | +| --- | --- | --- | +| `private` | Cluster / reef-internal only | in-cluster DNS, same-reef services | +| `operator` | Reachable via ops-bridge / SSH tunnel | operator and approved agents | +| `public` | Internet | anyone the Ingress / DNS / UFW surface allows | + +Rules: + +1. New reefs, rails, and rapps default to `private` or `operator`. + Prefer `operator` when a human must debug before admission. +2. `public` requires **both** `production-approved` (ADR-0006 binding, or + the equivalent rail/rapp state this workplan chooses) **and** an + explicit exposure grant in the declaration. +3. A working deploy, a `hosts_rail` / `binds_rapp` line, or an Ingress + object is not a grant. +4. Ops-bridge is the normal path to a shielded thing. That is the same + trade as ADR-005 in `railiance-infra`: convenience depends on the + tunnel; the Internet does not see the service. + +Do not unify the three readiness enums in this workplan unless T01 +explicitly decides to. Exposure can sit beside them. + +## Boundaries + +This workplan may: + +- amend ADR-0006 or add a sibling ADR for exposure posture +- extend family schemas and `tools/validate-family-declarations.py` +- write the exposure contract and the exception register +- **route** implementation workplans to owning repos + +It must not: + +- implement NetworkPolicy, Ingress classes, or UFW rules here +- take down live public services as a side effect of the contract +- put booked cost, credentials, or customer numbers in declarations + +## Tasks + +### T01 — Ratify the exposure rule + +```task +id: RMASTER-WP-0023-T01 +status: todo +priority: high +``` + +Review and accept or amend the decision table above. Record it as an ADR +(amend ADR-0006 or add ADR-0008). Name the default posture for a new +reef, a new rail, and a new rapp. Decide whether `public` keys off the +**binding** admission state (preferred) or off `rapp.yaml` +`readiness_state` (currently has no `production-approved`). + +**Done when:** the ADR is accepted and the three readiness enums are +either left distinct on purpose or given a written unification plan. + +### T02 — Write the exposure contract + +```task +id: RMASTER-WP-0023-T02 +status: todo +priority: high +``` + +One contract, `docs/exposure-posture-contract.md`, that says what each +posture means **per family**: + +| Family | `private` | `operator` | `public` | +| --- | --- | --- | --- | +| `reef-*` | no extra host ports; no public DNS for new members | ops-bridge / SSH only | declared host/DNS surface | +| `rail-*` | no public Ingress class; default-deny NetworkPolicy | tunnel to the rail's API or admin path | rail may emit public Ingress | +| `rapp-*` | ClusterIP / in-cluster DNS only | named tunnel (local port) | named public hostname | + +Include the fail-closed rule: missing field means `private`. + +**Done when:** implementation repos can implement without inventing +vocabulary. + +### T03 — Schema and validator + +```task +id: RMASTER-WP-0023-T03 +status: wait +priority: high +``` + +Depends on T01. + +Add `exposure` (or the T01 name) to `rapp.schema.json`, `rail.schema.json`, +and `reef.schema.json`. Validator must fail when: + +- posture is `public` and the relevant admission/readiness state is not + production-approved +- posture is `public` and no grant object is present (hostname / port / + reason / approved_on) +- a live family declaration omits the field after the migration window + +Keep the field additive first so existing declarations do not break the +day the schema lands; then require it. + +**Done when:** `tools/validate-family-declarations.py` rejects a public +rapp that is not production-approved, and the testdata tree has good and +bad fixtures. + +### T04 — Exception register for already-public surfaces + +```task +id: RMASTER-WP-0023-T04 +status: todo +priority: medium +``` + +List current public listeners on `reef-railiance` (and CoulombCore if in +scope) as explicit grants: Forgejo, Coulomb Social, Host Europe Nydus +2224, anything else found. Each grant: hostname or port, owning rapp or +provider, why it is public, residual risk owner. + +Do not invent a shutdown plan here. The register exists so the contract +does not pretend the reef is already private. + +**Done when:** every known public surface is either a grant or a routed +task to close it. + +### T05 — Route enforcement workplans + +```task +id: RMASTER-WP-0023-T05 +status: wait +priority: high +``` + +Depends on T01 and T02. + +Write the handoff workplan files (or precise intake notes) in the owning +repos. Do not implement there from this repo. + +| Owner | What to enforce | +| --- | --- | +| `rail-kubernetes` (or `railiance-cluster` until the rail owns networking) | default-deny NetworkPolicy; no public Ingress unless the grant exists; ClusterIP as the paved Service | +| `reef-railiance` | first live admission: new binds stay `operator`; existing public binds appear in the T04 register | +| `railiance-infra` | new reefs do not get 80/443/6443 until the reef grant exists; not the rapp gate | +| `railiance-enablement` | templates emit private Services + tunnel docs, never a public Ingress by default | + +**Done when:** each owning repo has a workplan or an accepted intake, with +a back-link to this ID. + +## Suggested review order + +1. T01 (you) — accept or change the table. +2. T02 + T04 can proceed in parallel after T01. +3. T03 after the field names are stable. +4. T05 last; it is the only task that creates work in other repos. + +## Outcome + +Pending review. This file is a draft (`status: proposed`). Register it +with `statehub fix-consistency` in `railiance-master` after you accept the +shape. Implementation stays out of this repo.