servicemonitor-ports-resolve could not audit a PodMonitor with no in-repo Service, and no-plaintext-secrets requires a template's secret value to be a bare placeholder. Co-Authored-By: Claude Opus 5.5 <[email protected]>
150 lines
6.4 KiB
YAML
150 lines
6.4 KiB
YAML
---
|
|
# Namespace baseline: deny everything both ways, except DNS to kube-system.
|
|
#
|
|
# SINGLE OWNER. This namespace is not labelled app-namespace=true, so Kyverno's
|
|
# require-default-deny-baseline generate rule does not select it and never writes this object;
|
|
# Flux is its only writer (the same posture as kured / crafty / headlamp / homepage). Do NOT add
|
|
# the app-namespace label to namespace.yaml without deleting this object in the same change, or
|
|
# the two controllers fight over it every reconcile (the 2026-09-30 drift-loop root cause).
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: default-deny
|
|
namespace: cloudflared
|
|
labels:
|
|
homelab.keithjackson.dev/baseline: default-deny
|
|
spec:
|
|
podSelector: {}
|
|
policyTypes:
|
|
- Ingress
|
|
- Egress
|
|
ingress: []
|
|
egress:
|
|
- to:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: UDP
|
|
port: 53
|
|
- protocol: TCP
|
|
port: 53
|
|
---
|
|
# The ONLY ingress: Prometheus scraping :2000. Shipped in the same file as the baseline on purpose
|
|
# (the Longhorn/kured lesson): a deny-all without this allow leaves the ServiceMonitor's targets
|
|
# DISCOVERED and every scrape timing out, which is the state the CloudflaredTunnelDown rule is
|
|
# written to fire on rather than go quiet in.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: cloudflared-allow-prometheus-scrape
|
|
namespace: cloudflared
|
|
labels:
|
|
app.kubernetes.io/name: cloudflared
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: cloudflared
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: monitoring
|
|
ports:
|
|
- protocol: TCP
|
|
port: 2000
|
|
---
|
|
# Egress: the Cloudflare edge, the shared Gateway, and the Gateway's backends — nothing else.
|
|
#
|
|
# 1. world tcp+udp/7844 — the tunnel itself (QUIC on udp, HTTP/2 fallback on tcp).
|
|
# world tcp/443 — Cloudflare's API / update endpoints that cloudflared calls at start.
|
|
# Expressed as `world` + port rather than Cloudflare CIDRs: the edge ranges are Cloudflare's to
|
|
# change, and a stale CIDR list would fail as a silent outage. Narrowing to toCIDR is a
|
|
# possible later hardening; it is not done blind.
|
|
#
|
|
# 2. `ingress` entity tcp/443 — the shared Gateway's Envoy. Traffic to the
|
|
# cilium-gateway-shared Service is handed to Envoy, whose identity is reserved:ingress.
|
|
#
|
|
# 3. The Gateway's BACKENDS, by namespace. NOT optional: Cilium >= 1.16.8 (CVE-2025-30162; this
|
|
# cluster runs 1.20.x) makes the Gateway's Envoy re-evaluate THIS pod's egress policy against
|
|
# the identity of the backend it routes the request to. With only rule 2, Envoy accepts the TCP
|
|
# connection and then answers every request `403 Access denied` — measured for buildkit ->
|
|
# gitea on 2026-10-30, see platform/buildkit/networkpolicy.yaml. So the backends are named:
|
|
# * any namespace opted in to the shared Gateway (gateway-access=shared) — which is the set
|
|
# of namespaces that can serve a route at all, so this tracks new apps automatically;
|
|
# * MINUS the LAN/VPN-only services (operator decisions 2026-11-08): plex, jackett, pihole,
|
|
# semaphore, qbittorrent, radarr, sonarr, headlamp. config.yaml already 404s their
|
|
# hostnames; this is the second, independent layer: even a request that got past the
|
|
# ingress rules would be refused by Envoy for these backends.
|
|
# unifi and crafty-mc are not here because they are L4 LoadBalancer VIPs, not Gateway
|
|
# backends; the crafty WEB UI (crafty.<domain>, an HTTPRoute) is deliberately reachable.
|
|
#
|
|
# WHY NOT the `- {}` allow-all egress gitea-actions uses (apps/wave-1/gitea-actions.yaml)?
|
|
# It would satisfy the L7 re-check just as well, but `{}` admits EVERY identity, including
|
|
# kube-apiserver and host — which operator decision 4 (the Kubernetes API is never reachable
|
|
# through the tunnel) rules out for the one pod that carries internet traffic in. The
|
|
# namespace-label selector is the narrowest rule that still names every possible backend.
|
|
# Verified 2026-11-08: Cilium identities carry `k8s:io.cilium.k8s.namespace.labels.
|
|
# gateway-access`, and every namespace with an HTTPRoute on the shared Gateway is labelled
|
|
# gateway-access=shared.
|
|
#
|
|
# BACKENDS NEED NO INGRESS CHANGE. Envoy forwards to a backend as reserved:ingress, which every
|
|
# backend already admits; the source-identity check happens on THIS pod's egress, not on the
|
|
# destination's ingress. Measured 2026-10-30 (decisions/2026-10-30-gitea-admit-buildkit.md):
|
|
# gitea-actions is in none of Gitea's ingress allows and its requests through the Gateway reach
|
|
# Gitea, while Gitea's endpoint shows only a `reserved:ingress` allow row. So, unlike a
|
|
# ClusterIP client, cloudflared is not added to any app's inClusterClients. That also means
|
|
# this rule does not open a direct cloudflared -> app pod path into any app namespace whose
|
|
# ingress admits only reserved:ingress and named peers.
|
|
#
|
|
# DELIBERATELY ABSENT: kube-apiserver, host, remote-node, cluster, all. The Kubernetes API stays
|
|
# local-only (operator decision 2026-11-08); conformance/tests/l0-static/cloudflared-ingress-rules
|
|
# fails the gate if any of those entities appears here.
|
|
apiVersion: cilium.io/v2
|
|
kind: CiliumNetworkPolicy
|
|
metadata:
|
|
name: cloudflared-allow-egress
|
|
namespace: cloudflared
|
|
labels:
|
|
app.kubernetes.io/name: cloudflared
|
|
spec:
|
|
endpointSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: cloudflared
|
|
egress:
|
|
- toEntities:
|
|
- world
|
|
toPorts:
|
|
- ports:
|
|
- port: "7844"
|
|
protocol: TCP
|
|
- port: "7844"
|
|
protocol: UDP
|
|
- port: "443"
|
|
protocol: TCP
|
|
- toEntities:
|
|
- ingress
|
|
toPorts:
|
|
- ports:
|
|
- port: "443"
|
|
protocol: TCP
|
|
- toEndpoints:
|
|
- matchExpressions:
|
|
- key: io.cilium.k8s.namespace.labels.gateway-access
|
|
operator: In
|
|
values:
|
|
- shared
|
|
- key: io.kubernetes.pod.namespace
|
|
operator: NotIn
|
|
values:
|
|
- plex
|
|
- jackett
|
|
- pihole
|
|
- semaphore
|
|
- qbittorrent
|
|
- radarr
|
|
- sonarr
|
|
- headlamp
|