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.
185 lines
6.8 KiB
Markdown
185 lines
6.8 KiB
Markdown
# 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`
|