{
  "metadata": {
    "cluster": "production-demo",
    "context": "production-demo",
    "read_only": true,
    "generated_at": "2026-10-25T23:09:53.733653+00:00",
    "collected_at": "2026-10-26T09:00:00+00:00"
  },
  "findings": [
    {
      "id": "K8S-INGRESS-001",
      "category": "networking",
      "severity": "CRITICAL",
      "title": "ingress-nginx is vulnerable to CVE-2025-1974 (IngressNightmare)",
      "explanation": "The ingress-nginx controller version is affected by CVE-2025-1974 and related admission-controller CVEs (CVE-2025-1097, -1098, -24514, -24513), fixed in v1.11.5 and v1.12.1. Any pod that can reach the admission webhook can achieve remote code execution in the controller.",
      "evidence": "Controller image: registry.k8s.io/ingress-nginx/controller:v1.11.2@sha256:d5f8217feeac4887cb1ed21f27c2674e58be06bd8f5184cacea2a69abaf78dce (v1.11.2)\nFixed in: v1.11.5, v1.12.1",
      "affected_resources": [
        "ingress-nginx/ingress-nginx-controller-6f7d8c9b5-n2p3q"
      ],
      "recommendation": "Upgrade the controller to v1.12.1 or later now. If that cannot happen immediately, disable the admission webhook (`controller.admissionWebhooks.enabled=false`) as a temporary mitigation.",
      "remediation": null,
      "impact": "CVE-2025-1974 (\"IngressNightmare\", CVSS 9.8) lets anyone who can reach the controller's admission webhook from the pod network execute code in the controller. The controller can read every Secret in the cluster by default, so this leads to a full credential compromise.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-LIFECYCLE",
      "validation": "The controller image is v1.11.5+ or v1.12.1+, or the admission webhook is disabled; re-run the audit.",
      "rollback": "`helm rollback <release> -n <ns>` restores the previous controller version. Do not roll back past the patched release except to restore service temporarily.",
      "effort": "M"
    },
    {
      "id": "K8S-SEC-001",
      "category": "security",
      "severity": "CRITICAL",
      "title": "Privileged containers detected",
      "explanation": "1 container in 1 workload in namespace payments runs with `securityContext.privileged: true`.",
      "evidence": "payments/payments-api (Deployment) container=payments-api: securityContext.privileged=true",
      "affected_resources": [
        "payments/payments-api-7d9f8c5b6-k2p9x:payments-api",
        "payments/payments-api-7d9f8c5b6-r8w4n:payments-api"
      ],
      "recommendation": "Remove privileged mode and grant only the specific capabilities the workload needs, or document and isolate a justified exception.",
      "remediation": "Set `securityContext.privileged: false`, then add back only required capabilities via `capabilities.add`. Test in staging first; a privileged requirement usually means device or sysctl access that needs a narrower alternative.",
      "impact": "A privileged container has all capabilities and full device access. Compromising the application is equivalent to root on the node, and from there to every workload and Secret on that node.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "M"
    },
    {
      "id": "K8S-CAPACITY-002",
      "category": "capacity",
      "severity": "HIGH",
      "title": "Nodes are running close to capacity",
      "explanation": "1 node has requests or measured usage at or above 85% of allocatable, leaving little room for rollouts (surge pods), rescheduling after a node failure, or load spikes.",
      "evidence": "k3s-agent-2: memory requested 91%, memory used 92%",
      "affected_resources": [
        "k3s-agent-2"
      ],
      "recommendation": "Rebalance or right-size workloads, and add capacity if N-1 headroom is also short (K8S-CAPACITY-001).",
      "remediation": null,
      "impact": "Pods whose actual usage is near or above node capacity are at risk of eviction or OOM kills, and new pods may fail to schedule during rollouts (which need surge capacity).",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-CAPACITY",
      "validation": "Node CPU and memory requests (and, with metrics-server, actual usage) stay below 80% of allocatable.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-CERT-001",
      "category": "networking",
      "severity": "HIGH",
      "title": "cert-manager certificates are not Ready",
      "explanation": "1 Certificate could not be issued or renewed.",
      "evidence": "payments/api-tls (api.demo.example): Ready=False, reason=Failed: HTTP-01 self check failed for api.demo.example: connection refused",
      "affected_resources": [
        "payments/api-tls"
      ],
      "recommendation": "Inspect the failing Order/Challenge (`kubectl describe certificaterequest,order,challenge -n <ns>`); common causes are DNS not pointing at the ingress, HTTP-01 blocked by redirects or NetworkPolicy, or issuer credentials.",
      "remediation": null,
      "impact": "cert-manager cannot issue or renew this certificate. The existing certificate keeps serving until it expires, and then clients see TLS errors.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-HEALTH",
      "validation": "`kubectl get certificate -n <ns>` shows READY=True; `cmctl status certificate <name> -n <ns>` shows no failed orders.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-CERT-002",
      "category": "networking",
      "severity": "HIGH",
      "title": "TLS certificates expire within 14 days",
      "explanation": "1 certificate is close to expiry. cert-manager renews 30 days ahead by default, so renewal is not succeeding.",
      "evidence": "payments/api-tls (api.demo.example): notAfter=2026-11-01 (5 days left), renewalTime=2026-10-02T08:15:00Z",
      "affected_resources": [
        "payments/api-tls"
      ],
      "recommendation": "Fix the renewal failure first (see K8S-CERT-001), then force renewal with `cmctl renew <name> -n <ns>`.",
      "remediation": null,
      "impact": "Clients will reject the endpoint when the certificate expires. cert-manager normally renews 30 days ahead, so a certificate this close to expiry means renewal is failing.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-HEALTH",
      "validation": "`kubectl get certificate -n <ns> <name> -o jsonpath='{.status.notAfter}'` is more than 30 days away.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-CLUSTER-003",
      "category": "cluster",
      "severity": "HIGH",
      "title": "Kubernetes 1.31 is past end of support",
      "explanation": "K3s 1.31 reached end of standard support on 2025-11-11. No security patches are published for it. Support dates are from the upstream lifecycle table (endoflife.date snapshot retrieved 2026-10-26). K3s follows the upstream Kubernetes patch-support window.",
      "evidence": "Detected version: v1.31.6+k3s1 (source: API server)\nDistribution: K3s\nMinor 1.31 end of standard support: 2025-11-11\nOldest minor still in standard support on 2026-10-26: 1.34; newest: 1.37",
      "affected_resources": [
        "cluster (v1.31.6+k3s1)"
      ],
      "recommendation": "Plan the upgrade path one minor version at a time to at least 1.34 (preferably 1.37). Check deprecated APIs before each step (e.g. with `pluto` or `kubent`) and confirm that add-ons (CNI, CSI, ingress, operators) support the target version.",
      "remediation": null,
      "impact": "Security fixes for Kubernetes CVEs are no longer published for this minor version. Upgrades must step through each minor version, so the longer it waits, the larger and riskier the upgrade becomes.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-LIFECYCLE",
      "validation": "`kubectl version` reports a supported minor version on every node; the audit no longer emits this ID.",
      "rollback": "Upgrades are not reversible in place. Take and verify a datastore snapshot (etcd/K3s) before each minor-version step and rehearse the upgrade on a non-production cluster first.",
      "effort": "L"
    },
    {
      "id": "K8S-CP-001",
      "category": "cluster",
      "severity": "HIGH",
      "title": "Control plane runs on a single server node",
      "explanation": "The cluster has one control-plane node and one etcd member. The API server, scheduler, controllers, and datastore all depend on that one machine.",
      "evidence": "k3s-server-1: roles=['control-plane', 'etcd', 'master']\nDatastore: embedded etcd",
      "affected_resources": [
        "k3s-server-1"
      ],
      "recommendation": "Run three server nodes (an odd number for etcd quorum) on separate hosts or failure domains, and put a fixed registration address (VIP or load balancer) in front of the API server.",
      "remediation": "K3s: on two new hosts, join as servers against the existing one:\n  curl -sfL https://get.k3s.io | K3S_TOKEN=<token> sh -s - server --server https://<fixed-address>:6443\nTake and verify an etcd snapshot first (`k3s etcd-snapshot save`).",
      "impact": "If the only control-plane node fails, the API server and controllers stop. Running pods keep running, but nothing can be scheduled, scaled, healed, or deployed until it is restored.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get nodes -l node-role.kubernetes.io/control-plane` lists 3 Ready nodes; for embedded etcd, `etcdctl member list` (or the K3s etcd metrics) shows 3 healthy members.",
      "rollback": "Adding servers is additive. Remove a newly joined server cleanly (`kubectl delete node`, then remove the etcd member) if it fails to join; never remove the original server while it is the only healthy member.",
      "effort": "L"
    },
    {
      "id": "K8S-GITOPS-002",
      "category": "gitops",
      "severity": "HIGH",
      "title": "Argo CD applications are Degraded or Missing",
      "explanation": "1 Argo CD application applications report resources that are Degraded or Missing.",
      "evidence": "argocd/checkout: health=Degraded (Deployment/checkout-worker=Degraded)",
      "affected_resources": [
        "argocd/checkout"
      ],
      "recommendation": "Investigate the listed resources (`argocd app get <app>`); Degraded usually means failing pods or an unready dependency.",
      "remediation": null,
      "impact": "Argo CD reports resources in this application as Degraded or Missing, so the service it delivers may be impaired.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-HEALTH",
      "validation": "`argocd app get <app>` shows Health: Healthy.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-INGRESS-002",
      "category": "networking",
      "severity": "HIGH",
      "title": "ingress-nginx is retired upstream",
      "explanation": "The Kubernetes project announced (November 2025) that ingress-nginx is retired after March 2026: no further releases, bug fixes, or security patches. This cluster terminates its external traffic on it.",
      "evidence": "ingress-nginx/ingress-nginx-controller-6f7d8c9b5-n2p3q: registry.k8s.io/ingress-nginx/controller:v1.11.2@sha256:d5f8217feeac4887cb1ed21f27c2674e58be06bd8f5184cacea2a69abaf78dce",
      "affected_resources": [
        "ingress-nginx/ingress-nginx-controller-6f7d8c9b5-n2p3q"
      ],
      "recommendation": "Plan a migration to a maintained implementation (a Gateway API controller such as Envoy Gateway, Cilium, Istio, or NGINX Gateway Fabric, or another maintained ingress controller), running it alongside the current controller and moving hosts one at a time.",
      "remediation": null,
      "impact": "The Kubernetes project retired ingress-nginx in March 2026. No further releases or security fixes are published, so any future vulnerability in the component that terminates all external traffic stays open permanently.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-LIFECYCLE",
      "validation": "Traffic is served by the replacement (Gateway API implementation or another maintained ingress controller); the old controller is removed.",
      "rollback": "Run the replacement alongside the old controller and shift DNS/LoadBalancer traffic gradually, so rollback is a DNS or Service selector change.",
      "effort": "L"
    },
    {
      "id": "K8S-INGRESS-003",
      "category": "networking",
      "severity": "HIGH",
      "title": "ingress-nginx allows snippet annotations",
      "explanation": "`allow-snippet-annotations` is enabled, so anyone who can create or edit an Ingress in any namespace can inject raw NGINX configuration into the shared controller. 1 Ingress uses snippets today.",
      "evidence": "ConfigMap ingress-nginx/ingress-nginx-controller: allow-snippet-annotations=true\npayments/payments-api: configuration-snippet",
      "affected_resources": [
        "payments/payments-api"
      ],
      "recommendation": "Set `allow-snippet-annotations: \"false\"` and replace the snippets with first-class annotations or controller-level configuration.",
      "remediation": null,
      "impact": "Snippet annotations inject raw NGINX configuration. Anyone allowed to create an Ingress can then read files from the controller, including its service-account token, which can read all Secrets (CVE-2021-25742 class).",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl get cm -n <ns> <controller-configmap> -o yaml` shows `allow-snippet-annotations: \"false\"`; ingresses that used snippets still behave as expected.",
      "rollback": "Set `allow-snippet-annotations` back to \"true\" in the controller ConfigMap.",
      "effort": "S"
    },
    {
      "id": "K8S-K3S-003",
      "category": "backup-recovery",
      "severity": "HIGH",
      "title": "etcd snapshots are stored only on the server's local disk",
      "explanation": "All 4 ready snapshot(s) are on node-local paths. If the server's disk is lost, the snapshots are lost with it.",
      "evidence": "Snapshot locations: file:///var/lib/rancher/k3s/server/db/snapshots/\nNodes holding snapshots: k3s-server-1\nNo snapshot has an s3:// location",
      "affected_resources": [],
      "recommendation": "Configure off-node snapshot storage with the K3s `--etcd-s3` options (any S3-compatible bucket) and keep at least 7 days of snapshots.",
      "remediation": "Add to /etc/rancher/k3s/config.yaml on each server, then restart k3s one server at a time:\n  etcd-s3: true\n  etcd-s3-bucket: <bucket>\n  etcd-s3-endpoint: <endpoint>\n  etcd-snapshot-retention: 14",
      "impact": "Snapshots stored only on the server's own disk are lost with that disk, which is exactly the failure they exist to recover from.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "New ETCDSnapshotFile objects show an `s3://` location, and the object exists in the bucket.",
      "rollback": "Removing the `--etcd-s3*` flags reverts to local-only snapshots; existing S3 snapshots are unaffected.",
      "effort": "S"
    },
    {
      "id": "K8S-LONGHORN-002",
      "category": "storage",
      "severity": "HIGH",
      "title": "Longhorn volumes without effective replica redundancy",
      "explanation": "1 volume cannot survive the loss of a node: they have a single replica, or more replicas than there are nodes to place them on.",
      "evidence": "pvc-2c4e6a8b-redis-data (PVC data/redis-data-redis-0): numberOfReplicas=1",
      "affected_resources": [
        "pvc-2c4e6a8b-redis-data"
      ],
      "recommendation": "Set `numberOfReplicas: 3` (minimum 2) on the volumes and in the StorageClass `parameters.numberOfReplicas`, with at least that many schedulable storage nodes.",
      "remediation": null,
      "impact": "With one replica, the volume's data exists on a single node's disk. Losing that disk or node loses the data, and the volume is unavailable whenever that node is drained.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get volumes.longhorn.io -n longhorn-system -o custom-columns=NAME:.metadata.name,R:.spec.numberOfReplicas` shows 2+ (3 recommended), and robustness=healthy after rebuild.",
      "rollback": "Reducing `numberOfReplicas` again deletes the extra replicas; there is no data risk in reverting.",
      "effort": "S"
    },
    {
      "id": "K8S-LONGHORN-003",
      "category": "backup-recovery",
      "severity": "HIGH",
      "title": "Longhorn has no backup target configured",
      "explanation": "5 Longhorn volumes exist, but no backup target is set, so Longhorn cannot copy volume data off the cluster. Recurring snapshots, if any, stay on the same disks as the data.",
      "evidence": "Setting backup-target: empty\nRecurringJob daily-snapshot: task=snapshot, cron=0 1 * * *",
      "affected_resources": [],
      "recommendation": "Configure an S3-compatible or NFS backup target, then add a recurring `backup` job to the default group.",
      "remediation": "  apiVersion: longhorn.io/v1beta2\n  kind: RecurringJob\n  metadata: {name: daily-backup, namespace: longhorn-system}\n  spec: {task: backup, cron: \"0 2 * * *\", retain: 14, concurrency: 2, groups: [default]}",
      "impact": "Longhorn snapshots live on the same disks as the volume. Without a backup target, there is no copy of volume data outside the cluster, and a cluster-wide loss (storage failure, ransomware, accidental namespace delete) is unrecoverable.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "Longhorn shows the backup target as Available, and a test volume backup appears in the bucket and can be restored to a new volume.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-NET-008",
      "category": "networking",
      "severity": "HIGH",
      "title": "Non-HTTP service exposed on a public IP without source restriction",
      "explanation": "1 LoadBalancer Service publishes non-web ports on a public address and accept connections from any source. Public ports 80/443 on an ingress controller are expected and not reported here.",
      "evidence": "data/payments-db-external: external IP 203.0.113.11 (public), ports=[5432], loadBalancerSourceRanges unset",
      "affected_resources": [
        "data/payments-db-external"
      ],
      "recommendation": "Change the Service to ClusterIP and reach it over a VPN or bastion, or set `loadBalancerSourceRanges` to the client CIDRs that need access.",
      "remediation": "  spec:\n    loadBalancerSourceRanges: [\"<office-cidr>/32\", \"<vpn-cidr>/24\"]\nkube-proxy enforces these ranges for MetalLB and cloud load balancers alike; confirm with a connection test from outside.",
      "impact": "A non-HTTP service (database, cache, admin protocol) is assigned a public address with no source restriction. Internet-wide scanners find such ports within hours, and attackers then try credential stuffing and protocol exploits directly against them.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-EXPOSURE",
      "validation": "From a host outside your network, `nc -zv <external-ip> <port>` is refused; legitimate clients still connect.",
      "rollback": "Re-apply the previous Service spec; the MetalLB/cloud address is re-announced within seconds.",
      "effort": "S"
    },
    {
      "id": "K8S-PG-001",
      "category": "database",
      "severity": "HIGH",
      "title": "PostgreSQL clusters without a standby for failover",
      "explanation": "1 CloudNativePG cluster runs a single instance, so there is nothing to fail over to.",
      "evidence": "data/analytics-db: spec.instances=1 (primary only, no standby)",
      "affected_resources": [
        "data/analytics-db"
      ],
      "recommendation": "Set `spec.instances: 3` (minimum 2) with the default pod anti-affinity, so a standby can be promoted automatically.",
      "remediation": null,
      "impact": "With a single PostgreSQL instance there is no standby to promote. A node or disk failure means downtime until the instance is rescheduled and its volume reattached, or data loss back to the last backup if the volume is lost.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl cnpg status <cluster> -n <ns>` shows a primary and at least one streaming replica; a planned `kubectl cnpg promote` completes within the target RTO.",
      "rollback": "Reduce `spec.instances` again; CloudNativePG removes the replica without touching the primary.",
      "effort": "M"
    },
    {
      "id": "K8S-PG-003",
      "category": "backup-recovery",
      "severity": "HIGH",
      "title": "PostgreSQL clusters have no backup configured",
      "explanation": "1 CloudNativePG cluster has no object-store backup, WAL archive, or volume-snapshot backup configured.",
      "evidence": "data/analytics-db: no spec.backup.barmanObjectStore, volumeSnapshot, or barman-cloud plugin configured",
      "affected_resources": [
        "data/analytics-db"
      ],
      "recommendation": "Configure the barman-cloud plugin (or `spec.backup.barmanObjectStore`) with an off-cluster bucket, plus a daily ScheduledBackup.",
      "remediation": "  apiVersion: postgresql.cnpg.io/v1\n  kind: ScheduledBackup\n  metadata: {name: <cluster>-daily, namespace: <ns>}\n  spec: {schedule: \"0 0 3 * * *\", backupOwnerReference: self, cluster: {name: <cluster>}}",
      "impact": "There is no backup or WAL archive outside the cluster's own volumes. Accidental deletion, corruption that replicates, or loss of the storage layer cannot be recovered, and point-in-time recovery is impossible.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "A Backup object completes, WAL archiving reports `ContinuousArchiving=True`, and a recovery into a new Cluster (`bootstrap.recovery`) from that backup succeeds.",
      "rollback": null,
      "effort": "M"
    },
    {
      "id": "K8S-PG-004",
      "category": "backup-recovery",
      "severity": "HIGH",
      "title": "PostgreSQL backups or WAL archiving are failing",
      "explanation": "1 CloudNativePG cluster has backups configured, but the evidence shows they are not currently producing usable recovery points.",
      "evidence": "data/payments-db: ContinuousArchiving=False (unexpected failure invoking barman-cloud-wal-archive: exit status 4 (AccessDenied)); latest Backup payments-db-daily-20261025030000 failed: AccessDenied: barman-cloud-backup could not write to s3://demo-pg-backups; last successful base backup 5 days ago (2026-10-21T03:04:12+00:00)",
      "affected_resources": [
        "data/payments-db"
      ],
      "recommendation": "Fix the archive destination (credentials, bucket permissions, network egress), confirm `ContinuousArchiving=True`, and take an on-demand backup to verify.",
      "remediation": null,
      "impact": "Backups or WAL archiving are configured but failing. The achievable recovery point keeps getting older while the configuration suggests protection is in place.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "`kubectl cnpg status <cluster> -n <ns>` shows a recent successful backup and continuous archiving working.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-RBAC-001",
      "category": "identity-rbac",
      "severity": "HIGH",
      "title": "Non-default subjects hold cluster-admin cluster-wide",
      "explanation": "1 ClusterRoleBinding grants the cluster-admin ClusterRole cluster-wide, in addition to the Kubernetes default `system:masters` binding (which is excluded).",
      "evidence": "ClusterRoleBinding ops-admins -> cluster-admin; subjects: Group:ops-team, User:alice@demo.example, ServiceAccount:kube-system/ci-admin",
      "affected_resources": [
        "ops-admins"
      ],
      "recommendation": "Replace each binding with a scoped role for its actual purpose (CI deploys, operators, humans), and keep cluster-admin for a documented, audited break-glass path.",
      "remediation": null,
      "impact": "Subjects bound to cluster-admin cluster-wide can read every Secret, change RBAC, and run privileged pods on any node; one compromised credential is a full cluster compromise.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl auth can-i '*' '*' --as=<subject>` returns `no` for every subject that should not be a cluster administrator.",
      "rollback": "Keep the previous binding manifest. Re-applying it restores access immediately. Remove bindings one at a time and confirm with the owner first.",
      "effort": "M"
    },
    {
      "id": "K8S-RBAC-003",
      "category": "identity-rbac",
      "severity": "HIGH",
      "title": "cluster-admin ClusterRole granted within a namespace",
      "explanation": "A RoleBinding that references the cluster-admin ClusterRole grants its permissions only inside the RoleBinding's namespace. It does not grant cluster-wide access. Within that namespace, however, the subject has unrestricted control, including all Secrets and the ability to create Roles, RoleBindings, and pods. Severity is raised to HIGH where that control can be used to escape the namespace: in a system namespace, or where no Pod Security Standard prevents privileged pods.",
      "evidence": "RoleBinding payments/deploy-bot-admin -> ClusterRole cluster-admin; subjects: ServiceAccount:payments/deploy-bot  [namespace has no enforced Pod Security Standard: privileged pods can be created]",
      "affected_resources": [
        "payments/deploy-bot-admin"
      ],
      "recommendation": "Replace the binding with a Role that grants only what the subject uses (for a deploy bot, typically updating Deployments and reading ConfigMaps). The built-in `edit` role is not a safe substitute: it still reads Secrets and creates pods, and `admin` can also create RoleBindings. Enforce at least the `baseline` Pod Security Standard on the namespace so pod creation cannot become a node escape.",
      "remediation": null,
      "impact": "Within the namespace, the subject has full control: it can read every Secret, including service-account tokens, create Roles and RoleBindings, and create pods. Without an enforced Pod Security Standard, it can create a privileged pod and escape to the node, which extends the impact beyond the namespace.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl auth can-i --list -n <ns> --as=<subject>` shows only the intended verbs; `kubectl auth can-i get secrets -A --as=<subject>` is unaffected (should be `no`).",
      "rollback": "Re-apply the previous RoleBinding from Git; access is restored immediately.",
      "effort": "S"
    },
    {
      "id": "K8S-RBAC-005",
      "category": "identity-rbac",
      "severity": "HIGH",
      "title": "Broad read access to Secrets granted",
      "explanation": "1 custom role grant get/list/watch on Secrets without restricting resourceNames and are bound to at least one subject outside the platform namespaces. Built-in roles and unbound roles are excluded.",
      "evidence": "ClusterRole ci-deployer: verbs=['get', 'list', 'create', 'update'] resources=['secrets', 'configmaps']\n  bound to: User:gitlab-ci (cluster-wide via ClusterRoleBinding gitlab-ci-deployer)",
      "affected_resources": [
        "ci-deployer"
      ],
      "recommendation": "Scope Secret access with `resourceNames`, or deliver credentials through a CSI/external-secrets driver instead of namespace-wide read.",
      "remediation": null,
      "impact": "Read access to Secrets exposes every credential stored in them: database passwords, API keys, TLS keys, and service-account tokens that may carry further permissions.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl auth can-i list secrets -n <ns> --as=<subject>` returns `no` where not required.",
      "rollback": "Re-apply the previous role from Git.",
      "effort": "M"
    },
    {
      "id": "K8S-RBAC-009",
      "category": "identity-rbac",
      "severity": "HIGH",
      "title": "Permissions granted to all users or anonymous callers",
      "explanation": "1 binding grants a role to a built-in group that contains every authenticated identity (every user and every service-account token in the cluster) or every anonymous caller. Kubernetes' own default discovery bindings are excluded. Severity is CRITICAL when the role can modify resources or read Secrets, HIGH when it is read-only.",
      "evidence": "ClusterRoleBinding all-users-view -> ClusterRole view; subjects: Group:system:authenticated",
      "affected_resources": [
        "all-users-view"
      ],
      "recommendation": "Remove these bindings and grant the role to the specific users, groups, or service accounts that need it.",
      "remediation": null,
      "impact": "Built-in groups such as system:authenticated or system:anonymous include every user and service account (or everyone with network access to the API server). Anything granted to them is granted to all.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl auth can-i --list --as=system:anonymous` shows only the default discovery permissions.",
      "rollback": "Re-apply the previous binding from Git.",
      "effort": "S"
    },
    {
      "id": "K8S-SECRET-001",
      "category": "secrets",
      "severity": "HIGH",
      "title": "Credential stored in a plaintext environment variable",
      "explanation": "1 credential-shaped environment variable is set with a literal `value:` instead of `valueFrom.secretKeyRef`. The audit records only the variable name and location, never the value.",
      "evidence": "payments/payments-api (Deployment) container=payments-api: env DB_PASSWORD uses a literal `value:` (value not recorded)",
      "affected_resources": [
        "payments/payments-api-7d9f8c5b6-k2p9x:payments-api:DB_PASSWORD",
        "payments/payments-api-7d9f8c5b6-r8w4n:payments-api:DB_PASSWORD"
      ],
      "recommendation": "Move the value into a Secret (or external secret store), reference it with `valueFrom.secretKeyRef`, and rotate the credential.",
      "remediation": null,
      "impact": "The credential is stored in the workload spec. Anyone who can read Deployments or Pods, a much wider group than Secret readers, can read it, and it is likely also in Git history and CI logs.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "The env var uses `valueFrom.secretKeyRef`; the old credential has been rotated and no longer authenticates.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-WORKLOAD-002",
      "category": "workloads",
      "severity": "HIGH",
      "title": "Unhealthy container state detected",
      "explanation": "1 container is failing to start or repeatedly crashing at audit time.",
      "evidence": "checkout/checkout-worker (Deployment) container=checkout-worker: CrashLoopBackOff, restartCount=47, last exit: Error (code 1)",
      "affected_resources": [
        "checkout/checkout-worker-6b8d4f9c7-m4n2p:checkout-worker"
      ],
      "recommendation": "Investigate image availability, application logs, configuration, and restart causes.",
      "remediation": "Inspect before restarting anything: `kubectl describe pod <ns>/<pod>` and `kubectl logs <ns>/<pod> -c <container> --previous`.",
      "impact": "The container is not serving. If it backs a user-facing Service, capacity is reduced or the feature is down.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-HEALTH",
      "validation": "The pod reaches Running/Ready with a stable restart count over 30 minutes.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-WORKLOAD-003",
      "category": "workloads",
      "severity": "HIGH",
      "title": "Workloads have unavailable replicas",
      "explanation": "1 workload has fewer ready replicas than desired at audit time.",
      "evidence": "checkout/checkout-worker (Deployment): ready 0/1",
      "affected_resources": [
        "checkout/checkout-worker"
      ],
      "recommendation": "Review events, scheduling, probes, image pulls, dependencies, and rollout status.",
      "remediation": null,
      "impact": "The workload is running below its intended capacity, and its availability margin is consumed.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-HEALTH",
      "validation": "`kubectl get deploy,sts -A` shows READY equal to desired for the listed workloads.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-BACKUP-003",
      "category": "backup-recovery",
      "severity": "HIGH",
      "title": "Persistent volumes with no backup coverage",
      "explanation": "3 bound PersistentVolumeClaims are not covered by any backup mechanism the audit can see (Velero schedules, Longhorn recurring backups, or CloudNativePG backups). Host-level or application-level backups outside the cluster would not be visible, so confirm before treating this as final.",
      "evidence": "data/analytics-db-1: storageClass=longhorn, PostgreSQL cluster analytics-db has no backup\ndata/redis-data-redis-0: storageClass=longhorn, Longhorn has no backup target\nmonitoring/prometheus-k8s-db-prometheus-k8s-0: storageClass=longhorn, Longhorn has no backup target",
      "affected_resources": [
        "data/analytics-db-1",
        "data/redis-data-redis-0",
        "monitoring/prometheus-k8s-db-prometheus-k8s-0"
      ],
      "recommendation": "Decide the required RPO per volume, then cover each one with a mechanism that writes off-cluster, and test a restore.",
      "remediation": null,
      "impact": "Data on these volumes has no copy outside the volume itself. Losing the volume, through storage failure, deletion, or corruption, loses the data.",
      "confidence": "LIKELY",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "Each listed claim appears in a successful backup (Velero, Longhorn backup, or database-level backup) within the agreed RPO.",
      "rollback": null,
      "effort": "M"
    },
    {
      "id": "K8S-CAPACITY-001",
      "category": "capacity",
      "severity": "HIGH",
      "title": "Cluster cannot absorb the loss of its largest node",
      "explanation": "Across 3 schedulable nodes, the total requested resources exceed what would remain if the largest node were lost or drained. Requests only count what pods declare, so the real shortfall is larger if containers without requests (K8S-RES-001) exist.",
      "evidence": "Memory: 26.1 GiB requested vs 23.0 GiB allocatable without k3s-agent-1\nTotals: 7000m / 20000m CPU requested, 26.1 GiB / 38.5 GiB memory requested",
      "affected_resources": [],
      "recommendation": "Add a node (or larger nodes) so N-1 capacity covers all requests with ~20% headroom, or reduce over-sized requests after measuring actual usage.",
      "remediation": null,
      "impact": "If the largest worker node fails or is drained, the remaining nodes do not have enough allocatable capacity for the requested resources. Pods stay Pending until the node returns, so node maintenance or a node failure becomes a partial outage.",
      "confidence": "LIKELY",
      "classification": "RISK",
      "root_cause": "RC-CAPACITY",
      "validation": "Sum of requests stays below the allocatable capacity of all nodes minus the largest (N-1) after right-sizing or adding capacity.",
      "rollback": null,
      "effort": "M"
    },
    {
      "id": "K8S-GITOPS-001",
      "category": "gitops",
      "severity": "MEDIUM",
      "title": "Argo CD applications have drifted from Git",
      "explanation": "1 Argo CD application applications report OutOfSync: the live state differs from what Git declares.",
      "evidence": "argocd/payments: OutOfSync (Deployment/payments-api)",
      "affected_resources": [
        "argocd/payments"
      ],
      "recommendation": "Review `argocd app diff` for each application; either commit the live change to Git or sync to discard it.",
      "remediation": null,
      "impact": "The live state differs from Git. Either an unreviewed change was made in the cluster, or a reviewed change has not been deployed. In both cases Git is not an accurate record of production.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-DELIVERY",
      "validation": "`argocd app diff <app>` is empty and the application reports Synced.",
      "rollback": "Syncing applies Git state over manual changes. Capture `argocd app diff` output before syncing, so a deliberate hotfix can be re-applied through Git.",
      "effort": "S"
    },
    {
      "id": "K8S-GITOPS-004",
      "category": "gitops",
      "severity": "MEDIUM",
      "title": "Argo CD projects allow any repository and any destination",
      "explanation": "Applications use projects that place no restriction on source repositories or destination namespaces (the Argo CD `default` project ships this way).",
      "evidence": "AppProject argocd/default: sourceRepos=['*'], destinations=[*/*]; used by argocd/checkout, argocd/monitoring, argocd/payments, argocd/storefront",
      "affected_resources": [
        "argocd/default"
      ],
      "recommendation": "Create per-team AppProjects listing allowed repos, destination namespaces, and cluster-scoped resource kinds, and move applications off `default`.",
      "remediation": null,
      "impact": "Any Application in this project can deploy any repository to any namespace, including kube-system. Write access to Argo CD, or to any watched repo, is effectively cluster-admin.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`argocd proj get <project>` lists only the intended repos and destination namespaces.",
      "rollback": "Re-apply the previous AppProject spec from Git.",
      "effort": "S"
    },
    {
      "id": "K8S-HA-001",
      "category": "resilience",
      "severity": "MEDIUM",
      "title": "Workloads run a single replica",
      "explanation": "5 workloads in namespaces checkout, data, ingress-nginx, kube-system, payments run exactly one replica, including traffic-path components (ingress-nginx/ingress-nginx-controller, kube-system/coredns) whose restart interrupts all requests through them. Single-replica platform controllers that use leader election are excluded.",
      "evidence": "payments/payments-worker (Deployment): spec.replicas=1\ncheckout/checkout-worker (Deployment): spec.replicas=1\ningress-nginx/ingress-nginx-controller (Deployment): spec.replicas=1\nkube-system/coredns (Deployment): spec.replicas=1\ndata/redis (StatefulSet): spec.replicas=1",
      "affected_resources": [
        "payments/payments-worker",
        "checkout/checkout-worker",
        "ingress-nginx/ingress-nginx-controller",
        "kube-system/coredns",
        "data/redis"
      ],
      "recommendation": "Run at least 2 replicas for workloads that must stay available during node maintenance or rollouts, combined with a PodDisruptionBudget and topology spread across nodes.",
      "remediation": null,
      "impact": "Any node drain, eviction, crash, or rolling update briefly takes the workload fully offline.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get deploy <name> -n <ns>` shows 2+ ready replicas; drain one node in a maintenance window and confirm the service stays reachable.",
      "rollback": "Scaling back down (`kubectl scale --replicas=1`) is immediate. Check first that the application tolerates multiple instances (leader election, schedulers, local state).",
      "effort": "S"
    },
    {
      "id": "K8S-HA-004",
      "category": "resilience",
      "severity": "MEDIUM",
      "title": "Replicas are co-located on a single node",
      "explanation": "1 multi-replica workload has every running replica on the same node, so the replica count does not provide node-level redundancy.",
      "evidence": "payments/payments-api (Deployment): 2 running replicas, all on node k3s-agent-1",
      "affected_resources": [
        "payments/payments-api"
      ],
      "recommendation": "Add `topologySpreadConstraints` (topologyKey `kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway` to start) or preferred pod anti-affinity, then let the next rollout redistribute replicas.",
      "remediation": null,
      "impact": "Every ready replica runs on the same node, so the redundancy is nominal: losing that node takes the whole workload offline.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get pods -n <ns> -l <selector> -o wide` shows replicas on different nodes after a rollout.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-HA-005",
      "category": "resilience",
      "severity": "MEDIUM",
      "title": "PodDisruptionBudgets block all voluntary disruptions",
      "explanation": "1 PodDisruptionBudget allows zero disruptions even when every selected pod is healthy, so node drains and upgrades stall on these pods.",
      "evidence": "checkout/checkout-api: minAvailable=2 with 2 expected pods",
      "affected_resources": [
        "checkout/checkout-api"
      ],
      "recommendation": "Use `maxUnavailable: 1` (or `minAvailable` below the replica count) so one pod at a time can be moved, and scale the workload up if it cannot tolerate losing one replica.",
      "remediation": null,
      "impact": "The PodDisruptionBudget currently allows zero disruptions. `kubectl drain` and cluster upgrades will block on these pods until someone intervenes, which usually leads to forced deletions during maintenance.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get pdb -A` shows ALLOWED DISRUPTIONS >= 1 for the listed budgets while all pods are healthy.",
      "rollback": "Re-apply the previous PodDisruptionBudget spec from Git.",
      "effort": "S"
    },
    {
      "id": "K8S-NET-001",
      "category": "networking",
      "severity": "MEDIUM",
      "title": "Namespaces without NetworkPolicy",
      "explanation": "8 namespaces run pods but contain no NetworkPolicy, so every pod in them accepts traffic from, and can connect to, any pod in the cluster (assuming a policy-enforcing CNI).",
      "evidence": "argocd: 3 pods, 0 NetworkPolicies\ncert-manager: 1 pods, 0 NetworkPolicies\ncheckout: 3 pods, 0 NetworkPolicies\ningress-nginx: 1 pods, 0 NetworkPolicies\nlonghorn-system: 3 pods, 0 NetworkPolicies\nmetallb-system: 4 pods, 0 NetworkPolicies\nmonitoring: 2 pods, 0 NetworkPolicies\npayments: 3 pods, 0 NetworkPolicies",
      "affected_resources": [
        "argocd",
        "cert-manager",
        "checkout",
        "ingress-nginx",
        "longhorn-system",
        "metallb-system",
        "monitoring",
        "payments"
      ],
      "recommendation": "Introduce a default-deny baseline, then add explicit ingress and egress rules required by each workload.",
      "remediation": "Apply a default-deny baseline per namespace after adding the allow rules it needs (DNS egress at minimum):\n  kubectl apply -n <ns> -f - <<'EOF'\n  apiVersion: networking.k8s.io/v1\n  kind: NetworkPolicy\n  metadata: {name: default-deny}\n  spec: {podSelector: {}, policyTypes: [Ingress, Egress]}\n  EOF",
      "impact": "Any compromised pod can connect to every other pod in the cluster, including databases and internal admin endpoints. Nothing in the network limits lateral movement.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-EXPOSURE",
      "validation": "From a test pod in another namespace, `nc -zv <service>.<ns> <port>` fails for non-allowed traffic and succeeds for allowed traffic.",
      "rollback": "`kubectl delete networkpolicy default-deny -n <ns>` restores the previous allow-all behaviour immediately.",
      "effort": "M"
    },
    {
      "id": "K8S-NET-002",
      "category": "networking",
      "severity": "MEDIUM",
      "title": "Ingress resources without TLS",
      "explanation": "1 Ingress serves their hosts without a TLS section, so traffic to them is plain HTTP unless TLS is terminated before the ingress controller.",
      "evidence": "monitoring/grafana: hosts=['grafana.demo.example'], spec.tls absent",
      "affected_resources": [
        "monitoring/grafana"
      ],
      "recommendation": "Configure TLS and an HTTP-to-HTTPS redirect at the ingress layer.",
      "remediation": "With cert-manager: add the annotation `cert-manager.io/cluster-issuer: <issuer>` and\n  tls: [{hosts: [<host>], secretName: <name>-tls}]\nto the Ingress spec.",
      "impact": "Credentials and session cookies sent to this host can be read or modified on the network path.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-EXPOSURE",
      "validation": "`curl -sI http://<host>` redirects to https, and `curl -sI https://<host>` presents a valid certificate.",
      "rollback": "Remove the `tls` section and the redirect annotation from the Ingress.",
      "effort": "S"
    },
    {
      "id": "K8S-PG-005",
      "category": "database",
      "severity": "MEDIUM",
      "title": "PostgreSQL instances share a single node",
      "explanation": "Every instance of these clusters runs on the same node, so automatic failover cannot protect against a node failure.",
      "evidence": "data/payments-db: all 2 instances on node k3s-agent-2",
      "affected_resources": [
        "data/payments-db"
      ],
      "recommendation": "Keep `affinity.enablePodAntiAffinity: true` and set `podAntiAffinityType: required` with enough nodes to place each instance separately.",
      "remediation": null,
      "impact": "The primary and its replicas run on the same node, so one node failure takes out every instance and automatic failover cannot help.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get pods -n <ns> -l cnpg.io/cluster=<name> -o wide` shows each instance on a different node.",
      "rollback": "Revert the affinity settings in the Cluster spec; CloudNativePG reschedules replicas one at a time.",
      "effort": "S"
    },
    {
      "id": "K8S-RBAC-006",
      "category": "identity-rbac",
      "severity": "MEDIUM",
      "title": "pods/exec, pods/attach, or pods/portforward granted",
      "explanation": "1 custom role grant interactive access into running pods and are bound to at least one subject outside the platform namespaces. Built-in roles and unbound roles are excluded.",
      "evidence": "ClusterRole support-debug: verbs=['create'] resources=['pods/exec', 'pods/portforward']\n  bound to: Group:support-team (in namespace payments via RoleBinding payments/support-debug)",
      "affected_resources": [
        "support-debug"
      ],
      "recommendation": "Restrict to the operators and namespaces that need it, ideally through an audited break-glass role.",
      "remediation": null,
      "impact": "exec/attach/port-forward give an interactive path into running pods, with their mounted Secrets and network position. This is a common lateral-movement and data-exfiltration route.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl auth can-i create pods/exec -n <ns> --as=<subject>` returns `no` for non-operators.",
      "rollback": "Re-apply the previous role from Git.",
      "effort": "S"
    },
    {
      "id": "K8S-RBAC-007",
      "category": "identity-rbac",
      "severity": "MEDIUM",
      "title": "The default ServiceAccount is bound to a role",
      "explanation": "1 binding grants permissions to a namespace's `default` ServiceAccount, which every pod without an explicit serviceAccountName uses.",
      "evidence": "RoleBinding checkout/checkout-default-edit -> edit; subjects: ServiceAccount:checkout/default",
      "affected_resources": [
        "checkout/checkout-default-edit"
      ],
      "recommendation": "Create a dedicated ServiceAccount per workload with only the permissions it needs, leave `default` unbound, and set `automountServiceAccountToken: false` on it.",
      "remediation": null,
      "impact": "Every pod that does not name a ServiceAccount inherits these permissions, including pods created later for unrelated purposes.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-ACCESS",
      "validation": "`kubectl auth can-i --list -n <ns> --as=system:serviceaccount:<ns>:default` shows no extra permissions.",
      "rollback": "Re-apply the previous binding from Git.",
      "effort": "S"
    },
    {
      "id": "K8S-RES-001",
      "category": "workloads",
      "severity": "MEDIUM",
      "title": "Containers missing CPU or memory requests",
      "explanation": "2 containers in 2 workloads in namespaces longhorn-system, payments have no CPU and/or memory request, so the scheduler places them without reserving capacity.",
      "evidence": "payments/payments-worker (Deployment) container=payments-worker: resources.requests has no cpu or memory (requests={})\nlonghorn-system/longhorn-manager (DaemonSet) container=longhorn-manager: resources.requests has no cpu or memory (requests={})",
      "affected_resources": [
        "payments/payments-worker-5f6b7c8d9-x2v7q:payments-worker",
        "longhorn-system/longhorn-manager-4xk2p:longhorn-manager",
        "longhorn-system/longhorn-manager-7mn3q:longhorn-manager",
        "longhorn-system/longhorn-manager-9rt5s:longhorn-manager"
      ],
      "recommendation": "Set CPU and memory requests from observed usage (p95 over a representative week), and add a namespace LimitRange so new containers get defaults.",
      "remediation": "Add `resources.requests` to the container spec in Git, e.g.:\n  resources:\n    requests: {cpu: 100m, memory: 256Mi}\nSize from `kubectl top pod -n <ns> --containers` or Prometheus `container_memory_working_set_bytes`.",
      "impact": "Without requests the scheduler cannot account for the pod, so nodes become overcommitted. The pod also runs as BestEffort (or low-priority Burstable) and is evicted first under node pressure.",
      "confidence": "CONFIRMED",
      "classification": "RISK",
      "root_cause": "RC-CAPACITY",
      "validation": "`kubectl get pod <pod> -n <ns> -o jsonpath='{.status.qosClass}'` reports Burstable or Guaranteed.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "M"
    },
    {
      "id": "K8S-PSS-001",
      "category": "pod-security-standards",
      "severity": "MEDIUM",
      "title": "Namespaces have no enforced Pod Security Standard",
      "explanation": "8 namespaces have no `pod-security.kubernetes.io/enforce` label, so Pod Security Admission applies the `privileged` level (no restriction) to them.",
      "evidence": "payments: enforce unset (warn=unset, audit=unset)\ncheckout: enforce unset (warn=unset, audit=unset)\ndata: enforce unset (warn=unset, audit=unset)\nmonitoring: enforce unset (warn=unset, audit=unset)\nargocd: enforce unset (warn=unset, audit=unset)\ncert-manager: enforce unset (warn=unset, audit=unset)\ningress-nginx: enforce unset (warn=unset, audit=unset)\ndefault: enforce unset (warn=unset, audit=unset)",
      "affected_resources": [
        "payments",
        "checkout",
        "data",
        "monitoring",
        "argocd",
        "cert-manager",
        "ingress-nginx",
        "default"
      ],
      "recommendation": "Label each namespace with `warn` and `audit` set to `baseline` (or `restricted`), fix the reported workloads, then add `enforce`.",
      "remediation": "  kubectl label --dry-run=server --overwrite ns <ns> pod-security.kubernetes.io/enforce=baseline\n(the dry run lists existing pods that would violate the level without blocking anything)",
      "impact": "Nothing prevents a new privileged or host-mounting pod in this namespace. The workload findings in this report can be reintroduced at any time.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "`kubectl label --dry-run=server --overwrite ns <ns> pod-security.kubernetes.io/enforce=baseline` returns no warnings, then apply the label.",
      "rollback": "`kubectl label ns <ns> pod-security.kubernetes.io/enforce-` removes enforcement immediately; running pods are unaffected.",
      "effort": "M"
    },
    {
      "id": "K8S-RES-002",
      "category": "workloads",
      "severity": "MEDIUM",
      "title": "Containers missing memory limits",
      "explanation": "3 containers in 3 workloads in namespaces checkout, longhorn-system, payments have no memory limit. CPU limits are intentionally not required by this check.",
      "evidence": "payments/payments-worker (Deployment) container=payments-worker: resources.limits.memory unset (limits={})\ncheckout/checkout-api (Deployment) container=checkout-api: resources.limits.memory unset (limits={})\nlonghorn-system/longhorn-manager (DaemonSet) container=longhorn-manager: resources.limits.memory unset (limits={})",
      "affected_resources": [
        "payments/payments-worker-5f6b7c8d9-x2v7q:payments-worker",
        "checkout/checkout-api-6c8d9e7f5-a1b2c:checkout-api",
        "checkout/checkout-api-6c8d9e7f5-d3e4f:checkout-api",
        "longhorn-system/longhorn-manager-4xk2p:longhorn-manager",
        "longhorn-system/longhorn-manager-7mn3q:longhorn-manager",
        "longhorn-system/longhorn-manager-9rt5s:longhorn-manager"
      ],
      "recommendation": "Set a memory limit at or modestly above observed peak working set; leave CPU limits off unless there is a specific reason (e.g. strict tenant isolation).",
      "remediation": "Add `resources.limits.memory` after measuring peak working set; do not apply blind limits to production.",
      "impact": "A container without a memory limit can consume node memory until the kubelet evicts neighbours or the kernel OOM-kills an unrelated process. CPU limits are deliberately not required: CPU is compressible, and CPU limits often cause throttling.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-CAPACITY",
      "validation": "Watch `container_memory_working_set_bytes` against the new limit for a full traffic cycle; there are no OOMKilled restarts.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "M"
    },
    {
      "id": "K8S-SEC-002",
      "category": "security",
      "severity": "MEDIUM",
      "title": "Containers may run as root",
      "explanation": "2 containers in 2 workloads in namespaces checkout, payments do not set `runAsNonRoot: true` or a non-zero `runAsUser`, so they run as the image's default user (often root).",
      "evidence": "payments/payments-api (Deployment) container=payments-api: runAsNonRoot=unset, runAsUser=unset (image default)\ncheckout/checkout-api (Deployment) container=checkout-api: runAsNonRoot=unset, runAsUser=unset (image default)",
      "affected_resources": [
        "payments/payments-api-7d9f8c5b6-k2p9x:payments-api",
        "payments/payments-api-7d9f8c5b6-r8w4n:payments-api",
        "checkout/checkout-api-6c8d9e7f5-a1b2c:checkout-api",
        "checkout/checkout-api-6c8d9e7f5-d3e4f:checkout-api"
      ],
      "recommendation": "Set runAsNonRoot: true and a non-zero runAsUser where the image supports it.",
      "remediation": "Set `securityContext: {runAsNonRoot: true, runAsUser: <uid>}` on the pod or container spec, then verify the image can run as that UID before rolling out.",
      "impact": "Root inside the container is not root on the host, but it widens the impact of any container-escape vulnerability and of writable host mounts.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "M"
    },
    {
      "id": "K8S-SEC-004",
      "category": "security",
      "severity": "MEDIUM",
      "title": "allowPrivilegeEscalation is not explicitly disabled",
      "explanation": "2 containers in 2 workloads in namespaces checkout, ingress-nginx leave `allowPrivilegeEscalation` unset (it defaults to true) or set it to true.",
      "evidence": "checkout/checkout-api (Deployment) container=checkout-api: allowPrivilegeEscalation=unset (defaults to true)\ningress-nginx/ingress-nginx-controller (Deployment) container=controller: allowPrivilegeEscalation=True",
      "affected_resources": [
        "checkout/checkout-api-6c8d9e7f5-a1b2c:checkout-api",
        "checkout/checkout-api-6c8d9e7f5-d3e4f:checkout-api",
        "ingress-nginx/ingress-nginx-controller-6f7d8c9b5-n2p3q:controller"
      ],
      "recommendation": "Set `allowPrivilegeEscalation: false` unless a container specifically requires privilege escalation.",
      "remediation": "Add `securityContext.allowPrivilegeEscalation: false` to the container spec and confirm the workload still functions.",
      "impact": "Processes can gain more privileges than their parent (setuid binaries), which helps an attacker escalate inside the container.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-WORKLOAD-001",
      "category": "workloads",
      "severity": "MEDIUM",
      "title": "Mutable or latest image tags detected",
      "explanation": "1 container in 1 workload in namespace payments references an image by `:latest` or with no tag at all.",
      "evidence": "payments/payments-worker (Deployment) container=payments-worker: image=registry.demo.internal/payments/payments-worker:latest (:latest)",
      "affected_resources": [
        "payments/payments-worker-5f6b7c8d9-x2v7q:payments-worker"
      ],
      "recommendation": "Use immutable version tags or digests and promote tested artifacts.",
      "remediation": "Pin the image to a digest, e.g. `image: registry/app@sha256:<digest>`, and have CI publish and reference digests.",
      "impact": "A mutable tag can resolve to a different image on reschedule, so two replicas of the same Deployment may run different code, and a rollback may not restore the previous build.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-DELIVERY",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-BACKUP-004",
      "category": "backup-recovery",
      "severity": "MEDIUM",
      "title": "No evidence of a recent restore test",
      "explanation": "Backups exist, but the cluster shows no completed restore in the last 90 days (no Velero Restore, no CloudNativePG cluster recovered from backup). Restore drills performed elsewhere would not be visible, so this needs confirmation. Until a restore has been exercised, the achievable RTO is unknown.",
      "evidence": "Observed recovery points (best-case RPO today):\n  PostgreSQL data/payments-db: last successful base backup 5 days ago\n  K3s etcd snapshot: newest 9h ago",
      "affected_resources": [],
      "recommendation": "Run and record a restore drill: restore one namespace with Velero into a scratch namespace, recover the database into a new CloudNativePG Cluster, and restore a Longhorn backup to a new volume; time each step against the agreed RTO.",
      "remediation": null,
      "impact": "A backup that has never been restored is an untested assumption. Restore failures (missing keys, incompatible versions, incomplete scope) are found at the worst possible time, and RTO is unknown.",
      "confidence": "NEEDS_REVIEW",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "A documented restore drill (time to restore, data verified, gaps found) completed within the last 90 days.",
      "rollback": null,
      "effort": "M"
    },
    {
      "id": "K8S-GITOPS-003",
      "category": "gitops",
      "severity": "LOW",
      "title": "Argo CD applications do not self-heal drift",
      "explanation": "2 Argo CD applications applications have no automated sync with self-heal, so drift is reported but not corrected.",
      "evidence": "argocd/payments: automated=no, selfHeal=False, prune=False\nargocd/checkout: automated=yes, selfHeal=False, prune=True",
      "affected_resources": [
        "argocd/payments",
        "argocd/checkout"
      ],
      "recommendation": "Enable `syncPolicy.automated` with `selfHeal: true` (and `prune: true` where safe), or document why manual sync is required.",
      "remediation": null,
      "impact": "Without automated sync and self-heal, drift is detected but not corrected. Manual `kubectl` changes persist until someone notices.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-DELIVERY",
      "validation": "The Application spec has `syncPolicy.automated: {prune: true, selfHeal: true}` (or a documented reason not to).",
      "rollback": "Remove `syncPolicy.automated` to return to manual sync.",
      "effort": "S"
    },
    {
      "id": "K8S-GOV-001",
      "category": "governance",
      "severity": "LOW",
      "title": "Application namespaces without a ResourceQuota",
      "explanation": "3 application namespaces have no ResourceQuota, so nothing caps their aggregate CPU, memory, or object count.",
      "evidence": "checkout: 0 ResourceQuotas\ndata: 0 ResourceQuotas\npayments: 0 ResourceQuotas",
      "affected_resources": [
        "checkout",
        "data",
        "payments"
      ],
      "recommendation": "Define a ResourceQuota per namespace sized to the tenant's expected usage plus headroom.",
      "remediation": null,
      "impact": "One namespace can consume the cluster's whole capacity through a runaway HPA, a bad deploy, or a fork loop, and starve every other tenant.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-CAPACITY",
      "validation": "`kubectl describe quota -n <ns>` shows usage well below hard limits for normal operation.",
      "rollback": "`kubectl delete resourcequota <name> -n <ns>`; running pods are unaffected.",
      "effort": "S"
    },
    {
      "id": "K8S-GOV-002",
      "category": "governance",
      "severity": "LOW",
      "title": "Application namespaces without a LimitRange",
      "explanation": "3 application namespaces have no LimitRange, so new containers without requests (see K8S-RES-001) get no defaults.",
      "evidence": "checkout: 0 LimitRanges\ndata: 0 LimitRanges\npayments: 0 LimitRanges",
      "affected_resources": [
        "checkout",
        "data",
        "payments"
      ],
      "recommendation": "Define a LimitRange with default requests (and a default memory limit) for each namespace.",
      "remediation": null,
      "impact": null,
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-CAPACITY",
      "validation": "New pods in the namespace receive default requests (`kubectl get pod -o jsonpath='{..resources}'`).",
      "rollback": "`kubectl delete limitrange <name> -n <ns>`; it only affects pods created afterwards.",
      "effort": "S"
    },
    {
      "id": "K8S-HA-002",
      "category": "resilience",
      "severity": "LOW",
      "title": "Multi-replica workloads have no matching PodDisruptionBudget",
      "explanation": "1 multi-replica workload is not selected by any PodDisruptionBudget. This check matches each budget's label selector against the workload's pod template labels, so a PDB that exists in the namespace but selects other pods does not count.",
      "evidence": "payments/payments-api (Deployment): replicas=2, pod labels={\"app\": \"payments-api\"}",
      "affected_resources": [
        "payments/payments-api"
      ],
      "recommendation": "Add a PodDisruptionBudget whose selector matches the workload, with `maxUnavailable: 1` (or a `minAvailable` lower than the replica count).",
      "remediation": "  apiVersion: policy/v1\n  kind: PodDisruptionBudget\n  metadata: {name: <app>, namespace: <ns>}\n  spec:\n    maxUnavailable: 1\n    selector: {matchLabels: {app: <app>}}",
      "impact": "Without a matching PodDisruptionBudget, a node drain or cluster upgrade may evict every replica of the workload at once.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-SPOF",
      "validation": "`kubectl get pdb -n <ns>` shows a PDB whose selector matches the workload, with ALLOWED DISRUPTIONS >= 1.",
      "rollback": "Delete the PodDisruptionBudget; this has no effect on running pods.",
      "effort": "S"
    },
    {
      "id": "K8S-HA-007",
      "category": "resilience",
      "severity": "LOW",
      "title": "PodDisruptionBudgets select no pods",
      "explanation": "1 PodDisruptionBudget matches no pods, which usually means a label changed and the intended workload is now unprotected.",
      "evidence": "payments/payments-api-legacy: selector={\"matchLabels\": {\"app\": \"payments-api-v1\"}} matches no pods or workloads",
      "affected_resources": [
        "payments/payments-api-legacy"
      ],
      "recommendation": "Fix the selector to match the intended workload, or delete the budget.",
      "remediation": null,
      "impact": "A PodDisruptionBudget that selects no pods protects nothing. It usually means a label changed and the workload it was meant for is now unprotected.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-SPOF",
      "validation": null,
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-IMG-001",
      "category": "supply-chain",
      "severity": "LOW",
      "title": "Images reference the default registry implicitly",
      "explanation": "6 containers in 6 workloads in namespaces checkout, data, kube-system, longhorn-system pull from Docker Hub implicitly (no registry host in the image reference).",
      "evidence": "checkout/checkout-worker (Deployment) container=checkout-worker: image=checkout/worker:3.1.0 (resolves to docker.io)\ndata/redis (StatefulSet) container=redis: image=redis:7.2.5 (resolves to docker.io)\nlonghorn-system/longhorn-manager (DaemonSet) container=longhorn-manager: image=longhornio/longhorn-manager:v1.7.2 (resolves to docker.io)\nkube-system/coredns (Deployment) container=coredns: image=rancher/mirrored-coredns-coredns:1.11.3 (resolves to docker.io)\nkube-system/local-path-provisioner (Deployment) container=local-path-provisioner: image=rancher/local-path-provisioner:v0.0.30 (resolves to docker.io)\nkube-system/metrics-server (Deployment) container=metrics-server: image=rancher/mirrored-metrics-server:v0.7.2 (resolves to docker.io)",
      "affected_resources": [
        "checkout/checkout-worker-6b8d4f9c7-m4n2p:checkout-worker",
        "data/redis-0:redis",
        "longhorn-system/longhorn-manager-4xk2p:longhorn-manager",
        "longhorn-system/longhorn-manager-7mn3q:longhorn-manager",
        "longhorn-system/longhorn-manager-9rt5s:longhorn-manager",
        "kube-system/coredns-ccb96694c-t6u7v:coredns",
        "kube-system/local-path-provisioner-5cf85fd84d-w8x9y:local-path-provisioner",
        "kube-system/metrics-server-5985cbc9d7-z1a2b:metrics-server"
      ],
      "recommendation": "Confirm images sourced from Docker Hub are from a vetted publisher, or mirror them into an approved registry.",
      "remediation": "Reference an explicit, approved registry host in the image string (e.g. `ghcr.io/org/app:tag`).",
      "impact": "Images pulled implicitly from Docker Hub are subject to rate limits and to publisher trust you have not explicitly reviewed.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-NET-003",
      "category": "networking",
      "severity": "LOW",
      "title": "NetworkPolicy rules that allow all traffic",
      "explanation": "1 rule matches every peer in existing NetworkPolicies, so the policy restricts less than it appears to.",
      "evidence": "data/postgres-access: an egress rule has no `to`, so it allows all destinations",
      "affected_resources": [
        "data/postgres-access"
      ],
      "recommendation": "Add explicit `from`/`to` selectors (podSelector/namespaceSelector/ipBlock) to the rule, or remove it if a default-deny is intended.",
      "remediation": null,
      "impact": "The policy looks restrictive in a manifest review but allows everything on the rule in question.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-EXPOSURE",
      "validation": null,
      "rollback": "Re-apply the previous NetworkPolicy from Git.",
      "effort": "S"
    },
    {
      "id": "K8S-NET-005",
      "category": "networking",
      "severity": "LOW",
      "title": "NodePort services detected",
      "explanation": "1 Service opens a port on every node's IP address.",
      "evidence": "monitoring/grafana: nodePorts=[30300]",
      "affected_resources": [
        "monitoring/grafana"
      ],
      "recommendation": "Confirm the exposure is intentional; prefer ClusterIP behind an ingress, or a LoadBalancer with source-range restrictions.",
      "remediation": null,
      "impact": "The service is reachable on every node IP, and node IPs are usually less protected than an ingress path.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-EXPOSURE",
      "validation": "`nc -zv <node-ip> <nodePort>` from outside the cluster is refused once the Service type is changed.",
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-SEC-006",
      "category": "security",
      "severity": "LOW",
      "title": "Containers do not drop all Linux capabilities",
      "explanation": "1 container in 1 workload in namespace checkout keeps the container runtime's default capability set instead of `drop: [ALL]`.",
      "evidence": "checkout/checkout-api (Deployment) container=checkout-api: capabilities.drop=unset",
      "affected_resources": [
        "checkout/checkout-api-6c8d9e7f5-a1b2c:checkout-api",
        "checkout/checkout-api-6c8d9e7f5-d3e4f:checkout-api"
      ],
      "recommendation": "Set `capabilities.drop: [ALL]` and add back only what is required.",
      "remediation": "Add `securityContext.capabilities.drop: [ALL]`, then add back only the specific capabilities the workload needs.",
      "impact": null,
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-SEC-007",
      "category": "security",
      "severity": "LOW",
      "title": "Container root filesystem is writable",
      "explanation": "5 containers in 5 workloads in namespaces checkout, data, ingress-nginx, monitoring, payments have a writable root filesystem.",
      "evidence": "payments/payments-api (Deployment) container=payments-api: readOnlyRootFilesystem unset or false\ncheckout/checkout-api (Deployment) container=checkout-api: readOnlyRootFilesystem unset or false\ndata/redis (StatefulSet) container=redis: readOnlyRootFilesystem unset or false\nmonitoring/grafana (Deployment) container=grafana: readOnlyRootFilesystem unset or false\ningress-nginx/ingress-nginx-controller (Deployment) container=controller: readOnlyRootFilesystem unset or false",
      "affected_resources": [
        "payments/payments-api-7d9f8c5b6-k2p9x:payments-api",
        "payments/payments-api-7d9f8c5b6-r8w4n:payments-api",
        "checkout/checkout-api-6c8d9e7f5-a1b2c:checkout-api",
        "checkout/checkout-api-6c8d9e7f5-d3e4f:checkout-api",
        "data/redis-0:redis",
        "monitoring/grafana-7b9c8d6f5-h7j8k:grafana",
        "ingress-nginx/ingress-nginx-controller-6f7d8c9b5-n2p3q:controller"
      ],
      "recommendation": "Set `readOnlyRootFilesystem: true` and mount writable emptyDir volumes only where the application needs to write.",
      "remediation": "Add `securityContext.readOnlyRootFilesystem: true`; add an `emptyDir` mount for any path the application writes to (e.g. `/tmp`).",
      "impact": null,
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-SEC-009",
      "category": "security",
      "severity": "LOW",
      "title": "No seccomp profile configured",
      "explanation": "2 containers in 2 workloads in namespaces checkout, payments run without a seccomp profile (Unconfined), so every syscall is available.",
      "evidence": "payments/payments-api (Deployment) container=payments-api: no seccompProfile at pod or container level\ncheckout/checkout-api (Deployment) container=checkout-api: no seccompProfile at pod or container level",
      "affected_resources": [
        "payments/payments-api-7d9f8c5b6-k2p9x:payments-api",
        "payments/payments-api-7d9f8c5b6-r8w4n:payments-api",
        "checkout/checkout-api-6c8d9e7f5-a1b2c:checkout-api",
        "checkout/checkout-api-6c8d9e7f5-d3e4f:checkout-api"
      ],
      "recommendation": "Set a seccompProfile at the pod or container level (RuntimeDefault at minimum).",
      "remediation": "Add `securityContext.seccompProfile: {type: RuntimeDefault}` at the pod level, or per-container if profiles differ.",
      "impact": null,
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": "Re-run the audit, and confirm the workload rolls out cleanly: `kubectl rollout status <kind>/<name> -n <ns>` and no new restarts in `kubectl get pods -n <ns>`.",
      "rollback": "Revert the manifest change in Git (or `kubectl rollout undo <kind>/<name> -n <ns>`) if the workload fails to start or its probes fail after the change.",
      "effort": "S"
    },
    {
      "id": "K8S-BACKUP-001",
      "category": "backup-recovery",
      "severity": "LOW",
      "title": "No scheduled backup of cluster resources",
      "explanation": "Velero is not installed, and no other resource-level backup was found. Argo CD can rebuild resources that are declared in Git, which lowers the severity, but objects created at runtime (Secrets not in Git, operator-created resources, certificates) would still be lost.",
      "evidence": "velero schedules: 0; Argo CD Applications: 4",
      "affected_resources": [],
      "recommendation": "Install Velero (or an equivalent) with a daily schedule of all namespaces to an off-cluster bucket, or document which mechanism restores non-Git state.",
      "remediation": null,
      "impact": "Without a backup of cluster resources, recovery after accidental deletion or cluster loss depends on rebuilding from Git and manual records. Objects not in Git (Secrets, CRs created by operators, runtime state) are lost.",
      "confidence": "NEEDS_REVIEW",
      "classification": "RISK",
      "root_cause": "RC-RECOVERY",
      "validation": "A scheduled backup completes, and a restore of one namespace into a scratch cluster or namespace succeeds.",
      "rollback": null,
      "effort": "M"
    },
    {
      "id": "K8S-NET-006",
      "category": "networking",
      "severity": "LOW",
      "title": "LoadBalancer services reachable from the whole internal network",
      "explanation": "1 LoadBalancer Service uses private addresses with no source restriction, so any host on the routed internal network can reach them. The audit cannot see firewalls outside the cluster.",
      "evidence": "argocd/argocd-server: external IP 10.20.0.50 (private), ports=[443], loadBalancerSourceRanges unset",
      "affected_resources": [
        "argocd/argocd-server"
      ],
      "recommendation": "Confirm the intended client networks and set `loadBalancerSourceRanges` accordingly.",
      "remediation": null,
      "impact": "Any host that can route to the LoadBalancer address can reach the service. The audit cannot see firewall rules outside the cluster, so the real reachability must be confirmed.",
      "confidence": "NEEDS_REVIEW",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-EXPOSURE",
      "validation": "Connect from a host outside the allowed range and confirm the connection is refused.",
      "rollback": "Remove `loadBalancerSourceRanges` from the Service.",
      "effort": "S"
    },
    {
      "id": "K8S-RBAC-008",
      "category": "identity-rbac",
      "severity": "LOW",
      "title": "Possibly unused ServiceAccounts",
      "explanation": "2 ServiceAccounts are not referenced by any running Pod (platform namespaces excluded) or workload template. CronJobs that are not running, or external tooling using their tokens, may still need them.",
      "evidence": "payments/deploy-bot: no Pod or workload spec references it\ncheckout/legacy-reporter: no Pod or workload spec references it",
      "affected_resources": [
        "payments/deploy-bot",
        "checkout/legacy-reporter"
      ],
      "recommendation": "Confirm each ServiceAccount is still required before removing it and its RBAC bindings.",
      "remediation": null,
      "impact": null,
      "confidence": "NEEDS_REVIEW",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-ACCESS",
      "validation": null,
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-SECRET-002",
      "category": "secrets",
      "severity": "LOW",
      "title": "Legacy long-lived ServiceAccount token Secrets present",
      "explanation": "1 long-lived service-account token Secret exists. These tokens never expire, unlike the short-lived tokens the TokenRequest API issues.",
      "evidence": "payments/deploy-bot-token: token for ServiceAccount deploy-bot",
      "affected_resources": [
        "payments/deploy-bot-token"
      ],
      "recommendation": "Confirm whether each is still used (external CI is common), migrate to short-lived tokens, and delete unused ones.",
      "remediation": null,
      "impact": null,
      "confidence": "NEEDS_REVIEW",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-ACCESS",
      "validation": null,
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-RBAC-011",
      "category": "identity-rbac",
      "severity": "INFO",
      "title": "High-privilege RBAC held by platform controllers (expected)",
      "explanation": "3 roles grant high-risk permissions only to service accounts of platform controllers (e.g. Argo CD, cert-manager, Longhorn). This is how these products work, but it concentrates privilege: compromising one of these controllers or its namespace is a cluster compromise.",
      "evidence": "ClusterRole argocd-application-controller: K8S-RBAC-002, K8S-RBAC-005\nClusterRole cert-manager-controller-certificates: K8S-RBAC-005\nClusterRole longhorn-role: K8S-RBAC-002, K8S-RBAC-005",
      "affected_resources": [
        "ClusterRole argocd-application-controller",
        "ClusterRole cert-manager-controller-certificates",
        "ClusterRole longhorn-role"
      ],
      "recommendation": "Restrict who can exec into, modify, or create pods in these controller namespaces, and keep the controllers on supported versions.",
      "remediation": null,
      "impact": "These controllers need broad permissions to do their job. The risk is concentration: anyone who can modify their deployments, exec into their pods, or read their service-account tokens inherits those permissions.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-ACCESS",
      "validation": null,
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-SEC-011",
      "category": "security",
      "severity": "INFO",
      "title": "Privileged platform components (expected by design)",
      "explanation": "2 platform containers hold host-level privileges that their product requires (storage data plane, load-balancer speaker, node agent). They are excluded from the CRITICAL workload checks above and listed here so the exception is explicit.",
      "evidence": "longhorn-system/longhorn-manager (DaemonSet) container=longhorn-manager (longhornio/longhorn-manager:v1.7.2): hostPath /dev, hostPath /proc, hostPath /var/lib/longhorn, privileged\nmetallb-system/speaker (DaemonSet) container=speaker (quay.io/metallb/speaker:v0.14.8): cap NET_RAW, hostNetwork=true",
      "affected_resources": [
        "longhorn-system/longhorn-manager (DaemonSet) container=longhorn-manager (longhornio/longhorn-manager:v1.7.2)",
        "metallb-system/speaker (DaemonSet) container=speaker (quay.io/metallb/speaker:v0.14.8)"
      ],
      "recommendation": "Keep these components in dedicated namespaces with a documented `privileged` Pod Security exception, restrict who can modify them, and keep them on supported versions.",
      "remediation": null,
      "impact": "These components need host-level privileges by design (storage, load-balancer, and node agents). They are listed so the exception is visible and documented, not as defects.",
      "confidence": "CONFIRMED",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": null,
      "rollback": null,
      "effort": "S"
    },
    {
      "id": "K8S-PSS-002",
      "category": "pod-security-standards",
      "severity": "INFO",
      "title": "System and platform namespaces have no Pod Security label",
      "explanation": "These namespaces host components that often legitimately need `privileged`. Recording that explicitly makes the exception a documented decision.",
      "evidence": "longhorn-system: enforce unset\nmetallb-system: enforce unset\nkube-system: enforce unset\nkube-public: enforce unset\nkube-node-lease: enforce unset",
      "affected_resources": [
        "longhorn-system",
        "metallb-system",
        "kube-system",
        "kube-public",
        "kube-node-lease"
      ],
      "recommendation": "Label them `pod-security.kubernetes.io/enforce: privileged` deliberately, or `baseline` where the components allow it.",
      "remediation": null,
      "impact": null,
      "confidence": "NEEDS_REVIEW",
      "classification": "BEST_PRACTICE",
      "root_cause": "RC-WORKLOAD-BASELINE",
      "validation": null,
      "rollback": null,
      "effort": "S"
    }
  ],
  "assessment": {
    "scorecard": {
      "overall": {
        "score": 41,
        "rating": "Poor"
      },
      "domains": [
        {
          "domain": "Control plane & lifecycle",
          "score": 51,
          "rating": "Poor",
          "findings": 2,
          "key_issue": "Kubernetes 1.31 is past end of support",
          "assessment": "Partial"
        },
        {
          "domain": "Workload security",
          "score": 39,
          "rating": "Critical",
          "findings": 8,
          "key_issue": "Privileged containers detected",
          "assessment": "Assessed"
        },
        {
          "domain": "Identity & secrets",
          "score": 14,
          "rating": "Critical",
          "findings": 9,
          "key_issue": "Non-default subjects hold cluster-admin cluster-wide",
          "assessment": "Assessed"
        },
        {
          "domain": "Network & exposure",
          "score": 8,
          "rating": "Critical",
          "findings": 11,
          "key_issue": "ingress-nginx is vulnerable to CVE-2025-1974 (IngressNightmare)",
          "assessment": "Partial"
        },
        {
          "domain": "Availability & failover",
          "score": 20,
          "rating": "Critical",
          "findings": 9,
          "key_issue": "PostgreSQL clusters without a standby for failover",
          "assessment": "Partial"
        },
        {
          "domain": "Storage",
          "score": 72,
          "rating": "Fair",
          "findings": 1,
          "key_issue": "Longhorn volumes without effective replica redundancy",
          "assessment": "Assessed"
        },
        {
          "domain": "Backup & recovery",
          "score": 19,
          "rating": "Critical",
          "findings": 7,
          "key_issue": "etcd snapshots are stored only on the server's local disk",
          "assessment": "Partial"
        },
        {
          "domain": "Capacity & resources",
          "score": 42,
          "rating": "Poor",
          "findings": 6,
          "key_issue": "Nodes are running close to capacity",
          "assessment": "Assessed"
        },
        {
          "domain": "GitOps & delivery",
          "score": 49,
          "rating": "Poor",
          "findings": 5,
          "key_issue": "Argo CD applications are Degraded or Missing",
          "assessment": "Assessed"
        },
        {
          "domain": "Observability",
          "score": 100,
          "rating": "Good",
          "findings": 0,
          "key_issue": "",
          "assessment": "Partial"
        }
      ],
      "counts": {
        "CRITICAL": 2,
        "HIGH": 24,
        "MEDIUM": 17,
        "LOW": 15,
        "INFO": 3
      },
      "confirmed_risks": 35,
      "needs_review": 5,
      "best_practice": 19
    },
    "root_causes": [
      {
        "key": "RC-WORKLOAD-BASELINE",
        "title": "No enforced workload security baseline",
        "description": "Pod Security Admission is not enforcing a baseline, so container hardening depends on each team's manifests. Fixing individual containers without enforcement lets the same issues return.",
        "max_severity": "CRITICAL",
        "count": 8,
        "findings": [
          {
            "id": "K8S-SEC-001",
            "title": "Privileged containers detected",
            "severity": "CRITICAL"
          },
          {
            "id": "K8S-PSS-001",
            "title": "Namespaces have no enforced Pod Security Standard",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-SEC-002",
            "title": "Containers may run as root",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-SEC-004",
            "title": "allowPrivilegeEscalation is not explicitly disabled",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-IMG-001",
            "title": "Images reference the default registry implicitly",
            "severity": "LOW"
          },
          {
            "id": "K8S-SEC-006",
            "title": "Containers do not drop all Linux capabilities",
            "severity": "LOW"
          },
          {
            "id": "K8S-SEC-007",
            "title": "Container root filesystem is writable",
            "severity": "LOW"
          },
          {
            "id": "K8S-SEC-009",
            "title": "No seccomp profile configured",
            "severity": "LOW"
          }
        ]
      },
      {
        "key": "RC-LIFECYCLE",
        "title": "Platform components are past their supported lifecycle",
        "description": "Kubernetes and add-on versions no longer receive security fixes. Every later vulnerability in these components stays unpatched, so this is fixed by an upgrade plan rather than per finding.",
        "max_severity": "CRITICAL",
        "count": 3,
        "findings": [
          {
            "id": "K8S-INGRESS-001",
            "title": "ingress-nginx is vulnerable to CVE-2025-1974 (IngressNightmare)",
            "severity": "CRITICAL"
          },
          {
            "id": "K8S-CLUSTER-003",
            "title": "Kubernetes 1.31 is past end of support",
            "severity": "HIGH"
          },
          {
            "id": "K8S-INGRESS-002",
            "title": "ingress-nginx is retired upstream",
            "severity": "HIGH"
          }
        ]
      },
      {
        "key": "RC-ACCESS",
        "title": "Access is granted more broadly than workloads and people need",
        "description": "RBAC grants, credentials, and service-account usage exceed least privilege, which increases the blast radius of any single compromised identity.",
        "max_severity": "HIGH",
        "count": 11,
        "findings": [
          {
            "id": "K8S-INGRESS-003",
            "title": "ingress-nginx allows snippet annotations",
            "severity": "HIGH"
          },
          {
            "id": "K8S-RBAC-001",
            "title": "Non-default subjects hold cluster-admin cluster-wide",
            "severity": "HIGH"
          },
          {
            "id": "K8S-RBAC-003",
            "title": "cluster-admin ClusterRole granted within a namespace",
            "severity": "HIGH"
          },
          {
            "id": "K8S-RBAC-005",
            "title": "Broad read access to Secrets granted",
            "severity": "HIGH"
          },
          {
            "id": "K8S-RBAC-009",
            "title": "Permissions granted to all users or anonymous callers",
            "severity": "HIGH"
          },
          {
            "id": "K8S-SECRET-001",
            "title": "Credential stored in a plaintext environment variable",
            "severity": "HIGH"
          },
          {
            "id": "K8S-GITOPS-004",
            "title": "Argo CD projects allow any repository and any destination",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-RBAC-006",
            "title": "pods/exec, pods/attach, or pods/portforward granted",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-RBAC-007",
            "title": "The default ServiceAccount is bound to a role",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-RBAC-008",
            "title": "Possibly unused ServiceAccounts",
            "severity": "LOW"
          },
          {
            "id": "K8S-SECRET-002",
            "title": "Legacy long-lived ServiceAccount token Secrets present",
            "severity": "LOW"
          }
        ]
      },
      {
        "key": "RC-SPOF",
        "title": "Single points of failure in the platform",
        "description": "Components with one instance, or replicas that share one failure domain, mean a single node or process failure causes an outage or data unavailability.",
        "max_severity": "HIGH",
        "count": 9,
        "findings": [
          {
            "id": "K8S-CP-001",
            "title": "Control plane runs on a single server node",
            "severity": "HIGH"
          },
          {
            "id": "K8S-LONGHORN-002",
            "title": "Longhorn volumes without effective replica redundancy",
            "severity": "HIGH"
          },
          {
            "id": "K8S-PG-001",
            "title": "PostgreSQL clusters without a standby for failover",
            "severity": "HIGH"
          },
          {
            "id": "K8S-HA-001",
            "title": "Workloads run a single replica",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-HA-004",
            "title": "Replicas are co-located on a single node",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-HA-005",
            "title": "PodDisruptionBudgets block all voluntary disruptions",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-PG-005",
            "title": "PostgreSQL instances share a single node",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-HA-002",
            "title": "Multi-replica workloads have no matching PodDisruptionBudget",
            "severity": "LOW"
          },
          {
            "id": "K8S-HA-007",
            "title": "PodDisruptionBudgets select no pods",
            "severity": "LOW"
          }
        ]
      },
      {
        "key": "RC-RECOVERY",
        "title": "Backup and recovery are not proven",
        "description": "Backups are missing, failing, kept only next to the data they protect, or have never been restored. RPO and RTO cannot be stated with confidence until a restore has been exercised.",
        "max_severity": "HIGH",
        "count": 7,
        "findings": [
          {
            "id": "K8S-K3S-003",
            "title": "etcd snapshots are stored only on the server's local disk",
            "severity": "HIGH"
          },
          {
            "id": "K8S-LONGHORN-003",
            "title": "Longhorn has no backup target configured",
            "severity": "HIGH"
          },
          {
            "id": "K8S-PG-003",
            "title": "PostgreSQL clusters have no backup configured",
            "severity": "HIGH"
          },
          {
            "id": "K8S-PG-004",
            "title": "PostgreSQL backups or WAL archiving are failing",
            "severity": "HIGH"
          },
          {
            "id": "K8S-BACKUP-003",
            "title": "Persistent volumes with no backup coverage",
            "severity": "HIGH"
          },
          {
            "id": "K8S-BACKUP-004",
            "title": "No evidence of a recent restore test",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-BACKUP-001",
            "title": "No scheduled backup of cluster resources",
            "severity": "LOW"
          }
        ]
      },
      {
        "key": "RC-CAPACITY",
        "title": "Capacity and resource governance are unmanaged",
        "description": "Requests, limits, quotas, and headroom are not set deliberately, so scheduling, eviction, and failure-tolerance behavior is unpredictable under load or node loss.",
        "max_severity": "HIGH",
        "count": 6,
        "findings": [
          {
            "id": "K8S-CAPACITY-002",
            "title": "Nodes are running close to capacity",
            "severity": "HIGH"
          },
          {
            "id": "K8S-CAPACITY-001",
            "title": "Cluster cannot absorb the loss of its largest node",
            "severity": "HIGH"
          },
          {
            "id": "K8S-RES-001",
            "title": "Containers missing CPU or memory requests",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-RES-002",
            "title": "Containers missing memory limits",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-GOV-001",
            "title": "Application namespaces without a ResourceQuota",
            "severity": "LOW"
          },
          {
            "id": "K8S-GOV-002",
            "title": "Application namespaces without a LimitRange",
            "severity": "LOW"
          }
        ]
      },
      {
        "key": "RC-EXPOSURE",
        "title": "Network exposure is not deliberately controlled",
        "description": "Entry points (LoadBalancers, NodePorts, ingress) and in-cluster traffic are not restricted to what is intended, so the effective attack surface is larger than the architecture requires.",
        "max_severity": "HIGH",
        "count": 6,
        "findings": [
          {
            "id": "K8S-NET-008",
            "title": "Non-HTTP service exposed on a public IP without source restriction",
            "severity": "HIGH"
          },
          {
            "id": "K8S-NET-001",
            "title": "Namespaces without NetworkPolicy",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-NET-002",
            "title": "Ingress resources without TLS",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-NET-003",
            "title": "NetworkPolicy rules that allow all traffic",
            "severity": "LOW"
          },
          {
            "id": "K8S-NET-005",
            "title": "NodePort services detected",
            "severity": "LOW"
          },
          {
            "id": "K8S-NET-006",
            "title": "LoadBalancer services reachable from the whole internal network",
            "severity": "LOW"
          }
        ]
      },
      {
        "key": "RC-HEALTH",
        "title": "Components are currently unhealthy",
        "description": "These describe live failures observed at audit time. They are usually symptoms; confirm the cause before treating them as configuration defects.",
        "max_severity": "HIGH",
        "count": 5,
        "findings": [
          {
            "id": "K8S-CERT-001",
            "title": "cert-manager certificates are not Ready",
            "severity": "HIGH"
          },
          {
            "id": "K8S-CERT-002",
            "title": "TLS certificates expire within 14 days",
            "severity": "HIGH"
          },
          {
            "id": "K8S-GITOPS-002",
            "title": "Argo CD applications are Degraded or Missing",
            "severity": "HIGH"
          },
          {
            "id": "K8S-WORKLOAD-002",
            "title": "Unhealthy container state detected",
            "severity": "HIGH"
          },
          {
            "id": "K8S-WORKLOAD-003",
            "title": "Workloads have unavailable replicas",
            "severity": "HIGH"
          }
        ]
      },
      {
        "key": "RC-DELIVERY",
        "title": "Deployed state is not reliably controlled from Git",
        "description": "GitOps applications drift, fail to sync, or are scoped too broadly, so what runs in the cluster is not guaranteed to match the reviewed configuration.",
        "max_severity": "MEDIUM",
        "count": 3,
        "findings": [
          {
            "id": "K8S-GITOPS-001",
            "title": "Argo CD applications have drifted from Git",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-WORKLOAD-001",
            "title": "Mutable or latest image tags detected",
            "severity": "MEDIUM"
          },
          {
            "id": "K8S-GITOPS-003",
            "title": "Argo CD applications do not self-heal drift",
            "severity": "LOW"
          }
        ]
      }
    ],
    "roadmap": [
      {
        "phase": "Phase 1: Immediate",
        "timeframe": "within 7 days",
        "description": "Every CRITICAL finding, plus confirmed HIGH risks that can be fixed in under a day: active exposure, exploitable vulnerabilities, and data that is unprotected right now.",
        "groups": [
          {
            "root_cause": "RC-WORKLOAD-BASELINE",
            "title": "No enforced workload security baseline",
            "items": [
              {
                "id": "K8S-SEC-001",
                "title": "Privileged containers detected",
                "severity": "CRITICAL",
                "effort": "M",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-LIFECYCLE",
            "title": "Platform components are past their supported lifecycle",
            "items": [
              {
                "id": "K8S-INGRESS-001",
                "title": "ingress-nginx is vulnerable to CVE-2025-1974 (IngressNightmare)",
                "severity": "CRITICAL",
                "effort": "M",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-ACCESS",
            "title": "Access is granted more broadly than workloads and people need",
            "items": [
              {
                "id": "K8S-INGRESS-003",
                "title": "ingress-nginx allows snippet annotations",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-RBAC-003",
                "title": "cluster-admin ClusterRole granted within a namespace",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-RBAC-009",
                "title": "Permissions granted to all users or anonymous callers",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-SECRET-001",
                "title": "Credential stored in a plaintext environment variable",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-RECOVERY",
            "title": "Backup and recovery are not proven",
            "items": [
              {
                "id": "K8S-K3S-003",
                "title": "etcd snapshots are stored only on the server's local disk",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-LONGHORN-003",
                "title": "Longhorn has no backup target configured",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-PG-004",
                "title": "PostgreSQL backups or WAL archiving are failing",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-CAPACITY",
            "title": "Capacity and resource governance are unmanaged",
            "items": [
              {
                "id": "K8S-CAPACITY-002",
                "title": "Nodes are running close to capacity",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-HEALTH",
            "title": "Components are currently unhealthy",
            "items": [
              {
                "id": "K8S-CERT-001",
                "title": "cert-manager certificates are not Ready",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-CERT-002",
                "title": "TLS certificates expire within 14 days",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-GITOPS-002",
                "title": "Argo CD applications are Degraded or Missing",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-WORKLOAD-002",
                "title": "Unhealthy container state detected",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-WORKLOAD-003",
                "title": "Workloads have unavailable replicas",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-EXPOSURE",
            "title": "Network exposure is not deliberately controlled",
            "items": [
              {
                "id": "K8S-NET-008",
                "title": "Non-HTTP service exposed on a public IP without source restriction",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-SPOF",
            "title": "Single points of failure in the platform",
            "items": [
              {
                "id": "K8S-LONGHORN-002",
                "title": "Longhorn volumes without effective replica redundancy",
                "severity": "HIGH",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          }
        ],
        "count": 17
      },
      {
        "phase": "Phase 2: Short term",
        "timeframe": "within 30 days",
        "description": "Remaining high-severity items, which usually need design work or a change window, plus medium risks that reduce resilience or widen the attack surface.",
        "groups": [
          {
            "root_cause": "RC-ACCESS",
            "title": "Access is granted more broadly than workloads and people need",
            "items": [
              {
                "id": "K8S-RBAC-001",
                "title": "Non-default subjects hold cluster-admin cluster-wide",
                "severity": "HIGH",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-RBAC-005",
                "title": "Broad read access to Secrets granted",
                "severity": "HIGH",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-GITOPS-004",
                "title": "Argo CD projects allow any repository and any destination",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-RBAC-006",
                "title": "pods/exec, pods/attach, or pods/portforward granted",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-RBAC-007",
                "title": "The default ServiceAccount is bound to a role",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-RECOVERY",
            "title": "Backup and recovery are not proven",
            "items": [
              {
                "id": "K8S-PG-003",
                "title": "PostgreSQL clusters have no backup configured",
                "severity": "HIGH",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-BACKUP-003",
                "title": "Persistent volumes with no backup coverage",
                "severity": "HIGH",
                "effort": "M",
                "confidence": "LIKELY"
              }
            ]
          },
          {
            "root_cause": "RC-CAPACITY",
            "title": "Capacity and resource governance are unmanaged",
            "items": [
              {
                "id": "K8S-CAPACITY-001",
                "title": "Cluster cannot absorb the loss of its largest node",
                "severity": "HIGH",
                "effort": "M",
                "confidence": "LIKELY"
              },
              {
                "id": "K8S-RES-001",
                "title": "Containers missing CPU or memory requests",
                "severity": "MEDIUM",
                "effort": "M",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-LIFECYCLE",
            "title": "Platform components are past their supported lifecycle",
            "items": [
              {
                "id": "K8S-CLUSTER-003",
                "title": "Kubernetes 1.31 is past end of support",
                "severity": "HIGH",
                "effort": "L",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-INGRESS-002",
                "title": "ingress-nginx is retired upstream",
                "severity": "HIGH",
                "effort": "L",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-SPOF",
            "title": "Single points of failure in the platform",
            "items": [
              {
                "id": "K8S-CP-001",
                "title": "Control plane runs on a single server node",
                "severity": "HIGH",
                "effort": "L",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-PG-001",
                "title": "PostgreSQL clusters without a standby for failover",
                "severity": "HIGH",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-HA-001",
                "title": "Workloads run a single replica",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-HA-004",
                "title": "Replicas are co-located on a single node",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-HA-005",
                "title": "PodDisruptionBudgets block all voluntary disruptions",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-PG-005",
                "title": "PostgreSQL instances share a single node",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-DELIVERY",
            "title": "Deployed state is not reliably controlled from Git",
            "items": [
              {
                "id": "K8S-GITOPS-001",
                "title": "Argo CD applications have drifted from Git",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-EXPOSURE",
            "title": "Network exposure is not deliberately controlled",
            "items": [
              {
                "id": "K8S-NET-001",
                "title": "Namespaces without NetworkPolicy",
                "severity": "MEDIUM",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-NET-002",
                "title": "Ingress resources without TLS",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          }
        ],
        "count": 20
      },
      {
        "phase": "Phase 3: Planned",
        "timeframe": "within 90 days",
        "description": "Best-practice baselines and items that need confirmation, best handled as standards rather than one-off fixes.",
        "groups": [
          {
            "root_cause": "RC-RECOVERY",
            "title": "Backup and recovery are not proven",
            "items": [
              {
                "id": "K8S-BACKUP-004",
                "title": "No evidence of a recent restore test",
                "severity": "MEDIUM",
                "effort": "M",
                "confidence": "NEEDS_REVIEW"
              },
              {
                "id": "K8S-BACKUP-001",
                "title": "No scheduled backup of cluster resources",
                "severity": "LOW",
                "effort": "M",
                "confidence": "NEEDS_REVIEW"
              }
            ]
          },
          {
            "root_cause": "RC-CAPACITY",
            "title": "Capacity and resource governance are unmanaged",
            "items": [
              {
                "id": "K8S-RES-002",
                "title": "Containers missing memory limits",
                "severity": "MEDIUM",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-GOV-001",
                "title": "Application namespaces without a ResourceQuota",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-GOV-002",
                "title": "Application namespaces without a LimitRange",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-DELIVERY",
            "title": "Deployed state is not reliably controlled from Git",
            "items": [
              {
                "id": "K8S-WORKLOAD-001",
                "title": "Mutable or latest image tags detected",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-GITOPS-003",
                "title": "Argo CD applications do not self-heal drift",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-WORKLOAD-BASELINE",
            "title": "No enforced workload security baseline",
            "items": [
              {
                "id": "K8S-PSS-001",
                "title": "Namespaces have no enforced Pod Security Standard",
                "severity": "MEDIUM",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-SEC-002",
                "title": "Containers may run as root",
                "severity": "MEDIUM",
                "effort": "M",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-SEC-004",
                "title": "allowPrivilegeEscalation is not explicitly disabled",
                "severity": "MEDIUM",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-IMG-001",
                "title": "Images reference the default registry implicitly",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-SEC-006",
                "title": "Containers do not drop all Linux capabilities",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-SEC-007",
                "title": "Container root filesystem is writable",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-SEC-009",
                "title": "No seccomp profile configured",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          },
          {
            "root_cause": "RC-ACCESS",
            "title": "Access is granted more broadly than workloads and people need",
            "items": [
              {
                "id": "K8S-RBAC-008",
                "title": "Possibly unused ServiceAccounts",
                "severity": "LOW",
                "effort": "S",
                "confidence": "NEEDS_REVIEW"
              },
              {
                "id": "K8S-SECRET-002",
                "title": "Legacy long-lived ServiceAccount token Secrets present",
                "severity": "LOW",
                "effort": "S",
                "confidence": "NEEDS_REVIEW"
              }
            ]
          },
          {
            "root_cause": "RC-EXPOSURE",
            "title": "Network exposure is not deliberately controlled",
            "items": [
              {
                "id": "K8S-NET-003",
                "title": "NetworkPolicy rules that allow all traffic",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-NET-005",
                "title": "NodePort services detected",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-NET-006",
                "title": "LoadBalancer services reachable from the whole internal network",
                "severity": "LOW",
                "effort": "S",
                "confidence": "NEEDS_REVIEW"
              }
            ]
          },
          {
            "root_cause": "RC-SPOF",
            "title": "Single points of failure in the platform",
            "items": [
              {
                "id": "K8S-HA-002",
                "title": "Multi-replica workloads have no matching PodDisruptionBudget",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              },
              {
                "id": "K8S-HA-007",
                "title": "PodDisruptionBudgets select no pods",
                "severity": "LOW",
                "effort": "S",
                "confidence": "CONFIRMED"
              }
            ]
          }
        ],
        "count": 21
      }
    ],
    "coverage": [
      {
        "area": "Kubernetes version and support lifecycle",
        "status": "Assessed",
        "detail": "Evaluated as K3s against the bundled lifecycle snapshot (retrieved 2026-10-26).",
        "domain": "Control plane & lifecycle"
      },
      {
        "area": "Control-plane availability and datastore",
        "status": "Assessed",
        "detail": "1 control-plane node(s); datastore: embedded etcd.",
        "domain": "Control plane & lifecycle"
      },
      {
        "area": "K3s etcd snapshots",
        "status": "Assessed",
        "detail": "ETCDSnapshotFile objects inspected (location, readiness, age).",
        "domain": "Backup & recovery"
      },
      {
        "area": "Control-plane configuration (API server flags, admission plugins, audit logging, encryption at rest, kubelet config)",
        "status": "Not assessed",
        "detail": "Not exposed by the Kubernetes API. Requires host access to the server nodes (or the provider console for managed clusters).",
        "domain": "Control plane & lifecycle"
      },
      {
        "area": "Node operating system and host hardening",
        "status": "Not assessed",
        "detail": "Outside the Kubernetes API. Covered by a separate host audit (`devops-tool audit security`) on each node.",
        "domain": "Control plane & lifecycle"
      },
      {
        "area": "Workload and container security",
        "status": "Assessed",
        "detail": "All pods: privilege, host access, capabilities, users, seccomp, images.",
        "domain": "Workload security"
      },
      {
        "area": "Pod Security Admission",
        "status": "Assessed",
        "detail": "Namespace enforce/warn/audit labels cross-checked against running workloads.",
        "domain": "Workload security"
      },
      {
        "area": "RBAC",
        "status": "Assessed",
        "detail": "Roles evaluated by what they grant and to whom they are bound; built-in roles excluded. Identity-provider group membership (who is in a Group subject) is not visible.",
        "domain": "Identity & secrets"
      },
      {
        "area": "Secrets handling",
        "status": "Assessed",
        "detail": "Secret metadata and env-var usage only; secret values are never read into the report.",
        "domain": "Identity & secrets"
      },
      {
        "area": "Network segmentation (NetworkPolicy)",
        "status": "Partial",
        "detail": "Policy objects assessed. Whether the CNI enforces them was not verified with traffic tests.",
        "domain": "Network & exposure"
      },
      {
        "area": "External exposure (LoadBalancer, NodePort, Ingress, host network)",
        "status": "Partial",
        "detail": "All in-cluster entry points inventoried and classified as public or private. Firewalls, NAT, and DNS outside the cluster are not visible.",
        "domain": "Network & exposure"
      },
      {
        "area": "Ingress controller (ingress-nginx)",
        "status": "Assessed",
        "detail": "Version, CVE exposure, project lifecycle, snippet configuration.",
        "domain": "Network & exposure"
      },
      {
        "area": "TLS certificates (cert-manager)",
        "status": "Assessed",
        "detail": "Certificate readiness, expiry, and issuer health.",
        "domain": "Network & exposure"
      },
      {
        "area": "Load balancer (MetalLB)",
        "status": "Assessed",
        "detail": "Address pools and advertisements.",
        "domain": "Network & exposure"
      },
      {
        "area": "Workload availability and single points of failure",
        "status": "Assessed",
        "detail": "Replica counts, PodDisruptionBudget selector matching and live disruption status, replica placement across nodes.",
        "domain": "Availability & failover"
      },
      {
        "area": "PostgreSQL (CloudNativePG) replication and failover",
        "status": "Partial",
        "detail": "Instance count, readiness, placement, backup and WAL-archive status assessed. Replication lag, synchronous-replication settings, and an actual failover test require database access.",
        "domain": "Availability & failover"
      },
      {
        "area": "Storage (Longhorn)",
        "status": "Assessed",
        "detail": "Volume robustness, replica counts versus schedulable nodes, backup target and recurring jobs.",
        "domain": "Storage"
      },
      {
        "area": "Backups",
        "status": "Partial",
        "detail": "Velero, Longhorn, CloudNativePG, and K3s etcd snapshot configuration and status assessed, plus per-PVC coverage. Backups made outside the cluster (host, hypervisor, or database-native tooling) are not visible.",
        "domain": "Backup & recovery"
      },
      {
        "area": "Restore readiness and RTO",
        "status": "Not assessed",
        "detail": "Restore evidence inside the cluster was checked, but RTO can only be established with a timed restore drill (available as an extended engagement).",
        "domain": "Backup & recovery"
      },
      {
        "area": "Capacity and resource utilization",
        "status": "Assessed",
        "detail": "Requests versus allocatable per node and N-1 headroom; actual usage from metrics-server.",
        "domain": "Capacity & resources"
      },
      {
        "area": "GitOps (Argo CD)",
        "status": "Assessed",
        "detail": "Sync status, health, self-heal, last operation, project scoping.",
        "domain": "GitOps & delivery"
      },
      {
        "area": "Monitoring and alerting",
        "status": "Partial",
        "detail": "Presence of in-cluster monitoring only. Alert rules, routing, and on-call coverage need a walkthrough with the platform owner.",
        "domain": "Observability"
      }
    ],
    "inventory": {
      "platform": {
        "distribution": "k3s",
        "datastore": "embedded etcd",
        "control_plane_nodes": 1,
        "nodes": 3,
        "components": {
          "Longhorn": true,
          "CloudNativePG": true,
          "Velero": false,
          "MetalLB": true,
          "ingress-nginx": true,
          "cert-manager": true,
          "Argo CD": true,
          "metrics-server": true
        }
      },
      "exposure": [
        {
          "entry": "ingress-nginx/ingress-nginx-controller",
          "type": "LoadBalancer",
          "address": "203.0.113.10",
          "ports": "80, 443",
          "public": true,
          "restriction": "none"
        },
        {
          "entry": "data/payments-db-external",
          "type": "LoadBalancer",
          "address": "203.0.113.11",
          "ports": "5432",
          "public": true,
          "restriction": "none"
        },
        {
          "entry": "argocd/argocd-server",
          "type": "LoadBalancer",
          "address": "10.20.0.50",
          "ports": "443",
          "public": false,
          "restriction": "none"
        },
        {
          "entry": "monitoring/grafana",
          "type": "NodePort",
          "address": "node IPs",
          "ports": "3000",
          "public": null,
          "restriction": "none"
        },
        {
          "entry": "storefront/storefront",
          "type": "Ingress",
          "address": "shop.demo.example",
          "ports": "443",
          "public": null,
          "restriction": "TLS"
        },
        {
          "entry": "payments/payments-api",
          "type": "Ingress",
          "address": "api.demo.example",
          "ports": "443",
          "public": null,
          "restriction": "TLS"
        },
        {
          "entry": "monitoring/grafana",
          "type": "Ingress",
          "address": "grafana.demo.example",
          "ports": "80",
          "public": null,
          "restriction": "no TLS"
        },
        {
          "entry": "metallb-system/speaker",
          "type": "host network",
          "address": "3 node(s)",
          "ports": "all ports it binds",
          "public": null,
          "restriction": "none"
        }
      ],
      "capacity": [
        {
          "node": "k3s-server-1",
          "roles": [
            "control-plane",
            "etcd",
            "master"
          ],
          "schedulable": true,
          "cpu_allocatable_m": 4000,
          "cpu_requested_m": 900,
          "memory_allocatable": 8053063680,
          "memory_requested": 2569011200,
          "cpu_used_m": 1350,
          "memory_used": 4939211776
        },
        {
          "node": "k3s-agent-1",
          "roles": [
            "worker"
          ],
          "schedulable": true,
          "cpu_allocatable_m": 8000,
          "cpu_requested_m": 2950,
          "memory_allocatable": 16642998272,
          "memory_requested": 10267656192,
          "cpu_used_m": 3120,
          "memory_used": 11811160064
        },
        {
          "node": "k3s-agent-2",
          "roles": [
            "worker"
          ],
          "schedulable": true,
          "cpu_allocatable_m": 8000,
          "cpu_requested_m": 3150,
          "memory_allocatable": 16642998272,
          "memory_requested": 15166603264,
          "cpu_used_m": 2870,
          "memory_used": 15247133696
        }
      ],
      "recovery_points": [
        "PostgreSQL data/payments-db: last successful base backup 5 days ago",
        "K3s etcd snapshot: newest 9h ago"
      ]
    }
  }
}
