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
|
|
@ -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`
|
||||
Loading…
Add table
Add a link
Reference in a new issue