diff --git a/workplans/RMASTER-WP-0023-private-by-default-exposure.md b/workplans/RMASTER-WP-0023-private-by-default-exposure.md deleted file mode 100644 index fc5e95a..0000000 --- a/workplans/RMASTER-WP-0023-private-by-default-exposure.md +++ /dev/null @@ -1,321 +0,0 @@ ---- -id: RMASTER-WP-0023 -type: workplan -title: "Private-by-default exposure until production admission" -domain: financials -repo: railiance-master -status: ready -owner: bernd -topic_slug: railiance -planning_priority: high -planning_order: 23 -created: "2026-08-15" -updated: "2026-08-15" -related: - - ADR-0006 - - RMASTER-WP-0017 - - RMASTER-WP-0019 - - RMASTER-WP-0021 - - RMASTER-WP-0022 - - RAIL-HO-WP-0009 -related_repos: - - railiance-master - - rail-kubernetes - - railiance-cluster - - reef-railiance - - reef-storage - - railiance-infra - - railiance-enablement - - ops-bridge ---- - -# 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. Do not use this workplan to re-public `6443`. -- `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. -- `ops-bridge` — the paved path to a shielded thing; not the policy owner. - -## 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. Admission answers "may this binding run in - production?" Exposure answers "who may reach the listener?" Keep them - beside each other. -- Family schemas (`schemas/{reef,rail,rapp}.schema.json`) — no - `exposure` / `network_posture` field. Rapp already uses `public` on - `data_classification`; the new field must not reuse that name as a - top-level enum collision. -- 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. -- Provider-delegated reefs (`reef-storage`, Scaleway S3) already expose a - provider API on the internet. That is a different category from a - listener we open. - -## Decision to ratify (T01) - -Recommended default, for review. T01 may amend; it should not start from -a blank page. - -| 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. A ClusterIP service -plus a named tunnel is still `private` at the packet layer. - -Rules: - -1. New reefs, rails, and rapps default to `private`. Use `operator` only - when a named admin or API surface must be reachable before admission - (k3s API, OpenBao UI). Do not prefer `operator` as the debug default - for every new rapp — that grows an ops-bridge catalog by accident. -2. `public` requires **both** an ADR-0006 binding of - `production-approved` **and** an explicit exposure grant. Do not key - `public` off `rapp.yaml` `readiness_state` (it has no - `production-approved`) and do not add that value to the rapp enum in - this workplan. -3. A reef has no `production-approved` lifecycle. A public reef surface - (host port, public DNS we publish) is a **substrate grant**, not a - binding admission. T01 must name that grant shape separately. -4. A working deploy, a `hosts_rail` / `binds_rapp` line, or an Ingress - object is not a grant. -5. A grant is accepted residual risk (reason, approved_on, residual-risk - owner, hostname or port). It is not a self-serve wish in the - declaration. -6. 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. -7. `6443` / the k3s API is not a grantable public surface. RAIL-HO-WP-0009 - already closed it; this contract must not become a way to reopen it. - -Do not unify the three readiness enums in this workplan. Exposure sits -beside them. Record the rule as **ADR-0008** (sibling), not as an edit -that mixes "is this binding production-safe?" with "who may reach it?" - -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 rapp/rail grant exists | -| `reef-*` | host ports and public DNS we publish | 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. - -## Boundaries - -This workplan may: - -- add ADR-0008 for exposure posture and point ADR-0006 at it -- extend family schemas and `tools/validate-family-declarations.py` -- write the exposure contract and a dated exception snapshot -- **route** implementation work to owning repos (intake notes or State - Hub messages). The owning-repo agent files the workplan. This repo - does not implement there and does not silently create four sibling - workplans as a side effect of registration. - -It must not: - -- implement NetworkPolicy, Ingress classes, UFW rules, or ops-bridge - tunnels here -- take down live public services as a side effect of the contract -- put booked cost, credentials, or customer numbers in declarations -- inventory CoulombCore host ports (that stays with RAIL-HO-WP-0009 / - `railiance-infra` until CoulombCore is a declared reef) -- absorb Q7 / Goss declared-vs-live *reaction* (page a human, open a - task, block a rollout). Inbox `ae73593a` handed that to - `railiance-master`; it is a separate workplan -- treat provider-native internet APIs (Scaleway S3, other - provider-delegated endpoints) as the same grant as a listener we open. - This contract covers listeners we control: host ports, Ingress, - Service types, and public DNS we publish -- migrate live family declarations onto the new field (owning repos, via - T05) -- become a living port inventory. After T04, updates live in the owning - declarations - -## Tasks - -### T01 — Ratify the exposure rule - -```task -id: RMASTER-WP-0023-T01 -status: todo -priority: high -``` - -Human review. Accept or amend the recommended table above. Record ADR-0008. -Name the default posture for a new reef, a new rail, and a new rapp -(recommended: `private` for all three). Confirm that `public` keys off -the **binding** admission state and that a reef public surface uses a -substrate grant. Confirm `6443` is not grantable as `public`. - -**Done when:** ADR-0008 is accepted, the three readiness enums are left -distinct on purpose, and the per-family meaning of `public` is written -down. - -### T02 — Write the exposure contract - -```task -id: RMASTER-WP-0023-T02 -status: wait -priority: high -``` - -Depends on T01. - -One contract, `docs/exposure-posture-contract.md`, that says what each -posture means **per family**: - -| Family | `private` | `operator` | `public` | -| --- | --- | --- | --- | -| `reef-*` compute | no extra host ports; no public DNS for new members | ops-bridge / SSH only | declared host/DNS surface plus substrate grant | -| `reef-*` provider-delegated | we publish no extra name | n/a | out of this enum; provider API is not a listener we open | -| `rail-*` | no public Ingress class; default-deny NetworkPolicy | tunnel to the rail's API or admin path | rail may emit public Ingress for a granted binding | -| `rapp-*` | ClusterIP / in-cluster DNS only | named tunnel (local port) on the same ClusterIP | named public hostname plus binding grant | - -Include the fail-closed rule: missing field means `private`. -Include the conflict rule: rapp `public` + reef without a public -substrate grant is invalid. - -**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 the family schemas. Share the -vocabulary; do not pretend one object shape fits all three. Validator -must fail when: - -- a rapp or rail posture is `public` and the relevant **binding** is not - `production-approved` -- posture is `public` and no grant object is present (hostname / port / - reason / approved_on / residual-risk owner) -- a rapp is `public` and its reef has not granted a public surface - -Keep the field additive so existing declarations do not break the day -the schema lands. Requiring the field on live declarations is a T05 -migration in the owning repos, not a T03 done-when. - -Name the field so it cannot be confused with rapp -`data_classification: public`. - -**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. Live sibling repos are unchanged. - -### T04 — Exception snapshot for already-public surfaces - -```task -id: RMASTER-WP-0023-T04 -status: todo -priority: medium -``` - -Dated snapshot in this repo so the contract does not pretend -`reef-railiance` is already private. Seed from what is already named: -Forgejo, Coulomb Social, Host Europe Nydus 2224, then fill anything else -found on that reef. - -Each entry: hostname or port, owning rapp or provider, why it is public, -residual-risk owner. Same shape T01 names for a grant. - -This snapshot is not the living source of truth. T05 files the grant -objects into the owning declarations. Later changes happen there. - -Out of scope: CoulombCore, shutdown plans, provider-native APIs. - -**Done when:** every known public surface on `reef-railiance` is either a -snapshot grant or a routed task to close it. - -### T05 — Route enforcement work - -```task -id: RMASTER-WP-0023-T05 -status: wait -priority: high -``` - -Depends on T01 and T02. - -Write intake notes here or send State Hub messages to the owning-repo -agents. Those agents file the workplan in their repo. 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 `private` (or `operator` only for a named admin path); existing public binds receive the T04 grants | -| `railiance-infra` | new reefs do not get 80/443 until a reef substrate grant exists; 6443 stays operator-only; this is 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 (human) — accept or change the recommended table. -2. T04 can start now as a census; its grant fields lock after T01. -3. T02 after T01. -4. T03 after the field names are stable. -5. T05 last; it is the only task that creates work in other repos. - -## Outcome - -Reviewed 2026-08-15 against current repo state (`status: ready`). T01 -still needs human ratification of ADR-0008. Implementation stays out of -this repo.