T-0.5 Kyverno L0 policies + CLI tests + admission scaffolding (R-TEST-01, R-NET-01, R-SEC-01/02, R-IDP-02, R-GITOPS-02)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
1 parent
bb08d0e048
commit
bb2e9fc931
28 files changed
+1243
-2
No files matched your search
@@ -0,0 +1,61 @@
|
||||
# Serves: R-SEC-01 (no plaintext secrets in git; SOPS-encrypted only)
|
||||
# Spec: docs/07-security-and-secrets.md "Secrets management (R-SEC-01)"; docs/05
|
||||
# "Secrets in GitOps"; ADR-0007 (SOPS + age).
|
||||
#
|
||||
# SCOPE / IMPORTANT NUANCE (see platform/kyverno/README.md and admission/README.md):
|
||||
# This policy is the CI / L0 gate over the SECRET MANIFESTS COMMITTED TO GIT.
|
||||
# A SOPS-encrypted Secret file carries a top-level `sops:` block (with `sops.mac`);
|
||||
# a plaintext Secret does not. We reject any committed Secret lacking that block.
|
||||
#
|
||||
# This policy is CI-ONLY. It MUST NOT be applied unchanged in-cluster: Flux
|
||||
# decrypts SOPS before the object reaches the API server, so an admitted Secret
|
||||
# never has a `sops:` block -- applying this at admission would reject EVERY real
|
||||
# Secret. The in-cluster admission variant enforces PROVENANCE instead
|
||||
# (admission/secret-provenance.yaml). This CI/admission divergence is an
|
||||
# escalation item (see report) because docs/07's phrase "reject a plaintext
|
||||
# Secret at admission" is not literally achievable post-decrypt.
|
||||
#
|
||||
# "As far as policy can express" (per task): value-level ciphertext and git-history
|
||||
# leakage are additionally covered by a secret scanner (gitleaks) per docs/07
|
||||
# acceptance gate "No plaintext secret exists anywhere in git history (scanned)".
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: disallow-plaintext-secrets
|
||||
annotations:
|
||||
policies.kyverno.io/title: Disallow plaintext Secrets (require SOPS)
|
||||
policies.kyverno.io/category: Secrets
|
||||
policies.kyverno.io/severity: critical
|
||||
homelab.keithjackson.dev/requirements: R-SEC-01
|
||||
homelab.keithjackson.dev/scope: ci-only
|
||||
policies.kyverno.io/description: >-
|
||||
Secret manifests committed to git must be SOPS-encrypted (top-level `sops`
|
||||
block present). Plaintext Secrets are rejected. CI/L0 gate only -- see the
|
||||
provenance-based admission variant for in-cluster enforcement.
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: false
|
||||
rules:
|
||||
- name: secrets-must-be-sops-encrypted
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Secret
|
||||
# Controller-generated Secrets (cert-manager, SA tokens, Helm) are never
|
||||
# committed to git, so the CI gate never sees them; the label keeps the rule
|
||||
# safe if ever reused where such Secrets exist.
|
||||
exclude:
|
||||
any:
|
||||
- resources:
|
||||
selector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/secret-managed-by: controller
|
||||
validate:
|
||||
message: >-
|
||||
Secrets committed to git must be SOPS-encrypted: the manifest must carry a
|
||||
top-level `sops` block (with a `mac`). No plaintext secrets (R-SEC-01,
|
||||
docs/07, ADR-0007).
|
||||
pattern:
|
||||
sops:
|
||||
mac: "?*"
|
||||
@@ -0,0 +1,48 @@
|
||||
# Serves: R-IDP-02 (every service authenticates against the IdP; no unauthenticated UI)
|
||||
# Spec: docs/15-identity-and-access.md; docs/13 `auth.mode: oidc|forward-auth|native|none`.
|
||||
#
|
||||
# Every workload must declare which authentication mode it uses so the platform can
|
||||
# wire the correct integration (native OIDC Keycloak client, oauth2-proxy forward-auth
|
||||
# at the Gateway, a documented `native` exception e.g. Plex, or `none`). A missing
|
||||
# auth declaration would let a bare management UI onto the network unprotected, which
|
||||
# docs/15 forbids ("no management UI sits unauthenticated").
|
||||
#
|
||||
# Matches Pods; pod-controller autogen extends to Deployment/StatefulSet/etc.
|
||||
# LABEL KEY is a PROPOSED contract owned jointly with WS-SEC/WS-GITOPS -- see
|
||||
# platform/kyverno/labels.md. The VALUES are fixed by docs/13.
|
||||
# NOTE: `none` is permitted by the enum (docs/13) but is "discouraged / internal-only";
|
||||
# the ServiceDefinition-level policy additionally forbids `public` + `none`.
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-auth-mode-label
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require auth-mode label on workloads
|
||||
policies.kyverno.io/category: Identity
|
||||
policies.kyverno.io/severity: medium
|
||||
homelab.keithjackson.dev/requirements: R-IDP-02
|
||||
policies.kyverno.io/description: >-
|
||||
Pods in application namespaces must declare
|
||||
homelab.keithjackson.dev/auth-mode = oidc | forward-auth | native | none.
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: require-auth-mode
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/app-namespace: "true"
|
||||
validate:
|
||||
message: >-
|
||||
Pods in application namespaces must carry the label
|
||||
homelab.keithjackson.dev/auth-mode set to one of
|
||||
oidc | forward-auth | native | none (R-IDP-02, docs/15).
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
homelab.keithjackson.dev/auth-mode: "oidc | forward-auth | native | none"
|
||||
@@ -0,0 +1,66 @@
|
||||
# Serves: R-NET-01 (default-deny baseline present in every application namespace)
|
||||
# Spec: docs/03-networking-isolation-and-egress.md "Default-deny baseline (R-NET-01)".
|
||||
#
|
||||
# GUARANTEE (not merely a check): whenever an application namespace is created,
|
||||
# Kyverno GENERATES the deny-all (ingress+egress) baseline NetworkPolicy, allowing
|
||||
# only DNS egress. A workload with no other allow rules can then do nothing but
|
||||
# resolve DNS -- the intended starting posture (docs/03).
|
||||
#
|
||||
# This is the primary R-NET-01 control. The companion policy
|
||||
# `validate-default-deny-shape` additionally rejects a MALFORMED hand-written
|
||||
# baseline (defense in depth). Per-service `dependencies`/egress allows are layered
|
||||
# on top by WS-NET's rendered CiliumNetworkPolicies (T-2.3), never by weakening this.
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-default-deny-baseline
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require default-deny NetworkPolicy baseline
|
||||
policies.kyverno.io/category: Network Isolation
|
||||
policies.kyverno.io/severity: high
|
||||
homelab.keithjackson.dev/requirements: R-NET-01
|
||||
policies.kyverno.io/description: >-
|
||||
Every application namespace must carry a deny-all (ingress+egress) baseline
|
||||
NetworkPolicy that permits only DNS egress. Generated automatically on
|
||||
namespace creation so the baseline can never be forgotten.
|
||||
spec:
|
||||
# Also backfill namespaces that already exist when the policy lands.
|
||||
generateExisting: true
|
||||
rules:
|
||||
- name: generate-default-deny
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Namespace
|
||||
selector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/app-namespace: "true"
|
||||
generate:
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
name: default-deny
|
||||
namespace: "{{ request.object.metadata.name }}"
|
||||
synchronize: true
|
||||
data:
|
||||
metadata:
|
||||
labels:
|
||||
homelab.keithjackson.dev/baseline: default-deny
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
# Deny all ingress.
|
||||
ingress: []
|
||||
# Deny all egress EXCEPT DNS to the in-cluster resolver (kube-system).
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
@@ -0,0 +1,48 @@
|
||||
# Serves: R-NET-05 (egress is a declared property; every workload declares its class)
|
||||
# Spec: docs/03 "Egress is a declared property"; docs/13 `egress: internet | vpn`.
|
||||
#
|
||||
# The egress class is enforced OUTSIDE the service (Cilium Egress Gateway selects
|
||||
# pods by label -> steers `vpn` pods through the shared PIA/Gluetun gateway, ADR-0013).
|
||||
# For that steering to be possible the label MUST exist on the pod and be one of the
|
||||
# two valid classes. A missing/typo'd class would silently fall back to the plain
|
||||
# internet path -- a fail-OPEN egress leak -- so we require it explicitly (fail-closed
|
||||
# posture, coordination-protocol.md #8).
|
||||
#
|
||||
# Matches Pods; pod-controller autogen extends to Deployment/StatefulSet/etc.
|
||||
# LABEL KEY is a PROPOSED contract owned jointly with WS-NET/WS-GITOPS -- see
|
||||
# platform/kyverno/labels.md and the report's escalation note. The VALUES
|
||||
# (internet|vpn) are fixed by docs/03 + docs/13.
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-egress-class-label
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require egress-class label on workloads
|
||||
policies.kyverno.io/category: Network Isolation
|
||||
policies.kyverno.io/severity: high
|
||||
homelab.keithjackson.dev/requirements: R-NET-05
|
||||
policies.kyverno.io/description: >-
|
||||
Pods in application namespaces must declare
|
||||
homelab.keithjackson.dev/egress-class = internet | vpn.
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: require-egress-class
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/app-namespace: "true"
|
||||
validate:
|
||||
message: >-
|
||||
Pods in application namespaces must carry the label
|
||||
homelab.keithjackson.dev/egress-class set to 'internet' or 'vpn' (R-NET-05,
|
||||
docs/03).
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
homelab.keithjackson.dev/egress-class: "internet | vpn"
|
||||
@@ -0,0 +1,75 @@
|
||||
# Serves: R-SEC-02 (least network reach / least privilege), supports R-NET-01.
|
||||
# Spec: docs/07 "Identity/least privilege ... least network reach (R-SEC-02)";
|
||||
# docs/03 default-deny.
|
||||
#
|
||||
# WHY: a pod on hostNetwork bypasses the CNI (Cilium) datapath and therefore
|
||||
# ESCAPES the default-deny NetworkPolicy fabric -- it is the most direct way to
|
||||
# defeat R-NET-01. hostPID/hostIPC and privileged containers likewise break
|
||||
# workload isolation. Application workloads never legitimately need these
|
||||
# (platform components that do -- e.g. Cilium, node-exporter -- live in platform
|
||||
# namespaces that do NOT carry the app-namespace label, so they are out of scope).
|
||||
#
|
||||
# Matches Pods; Kyverno pod-controller autogen extends the same checks to
|
||||
# Deployment/StatefulSet/DaemonSet/Job/CronJob pod templates automatically.
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: restrict-host-namespaces
|
||||
annotations:
|
||||
policies.kyverno.io/title: Restrict host namespaces and privilege in app namespaces
|
||||
policies.kyverno.io/category: Least Privilege
|
||||
policies.kyverno.io/severity: high
|
||||
homelab.keithjackson.dev/requirements: R-SEC-02,R-NET-01
|
||||
policies.kyverno.io/description: >-
|
||||
Application-namespace pods must not use hostNetwork/hostPID/hostIPC (which
|
||||
bypass default-deny) or run privileged containers.
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: disallow-host-namespaces
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/app-namespace: "true"
|
||||
validate:
|
||||
message: >-
|
||||
Application workloads must not set hostNetwork/hostPID/hostIPC -- host
|
||||
networking bypasses the default-deny NetworkPolicy fabric (R-SEC-02,
|
||||
R-NET-01).
|
||||
deny:
|
||||
conditions:
|
||||
any:
|
||||
- key: "{{ request.object.spec.hostNetwork || `false` }}"
|
||||
operator: Equals
|
||||
value: true
|
||||
- key: "{{ request.object.spec.hostPID || `false` }}"
|
||||
operator: Equals
|
||||
value: true
|
||||
- key: "{{ request.object.spec.hostIPC || `false` }}"
|
||||
operator: Equals
|
||||
value: true
|
||||
- name: disallow-privileged-containers
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/app-namespace: "true"
|
||||
validate:
|
||||
message: >-
|
||||
Application containers must not run privileged (R-SEC-02).
|
||||
foreach:
|
||||
- list: "request.object.spec.containers"
|
||||
deny:
|
||||
conditions:
|
||||
any:
|
||||
- key: "{{ element.securityContext.privileged || `false` }}"
|
||||
operator: Equals
|
||||
value: true
|
||||
@@ -0,0 +1,43 @@
|
||||
# Serves: R-NET-01 (baseline must actually deny both directions)
|
||||
# Spec: docs/03 "Deny all ingress and deny all egress by default".
|
||||
#
|
||||
# Catches a MALFORMED baseline: a NetworkPolicy claiming to be the default-deny
|
||||
# baseline (label homelab.keithjackson.dev/baseline=default-deny) that does not
|
||||
# actually cover BOTH Ingress and Egress is rejected. This is the "test the
|
||||
# behavior, not the existence" guard for R-NET-01 at the manifest layer -- an
|
||||
# object existing is not proof it denies (docs/14 principle 1).
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: validate-default-deny-shape
|
||||
annotations:
|
||||
policies.kyverno.io/title: Validate default-deny baseline shape
|
||||
policies.kyverno.io/category: Network Isolation
|
||||
policies.kyverno.io/severity: high
|
||||
homelab.keithjackson.dev/requirements: R-NET-01
|
||||
policies.kyverno.io/description: >-
|
||||
A NetworkPolicy labelled as the default-deny baseline must set policyTypes
|
||||
to include both Ingress and Egress. Rejects a baseline that silently leaves
|
||||
a direction open.
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: baseline-must-deny-both-directions
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- NetworkPolicy
|
||||
selector:
|
||||
matchLabels:
|
||||
homelab.keithjackson.dev/baseline: default-deny
|
||||
validate:
|
||||
message: >-
|
||||
A default-deny baseline NetworkPolicy must set spec.policyTypes to include
|
||||
BOTH Ingress and Egress (R-NET-01, docs/03).
|
||||
pattern:
|
||||
spec:
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
Reference in new issue
Block a user