feat: implement RMASTER-WP-0023 private-by-default exposure
Add the exposure contract, additive family schema fields, validator checks and fixtures, the reef-railiance exception snapshot, and routed intakes. Enforcement stays in the owning repos.
This commit is contained in:
parent
6301798fab
commit
4a664533d3
24 changed files with 998 additions and 13 deletions
185
docs/exposure-posture-contract.md
Normal file
185
docs/exposure-posture-contract.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Exposure Posture Contract
|
||||
|
||||
Date: 2026-08-15
|
||||
Status: Adopted under ADR-0008 / RMASTER-WP-0023-T02
|
||||
|
||||
## Purpose
|
||||
|
||||
Say what `private`, `operator`, and `public` mean for each family, so
|
||||
implementation repos can enforce one rule without inventing vocabulary.
|
||||
|
||||
ADR-0008 is the decision. This file is the per-family reading. Schema
|
||||
shapes live in `schemas/{rapp,rail,reef}.schema.json`. Do not restate
|
||||
those fields here except to name the objects the validator checks.
|
||||
|
||||
Admission (ADR-0006) answers "may this binding run in production?"
|
||||
Exposure answers "who may reach the listener?" A
|
||||
`production-approved` binding is still private until a grant exists.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
| 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 |
|
||||
|
||||
The field name is `exposure`. Rapp `data_classification: public` is a
|
||||
different field.
|
||||
|
||||
Missing `exposure` means `private`. That is fail-closed.
|
||||
|
||||
`operator` does not open a host port or a public Ingress. A ClusterIP
|
||||
Service plus a named tunnel is still `private` at the packet layer.
|
||||
|
||||
Default for a new reef, rail, or rapp is `private`. Use `operator` only
|
||||
for a named admin or API surface that must be reachable before admission.
|
||||
|
||||
## What this contract covers
|
||||
|
||||
Listeners we control:
|
||||
|
||||
- host ports on a compute reef
|
||||
- Ingress and Service types a rail may emit
|
||||
- public DNS names we publish
|
||||
|
||||
Out of this enum:
|
||||
|
||||
- provider-native internet APIs (Scaleway S3 and other
|
||||
provider-delegated endpoints)
|
||||
- CoulombCore host inventory until it is a declared reef
|
||||
- Q7 / Goss declared-vs-live reaction
|
||||
|
||||
`6443` / the k3s API is not a grantable public surface.
|
||||
|
||||
## Grant object
|
||||
|
||||
A grant is accepted residual risk, not a self-serve wish. Required
|
||||
fields:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `hostname` or `port` | at least one; the listener being granted |
|
||||
| `reason` | why this must be public |
|
||||
| `approved_on` | ISO date `YYYY-MM-DD` |
|
||||
| `residual_risk_owner` | who accepts the residual risk |
|
||||
|
||||
A working deploy, a `hosts_rail` / `binds_rapp` line, or an Ingress
|
||||
object is not a grant.
|
||||
|
||||
Rapp and rail carry a single `exposure.grant` when `posture` is
|
||||
`public`. A compute reef carries `exposure.grants` (a list) because one
|
||||
substrate can publish more than one host or DNS surface.
|
||||
|
||||
## Per-family meaning
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
|
||||
### Rapp
|
||||
|
||||
`exposure.posture` is the intended consumer-facing listener.
|
||||
|
||||
`public` is legal only when all of these hold:
|
||||
|
||||
1. `exposure.grant` is present
|
||||
2. `exposure.binding_admission` is `production-approved`
|
||||
3. at least one bound reef has `exposure.posture: public` and a
|
||||
substrate grant
|
||||
|
||||
`binding_admission` is the ADR-0006 state of the reef binding being
|
||||
published. It is not `readiness_state` on `rapp.yaml`. Do not add
|
||||
`production-approved` to the rapp readiness enum for this purpose.
|
||||
|
||||
A rapp cannot be `public` on a reef that has not granted a public
|
||||
surface. Conflicting declarations fail closed.
|
||||
|
||||
### Rail
|
||||
|
||||
`exposure.posture` is which listener classes the rail may emit.
|
||||
|
||||
- `private` (default): the rail must not emit a public Ingress class.
|
||||
ClusterIP is the paved Service.
|
||||
- `operator`: a named tunnel to the rail's own API or admin path.
|
||||
- `public`: the rail may emit public Ingress **for a granted binding**.
|
||||
The rail still needs its own `exposure.grant`, and
|
||||
`readiness_state` must be `production-approved`. A rapp grant is
|
||||
still required before a specific hostname is published.
|
||||
|
||||
A rail that is `private` may still *host* a public rapp if the reef has
|
||||
granted a public surface and the rapp has a grant. The rail's posture
|
||||
is about what the rail itself publishes (its API, default Ingress
|
||||
class), not a veto on every granted rapp. Implementation repos enforce
|
||||
"no public Ingress unless a grant exists" at the rapp/binding, not by
|
||||
forcing every rail to `public`.
|
||||
|
||||
### Reef
|
||||
|
||||
`exposure.posture` is the host-port and public-DNS surface we publish.
|
||||
|
||||
- `private` (default): no extra host ports; no public DNS for new
|
||||
members. SSH for operator login is the ops path, not a public grant.
|
||||
- `operator`: ops-bridge / SSH only. No 80/443.
|
||||
- `public`: one or more substrate grants in `exposure.grants`.
|
||||
|
||||
A reef has no `production-approved` lifecycle. `public` on a reef is
|
||||
the substrate grant, not `lifecycle_state`.
|
||||
|
||||
A provider-delegated reef (no `primary_rail`, no `hosted_rails`) must
|
||||
not set `exposure.posture: public`. The provider API is not a listener
|
||||
we open.
|
||||
|
||||
## Operator tunnel
|
||||
|
||||
When `posture` is `operator`, name the tunnel:
|
||||
|
||||
```yaml
|
||||
exposure:
|
||||
posture: operator
|
||||
tunnel:
|
||||
name: k3s-api-railiance01
|
||||
local_port: 16444
|
||||
```
|
||||
|
||||
The tunnel is documentation of the paved path. It does not change
|
||||
packet reachability from the internet.
|
||||
|
||||
## Enforcement split
|
||||
|
||||
| Owner | What to enforce |
|
||||
| --- | --- |
|
||||
| `rail-kubernetes` / `railiance-cluster` | default-deny NetworkPolicy; no public Ingress unless the grant exists; ClusterIP as the paved Service |
|
||||
| `reef-railiance` | new binds stay `private` (or `operator` only for a named admin path); existing public binds receive named grants |
|
||||
| `railiance-infra` | new reefs do not get 80/443 until a reef substrate grant exists; 6443 stays operator-only |
|
||||
| `railiance-enablement` | templates emit private Services + tunnel docs, never a public Ingress by default |
|
||||
|
||||
This contract does not install those controls. Routing is
|
||||
`RMASTER-WP-0023-T05`.
|
||||
|
||||
## Existing public surfaces
|
||||
|
||||
A dated snapshot of already-public listeners on `reef-railiance` lives
|
||||
in
|
||||
[`docs/evidence/reef-railiance-exposure-snapshot-2026-08-15.md`](evidence/reef-railiance-exposure-snapshot-2026-08-15.md).
|
||||
That snapshot is not the living source of truth. Grants move into the
|
||||
owning declarations.
|
||||
|
||||
## Schema and validator
|
||||
|
||||
`exposure` is additive. Existing declarations that omit it remain
|
||||
valid and mean `private`.
|
||||
|
||||
`tools/validate-family-declarations.py` fails when:
|
||||
|
||||
- a rapp is `public` and `binding_admission` is not
|
||||
`production-approved`
|
||||
- a rail is `public` and `readiness_state` is not
|
||||
`production-approved`
|
||||
- posture is `public` and the grant object is missing
|
||||
- a rapp is `public` and no bound reef has granted a public surface
|
||||
- a grant names port `6443`
|
||||
- a provider-delegated reef sets `posture: public`
|
||||
Loading…
Add table
Add a link
Reference in a new issue