2026-08-15 19:19:07 +02:00
---
id: RMASTER-WP-0023
type: workplan
title: "Private-by-default exposure until production admission"
domain: financials
repo: railiance-master
2026-08-15 20:08:37 +02:00
status: finished
owner: grok
2026-08-15 19:19:07 +02:00
topic_slug: railiance
2026-08-15 19:34:48 +02:00
planning_priority: high
planning_order: 23
2026-08-15 19:19:07 +02:00
created: "2026-08-15"
updated: "2026-08-15"
related:
2026-08-15 19:34:48 +02:00
- ADR-0006
2026-08-15 19:50:46 +02:00
- ADR-0008
2026-08-15 19:19:07 +02:00
- RMASTER-WP-0017
2026-08-15 19:34:48 +02:00
- RMASTER-WP-0019
2026-08-15 19:19:07 +02:00
- RMASTER-WP-0021
2026-08-15 19:34:48 +02:00
- RMASTER-WP-0022
2026-08-15 19:19:07 +02:00
- RAIL-HO-WP-0009
related_repos:
- railiance-master
- rail-kubernetes
- railiance-cluster
- reef-railiance
2026-08-15 19:34:48 +02:00
- reef-storage
2026-08-15 19:19:07 +02:00
- railiance-infra
- railiance-enablement
2026-08-15 19:34:48 +02:00
- ops-bridge
2026-08-15 19:35:38 +02:00
state_hub_workstream_id: "50303bdf-aada-4c38-8782-d4e63b603e65"
2026-08-15 19:19:07 +02:00
---
# 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
2026-08-15 19:34:48 +02:00
rapps. Do not use this workplan to re-public `6443` .
2026-08-15 19:19:07 +02:00
- `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.
2026-08-15 19:34:48 +02:00
- `ops-bridge` — the paved path to a shielded thing; not the policy owner.
2026-08-15 19:19:07 +02:00
## What already exists
- ADR-0006 and `docs/reef-production-readiness-contract.md` — admission
states; network isolation and extra gates for internet-facing or
2026-08-15 19:34:48 +02:00
high-value workloads. Admission answers "may this binding run in
production?" Exposure answers "who may reach the listener?" Keep them
beside each other.
2026-08-15 19:19:07 +02:00
- Family schemas (`schemas/{reef,rail,rapp}.schema.json` ) — no
2026-08-15 19:34:48 +02:00
`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.
2026-08-15 19:19:07 +02:00
- 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.
2026-08-15 19:34:48 +02:00
- 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.
2026-08-15 19:19:07 +02:00
2026-08-15 19:50:46 +02:00
## Decision (T01, accepted 2026-08-15)
2026-08-15 19:19:07 +02:00
2026-08-15 19:50:46 +02:00
Ratified as written. Recorded in
[`docs/adr/ADR-0008-private-by-default-exposure.md` ](../docs/adr/ADR-0008-private-by-default-exposure.md ).
2026-08-15 19:19:07 +02:00
| Posture | Meaning | Who may reach it |
| --- | --- | --- |
2026-08-15 19:34:48 +02:00
| `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.
2026-08-15 19:19:07 +02:00
Rules:
2026-08-15 19:34:48 +02:00
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
2026-08-15 19:19:07 +02:00
object is not a grant.
2026-08-15 19:34:48 +02:00
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
2026-08-15 19:19:07 +02:00
trade as ADR-005 in `railiance-infra` : convenience depends on the
tunnel; the Internet does not see the service.
2026-08-15 19:34:48 +02:00
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:
2026-08-15 19:19:07 +02:00
2026-08-15 19:34:48 +02:00
| 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.
2026-08-15 19:19:07 +02:00
## Boundaries
This workplan may:
2026-08-15 19:34:48 +02:00
- add ADR-0008 for exposure posture and point ADR-0006 at it
2026-08-15 19:19:07 +02:00
- extend family schemas and `tools/validate-family-declarations.py`
2026-08-15 19:34:48 +02:00
- 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.
2026-08-15 19:19:07 +02:00
It must not:
2026-08-15 19:34:48 +02:00
- implement NetworkPolicy, Ingress classes, UFW rules, or ops-bridge
tunnels here
2026-08-15 19:19:07 +02:00
- take down live public services as a side effect of the contract
- put booked cost, credentials, or customer numbers in declarations
2026-08-15 19:34:48 +02:00
- 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
2026-08-15 19:19:07 +02:00
## Tasks
### T01 — Ratify the exposure rule
```task
id: RMASTER-WP-0023-T01
2026-08-15 19:50:46 +02:00
status: done
2026-08-15 19:19:07 +02:00
priority: high
2026-08-15 19:35:38 +02:00
state_hub_task_id: "40d50848-d59e-4f7c-98df-a442dce90439"
2026-08-15 19:19:07 +02:00
```
2026-08-15 19:34:48 +02:00
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` .
2026-08-15 19:19:07 +02:00
2026-08-15 19:34:48 +02:00
**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.
2026-08-15 19:19:07 +02:00
2026-08-15 19:50:46 +02:00
Completed 2026-08-15: operator accepted the recommended table as
written. ADR-0008 records the postures, the per-family meaning of
`public` , the grant shape, and the 6443 exclusion. Readiness enums stay
distinct.
2026-08-15 19:19:07 +02:00
### T02 — Write the exposure contract
```task
id: RMASTER-WP-0023-T02
2026-08-15 20:08:37 +02:00
status: done
2026-08-15 19:19:07 +02:00
priority: high
2026-08-15 19:35:38 +02:00
state_hub_task_id: "96db2a2d-670c-4b98-b6f3-860f9b3009b3"
2026-08-15 19:19:07 +02:00
```
2026-08-15 19:34:48 +02:00
Depends on T01.
2026-08-15 19:19:07 +02:00
One contract, `docs/exposure-posture-contract.md` , that says what each
posture means **per family** :
| Family | `private` | `operator` | `public` |
| --- | --- | --- | --- |
2026-08-15 19:34:48 +02:00
| `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 |
2026-08-15 19:19:07 +02:00
Include the fail-closed rule: missing field means `private` .
2026-08-15 19:34:48 +02:00
Include the conflict rule: rapp `public` + reef without a public
substrate grant is invalid.
2026-08-15 19:19:07 +02:00
**Done when:** implementation repos can implement without inventing
vocabulary.
2026-08-15 20:08:37 +02:00
Completed 2026-08-15: `docs/exposure-posture-contract.md` names the
per-family reading, grant shape, fail-closed default, conflict rule,
6443 exclusion, and provider-delegated carve-out.
2026-08-15 19:19:07 +02:00
### T03 — Schema and validator
```task
id: RMASTER-WP-0023-T03
2026-08-15 20:08:37 +02:00
status: done
2026-08-15 19:19:07 +02:00
priority: high
2026-08-15 19:35:38 +02:00
state_hub_task_id: "03070be7-925e-4aee-9938-85b03b8cbe37"
2026-08-15 19:19:07 +02:00
```
Depends on T01.
2026-08-15 19:34:48 +02:00
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:
2026-08-15 19:19:07 +02:00
2026-08-15 19:34:48 +02:00
- a rapp or rail posture is `public` and the relevant **binding** is not
`production-approved`
2026-08-15 19:19:07 +02:00
- posture is `public` and no grant object is present (hostname / port /
2026-08-15 19:34:48 +02:00
reason / approved_on / residual-risk owner)
- a rapp is `public` and its reef has not granted a public surface
2026-08-15 19:19:07 +02:00
2026-08-15 19:34:48 +02:00
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` .
2026-08-15 19:19:07 +02:00
**Done when:** `tools/validate-family-declarations.py` rejects a public
rapp that is not production-approved, and the testdata tree has good and
2026-08-15 19:34:48 +02:00
bad fixtures. Live sibling repos are unchanged.
2026-08-15 19:19:07 +02:00
2026-08-15 20:08:37 +02:00
Completed 2026-08-15: additive `exposure` on all three family schemas.
Validator rejects unapproved public rapps, missing grants, public rapps
on a private reef, port 6443, and public provider-delegated reefs.
Fixtures: `good-exposure` and `bad-exposure` . Live declarations omitted
the field and still validate.
2026-08-15 19:34:48 +02:00
### T04 — Exception snapshot for already-public surfaces
2026-08-15 19:19:07 +02:00
```task
id: RMASTER-WP-0023-T04
2026-08-15 20:08:37 +02:00
status: done
2026-08-15 19:19:07 +02:00
priority: medium
2026-08-15 19:35:38 +02:00
state_hub_task_id: "6576cc7c-6c85-4d3b-b0cc-dfba4e9b4774"
2026-08-15 19:19:07 +02:00
```
2026-08-15 19:34:48 +02:00
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.
2026-08-15 19:19:07 +02:00
2026-08-15 19:34:48 +02:00
Out of scope: CoulombCore, shutdown plans, provider-native APIs.
2026-08-15 19:19:07 +02:00
2026-08-15 19:34:48 +02:00
**Done when:** every known public surface on `reef-railiance` is either a
snapshot grant or a routed task to close it.
2026-08-15 19:19:07 +02:00
2026-08-15 20:08:37 +02:00
Completed 2026-08-15:
`docs/evidence/reef-railiance-exposure-snapshot-2026-08-15.md` .
Grants: Forgejo, Coulomb Social, reuse-surface, Nydus 2224. Routed:
`bao.coulomb.social` verify/close; CoulombCore names stay out.
2026-08-15 19:34:48 +02:00
### T05 — Route enforcement work
2026-08-15 19:19:07 +02:00
```task
id: RMASTER-WP-0023-T05
2026-08-15 20:08:37 +02:00
status: done
2026-08-15 19:19:07 +02:00
priority: high
2026-08-15 19:35:38 +02:00
state_hub_task_id: "d780df59-c67d-486e-8e8a-15e5043cd513"
2026-08-15 19:19:07 +02:00
```
Depends on T01 and T02.
2026-08-15 19:34:48 +02:00
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.
2026-08-15 19:19:07 +02:00
| 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 |
2026-08-15 19:34:48 +02:00
| `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 |
2026-08-15 19:19:07 +02:00
| `railiance-enablement` | templates emit private Services + tunnel docs, never a public Ingress by default |
2026-08-15 19:34:48 +02:00
**Done when:** each owning repo has a workplan or an accepted intake,
with a back-link to this ID.
2026-08-15 19:19:07 +02:00
2026-08-15 20:08:37 +02:00
Completed 2026-08-15: intakes in
2026-08-15 20:52:04 +02:00
`docs/exposure-enforcement-intakes.md` . Child workplans filed:
`RAIL-K8S-WP-0003` , `REEF-RAILIANCE-WP-0004` , `RAIL-HO-WP-0010` ,
`RAIL-EN-WP-0001` .
2026-08-15 20:08:37 +02:00
2026-08-15 19:19:07 +02:00
## Suggested review order
2026-08-15 19:34:48 +02:00
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.
2026-08-15 19:19:07 +02:00
## Outcome
2026-08-15 20:08:37 +02:00
Finished 2026-08-15. ADR-0008, the exposure contract, additive schemas,
validator fixtures, the reef-railiance snapshot, and routed intakes are
in this repo. Packet-level enforcement stays with the owning repos.