railiance-master/workplans/RMASTER-WP-0023-private-by-default-exposure.md
codex c465d36ead chore: align WP-0020 with ADR-0008 and note 0023 children
Route public bao.coulomb.social as a close, not a grant. Record the
filed child workplans for private-by-default enforcement.
2026-08-15 20:52:04 +02:00

14 KiB

id type title domain repo status owner topic_slug planning_priority planning_order created updated related related_repos state_hub_workstream_id
RMASTER-WP-0023 workplan Private-by-default exposure until production admission financials railiance-master finished grok railiance high 23 2026-08-15 2026-08-15
ADR-0006
ADR-0008
RMASTER-WP-0017
RMASTER-WP-0019
RMASTER-WP-0021
RMASTER-WP-0022
RAIL-HO-WP-0009
railiance-master
rail-kubernetes
railiance-cluster
reef-railiance
reef-storage
railiance-infra
railiance-enablement
ops-bridge
50303bdf-aada-4c38-8782-d4e63b603e65

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 (declaredinstalledverifiedproduction-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 (T01, accepted 2026-08-15)

Ratified as written. Recorded in docs/adr/ADR-0008-private-by-default-exposure.md.

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

id: RMASTER-WP-0023-T01
status: done
priority: high
state_hub_task_id: "40d50848-d59e-4f7c-98df-a442dce90439"

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.

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.

T02 — Write the exposure contract

id: RMASTER-WP-0023-T02
status: done
priority: high
state_hub_task_id: "96db2a2d-670c-4b98-b6f3-860f9b3009b3"

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.

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.

T03 — Schema and validator

id: RMASTER-WP-0023-T03
status: done
priority: high
state_hub_task_id: "03070be7-925e-4aee-9938-85b03b8cbe37"

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.

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.

T04 — Exception snapshot for already-public surfaces

id: RMASTER-WP-0023-T04
status: done
priority: medium
state_hub_task_id: "6576cc7c-6c85-4d3b-b0cc-dfba4e9b4774"

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.

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.

T05 — Route enforcement work

id: RMASTER-WP-0023-T05
status: done
priority: high
state_hub_task_id: "d780df59-c67d-486e-8e8a-15e5043cd513"

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.

Completed 2026-08-15: intakes in docs/exposure-enforcement-intakes.md. Child workplans filed: RAIL-K8S-WP-0003, REEF-RAILIANCE-WP-0004, RAIL-HO-WP-0010, RAIL-EN-WP-0001.

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

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.