railiance-master/docs/exposure-posture-contract.md
codex a0c35b7438 feat(RMASTER-WP-0026): declare Taxonomy layer and consume NetKingdom §20
Add layer.yaml, RMASTER-ADR-0009, the consumption contract, and a tracked
non-answer for the five §20.3 questions. Split admission, exposure, and
authorization without renaming schema fields. Finish the workplan.

Assistant: grok
Assistant-Session: 01a04c9f-cd6b-7741-bce0-f1d9d1b3c3bc
2026-08-29 12:52:35 +02:00

6.9 KiB

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. Neither is an authorization decision. Whether an actor may perform an action is access-engine (ADR-0009).

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:

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