workplan: refine RMASTER-WP-0023 after review

Accept the workplan home and tighten scope: exposure sits
beside ADR-0006 as ADR-0008, public keys off binding
admission, T04 is a reef-railiance snapshot, enforcement
is routed, and Q7 / provider APIs stay out.
This commit is contained in:
codex 2026-08-15 19:34:48 +02:00
parent 48dba891b5
commit fb1b7dae73

View file

@ -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.