Security Policies¶
Four cluster-wide controls: Pod Security Admission, default-deny ingress
network policy, no API token on the default ServiceAccount, and two
admission policies on image references. Each is easy to turn on and hard to
turn on safely, so the pattern is the same for all of them: measure first,
enforce second, one namespace at a time.
At a glance¶
| Namespace | kube-system for the Application; it owns Namespace objects across the cluster |
| Stage | 11-policy, last, so it labels namespaces that already exist |
| Depends on | Cilium to enforce the network policies |
| If it is down | Nothing at the time; the labels and policies stay applied and only stop being corrected |
| Health check | kubectl get ns -L pod-security.kubernetes.io/enforce, kubectl get validatingadmissionpolicy |
| Pruning | Disabled. Pruning a Namespace deletes everything inside it, PVCs included |
| Files | payload/platform/security/ |
Configuration¶
Pod Security Admission¶
A namespace that does not opt in runs at privileged, which enforces nothing.
Each namespace in pod-security.yaml carries three labels:
| Label | Set to | Effect |
|---|---|---|
enforce |
The level the namespace demonstrably needs | Rejects pods that violate it |
audit |
Stricter | Records violations in the API server audit log |
warn |
Stricter | Warns whoever applies the manifest |
Enforcement is set to what already works, so nothing running breaks, while
warn and audit show what a stricter level would catch; tightening later is
an informed change.
| Namespace | Enforce | Why not stricter |
|---|---|---|
kube-system |
privileged |
Cilium, kube-vip and the control plane use host networking and host paths |
rook-ceph |
privileged |
OSDs need raw block devices |
monitoring |
privileged |
node-exporter is host-networked and reads /proc and /sys |
openbao |
privileged |
Adds IPC_LOCK to keep the root key out of swap, which baseline does not allow |
trivy-system |
privileged |
node-collector hostPath-mounts the kubelet, etcd and CNI directories for the CIS node checks |
cert-manager, external-secrets, argocd |
baseline |
— |
privileged here means "not yet reduced", not "unexamined": audit and warn
are still baseline or restricted on all of them. Namespace objects are
owned by this Application so the labels stay declarative rather than drifting
after CreateNamespace=true.
Network policies¶
Ingress only, in ten namespaces, as CiliumNetworkPolicy in
network-policies.yaml. Every policy also admits traffic from within the
namespace and from host and remote-node for probes.
Why CiliumNetworkPolicy and not NetworkPolicy¶
Two kinds of traffic have no pod identity a standard NetworkPolicy can name:
Gateway traffic, which reaches a backend from the per-node Envoy, and kubelet
health probes, which come from the node. A plain default-deny therefore kills
ingress and probes, and the pods restart forever in a way that looks like an
application fault. Cilium's fromEntities names them: ingress for
Envoy-proxied traffic, host and remote-node for the kubelet.
Scope¶
| Namespace | Who may connect from outside it |
|---|---|
openbao |
The Gateway (UI), Prometheus and ESO, on 8200 |
cert-manager |
Prometheus; the webhook is called by the API server from the node |
external-secrets |
Prometheus; the webhook is called by the API server from the node |
monitoring |
The Gateway (Grafana); kured on 9090; the Authentik outpost on 9090 and 9093; the Ceph mgr on 9090 and 3000; Loki's ruler on 9093 |
external-dns |
Prometheus |
kubelet-csr-approver |
Prometheus |
kured |
Prometheus |
logging |
Prometheus and Grafana, both in monitoring |
cnpg-system |
Prometheus; the webhooks are called by the API server from the node |
trivy-system |
Prometheus; scan jobs reach the Trivy server within the namespace |
The monitoring callers are easy to lose: Kured blocks every reboot when its
Prometheus query fails, the Authentik outpost is what prometheus.infra and
alertmanager.infra resolve to, and the Ceph dashboard pulls from both.
cnpg-system covers the operator only; each database lives in its workload's
namespace, so the rule admitting the operator on port 8000 belongs to that
namespace's policy — see Adding a Workload.
Egress is untouched: a default-deny there also needs DNS, the API server and every external endpoint, and getting it wrong takes the component down rather than leaving it exposed. Not covered, each for its own reason:
| Namespace | Why not yet |
|---|---|
kube-system |
Holds Cilium itself, the static control-plane pods and kube-vip |
rook-ceph |
Mons, OSDs and CSI plugins have a wide, partly host-level traffic matrix; Ceph health is the verification signal |
backup |
A node-agent doing volume backups through the CSI plugins, and a host-network CronJob reading etcd |
argocd, authentik |
Already carry NetworkPolicy objects from their own charts |
Default ServiceAccount tokens¶
A pod that names no serviceAccountName lands on default, which nothing
configures, and gets an API token it has no use for. A workload reaching the
API through default is already a misconfiguration, so
default-serviceaccounts.yaml sets automountServiceAccountToken: false on it
in every namespace with nothing to preserve, empty namespaces included. Left
out: kube-system, where the control plane authenticates with client
certificates and "very likely fine" is not the standard, and authentik,
where authentik-server sets no serviceAccountName — an upstream chart
default, to be fixed at the source.
Admission policies¶
Trivy reports a bad image after it is running; admission-policies.yaml
refuses or records it at admission, with the built-in
ValidatingAdmissionPolicy rather than a policy engine. Both match Pods and
every controller that produces them, so a bad Deployment is rejected at
kubectl apply rather than failing quietly in its ReplicaSet.
| Policy | Action | Why |
|---|---|---|
image-tag-pinned |
Deny |
A tag other than latest, or a digest. Nothing running violates it, and an unpinned image is the one Renovate can neither track nor roll back |
image-registry-allowed |
Audit and Warn |
The registries the cluster pulls from today; a reference with no registry counts as docker.io. Recorded to the audit log and shown to whoever applies, not enforced, until the record is clean |
Both carry failurePolicy: Ignore: an expression that errors lets the request
through rather than stopping every pod in the cluster. A real violation is
still refused.
Usage¶
Promoting the registry allowlist¶
-
Confirm nothing running would be refused, over the full Loki retention:
-
Change
validationActionson the binding inadmission-policies.yamlto["Deny"].
Tightening a namespace¶
-
See what running pods a stricter level would reject, without changing anything:
-
Catch what is not running right now — a nightly CronJob, say — from the
auditlabel's records over the full Loki retention window, in Grafana: -
Raise
enforceinpod-security.yaml.
Rolling out a network policy¶
One namespace at a time, watching Hubble between each:
kubectl -n kube-system port-forward svc/hubble-relay 4245:80
hubble observe --verdict DROPPED --namespace openbao --follow
Drops are also kept in Loki, so a policy that broke something overnight can
be read back: {job="hubble", verdict="DROPPED"} — see Cilium.
If something legitimate is dropped, put a single endpoint into audit mode — decisions logged, not enforced — to find the missing rule without an outage:
kubectl -n kube-system exec ds/cilium -- \
cilium endpoint config <endpoint-id> PolicyAuditMode=Enabled
Health check¶
kubectl get ns -L pod-security.kubernetes.io/enforce
kubectl get cnp -A
kubectl get validatingadmissionpolicy
Pitfalls¶
The token change is not retroactive
The mount is decided at admission, so existing pods keep their token until recreated. That makes the change safe to roll out, and means a posture scan will not agree it is fixed until things restart.