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:
codex 2026-08-15 20:08:37 +02:00
parent 6301798fab
commit 4a664533d3
24 changed files with 998 additions and 13 deletions

View file

@ -17,6 +17,7 @@ recorded before they are spread across implementation repos.
- [docs/repo-family-bootstrap-contract.md](docs/repo-family-bootstrap-contract.md)
- [docs/rail-composition-contract.md](docs/rail-composition-contract.md)
- [docs/reef-production-readiness-contract.md](docs/reef-production-readiness-contract.md)
- [docs/exposure-posture-contract.md](docs/exposure-posture-contract.md)
- [docs/qonto-knative-runtime-contract.md](docs/qonto-knative-runtime-contract.md)
- [docs/adr/ADR-0001-repository-prefix-architecture.md](docs/adr/ADR-0001-repository-prefix-architecture.md)
- [docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md](docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md)

View file

@ -73,8 +73,7 @@ enum.
Ops-bridge is the normal path to a shielded thing.
The detailed per-family reading lives in
[`docs/exposure-posture-contract.md`](../exposure-posture-contract.md)
once `RMASTER-WP-0023-T02` writes it.
[`docs/exposure-posture-contract.md`](../exposure-posture-contract.md).
## Consequences

View file

@ -0,0 +1,58 @@
# reef-railiance exposure snapshot
Date: 2026-08-15
Status: Dated snapshot under RMASTER-WP-0023-T04. Not the living source
of truth. T05 files grants into owning declarations.
Scope: public listeners on `reef-railiance` (current member
`Railiance01`). CoulombCore is out. Provider-native APIs are out.
This is not a shutdown plan.
Grant shape matches ADR-0008: hostname or port, reason, `approved_on`,
residual-risk owner.
## Snapshot grants
These surfaces are already published. They stay up. Each row is a grant
the owning declaration should carry.
| hostname or port | Owning rapp or provider | Why it is public | Residual-risk owner |
| --- | --- | --- | --- |
| `forgejo.coulomb.social` | `rapp-forgejo` (decided, not yet materialized; today `railiance-apps` / `railiance-infra`) | Canonical git forge, OCI, and npm registry for the estate | `railiance-infra` |
| `app.coulomb.social` | Coulomb Social (`coulomb-social` / `railiance-apps`; no `rapp-*` yet) | Public web app on Railiance01 | `railiance-apps` |
| `reuse.coulomb.social` | reuse-surface (`railiance-apps`; no `rapp-*` yet) | Public federation / reuse landing | `railiance-apps` |
| port `2224` | HostEurope Nydus (`nydus-ex-api`) | Provider VPS agent; required by the platform | `railiance-infra` |
Reef-level substrate reading: `80/443` are the host ports those
hostnames need. `reef-railiance` should carry a public substrate grant
for that DNS/Ingress surface when the declaration grows `exposure`.
Host UFW on Railiance01 currently declares OpenSSH and Nydus `2224`
only (RAIL-HO-WP-0009). The names above are still published DNS; the
host-port grant is what makes 80/443 legal under this contract.
## Not grants
| Surface | Why it is not a grant here |
| --- | --- |
| `22/tcp` SSH | Operator path, not a published application listener |
| `6443/tcp` k3s API | Closed by RAIL-HO-WP-0009; not grantable as `public` |
| `bao.coulomb.social` | OpenBao public name. Target on Railiance01 is private (`public_ingress: false`). If the name still answers on this reef, that is a routed close, not a grant |
| `kc.coulomb.social` | Keycloak; last recorded DNS still targeted CoulombCore |
| `gitea.coulomb.social` | Archived Gitea on CoulombCore |
| `coulomb.social` apex | Still Bubble.io until an explicit apex cutover |
| Scaleway S3 / `reef-storage` | Provider-native API; out of this enum |
## Routed follow-ups (not shutdowns)
| Item | Route |
| --- | --- |
| File the four snapshot grants into owning declarations once `exposure` exists | T05 → `reef-railiance`, `rapp-forgejo` (or `railiance-apps` until that rapp exists) |
| Confirm `bao.coulomb.social` does not still publish on Railiance01; close DNS or keep it private | `railiance-platform` / RMASTER-WP-0020 |
| Materialize `rapp-forgejo` so the forge grant has a family home | already decided in ADR-0003 / `docs/rapp-first-wave-candidates.md` |
| Name rapps for Coulomb Social and reuse-surface, or accept layer-repo ownership until then | `railiance-apps` |
## Done-when
Every known public surface on `reef-railiance` is either a snapshot
grant above or a routed follow-up. Living updates happen in the owning
declarations, not by editing this date-stamped file.

View file

@ -0,0 +1,73 @@
# Exposure enforcement intakes
Date: 2026-08-15
Source: `RMASTER-WP-0023-T05`
Back-link: `RMASTER-WP-0023`
These are intakes for the owning-repo agents. File a workplan in the
owning repo. Do not implement the controls from `railiance-master`.
Normative reading:
- [`docs/adr/ADR-0008-private-by-default-exposure.md`](adr/ADR-0008-private-by-default-exposure.md)
- [`docs/exposure-posture-contract.md`](exposure-posture-contract.md)
- Snapshot of already-public surfaces:
[`docs/evidence/reef-railiance-exposure-snapshot-2026-08-15.md`](evidence/reef-railiance-exposure-snapshot-2026-08-15.md)
## Intake: `rail-kubernetes` (cc `railiance-cluster` until the rail owns networking)
Enforce the packet path for rapps on Kubernetes:
- default-deny NetworkPolicy
- ClusterIP as the paved Service
- no public Ingress class unless the rapp has `exposure.posture: public`
and a grant, the binding is `production-approved`, and the reef has
granted a public surface
- a rail `exposure: public` is permission to *emit* Ingress for those
granted bindings, not a blanket public default
Do not define what "production-safe" means. That stays ADR-0006.
Suggested workplan id in the owning repo: a `RAIL-K8S-WP-*` or
`RAILIANCE-WP-*` that cites `RMASTER-WP-0023`.
## Intake: `reef-railiance`
First live admission of the family rule:
- new binds stay `private` (or `operator` only for a named admin path)
- add `exposure` to `declarations/reef.yaml` with substrate grants for
the snapshot rows that belong on the reef (`80/443` DNS/Ingress
surface, Nydus `2224`)
- do not take down Forgejo, Coulomb Social, reuse-surface, or Nydus
- do not re-public `6443`
Suggested workplan id: a `REEF-RAILIANCE-WP-*` that cites
`RMASTER-WP-0023`.
## Intake: `railiance-infra`
Host surface for **new** reefs:
- do not open 80/443 until the reef declaration carries a public
substrate grant
- `6443` stays operator-only (RAIL-HO-WP-0009 / ADR-005). This contract
is not a way to reopen it
- this is not the rapp admission desk. A rapp that shares Railiance01
ingress is gated by the rail/reef grants, not by a new S1 UFW rule
per rapp
- Nydus `2224` stays a named exception, already declared
Suggested workplan id: a `RAIL-HO-WP-*` that cites `RMASTER-WP-0023`.
## Intake: `railiance-enablement`
Templates and paved paths:
- emit private Services and tunnel docs by default
- never emit a public Ingress in a new-rapp or new-rail template
- if a template mentions `exposure`, default the field to `private` or
omit it (fail-closed)
Suggested workplan id: an enablement workplan that cites
`RMASTER-WP-0023`.

View 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`

View file

@ -98,6 +98,16 @@ JSON Schema cannot express these; `tools/validate-family-declarations.py` does:
- a rapp `ownership_repo` must not be the rapp repo itself
- declared member repos, named rails, and named reefs must resolve on disk
- a deployable name may belong to at most one rapp
- `exposure.posture: public` on a rapp requires `exposure.grant`,
`binding_admission: production-approved`, and a bound reef that has
granted a public surface (ADR-0008)
- `exposure.posture: public` on a rail requires `exposure.grant` and
`readiness_state: production-approved`
- `exposure.posture: public` on a reef requires `exposure.grants`; a
provider-delegated reef (no rail) cannot be public
- grant port `6443` is never legal
- omitted `exposure` means `private`; the field is additive and must
not be confused with rapp `data_classification: public`
## Rail schema (T04)

View file

@ -108,6 +108,20 @@
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/sourceDocument" }
},
"exposure": {
"type": "object",
"additionalProperties": false,
"required": ["posture"],
"description": "Which listener classes this rail may emit. Optional; omitted means private (ADR-0008). public is permission to emit Ingress for a granted binding, not a blanket public default.",
"properties": {
"posture": {
"enum": ["private", "operator", "public"],
"description": "Default if exposure is omitted: private."
},
"grant": { "$ref": "#/$defs/exposureGrant" },
"tunnel": { "$ref": "#/$defs/exposureTunnel" }
}
}
},
"allOf": [
@ -144,6 +158,23 @@
]
}
}
},
{
"description": "public rail exposure requires a grant object.",
"if": {
"required": ["exposure"],
"properties": {
"exposure": {
"required": ["posture"],
"properties": { "posture": { "const": "public" } }
}
}
},
"then": {
"properties": {
"exposure": { "required": ["grant"] }
}
}
}
],
"$defs": {
@ -173,6 +204,35 @@
"repo": { "$ref": "#/$defs/repoSlug" },
"path": { "type": "string", "minLength": 1 }
}
},
"exposureGrant": {
"type": "object",
"additionalProperties": false,
"required": ["reason", "approved_on", "residual_risk_owner"],
"description": "Accepted residual risk for a public rail listener. Must name a hostname or a port. Port 6443 is rejected by the validator.",
"properties": {
"hostname": { "type": "string", "minLength": 1 },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"reason": { "type": "string", "minLength": 1 },
"approved_on": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
"residual_risk_owner": { "type": "string", "minLength": 1 }
},
"anyOf": [
{ "required": ["hostname"] },
{ "required": ["port"] }
]
},
"exposureTunnel": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"description": "Named ops-bridge / SSH tunnel to the rail API or admin path. Does not change packet posture.",
"properties": {
"name": { "type": "string", "minLength": 1 },
"local_port": { "type": "integer", "minimum": 1, "maximum": 65535 }
}
}
}
}

View file

@ -108,6 +108,24 @@
"minItems": 1,
"items": { "$ref": "#/$defs/sourceDocument" },
"description": "Documents this declaration was derived from. All three live rapps already carry this consistently."
},
"exposure": {
"type": "object",
"additionalProperties": false,
"required": ["posture"],
"description": "Who may reach this rapp's listener. Optional; omitted means private (ADR-0008). Distinct from data_classification.",
"properties": {
"posture": {
"enum": ["private", "operator", "public"],
"description": "Intended consumer-facing listener. Default if exposure is omitted: private."
},
"grant": { "$ref": "#/$defs/exposureGrant" },
"tunnel": { "$ref": "#/$defs/exposureTunnel" },
"binding_admission": {
"enum": ["declared", "installed", "verified", "production-approved", "deprecated"],
"description": "ADR-0006 admission state of the reef binding this public listener sits on. Not rapp readiness_state. Required by the validator when posture is public."
}
}
}
},
"allOf": [
@ -119,6 +137,23 @@
"description": "consumers requires consumer_contract.",
"if": { "required": ["consumers"] },
"then": { "required": ["consumer_contract"] }
},
{
"description": "public rapp exposure requires a grant object.",
"if": {
"required": ["exposure"],
"properties": {
"exposure": {
"required": ["posture"],
"properties": { "posture": { "const": "public" } }
}
}
},
"then": {
"properties": {
"exposure": { "required": ["grant"] }
}
}
}
],
"$defs": {
@ -356,6 +391,35 @@
"repo": { "$ref": "#/$defs/repoSlug" },
"path": { "type": "string", "minLength": 1 }
}
},
"exposureGrant": {
"type": "object",
"additionalProperties": false,
"required": ["reason", "approved_on", "residual_risk_owner"],
"description": "Accepted residual risk for a public listener. Must name a hostname or a port. Port 6443 is rejected by the validator.",
"properties": {
"hostname": { "type": "string", "minLength": 1 },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"reason": { "type": "string", "minLength": 1 },
"approved_on": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
"residual_risk_owner": { "type": "string", "minLength": 1 }
},
"anyOf": [
{ "required": ["hostname"] },
{ "required": ["port"] }
]
},
"exposureTunnel": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"description": "Named ops-bridge / SSH tunnel. Does not change packet posture.",
"properties": {
"name": { "type": "string", "minLength": 1 },
"local_port": { "type": "integer", "minimum": 1, "maximum": 65535 }
}
}
}
}

View file

@ -78,8 +78,46 @@
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/sourceDocument" }
},
"exposure": {
"type": "object",
"additionalProperties": false,
"required": ["posture"],
"description": "Host-port and public-DNS surface this reef publishes. Optional; omitted means private (ADR-0008). public is a substrate grant, not reef lifecycle_state. Provider-delegated reefs must not set public.",
"properties": {
"posture": {
"enum": ["private", "operator", "public"],
"description": "Default if exposure is omitted: private."
},
"grants": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/exposureGrant" },
"description": "Substrate grants. Required by schema when posture is public."
},
"tunnel": { "$ref": "#/$defs/exposureTunnel" }
}
}
},
"allOf": [
{
"description": "public reef exposure requires at least one substrate grant.",
"if": {
"required": ["exposure"],
"properties": {
"exposure": {
"required": ["posture"],
"properties": { "posture": { "const": "public" } }
}
}
},
"then": {
"properties": {
"exposure": { "required": ["grants"] }
}
}
}
],
"$defs": {
"repoSlug": {
"type": "string",
@ -111,6 +149,35 @@
"repo": { "$ref": "#/$defs/repoSlug" },
"path": { "type": "string", "minLength": 1 }
}
},
"exposureGrant": {
"type": "object",
"additionalProperties": false,
"required": ["reason", "approved_on", "residual_risk_owner"],
"description": "Accepted residual risk for a reef host port or public DNS name we publish. Must name a hostname or a port. Port 6443 is rejected by the validator.",
"properties": {
"hostname": { "type": "string", "minLength": 1 },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"reason": { "type": "string", "minLength": 1 },
"approved_on": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
"residual_risk_owner": { "type": "string", "minLength": 1 }
},
"anyOf": [
{ "required": ["hostname"] },
{ "required": ["port"] }
]
},
"exposureTunnel": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"description": "Named ops-bridge / SSH path to this reef. Does not change packet posture.",
"properties": {
"name": { "type": "string", "minLength": 1 },
"local_port": { "type": "integer", "minimum": 1, "maximum": 65535 }
}
}
}
}

View file

@ -0,0 +1,12 @@
kind: execution-rail
rail_id: rail-kubernetes
repo: rail-kubernetes
ownership_repo: railiance-cluster
contract_version: 1.0.0
composition_kind: base
execution_architecture: kubernetes
readiness_state: verified
required_substrate_capabilities:
- kubernetes-api
supported_rollout_modes:
- stage1-run

View file

@ -0,0 +1,37 @@
kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-nogrant
repo: rapp-nogrant
ownership_repo: railiance-platform
contract_version: 1.0.0
readiness_state: declared
workload_identity:
name: nogrant
package_type: helm-managed-platform-service
data_classification: internal
criticality: medium
primary_rail: rail-kubernetes
supported_rails:
- rail-kubernetes
bound_reefs:
- reef-example
runtime_dependencies:
- kubernetes-api
composition:
purpose: Fixture rapp used to exercise exposure validation.
member_repos:
- repo: rapp-nogrant
role: package
deployables:
- nogrant
rollout_contract:
default_mode: helm-upgrade-install
smoke_contract:
required:
- example-status-ok
rollback_contract:
order:
- helm-rollback
exposure:
posture: public
binding_admission: production-approved

View file

@ -0,0 +1,42 @@
kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-unapproved
repo: rapp-unapproved
ownership_repo: railiance-platform
contract_version: 1.0.0
readiness_state: declared
workload_identity:
name: unapproved
package_type: helm-managed-platform-service
data_classification: internal
criticality: medium
primary_rail: rail-kubernetes
supported_rails:
- rail-kubernetes
bound_reefs:
- reef-example
runtime_dependencies:
- kubernetes-api
composition:
purpose: Fixture rapp used to exercise exposure validation.
member_repos:
- repo: rapp-unapproved
role: package
deployables:
- unapproved
rollout_contract:
default_mode: helm-upgrade-install
smoke_contract:
required:
- example-status-ok
rollback_contract:
order:
- helm-rollback
exposure:
posture: public
binding_admission: verified
grant:
hostname: unapproved.example
reason: should fail
approved_on: "2026-08-15"
residual_risk_owner: railiance-platform

View file

@ -0,0 +1,42 @@
kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-wrongreef
repo: rapp-wrongreef
ownership_repo: railiance-platform
contract_version: 1.0.0
readiness_state: declared
workload_identity:
name: wrongreef
package_type: helm-managed-platform-service
data_classification: internal
criticality: medium
primary_rail: rail-kubernetes
supported_rails:
- rail-kubernetes
bound_reefs:
- reef-closed
runtime_dependencies:
- kubernetes-api
composition:
purpose: Fixture rapp used to exercise exposure validation.
member_repos:
- repo: rapp-wrongreef
role: package
deployables:
- wrongreef
rollout_contract:
default_mode: helm-upgrade-install
smoke_contract:
required:
- example-status-ok
rollback_contract:
order:
- helm-rollback
exposure:
posture: public
binding_admission: production-approved
grant:
hostname: closed.example
reason: reef has no public surface
approved_on: "2026-08-15"
residual_risk_owner: railiance-platform

View file

@ -0,0 +1,12 @@
kind: substrate-reef
reef_id: reef-closed
repo: reef-closed
ownership_repo: railiance-infra
substrate_kind: server-group
lifecycle_state: active
criticality: high
primary_rail: rail-kubernetes
hosted_rails:
- rail-kubernetes
current_members:
- Closed01

View file

@ -0,0 +1,20 @@
kind: substrate-reef
reef_id: reef-example
repo: reef-example
ownership_repo: railiance-infra
substrate_kind: server-group
lifecycle_state: active
criticality: high
primary_rail: rail-kubernetes
hosted_rails:
- rail-kubernetes
current_members:
- Example01
exposure:
posture: public
grants:
- hostname: example.coulomb.social
port: 443
reason: fixture public DNS surface
approved_on: "2026-08-15"
residual_risk_owner: railiance-infra

View file

@ -0,0 +1,19 @@
kind: substrate-reef
reef_id: reef-k3s
repo: reef-k3s
ownership_repo: railiance-infra
substrate_kind: server-group
lifecycle_state: active
criticality: high
primary_rail: rail-kubernetes
hosted_rails:
- rail-kubernetes
current_members:
- K3s01
exposure:
posture: public
grants:
- port: 6443
reason: must be rejected
approved_on: "2026-08-15"
residual_risk_owner: railiance-infra

View file

@ -0,0 +1,15 @@
kind: substrate-reef
reef_id: reef-storage-example
repo: reef-storage-example
ownership_repo: railiance-platform
substrate_kind: object-storage
lifecycle_state: planned
criticality: high
current_members: []
exposure:
posture: public
grants:
- hostname: s3.example
reason: provider API is not a listener we open
approved_on: "2026-08-15"
residual_risk_owner: railiance-platform

View file

@ -0,0 +1,8 @@
{
"source": "fixture",
"captured_at": "2026-08-15T00:00:00Z",
"reef": "reef-example",
"deployables": [
{"name": "example", "namespace": "example", "kind": "helm-release"}
]
}

View file

@ -0,0 +1,21 @@
kind: execution-rail
rail_id: rail-knative
repo: rail-knative
ownership_repo: railiance-cluster
contract_version: 1.0.0
composition_kind: derived
base_rail: rail-kubernetes
base_rail_contract: 1.0.0
execution_architecture: knative-serving-on-kubernetes
readiness_state: verified
required_substrate_capabilities:
- kubernetes-api
- knative-serving
supported_rollout_modes:
- revision-canary
inherited_semantics:
- workload-identity
overridden_semantics:
- request-activation
compatibility_constraints:
- rail-kubernetes contract must equal 1.0.0

View file

@ -0,0 +1,12 @@
kind: execution-rail
rail_id: rail-kubernetes
repo: rail-kubernetes
ownership_repo: railiance-cluster
contract_version: 1.0.0
composition_kind: base
execution_architecture: kubernetes
readiness_state: verified
required_substrate_capabilities:
- kubernetes-api
supported_rollout_modes:
- stage1-run

View file

@ -0,0 +1,42 @@
kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-example
repo: rapp-example
ownership_repo: railiance-platform
contract_version: 1.0.0
readiness_state: declared
workload_identity:
name: example
package_type: helm-managed-platform-service
data_classification: internal
criticality: medium
primary_rail: rail-kubernetes
supported_rails:
- rail-kubernetes
bound_reefs:
- reef-example
runtime_dependencies:
- kubernetes-api
composition:
purpose: Fixture rapp used to exercise exposure validation.
member_repos:
- repo: rapp-example
role: package
deployables:
- example
rollout_contract:
default_mode: helm-upgrade-install
smoke_contract:
required:
- example-status-ok
rollback_contract:
order:
- helm-rollback
exposure:
posture: public
binding_admission: production-approved
grant:
hostname: example.coulomb.social
reason: fixture public rapp
approved_on: "2026-08-15"
residual_risk_owner: railiance-platform

View file

@ -0,0 +1,21 @@
kind: substrate-reef
reef_id: reef-example
repo: reef-example
ownership_repo: railiance-infra
substrate_kind: server-group
lifecycle_state: active
criticality: high
primary_rail: rail-kubernetes
hosted_rails:
- rail-kubernetes
- rail-knative
current_members:
- Example01
exposure:
posture: public
grants:
- hostname: example.coulomb.social
port: 443
reason: fixture public DNS surface
approved_on: "2026-08-15"
residual_risk_owner: railiance-infra

View file

@ -11,8 +11,11 @@ checks, in this order:
2. sibling-field constraints JSON Schema cannot express
3. declared member repos, named rails, and named reefs resolve on disk
4. reef bound_rapps matches the projection of rapp.bound_reefs
5. a deployable name belongs to at most one rapp
6. if --inventory is given, every live deployable belongs to exactly one rapp
5. exposure posture rules from ADR-0008 (public needs a grant and
approved binding; rapp public needs a public reef surface; 6443 is
never grantable; provider-delegated reefs cannot be public)
6. a deployable name belongs to at most one rapp
7. if --inventory is given, every live deployable belongs to exactly one rapp
This repo does not query a cluster. Live coverage consumes an inventory
file produced by an implementation repo (RMASTER-WP-0021-T06).
@ -53,6 +56,7 @@ DECL_BY_PREFIX = {
}
FLOATING_PIN = re.compile(r"^(latest|[\^~*]|.*\*|.*x$)", re.IGNORECASE)
SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
FORBIDDEN_PUBLIC_PORTS = {6443}
@dataclass
@ -260,6 +264,119 @@ def check_resolution(loaded: Loaded, present: set[str], root: Path, report: Repo
need(f"hosted_rails[{i}]", rail)
def exposure_block(doc: dict[str, Any]) -> dict[str, Any] | None:
block = doc.get("exposure")
return block if isinstance(block, dict) else None
def posture_of(doc: dict[str, Any]) -> str:
block = exposure_block(doc)
if block is None:
return "private"
posture = block.get("posture")
return posture if isinstance(posture, str) else "private"
def grants_of(loaded: Loaded) -> list[dict[str, Any]]:
block = exposure_block(loaded.doc) if isinstance(loaded.doc, dict) else None
if block is None:
return []
if loaded.family == "reef":
return [item for item in as_list(block.get("grants")) if isinstance(item, dict)]
grant = block.get("grant")
return [grant] if isinstance(grant, dict) else []
def reef_has_public_surface(loaded: Loaded) -> bool:
return (
loaded.family == "reef"
and isinstance(loaded.doc, dict)
and posture_of(loaded.doc) == "public"
and bool(grants_of(loaded))
)
def reject_forbidden_ports(loaded: Loaded, report: Report) -> None:
field_prefix = "exposure.grants" if loaded.family == "reef" else "exposure.grant"
for i, grant in enumerate(grants_of(loaded)):
port = grant.get("port")
if port in FORBIDDEN_PUBLIC_PORTS:
loc = field_prefix if loaded.family != "reef" else f"{field_prefix}[{i}].port"
if loaded.family != "reef":
loc = f"{field_prefix}.port"
report.add(
loaded.rel,
loc,
f"port {port} is not a grantable public surface",
)
def check_exposure(loaded_docs: list[Loaded], report: Report) -> None:
reefs: dict[str, Loaded] = {}
for loaded in loaded_docs:
if loaded.family != "reef" or not isinstance(loaded.doc, dict):
continue
reef_id = loaded.doc.get("reef_id")
if isinstance(reef_id, str):
reefs[reef_id] = loaded
for loaded in loaded_docs:
if not isinstance(loaded.doc, dict):
continue
reject_forbidden_ports(loaded, report)
posture = posture_of(loaded.doc)
block = exposure_block(loaded.doc) or {}
if loaded.family == "reef":
hosted = as_list(loaded.doc.get("hosted_rails"))
primary = loaded.doc.get("primary_rail")
provider_delegated = not primary and not hosted
if posture == "public" and provider_delegated:
report.add(
loaded.rel,
"exposure.posture",
"provider-delegated reef cannot use exposure.public",
)
if posture == "public" and not grants_of(loaded):
report.add(
loaded.rel,
"exposure.grants",
"public reef requires at least one substrate grant",
)
continue
if posture != "public":
continue
if not grants_of(loaded):
report.add(
loaded.rel,
"exposure.grant",
"public posture requires a grant object",
)
if loaded.family == "rail" and loaded.doc.get("readiness_state") != "production-approved":
report.add(
loaded.rel,
"readiness_state",
"public rail requires readiness_state production-approved",
)
if loaded.family != "rapp":
continue
admission = block.get("binding_admission")
if admission != "production-approved":
got = f"; got {admission!r}" if admission is not None else ""
report.add(
loaded.rel,
"exposure.binding_admission",
"public requires binding_admission production-approved "
f"(not rapp readiness_state){got}",
)
bound = [item for item in as_list(loaded.doc.get("bound_reefs")) if isinstance(item, str)]
if not any(reef_id in reefs and reef_has_public_surface(reefs[reef_id]) for reef_id in bound):
report.add(
loaded.rel,
"exposure.posture",
"public rapp is not bound to a reef that has granted a public surface",
)
def check_bound_rapps(loaded_docs: list[Loaded], report: Report) -> None:
derived: dict[str, set[str]] = defaultdict(set)
rapp_ids: set[str] = set()
@ -466,6 +583,7 @@ def validate(
check_reef(loaded, report)
check_resolution(loaded, present, root, report)
check_bound_rapps(loaded_docs, report)
check_exposure(loaded_docs, report)
check_deployable_uniqueness(loaded_docs, report)
check_undeclared(repos, loaded_docs, root, report)
if inventory_path is not None:
@ -521,6 +639,30 @@ def self_test() -> int:
if not bad.errors:
failures.append("bad-stale-bound produced no errors")
exposed_root = TESTDATA / "good-exposure"
exposed = validate(exposed_root, discover_repos(exposed_root), validators)
if exposed.errors:
failures.append(
"good-exposure fixture produced errors:\n "
+ "\n ".join(str(f) for f in exposed.errors)
)
bad_exp_root = TESTDATA / "bad-exposure"
bad_exp = validate(bad_exp_root, discover_repos(bad_exp_root), validators)
exp_messages = "\n".join(str(f) for f in bad_exp.errors)
expected_exposure = [
("binding_admission production-approved", "unapproved public rapp"),
("requires a grant object", "public rapp without grant"),
("not bound to a reef that has granted a public surface", "public rapp on a private reef"),
("port 6443 is not a grantable public surface", "6443 grant"),
("provider-delegated reef cannot use exposure.public", "public storage reef"),
]
for needle, label in expected_exposure:
if not any(needle in f.message for f in bad_exp.errors):
failures.append(f"bad-exposure did not flag {label}:\n{exp_messages}")
if not bad_exp.errors:
failures.append("bad-exposure produced no errors")
live_rails = [
Path("/home/worsch/rail-kubernetes"),
Path("/home/worsch/rail-knative"),
@ -541,7 +683,9 @@ def self_test() -> int:
return 1
print("self-test ok")
print(f" good: {good.files} file(s), {len(good.errors)} error(s)")
print(f" good-exposure: {exposed.files} file(s), {len(exposed.errors)} error(s)")
print(f" bad-stale-bound: {bad.files} file(s), {len(bad.errors)} error(s) (expected)")
print(f" bad-exposure: {bad_exp.files} file(s), {len(bad_exp.errors)} error(s) (expected)")
return 0

View file

@ -4,8 +4,8 @@ type: workplan
title: "Private-by-default exposure until production admission"
domain: financials
repo: railiance-master
status: active
owner: bernd
status: finished
owner: grok
topic_slug: railiance
planning_priority: high
planning_order: 23
@ -212,7 +212,7 @@ distinct.
```task
id: RMASTER-WP-0023-T02
status: todo
status: done
priority: high
state_hub_task_id: "96db2a2d-670c-4b98-b6f3-860f9b3009b3"
```
@ -236,11 +236,15 @@ 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
```task
id: RMASTER-WP-0023-T03
status: wait
status: done
priority: high
state_hub_task_id: "03070be7-925e-4aee-9938-85b03b8cbe37"
```
@ -268,11 +272,17 @@ Name the field so it cannot be confused with rapp
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
```task
id: RMASTER-WP-0023-T04
status: todo
status: done
priority: medium
state_hub_task_id: "6576cc7c-6c85-4d3b-b0cc-dfba4e9b4774"
```
@ -293,11 +303,16 @@ 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
```task
id: RMASTER-WP-0023-T05
status: wait
status: done
priority: high
state_hub_task_id: "d780df59-c67d-486e-8e8a-15e5043cd513"
```
@ -318,6 +333,12 @@ there from this repo.
**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`. State Hub messages sent to
`rail-kubernetes`, `railiance-cluster`, `reef-railiance`,
`railiance-infra`, and `railiance-enablement`. Owning-repo agents file
the workplans.
## Suggested review order
1. T01 (human) — accept or change the recommended table.
@ -328,6 +349,6 @@ with a back-link to this ID.
## Outcome
T01 accepted 2026-08-15 (`status: active`). ADR-0008 is the record.
Next: T02 contract and T04 exception snapshot. Implementation stays out
of this repo.
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.