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/repo-family-bootstrap-contract.md](docs/repo-family-bootstrap-contract.md)
- [docs/rail-composition-contract.md](docs/rail-composition-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/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/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-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) - [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. Ops-bridge is the normal path to a shielded thing.
The detailed per-family reading lives in The detailed per-family reading lives in
[`docs/exposure-posture-contract.md`](../exposure-posture-contract.md) [`docs/exposure-posture-contract.md`](../exposure-posture-contract.md).
once `RMASTER-WP-0023-T02` writes it.
## Consequences ## 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 - a rapp `ownership_repo` must not be the rapp repo itself
- declared member repos, named rails, and named reefs must resolve on disk - declared member repos, named rails, and named reefs must resolve on disk
- a deployable name may belong to at most one rapp - 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) ## Rail schema (T04)

View file

@ -108,6 +108,20 @@
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
"items": { "$ref": "#/$defs/sourceDocument" } "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": [ "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": { "$defs": {
@ -173,6 +204,35 @@
"repo": { "$ref": "#/$defs/repoSlug" }, "repo": { "$ref": "#/$defs/repoSlug" },
"path": { "type": "string", "minLength": 1 } "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, "minItems": 1,
"items": { "$ref": "#/$defs/sourceDocument" }, "items": { "$ref": "#/$defs/sourceDocument" },
"description": "Documents this declaration was derived from. All three live rapps already carry this consistently." "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": [ "allOf": [
@ -119,6 +137,23 @@
"description": "consumers requires consumer_contract.", "description": "consumers requires consumer_contract.",
"if": { "required": ["consumers"] }, "if": { "required": ["consumers"] },
"then": { "required": ["consumer_contract"] } "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": { "$defs": {
@ -356,6 +391,35 @@
"repo": { "$ref": "#/$defs/repoSlug" }, "repo": { "$ref": "#/$defs/repoSlug" },
"path": { "type": "string", "minLength": 1 } "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", "type": "array",
"minItems": 1, "minItems": 1,
"items": { "$ref": "#/$defs/sourceDocument" } "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": { "$defs": {
"repoSlug": { "repoSlug": {
"type": "string", "type": "string",
@ -111,6 +149,35 @@
"repo": { "$ref": "#/$defs/repoSlug" }, "repo": { "$ref": "#/$defs/repoSlug" },
"path": { "type": "string", "minLength": 1 } "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 2. sibling-field constraints JSON Schema cannot express
3. declared member repos, named rails, and named reefs resolve on disk 3. declared member repos, named rails, and named reefs resolve on disk
4. reef bound_rapps matches the projection of rapp.bound_reefs 4. reef bound_rapps matches the projection of rapp.bound_reefs
5. a deployable name belongs to at most one rapp 5. exposure posture rules from ADR-0008 (public needs a grant and
6. if --inventory is given, every live deployable belongs to exactly one rapp 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 This repo does not query a cluster. Live coverage consumes an inventory
file produced by an implementation repo (RMASTER-WP-0021-T06). 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) FLOATING_PIN = re.compile(r"^(latest|[\^~*]|.*\*|.*x$)", re.IGNORECASE)
SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
FORBIDDEN_PUBLIC_PORTS = {6443}
@dataclass @dataclass
@ -260,6 +264,119 @@ def check_resolution(loaded: Loaded, present: set[str], root: Path, report: Repo
need(f"hosted_rails[{i}]", rail) 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: def check_bound_rapps(loaded_docs: list[Loaded], report: Report) -> None:
derived: dict[str, set[str]] = defaultdict(set) derived: dict[str, set[str]] = defaultdict(set)
rapp_ids: set[str] = set() rapp_ids: set[str] = set()
@ -466,6 +583,7 @@ def validate(
check_reef(loaded, report) check_reef(loaded, report)
check_resolution(loaded, present, root, report) check_resolution(loaded, present, root, report)
check_bound_rapps(loaded_docs, report) check_bound_rapps(loaded_docs, report)
check_exposure(loaded_docs, report)
check_deployable_uniqueness(loaded_docs, report) check_deployable_uniqueness(loaded_docs, report)
check_undeclared(repos, loaded_docs, root, report) check_undeclared(repos, loaded_docs, root, report)
if inventory_path is not None: if inventory_path is not None:
@ -521,6 +639,30 @@ def self_test() -> int:
if not bad.errors: if not bad.errors:
failures.append("bad-stale-bound produced no 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 = [ live_rails = [
Path("/home/worsch/rail-kubernetes"), Path("/home/worsch/rail-kubernetes"),
Path("/home/worsch/rail-knative"), Path("/home/worsch/rail-knative"),
@ -541,7 +683,9 @@ def self_test() -> int:
return 1 return 1
print("self-test ok") print("self-test ok")
print(f" good: {good.files} file(s), {len(good.errors)} error(s)") 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-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 return 0

View file

@ -4,8 +4,8 @@ type: workplan
title: "Private-by-default exposure until production admission" title: "Private-by-default exposure until production admission"
domain: financials domain: financials
repo: railiance-master repo: railiance-master
status: active status: finished
owner: bernd owner: grok
topic_slug: railiance topic_slug: railiance
planning_priority: high planning_priority: high
planning_order: 23 planning_order: 23
@ -212,7 +212,7 @@ distinct.
```task ```task
id: RMASTER-WP-0023-T02 id: RMASTER-WP-0023-T02
status: todo status: done
priority: high priority: high
state_hub_task_id: "96db2a2d-670c-4b98-b6f3-860f9b3009b3" 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 **Done when:** implementation repos can implement without inventing
vocabulary. 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 ### T03 — Schema and validator
```task ```task
id: RMASTER-WP-0023-T03 id: RMASTER-WP-0023-T03
status: wait status: done
priority: high priority: high
state_hub_task_id: "03070be7-925e-4aee-9938-85b03b8cbe37" 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 rapp that is not production-approved, and the testdata tree has good and
bad fixtures. Live sibling repos are unchanged. 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 ### T04 — Exception snapshot for already-public surfaces
```task ```task
id: RMASTER-WP-0023-T04 id: RMASTER-WP-0023-T04
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "6576cc7c-6c85-4d3b-b0cc-dfba4e9b4774" 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 **Done when:** every known public surface on `reef-railiance` is either a
snapshot grant or a routed task to close it. 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 ### T05 — Route enforcement work
```task ```task
id: RMASTER-WP-0023-T05 id: RMASTER-WP-0023-T05
status: wait status: done
priority: high priority: high
state_hub_task_id: "d780df59-c67d-486e-8e8a-15e5043cd513" 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, **Done when:** each owning repo has a workplan or an accepted intake,
with a back-link to this ID. 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 ## Suggested review order
1. T01 (human) — accept or change the recommended table. 1. T01 (human) — accept or change the recommended table.
@ -328,6 +349,6 @@ with a back-link to this ID.
## Outcome ## Outcome
T01 accepted 2026-08-15 (`status: active`). ADR-0008 is the record. Finished 2026-08-15. ADR-0008, the exposure contract, additive schemas,
Next: T02 contract and T04 exception snapshot. Implementation stays out validator fixtures, the reef-railiance snapshot, and routed intakes are
of this repo. in this repo. Packet-level enforcement stays with the owning repos.