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
+190
-1
@@ -4,7 +4,8 @@
|
||||
Consumes ONE ServiceDefinition (schemas/servicedefinition.schema.json, catalog/*.yaml)
|
||||
and emits the concrete manifest set the renderer contract specifies
|
||||
(schemas/renderer-contract.md): Namespace + default-deny baseline, Deployment/Service or
|
||||
KubeVirt VirtualMachine + CDI DataVolume, PVCs, per-dependency CiliumNetworkPolicy allows,
|
||||
KubeVirt VirtualMachine + CDI DataVolume, PVCs, per-dependency and per-in-cluster-client
|
||||
CiliumNetworkPolicy allows,
|
||||
egress-class steering label, HTTPRoute(s), ServiceMonitor/alerts, Flux image-automation,
|
||||
and the auth-mode / backup wiring hints.
|
||||
|
||||
@@ -116,6 +117,7 @@ def apply_defaults(sd: dict) -> dict:
|
||||
s.setdefault("storage", [])
|
||||
s.setdefault("dependencies", [])
|
||||
s.setdefault("ingressRoutes", [])
|
||||
s.setdefault("inClusterClients", [])
|
||||
|
||||
# auth.mode default posture: forward-auth for any exposed UI, else none (internal, no
|
||||
# UI). An explicit auth block always wins (R-IDP-02).
|
||||
@@ -141,6 +143,13 @@ def apply_defaults(sd: dict) -> dict:
|
||||
s["storage"] = [{**{"class": "longhorn", "readOnly": False}, **v} for v in s["storage"]]
|
||||
# dependency protocol default is tcp.
|
||||
s["dependencies"] = [{**{"protocol": "tcp"}, **d} for d in s["dependencies"]]
|
||||
# in-cluster client protocol default is tcp (as for a dependency); its `name` — which only
|
||||
# ever appears in the rendered POLICY name — defaults to the namespace it admits, so the
|
||||
# common case declares just `namespace` + `port` and reads like a dependency edge.
|
||||
s["inClusterClients"] = [
|
||||
{**{"protocol": "tcp", "name": c.get("namespace", "")}, **c}
|
||||
for c in s["inClusterClients"]
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
@@ -700,6 +709,82 @@ def r_gateway_ingress_policy(s: dict):
|
||||
}
|
||||
|
||||
|
||||
def r_in_cluster_client_policies(s: dict) -> list:
|
||||
"""East-west allow: let a declared IN-CLUSTER client reach this service DIRECTLY (R-NET-02).
|
||||
|
||||
`ingressRoutes` opens the NORTH-SOUTH path only — r_gateway_ingress_policy admits the
|
||||
reserved `ingress` identity (the Gateway proxy) and nothing else — and `dependencies`
|
||||
compiles edges only BETWEEN CATALOG SERVICES (both halves are rendered from a catalog
|
||||
entry's own declaration). A PLATFORM CONTROLLER that dials this service's ClusterIP
|
||||
directly therefore had no authorable form at all, and under default-deny (R-NET-01) it is
|
||||
DROPPED.
|
||||
|
||||
That gap is not theoretical: prod's `flux-system` source-controller clones the platform's
|
||||
own repo from the in-cluster Gitea Service (clusters/prod/kustomization.yaml pins
|
||||
`url: http://gitea.gitea.svc.cluster.local:3000/...` so prod's GitOps depends on neither
|
||||
public DNS, Cloudflare, its own Gateway nor ${BASE_DOMAIN}). gitea-allow-gateway-ingress
|
||||
admits `fromEntities: [ingress]` only, so the clone was denied, the source could not
|
||||
reconcile — and the HTTPRoute that would have served a public name instead lives in a
|
||||
DOWNSTREAM tier that cannot apply while the source is down. Source down -> routes never
|
||||
rename -> source stays down. It had to be broken with a live `kubectl apply`, i.e. with a
|
||||
cluster object outside git, which one `kubectl delete` or one rebuild would take away.
|
||||
|
||||
Each entry renders ONE least-privilege ingress CiliumNetworkPolicy, shaped exactly like the
|
||||
reciprocal-dependency half (r_reverse_dependency_policies) but selecting the client by
|
||||
NAMESPACE alone, because a platform controller's pod labels are the upstream chart's to
|
||||
change and are not part of this platform's contract: `fromEndpoints` carrying ONLY the
|
||||
reserved `k8s:io.kubernetes.pod.namespace` label, `toPorts` carrying ONLY the declared
|
||||
port/protocol. Never `fromEntities: all`, and never the whole service. Additive: the
|
||||
namespace default-deny baseline keeps denying every undeclared source and port.
|
||||
|
||||
The policy NAME is `<service>-allow-<name>-ingress`, `name` defaulting to the namespace —
|
||||
it is operator-facing (`kubectl get cnp`, a Hubble drop), and letting an author pin it is
|
||||
what allows an allowance that already had to be applied live to be authored under its
|
||||
EXISTING name, so Flux adopts that object in place rather than creating a near-duplicate
|
||||
beside it and leaving the live one unmanaged.
|
||||
"""
|
||||
out = []
|
||||
for client in s["inClusterClients"]:
|
||||
proto = client.get("protocol", "tcp").upper()
|
||||
out.append(
|
||||
{
|
||||
"apiVersion": "cilium.io/v2",
|
||||
"kind": "CiliumNetworkPolicy",
|
||||
"metadata": {
|
||||
"name": f"{s['name']}-allow-{client['name']}-ingress",
|
||||
"namespace": s["name"],
|
||||
"labels": {"app.kubernetes.io/name": s["name"]},
|
||||
},
|
||||
"spec": {
|
||||
"endpointSelector": {
|
||||
"matchLabels": {"app.kubernetes.io/name": s["name"]}
|
||||
},
|
||||
"ingress": [
|
||||
{
|
||||
"fromEndpoints": [
|
||||
{
|
||||
"matchLabels": {
|
||||
"k8s:io.kubernetes.pod.namespace": client[
|
||||
"namespace"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"toPorts": [
|
||||
{
|
||||
"ports": [
|
||||
{"port": str(client["port"]), "protocol": proto}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _route_meta(s: dict, route: dict) -> dict:
|
||||
meta = {
|
||||
"name": f"{s['name']}-{route['port']}",
|
||||
@@ -1292,6 +1377,15 @@ def render(sd: dict, image_automation: bool = True, inbound: list | None = None)
|
||||
)
|
||||
)
|
||||
|
||||
for cnp in r_in_cluster_client_policies(s):
|
||||
docs.append(
|
||||
(
|
||||
"CiliumNetworkPolicy in-cluster client ingress allow (R-NET-02): "
|
||||
f"{cnp['metadata']['name']}",
|
||||
cnp,
|
||||
)
|
||||
)
|
||||
|
||||
ie = r_internet_egress(s)
|
||||
docs.append(
|
||||
(
|
||||
@@ -1808,6 +1902,101 @@ def _selftest() -> int:
|
||||
"a non-exposed service must not render a gateway-ingress allow (least privilege)",
|
||||
)
|
||||
|
||||
# 4c) inClusterClients -> one ADDITIVE, least-privilege ingress allow per entry that admits a
|
||||
# PLATFORM controller's NAMESPACE (not the reserved `ingress` identity, not `all`) to ONE
|
||||
# declared port. This is what keeps a cluster whose GitOps source is an in-cluster Service
|
||||
# reconcilable: fromEntities[ingress] does not cover a controller dialling the ClusterIP.
|
||||
icc_docs = render(
|
||||
{
|
||||
"name": "g",
|
||||
"source": {"image": "a:1"},
|
||||
"ingressRoutes": [{"host": "g.home.keithjackson.dev", "port": 3000}],
|
||||
"auth": {"mode": "none"},
|
||||
"inClusterClients": [
|
||||
{"name": "flux", "namespace": "flux-system", "port": 3000}
|
||||
],
|
||||
}
|
||||
)
|
||||
icc = [
|
||||
d[1]
|
||||
for d in icc_docs
|
||||
if d[1]["kind"] == "CiliumNetworkPolicy"
|
||||
and d[1]["metadata"]["name"] == "g-allow-flux-ingress"
|
||||
]
|
||||
check(
|
||||
len(icc) == 1,
|
||||
"an inClusterClients entry must render one ingress CNP named "
|
||||
"<service>-allow-<name>-ingress",
|
||||
)
|
||||
if icc:
|
||||
p = icc[0]
|
||||
check(
|
||||
p["metadata"]["namespace"] == "g"
|
||||
and p["spec"]["endpointSelector"]["matchLabels"]
|
||||
== {"app.kubernetes.io/name": "g"},
|
||||
"in-cluster client allow must live in the service's namespace and select the service",
|
||||
)
|
||||
ing = p["spec"]["ingress"]
|
||||
check(
|
||||
ing[0]["fromEndpoints"]
|
||||
== [{"matchLabels": {"k8s:io.kubernetes.pod.namespace": "flux-system"}}],
|
||||
"in-cluster client allow must match the client NAMESPACE only (reserved k8s: label)",
|
||||
)
|
||||
check(
|
||||
"fromEntities" not in ing[0],
|
||||
"in-cluster client allow must not widen to an entity (never fromEntities: all)",
|
||||
)
|
||||
icc_ports = {
|
||||
(pt["port"], pt["protocol"])
|
||||
for r in ing
|
||||
for tp in r["toPorts"]
|
||||
for pt in tp["ports"]
|
||||
}
|
||||
check(
|
||||
icc_ports == {("3000", "TCP")},
|
||||
"in-cluster client allow must be least-privilege (only the declared port/protocol)",
|
||||
)
|
||||
# ADDITIVE: default-deny ingress stays [] and the gateway allow is untouched.
|
||||
icc_dd = next(d[1] for d in icc_docs if d[1]["kind"] == "NetworkPolicy")
|
||||
check(
|
||||
icc_dd["spec"]["ingress"] == [],
|
||||
"in-cluster client allow must be ADDITIVE — default-deny ingress stays [] (R-NET-01)",
|
||||
)
|
||||
check(
|
||||
any(
|
||||
d[1]["metadata"].get("name") == "g-allow-gateway-ingress"
|
||||
for d in icc_docs
|
||||
if d[1]["kind"] == "CiliumNetworkPolicy"
|
||||
),
|
||||
"in-cluster client allow must not displace the north-south gateway allow",
|
||||
)
|
||||
# `name` is OPTIONAL and defaults to the namespace, so the common case is namespace + port.
|
||||
icc_default = render(
|
||||
{
|
||||
"name": "g",
|
||||
"source": {"image": "a:1"},
|
||||
"inClusterClients": [{"namespace": "monitoring", "port": 9090}],
|
||||
}
|
||||
)
|
||||
check(
|
||||
any(
|
||||
d[1]["metadata"].get("name") == "g-allow-monitoring-ingress"
|
||||
for d in icc_default
|
||||
if d[1]["kind"] == "CiliumNetworkPolicy"
|
||||
),
|
||||
"inClusterClients.name must default to the admitted namespace",
|
||||
)
|
||||
# A service that declares none must render NO such policy (grant-nothing-extra).
|
||||
check(
|
||||
not any(
|
||||
d[1]["kind"] == "CiliumNetworkPolicy"
|
||||
and d[1]["metadata"]["name"].endswith("-ingress")
|
||||
and "fromEndpoints" in d[1]["spec"].get("ingress", [{}])[0]
|
||||
for d in render({"name": "q", "source": {"image": "a:1"}})
|
||||
),
|
||||
"a service declaring no inClusterClients must render no in-cluster client allow",
|
||||
)
|
||||
|
||||
# 5) vm kind -> VirtualMachine + standalone DataVolume.
|
||||
vm_docs = render(
|
||||
{
|
||||
|
||||
@@ -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).
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://home.keithjackson.dev/schemas/servicedefinition.schema.json",
|
||||
"title": "ServiceDefinition",
|
||||
"description": "Operator-facing onboarding contract for the home k3s + Longhorn platform. One ServiceDefinition per file under catalog/. Captures INTENT; the platform compiles it into concrete resources (see schemas/renderer-contract.md). Canonical source of truth: spec docs/13-service-catalog.md. Serves R-GITOPS-02; ties into R-NET-*, R-STATE-*, R-TLS-*, R-DNS-*, R-OBS-*, R-UPD-*, R-IDP-*. Schema version: 1.4.0 (makes the `bulk` tier REAL: a bulk storage entry now carries `export` (the absolute NFS export path on the ${BULK_NFS_SERVER} host) and renders a STATIC, pre-bound NFS PersistentVolume + PVC over data that already exists outside the cluster, plus optional `subPath` and `readOnly` on any entry. Resolves the OQ-2 bulk-backing stub for STATIC pre-existing datasets; dynamic bulk provisioning remains unimplemented and unneeded). 1.3.0 added optional pod-level podSecurityContext for the container kind — k8s PodSecurityContext emitted verbatim onto the pod spec; its fsGroup unblocks non-root images e.g. semaphore/UID-1001 writing a mounted Longhorn PVC). 1.2.0 added optional per-container env/envFrom/lifecycle for the container kind — k8s EnvVar/EnvFromSource/Lifecycle shapes emitted verbatim onto the app container; unblocks headless-install/DB-backed apps e.g. gitea/nextcloud/semaphore/unifi/minecraft. 1.1.0 added the shape invariant exposure:public => auth.mode!=none (docs/13 v1.1 / docs/15 / R-IDP-02).",
|
||||
"description": "Operator-facing onboarding contract for the home k3s + Longhorn platform. One ServiceDefinition per file under catalog/. Captures INTENT; the platform compiles it into concrete resources (see schemas/renderer-contract.md). Canonical source of truth: spec docs/13-service-catalog.md. Serves R-GITOPS-02; ties into R-NET-*, R-STATE-*, R-TLS-*, R-DNS-*, R-OBS-*, R-UPD-*, R-IDP-*. Schema version: 1.5.0 (adds optional `inClusterClients` — additional in-cluster clients, named by NAMESPACE, admitted to a declared port of this service DIRECTLY, bypassing the shared Gateway. `ingressRoutes` admits only the Gateway proxy identity and `dependencies` only links catalog services, so a PLATFORM controller dialling a Service ClusterIP had no authorable form at all; that gap is what deadlocked prod's GitOps when its GitRepository moved to the in-cluster Gitea Service. Renders one least-privilege ingress CiliumNetworkPolicy per entry — the named namespace and the declared port, nothing wider). 1.4.0 (makes the `bulk` tier REAL: a bulk storage entry now carries `export` (the absolute NFS export path on the ${BULK_NFS_SERVER} host) and renders a STATIC, pre-bound NFS PersistentVolume + PVC over data that already exists outside the cluster, plus optional `subPath` and `readOnly` on any entry. Resolves the OQ-2 bulk-backing stub for STATIC pre-existing datasets; dynamic bulk provisioning remains unimplemented and unneeded). 1.3.0 added optional pod-level podSecurityContext for the container kind — k8s PodSecurityContext emitted verbatim onto the pod spec; its fsGroup unblocks non-root images e.g. semaphore/UID-1001 writing a mounted Longhorn PVC). 1.2.0 added optional per-container env/envFrom/lifecycle for the container kind — k8s EnvVar/EnvFromSource/Lifecycle shapes emitted verbatim onto the app container; unblocks headless-install/DB-backed apps e.g. gitea/nextcloud/semaphore/unifi/minecraft. 1.1.0 added the shape invariant exposure:public => auth.mode!=none (docs/13 v1.1 / docs/15 / R-IDP-02).",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "source"],
|
||||
@@ -257,6 +257,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"inClusterClients": {
|
||||
"type": "array",
|
||||
"description": "Additional IN-CLUSTER clients, identified by their NAMESPACE, permitted to reach this service's port DIRECTLY — east-west, bypassing the shared Gateway (docs/03; R-NET-01/02). Neither existing field can express this: `ingressRoutes` opens only the north-south path from the Gateway proxy identity (`fromEntities: [ingress]`), and `dependencies` compiles edges only between CATALOG services. A PLATFORM controller that dials this service's ClusterIP is therefore unauthorable — which is exactly how prod's GitOps deadlocked once its GitRepository moved to the in-cluster Gitea Service (source-controller DENIED => the source cannot reconcile => the downstream tier that would fix it never applies). Each entry renders ONE least-privilege ingress CiliumNetworkPolicy: this service's endpointSelector, `fromEndpoints` matching ONLY the named namespace, on ONLY the declared port — never `fromEntities: all`. Additive: the default-deny baseline still denies everything undeclared (R-NET-01). It is NOT a dependency edge and creates no wave ordering — the client is a platform component, not a catalog service.",
|
||||
"default": [],
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["namespace", "port"],
|
||||
"properties": {
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "The client's namespace, and the WHOLE identity that is matched (`k8s:io.kubernetes.pod.namespace`): every pod in it is admitted on the declared port. Name a platform-controller namespace whose entire membership may legitimately hold that reach (flux-system, monitoring), never a shared or multi-tenant one.",
|
||||
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
|
||||
"maxLength": 63
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port on THIS service the client may reach (the Service/container port). One entry per port; an undeclared port stays denied.",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["tcp", "udp"],
|
||||
"default": "tcp"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Short client name used in the rendered policy name `<service>-allow-<name>-ingress`. Defaults to `namespace`. It exists because that name is operator-facing — it is what `kubectl get cnp` and a Hubble drop show — and because an allowance already applied live can then be authored under its EXISTING name, so Flux adopts the live object in place instead of creating a near-duplicate and leaving an unmanaged leftover (gitea/flux-system: `gitea-allow-flux-ingress`).",
|
||||
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
|
||||
"maxLength": 40
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"description": "SSO integration (docs/15, ADR-0014; R-IDP-02/06). Default posture for any exposed UI is forward-auth unless it declares native OIDC (oidc) or a documented exception (native).",
|
||||
|
||||
Reference in new issue
Block a user