feat(renderer): inClusterClients — authorable direct east-west ingress (schema 1.5.0)
A platform controller that dials a service's ClusterIP directly had NO authorable
form in the ServiceDefinition contract:
* ingressRoutes renders the north-south allow only — gateway-ingress admits
`fromEntities: [ingress]`, the Gateway's Envoy identity, and nothing else;
* dependencies compiles edges only BETWEEN CATALOG SERVICES, from the source
entry's own declaration.
Under the default-deny baseline (R-NET-01) such a client is therefore DROPPED, and
the only way to unblock it was a live `kubectl apply` of a hand-written CNP — a
cluster object outside git, one `kubectl delete` or one rebuild from coming back.
`inClusterClients: [{namespace, port, protocol?, name?}]` closes that. Each entry
renders ONE least-privilege ingress CiliumNetworkPolicy: this service's
endpointSelector, `fromEndpoints` matching ONLY `k8s:io.kubernetes.pod.namespace`,
`toPorts` carrying ONLY the declared port/protocol — never `fromEntities: all`.
Additive: the namespace default-deny keeps denying everything undeclared (R-NET-02),
and the entry is NOT a dependency edge, so it creates no wave ordering.
Shape notes:
* selected by NAMESPACE, not pod labels: an upstream controller's labels belong to
its chart, not to this platform's contract. The cost is explicit — every pod in
that namespace is admitted on that port, so the field is for platform-controller
namespaces (flux-system, monitoring), never a shared one. The schema description
says so.
* optional `name` (default: the namespace) only names the rendered policy,
`<service>-allow-<name>-ingress`. That name is operator-facing (kubectl get cnp,
a Hubble drop), and pinning it lets an allowance that already had to be applied
live be authored under its EXISTING name, so Flux adopts the live object in place
instead of creating a near-duplicate and leaving the original unmanaged.
* one field, not a policy DSL: no arbitrary selectors, entities or CIDRs.
Covered by render.py --selftest (4c): name/namespace/selector/port shape, no
fromEntities widening, default-deny still `ingress: []`, the gateway allow not
displaced, `name` defaulting to the namespace, and nothing rendered when the field is
absent. No golden changes here — no catalog entry uses the field yet.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
a863542bf7
commit
1f20506829
3 files changed
+258
-4
No files matched your search
@@ -21,6 +21,7 @@ platform must render, and cites the requirement each mapping satisfies. Changing
|
||||
| `egress` | `internet` | R-NET-05 |
|
||||
| `storage` | `[]` (config PVC still expected for stateful svc) | R-STATE-02 |
|
||||
| `dependencies` | `[]` → reaches **only DNS** (default-deny) | R-NET-01 |
|
||||
| `inClusterClients` | `[]` → no direct east-west client; only the Gateway may reach an exposed port | R-NET-01/02 |
|
||||
| `auth.mode` | `forward-auth` for any exposed UI | R-IDP-02 |
|
||||
| `update.channel` | `stable` | R-UPD-01 |
|
||||
| `update.autoRollbackOnFailedHealth` | `true` | R-UPD-03 |
|
||||
@@ -130,6 +131,33 @@ platform must render, and cites the requirement each mapping satisfies. Changing
|
||||
- The `host` pattern is enforced to `<label>.home.keithjackson.dev` by the schema.
|
||||
- **Requirements:** **R-TLS-01/02/03** (docs/08), **R-DNS-01/02/03** (docs/09).
|
||||
|
||||
### `inClusterClients[]` (`namespace` / `port` / `protocol` / `name`) — direct east-west clients
|
||||
- Each entry renders ONE **CiliumNetworkPolicy** ingress allow on this service:
|
||||
`endpointSelector` = this service, `fromEndpoints` matching **only** the reserved
|
||||
`k8s:io.kubernetes.pod.namespace: <namespace>` label, `toPorts` carrying **only** the declared
|
||||
`port`/`protocol`. Name: `<service>-allow-<name>-ingress`, `name` defaulting to `namespace`.
|
||||
- **Why the field exists.** `ingressRoutes` opens the **north-south** path only (the gateway allow
|
||||
admits `fromEntities: [ingress]`, the Gateway proxy identity, and nothing else), and
|
||||
`dependencies` compiles edges **only between catalog services** — both halves come from a catalog
|
||||
entry's own declaration. A **platform controller** that dials this service's ClusterIP directly
|
||||
was therefore unauthorable, and under default-deny it is dropped. Prod hit exactly that: its
|
||||
`GitRepository` points at the in-cluster Gitea Service
|
||||
(`http://gitea.gitea.svc.cluster.local:3000/...`, clusters/prod/kustomization.yaml) so GitOps
|
||||
depends on no public DNS/Cloudflare/Gateway/`${BASE_DOMAIN}`, but `flux-system`'s
|
||||
source-controller was denied — and the HTTPRoute that would have served a public name lives in a
|
||||
**downstream** tier that cannot apply while the source is down (source down → routes never rename
|
||||
→ source stays down).
|
||||
- **Least privilege is part of the contract:** a namespace and a port, never `fromEntities: all`,
|
||||
never a port-less allow. The client is selected by **namespace** rather than pod labels because an
|
||||
upstream controller's labels are its chart's to change, not this platform's contract — so name a
|
||||
platform-controller namespace whose entire membership may hold that reach, never a shared one.
|
||||
- **Additive, and not a dependency:** the namespace default-deny baseline is unchanged
|
||||
(`ingress: []`), every undeclared source/port stays denied, and the entry creates **no wave
|
||||
ordering edge** — the client is a platform component, not a catalog service.
|
||||
- **Requirements:** **R-NET-01** (default-deny baseline stands), **R-NET-02** (explicit allow / deny
|
||||
undeclared), **R-GITOPS-02** (the allowance is authored in the one reviewable object rather than
|
||||
surviving as a live-applied cluster object). Auditable via Hubble (R-SEC-03, R-OBS-04).
|
||||
|
||||
### `auth` (`mode` / `groups[]`) — identity integration (Keycloak / oauth2-proxy)
|
||||
- `oidc` → register the app as a **Keycloak OIDC client** (declarative realm/client config, GitOps;
|
||||
client secret SOPS-encrypted). App does its own login redirect.
|
||||
@@ -176,7 +204,8 @@ platform must render, and cites the requirement each mapping satisfies. Changing
|
||||
1. **kind ↔ source:** container⇒`image`, vm⇒`vmTemplate` (schema-enforced).
|
||||
2. **auth ↔ groups:** `oidc`/`forward-auth` ⇒ non-empty `groups` (schema-enforced; R-IDP-06).
|
||||
3. **default-deny is unconditional:** every namespace gets the deny baseline regardless of
|
||||
`dependencies`; `dependencies` only *adds* allows (R-NET-01/02).
|
||||
`dependencies`; `dependencies` only *adds* allows (R-NET-01/02). The same holds for
|
||||
`inClusterClients`: it adds one namespace+port ingress allow and never relaxes the baseline.
|
||||
4. **egress is enforced outside the workload:** `vpn` renders gateway steering + kill-switch, never a
|
||||
per-service sidecar the app could bypass (R-NET-05/06).
|
||||
5. **TLS is never self-signed and needs no per-service step:** the wildcard Gateway cert covers every
|
||||
@@ -187,6 +216,6 @@ platform must render, and cites the requirement each mapping satisfies. Changing
|
||||
|
||||
## Requirement traceability (contract-level)
|
||||
Primary **R-GITOPS-02** (single reviewable object wires every requirement). Field-level: R-NET-01/02
|
||||
(dependencies), R-NET-05/06 (egress), R-STATE-01/02/04 (storage), R-STATE-03 (backup),
|
||||
(dependencies, inClusterClients), R-NET-05/06 (egress), R-STATE-01/02/04 (storage), R-STATE-03 (backup),
|
||||
R-TLS-01/02/03 + R-DNS-01/02/03 (ingressRoutes + exposure), R-OBS-01 (observability),
|
||||
R-UPD-01/02/03 (update), R-IDP-02/06 (auth), R-PLAT-02/03 (kind/source).
|
||||
Reference in new issue
Block a user