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