diff --git a/workplans/RMASTER-WP-0023-private-by-default-exposure.md b/workplans/RMASTER-WP-0023-private-by-default-exposure.md index 36d520f..fc5e95a 100644 --- a/workplans/RMASTER-WP-0023-private-by-default-exposure.md +++ b/workplans/RMASTER-WP-0023-private-by-default-exposure.md @@ -4,22 +4,29 @@ type: workplan title: "Private-by-default exposure until production admission" domain: financials repo: railiance-master -status: proposed +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 @@ -45,7 +52,7 @@ 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. + 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 @@ -56,14 +63,19 @@ It does **not** belong as the system owner in: 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. + 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. + `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` @@ -76,47 +88,97 @@ It does **not** belong as the system owner in: - 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) -Proposed default, for review: +Recommended default, for review. T01 may amend; it should not start from +a blank page. | 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 | +| `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` 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 +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. -4. Ops-bridge is the normal path to a shielded thing. That is the same +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 unless T01 -explicitly decides to. Exposure can sit beside them. +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: -- amend ADR-0006 or add a sibling ADR for exposure posture +- 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 the exception register -- **route** implementation workplans to owning repos +- 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, or UFW rules here +- 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 @@ -128,33 +190,39 @@ 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`). +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:** the ADR is accepted and the three readiness enums are -either left distinct on purpose or given a written unification plan. +**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: todo +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-*` | 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 | +| `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. @@ -169,23 +237,28 @@ 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: +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: -- posture is `public` and the relevant admission/readiness state is not - production-approved +- 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) -- a live family declaration omits the field after the migration window + reason / approved_on / residual-risk owner) +- a rapp is `public` and its reef has not granted a public surface -Keep the field additive first so existing declarations do not break the -day the schema lands; then require it. +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. +bad fixtures. Live sibling repos are unchanged. -### T04 — Exception register for already-public surfaces +### T04 — Exception snapshot for already-public surfaces ```task id: RMASTER-WP-0023-T04 @@ -193,18 +266,23 @@ 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. +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. -Do not invent a shutdown plan here. The register exists so the contract -does not pretend the reef is already private. +Each entry: hostname or port, owning rapp or provider, why it is public, +residual-risk owner. Same shape T01 names for a grant. -**Done when:** every known public surface is either a grant or a routed -task to close it. +This snapshot is not the living source of truth. T05 files the grant +objects into the owning declarations. Later changes happen there. -### T05 — Route enforcement workplans +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 @@ -214,28 +292,30 @@ 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. +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 `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 | +| `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. +**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. +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 -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. +Reviewed 2026-08-15 against current repo state (`status: ready`). T01 +still needs human ratification of ADR-0008. Implementation stays out of +this repo.