diff --git a/k8s/bases/apps/actual-budget/helm-release.yaml b/k8s/bases/apps/actual-budget/helm-release.yaml index a06786ec6..45e4461a2 100644 --- a/k8s/bases/apps/actual-budget/helm-release.yaml +++ b/k8s/bases/apps/actual-budget/helm-release.yaml @@ -187,7 +187,14 @@ spec: runAsUser: 1001 runAsGroup: 1001 securityContext: - readOnlyRootFilesystem: false + # Kubescape C-0017 (immutable container filesystem). The chart already + # mounts the only two paths actual-server writes to — the `tmp` emptyDir at + # /tmp (scratch/uploads) and the `data` PVC at /data (ACTUAL_DATA_DIR: + # account.sqlite, server-files/, user-files/) — so a read-only root FS needs + # no extra volumes here. Proven safe by the enablebanking-seed sidecar above, + # which runs the SAME actual-server image via node with + # readOnlyRootFilesystem: true and only /data + /tmp, healthy in prod. + readOnlyRootFilesystem: true runAsUser: 1001 runAsGroup: 1001 ingress: diff --git a/k8s/bases/apps/ascoachingogvaner/flux-kustomization.yaml b/k8s/bases/apps/ascoachingogvaner/flux-kustomization.yaml index ddfa8c1b6..a405901a3 100644 --- a/k8s/bases/apps/ascoachingogvaner/flux-kustomization.yaml +++ b/k8s/bases/apps/ascoachingogvaner/flux-kustomization.yaml @@ -21,3 +21,58 @@ spec: kind: OCIRepository name: ascoachingogvaner targetNamespace: ascoachingogvaner + patches: + # Kubescape C-0017 (immutable container filesystem): harden the app + # container to readOnlyRootFilesystem: true. The app image is + # nginxinc/nginx-unprivileged (a static Astro/adapter-static site on :8080, + # served as uid 1000), so this is patched platform-side rather than in the + # env-agnostic OCI artifact — it is a universal security control, not a + # provider-specific override, so it lives in the base (applies to every + # render). Pinned to name: ascoachingogvaner so it never touches the + # external-dns Deployment the same OCIRepository also delivers. + # + # nginx-unprivileged writes to a small set of paths at runtime; with a + # read-only root it needs those backed by writable emptyDir volumes or it + # crashes on startup ("mkdir ... Read-only file system"). Verified against + # the live pod's effective config (nginx -T): + # * /tmp REQUIRED — pid (/tmp/nginx.pid) + all temp paths + # (client_body/proxy/fastcgi/uwsgi/scgi_temp) resolve here. + # * /var/cache/nginx defensive — present in the base image; unused by this + # purely-static config today, mounted to stay robust if a + # future nginx/proxy_cache directive starts writing here. + # * /var/run defensive — symlink to /run; nginx-unprivileged's + # canonical writable set. Access + error logs already go + # to /dev/stdout + /dev/stderr, so no /var/log mount is + # needed. Docroot /usr/share/nginx/html stays read-only. + - target: + kind: Deployment + name: ascoachingogvaner + patch: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: ascoachingogvaner + spec: + template: + spec: + containers: + - name: ascoachingogvaner + securityContext: + readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-run + mountPath: /var/run + volumes: + - name: tmp + emptyDir: + sizeLimit: 256Mi + - name: nginx-cache + emptyDir: + sizeLimit: 256Mi + - name: nginx-run + emptyDir: + sizeLimit: 64Mi diff --git a/k8s/bases/apps/backstage/helm-release.yaml b/k8s/bases/apps/backstage/helm-release.yaml index e6afea947..e6fdd70b0 100644 --- a/k8s/bases/apps/backstage/helm-release.yaml +++ b/k8s/bases/apps/backstage/helm-release.yaml @@ -127,10 +127,32 @@ spec: - ALL seccompProfile: type: RuntimeDefault - # Backstage writes to /app and /tmp at runtime; a read-only root would - # need writable volume mounts. PSS-restricted does not require it here - # (validate-pod-security explicitly does not check readOnlyRootFilesystem). - readOnlyRootFilesystem: false + # Immutable root FS (Kubescape C-0017). Verified against the live 1.52.0 + # demo image: the backend (`node packages/backend`) writes NOTHING to the + # image FS at runtime (an exercised backend — health, catalog, frontend + # `/`, /api/app/config — produced zero writes). The classic read-only-root + # break (frontend config injected into packages/app/dist/static/ + # module-backstage..js — backstage#4135) does NOT occur here: this + # version serves frontend config from memory. The only runtime-writable + # path the backend can need is the OS temp-dir — the scaffolder's + # backend.workingDirectory and TechDocs prepare/generate default to /tmp — + # so we make just /tmp writable (extraVolumes below) and keep the rest + # read-only. /app is deliberately NOT made writable: an emptyDir over it + # would clobber the app bundle and nothing writes there at runtime. + readOnlyRootFilesystem: true + + # Writable scratch for the read-only root FS above. Only /tmp is needed + # (scaffolder working dir + TechDocs prepare/generate + transient Node + # scratch). Disk-backed emptyDir (NOT medium: Memory) so a large scaffolder + # clone / generated docs don't count against the 1Gi memory limit and OOM + # the backend; sizeLimit caps runaway growth (evicted, not OOM-killed). + extraVolumeMounts: + - name: tmp + mountPath: /tmp + extraVolumes: + - name: tmp + emptyDir: + sizeLimit: 1Gi extraEnvVars: # Backstage reads APP_CONFIG_ env keys as config overrides at diff --git a/k8s/bases/apps/crossview/helm-release.yaml b/k8s/bases/apps/crossview/helm-release.yaml index 0f8f290ad..320540b67 100644 --- a/k8s/bases/apps/crossview/helm-release.yaml +++ b/k8s/bases/apps/crossview/helm-release.yaml @@ -17,6 +17,81 @@ spec: sourceRef: kind: HelmRepository name: crossview + # Harden against Kubescape C-0017 (immutable container filesystem). The chart + # exposes app/database.containerSecurityContext but NO extraVolumes/ + # extraVolumeMounts knob, and it hardcodes only /app/logs (app) and + # /var/run/postgresql (postgres) as emptyDir volumes — so both the + # readOnlyRootFilesystem flip AND the extra writable mounts postgres needs are + # applied here as a post-render kustomize patch (strategic-merge merges the + # named containers/volumes additively; every other securityContext field — + # capabilities.drop, runAsUser, seccompProfile — is preserved). + postRenderers: + - kustomize: + patches: + # App (Go server) + wait-for-db init — flip root FS read-only. The + # crossview binary is a static CGO-free Go server that writes NOTHING to + # the filesystem at runtime (state lives in Postgres; sessions are a + # signed cookie, no server-side session files), so no extra writable + # mount is needed — the chart's hardcoded /app/logs emptyDir already + # covers the one path it mounts. The wait-for-db init only runs + # `psql -c "SELECT 1"` over TCP, so it is read-only-safe too. + - target: + kind: Deployment + name: crossview + patch: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: crossview + spec: + template: + spec: + initContainers: + - name: wait-for-db + securityContext: + readOnlyRootFilesystem: true + containers: + - name: crossview + securityContext: + readOnlyRootFilesystem: true + # Bundled Postgres (postgres:18.4, persistence disabled → data dir is an + # emptyDir). Flip the root FS read-only and add the writable mounts the + # postgres entrypoint genuinely needs: + # /var/lib/postgresql the image VOLUME. PG18 initdb writes the fresh + # cluster to PGDATA=/var/lib/postgresql/18/docker; + # with persistence off the chart mounts nothing + # here, so it must be a writable emptyDir. This is + # the ephemeral data dir — losing it on restart is + # by design (SSO-fronted read-only dashboard). + # /tmp the entrypoint mktemp's the initdb pwfile here. + # /var/run/postgresql (the socket dir) is already a chart emptyDir. + - target: + kind: Deployment + name: crossview-postgres + patch: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: crossview-postgres + spec: + template: + spec: + containers: + - name: postgres + securityContext: + readOnlyRootFilesystem: true + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql + - name: tmp + mountPath: /tmp + volumes: + - name: postgres-data + emptyDir: + sizeLimit: 2Gi + - name: tmp + emptyDir: + sizeLimit: 128Mi # https://github.com/crossplane-contrib/crossview/blob/main/helm/crossview/values.yaml values: global: diff --git a/k8s/bases/apps/homepage/helm-release.yaml b/k8s/bases/apps/homepage/helm-release.yaml index c0b1a2234..6fbec1df2 100644 --- a/k8s/bases/apps/homepage/helm-release.yaml +++ b/k8s/bases/apps/homepage/helm-release.yaml @@ -161,6 +161,33 @@ spec: limits: cpu: "2" memory: 512Mi + # Harden against Kubescape C-0017 (immutable container filesystem). The chart + # renders the container securityContext with `toYaml .Values.securityContext`, + # so this block REPLACES the chart default in full — it repeats the chart's + # restricted context verbatim and flips only readOnlyRootFilesystem to true + # (dropping any field here would silently weaken the context). + # + # Safe to make the root FS read-only: homepage's only runtime writes go to + # /app/config (boot-copied defaults + generated files, e.g. proxmox.yaml) and + # /app/config/logs (homepage.log) — and the chart's deployment template + # already hardcodes BOTH as writable `config`/`logs` emptyDir volumes + # (verified on the live prod pod). Everything else the app touches is either a + # read-only configMap subPath or /app/.next build artifacts written at + # image-build time; /app/.next/cache is never created at runtime, so no extra + # writable mount is needed. Flagger copies this pod template to + # homepage-primary, so the hardening propagates to the primary it manages. + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault config: allowedHosts: - ${domain} diff --git a/k8s/bases/apps/umami/helm-release.yaml b/k8s/bases/apps/umami/helm-release.yaml index 164b52334..049c9ddce 100644 --- a/k8s/bases/apps/umami/helm-release.yaml +++ b/k8s/bases/apps/umami/helm-release.yaml @@ -144,9 +144,32 @@ spec: capabilities: drop: - ALL - # Next.js (standalone) writes its cache under /app/.next, so the root - # filesystem cannot be read-only. - readOnlyRootFilesystem: false + # Kubescape C-0017 (immutable container filesystem). The Umami image is a + # plain Next.js standalone server (start-docker runs check-db → + # update-tracker → `node server.js`, with NO runtime build), so the root FS + # can be read-only now that the two dirs the server writes at runtime — + # /app/.next/cache (fetch/ISR data cache + optimized images) and /tmp — are + # backed by the writable emptyDirs below (extraVolumes/extraVolumeMounts). + readOnlyRootFilesystem: true + # Writable emptyDir mounts so readOnlyRootFilesystem: true does not crash the + # app. The chart appends extraVolumes to the pod spec.volumes and + # extraVolumeMounts to the umami container. Both dirs are owned/writable by + # the nextjs user (uid/gid 1001 = podSecurityContext.fsGroup/runAsUser), so no + # extra ownership fix is needed — the same read-only-root pattern the fleetdm + # HelmRelease uses. Only /app/.next/cache is mounted (not all of /app/.next), + # so the read-only build artifacts in the rest of /app/.next stay intact. + extraVolumes: + - name: next-cache + emptyDir: + sizeLimit: 512Mi + - name: tmp + emptyDir: + sizeLimit: 128Mi + extraVolumeMounts: + - name: next-cache + mountPath: /app/.next/cache + - name: tmp + mountPath: /tmp resources: requests: cpu: 50m diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/infrastructure-privileged.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/infrastructure-privileged.yaml index 627ee3497..639df646e 100644 --- a/k8s/bases/infrastructure/cluster-security-exceptions/infrastructure-privileged.yaml +++ b/k8s/bases/infrastructure/cluster-security-exceptions/infrastructure-privileged.yaml @@ -7,6 +7,10 @@ # SPIRE agent (identity) needs: hostPath, hostPID # KubeVirt (VM hosting) needs: privileged, hostPath, hostPID, NET_ADMIN # CDI (container disk images) needs: privileged for disk operations +# Kubescape's node-agent/storage are NOT exempted here by namespace — the kubescape +# namespace also hosts the operator, kubevuln, and the kubescape scheduler, which +# need no host access; they get a workload-scoped exception instead +# (kubescape-privileged.yaml). apiVersion: kubescape.io/v1beta1 kind: ClusterSecurityException metadata: @@ -15,8 +19,8 @@ spec: reason: >- CNI (Cilium), distributed storage (Longhorn), node telemetry (Coroot node-agent), identity (SPIRE agent), backup - (Velero node-agent), and virtualisation (KubeVirt, CDI) require - host-level access by design. + (Velero node-agent), and virtualisation (KubeVirt, CDI) require host-level + access by design. posture: - controlID: C-0057 action: ignore diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/kubescape-privileged.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/kubescape-privileged.yaml new file mode 100644 index 000000000..d41cdff12 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-security-exceptions/kubescape-privileged.yaml @@ -0,0 +1,53 @@ +--- +# Workload-scoped privileged exception for the two Kubescape components that +# genuinely need host access — deliberately NOT a namespace exception, because the +# kubescape namespace also hosts the operator, kubevuln, and the scheduler, which +# run fine hardened and must stay visible to these controls. +# - node-agent (DaemonSet): eBPF runtime detection — privileged, hostPath, hostPID. +# - storage (Deployment): the aggregated APIserver writes its backing store at +# runtime, so it cannot run a read-only root. +# The CSE resources match is kind+name only (no namespace field in the CRD); both +# names are unique among non-exempted namespaces — every other similarly-named +# workload (e.g. Coroot's node agent) lives in a namespace already covered by +# infrastructure-privileged.yaml. +apiVersion: kubescape.io/v1beta1 +kind: ClusterSecurityException +metadata: + name: kubescape-privileged +spec: + reason: >- + Kubescape's node-agent needs privileged host access (eBPF runtime detection) + and the storage aggregated APIserver writes its backing store at runtime; + scoped to those two workloads so the operator, kubevuln, and scheduler in the + same namespace stay subject to these controls. + posture: + - controlID: C-0057 + action: ignore + - controlID: C-0048 + action: ignore + - controlID: C-0045 + action: ignore + - controlID: C-0041 + action: ignore + - controlID: C-0038 + action: ignore + - controlID: C-0044 + action: ignore + - controlID: C-0046 + action: ignore + - controlID: C-0016 + action: ignore + - controlID: C-0013 + action: ignore + - controlID: C-0017 + action: ignore + - controlID: C-0055 + action: ignore + match: + resources: + - apiGroup: apps + kind: DaemonSet + name: node-agent + - apiGroup: apps + kind: Deployment + name: storage diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml index f6ed61768..ebd276587 100644 --- a/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml @@ -12,7 +12,9 @@ resources: - helm-chart-metadata.yaml - image-verification.yaml - infrastructure-privileged.yaml + - kubescape-privileged.yaml - pod-security-mutations.yaml + - readonly-rootfs-pending.yaml - service-account-tokens.yaml - talos-cis-control-plane-false-positives.yaml - talos-cis-worker-false-positives.yaml diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/pod-security-mutations.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/pod-security-mutations.yaml index f9f58298c..85d5ad577 100644 --- a/k8s/bases/infrastructure/cluster-security-exceptions/pod-security-mutations.yaml +++ b/k8s/bases/infrastructure/cluster-security-exceptions/pod-security-mutations.yaml @@ -1,26 +1,32 @@ --- -# Exempt pod-level security context controls cluster-wide. -# Kyverno ClusterPolicy "add-security-context" enforces these at pod -# admission time (seccompProfile, allowPrivilegeEscalation: false, -# readOnlyRootFilesystem: true, drop ALL capabilities). However, kubescape -# scans the Deployment/StatefulSet spec where these fields are absent. +# Exempt the pod-level security-context controls that Kyverno's +# "add-security-context" ClusterPolicy injects at pod admission (seccompProfile, +# allowPrivilegeEscalation: false, drop ALL capabilities, runAsNonRoot). Kubescape +# scans the stored Deployment/StatefulSet spec — where these Kyverno-injected +# fields are absent — so they are exempted cluster-wide. +# +# NOTE: readOnlyRootFilesystem (C-0017) is deliberately NOT exempted here anymore. +# It is now set GENUINELY in each workload's own spec wherever the app tolerates a +# read-only root (see the app HelmReleases + the Crossplane DeploymentRuntimeConfigs), +# so the control truly passes rather than being suppressed. The privileged infra +# namespaces keep their C-0017 exemption in infrastructure-privileged.yaml, and the +# small remainder that cannot yet run read-only is scoped in readonly-rootfs-pending.yaml. apiVersion: kubescape.io/v1beta1 kind: ClusterSecurityException metadata: name: pod-security-mutations spec: reason: >- - Kyverno mutate policy "add-security-context" enforces these controls at - pod admission. The Deployment spec does not contain these fields because - they are injected by Kyverno when pods are created. Kubescape scans the - stored spec, not the live pod. + Kyverno mutate policy "add-security-context" injects these controls + (seccompProfile, allowPrivilegeEscalation: false, drop ALL capabilities, + runAsNonRoot) at pod admission. The Deployment spec does not contain them + and Kubescape scans the stored spec, not the live pod. readOnlyRootFilesystem + (C-0017) is handled separately — set in-spec per workload. posture: - controlID: C-0211 action: ignore - controlID: C-0016 action: ignore - - controlID: C-0017 - action: ignore - controlID: C-0055 action: ignore - controlID: C-0013 diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/readonly-rootfs-pending.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/readonly-rootfs-pending.yaml new file mode 100644 index 000000000..dbf471252 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-security-exceptions/readonly-rootfs-pending.yaml @@ -0,0 +1,40 @@ +--- +# C-0017 (immutable container filesystem) exceptions for the few NON-privileged +# workloads whose root FS cannot yet be made read-only in-spec. Every other +# non-privileged workload now sets readOnlyRootFilesystem: true genuinely (its app +# HelmRelease / DeploymentRuntimeConfig), so this is the narrow remainder — scoped +# by namespace so it never suppresses C-0017 for the workloads that DO pass it. +# +# - opencost: the opencost-UI (nginx) entrypoint runs `sed -i` in-place over the +# baked /var/www *.js on every boot to inject BASE_URL/footer placeholders, so +# an immutable root FS crashes it, and an emptyDir over /var/www blanks the UI +# (the image bakes assets there and the entrypoint never repopulates an empty +# mount). Upstream fix needed: opencost/opencost#2655. The MAIN opencost +# container already runs readOnlyRootFilesystem: true — only the UI is exempt, +# but Kubescape scopes exceptions by namespace, so the namespace is listed. +# - wedding-app: the container securityContext lives in the wedding-app app repo's +# env-agnostic OCI artifact (deploy/deployment.yaml), not this platform repo. +# It is hardened there (readOnlyRootFilesystem + a /tmp emptyDir) in a follow-up +# app release; once that ships, remove wedding-app from this list. +apiVersion: kubescape.io/v1beta1 +kind: ClusterSecurityException +metadata: + name: readonly-rootfs-pending +spec: + reason: >- + opencost-UI (nginx) rewrites its baked /var/www assets in place at boot so it + cannot run a read-only root (upstream opencost/opencost#2655); wedding-app's + container securityContext lives in its own OCI artifact and is hardened in a + follow-up app release. Every other non-privileged workload sets + readOnlyRootFilesystem in-spec, so C-0017 genuinely passes for them. + posture: + - controlID: C-0017 + action: ignore + match: + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: + - opencost + - wedding-app diff --git a/k8s/bases/infrastructure/opencost/helm-release.yaml b/k8s/bases/infrastructure/opencost/helm-release.yaml index 8f45371cd..a034f50d3 100644 --- a/k8s/bases/infrastructure/opencost/helm-release.yaml +++ b/k8s/bases/infrastructure/opencost/helm-release.yaml @@ -187,6 +187,16 @@ spec: ui: enabled: true securityContext: + # C-0017 EXCEPTION (Kubescape immutable container filesystem): the + # opencost-ui entrypoint runs `sed -i` in-place over every *.js under + # /var/www on EACH start (injecting BASE_URL=/model + footer/aggregation + # placeholders) — unpredictable writes into the baked-in app-asset dir, + # so the root FS cannot be immutable. /var/www also cannot be an emptyDir: + # the image bakes assets via `COPY --from=builder /opt/standard /var/www` + # and the entrypoint never repopulates an empty mount → the UI would 404. + # The main `opencost` container DOES run readOnlyRootFilesystem:true; only + # this nginx-based UI keeps the exception. Revisit once the entrypoint + # stops mutating /var/www in place (upstream opencost/opencost#2655). readOnlyRootFilesystem: false resources: requests: diff --git a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-aws-iam.yaml b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-aws-iam.yaml index 77a728d9d..f1fb34e0a 100644 --- a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-aws-iam.yaml +++ b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-aws-iam.yaml @@ -11,9 +11,35 @@ spec: spec: containers: - name: package-runtime + # Kubescape C-0017 (immutable container filesystem) + the hardening + # set Kyverno injects at admission (Kubescape scans this stored spec, + # not the live pod). The upjet runtime writes Terraform workspaces to + # /tmp/; this provider has no managed resources today so a + # read-only root does not break it yet, but the writable /tmp emptyDir + # is added defensively (same code path as provider-upjet-unifi, which + # is actively failing on a read-only /tmp) so a future managed resource + # cannot silently fail to reconcile. runAsUser/runAsGroup left to + # Crossplane's package-manager default (2000). + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + seLinuxOptions: + level: s0 resources: requests: cpu: 10m memory: 128Mi limits: memory: 512Mi + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-family-aws.yaml b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-family-aws.yaml index 89e6cbaa8..4380e7fa2 100644 --- a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-family-aws.yaml +++ b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-family-aws.yaml @@ -13,9 +13,35 @@ spec: spec: containers: - name: package-runtime + # Kubescape C-0017 (immutable container filesystem) + the hardening + # set Kyverno injects at admission (Kubescape scans this stored spec, + # not the live pod). The upjet runtime writes Terraform workspaces to + # /tmp/; this provider has no managed resources today so a + # read-only root does not break it yet, but the writable /tmp emptyDir + # is added defensively (same code path as provider-upjet-unifi, which + # is actively failing on a read-only /tmp) so a future managed resource + # cannot silently fail to reconcile. runAsUser/runAsGroup left to + # Crossplane's package-manager default (2000). + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + seLinuxOptions: + level: s0 resources: requests: cpu: 10m memory: 128Mi limits: memory: 512Mi + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-upjet-unifi.yaml b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-upjet-unifi.yaml index a5b7fe7c7..c9e094c3b 100644 --- a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-upjet-unifi.yaml +++ b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config-upjet-unifi.yaml @@ -14,9 +14,36 @@ spec: spec: containers: - name: package-runtime + # Kubescape C-0017 (immutable container filesystem) + the hardening + # set Kyverno injects at admission (Kubescape scans this stored spec, + # not the live pod). This provider (devantler-tech provider-upjet-unifi + # v0.1.0, older upjet) DOES write Terraform workspaces to /tmp/, + # so a read-only root with no writable /tmp breaks EVERY managed + # resource — it is failing right now (cluster-wireguard SYNCED=False: + # "mkdir /tmp/: read-only file system"). Kyverno already forces + # the pod read-only with no /tmp mount, so this DRC adds the writable + # /tmp emptyDir that both fixes reconciliation and makes the read-only + # root safe. runAsUser/runAsGroup left to Crossplane's default (2000). + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + seLinuxOptions: + level: s0 resources: requests: cpu: 10m memory: 128Mi limits: memory: 512Mi + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config.yaml b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config.yaml index e2c04ea90..9f839373b 100644 --- a/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config.yaml +++ b/k8s/providers/hetzner/infrastructure/crossplane/deployment-runtime-config.yaml @@ -14,6 +14,27 @@ spec: spec: containers: - name: package-runtime + # Kubescape C-0017 (immutable container filesystem) + the hardening + # set Kyverno's add-security-context ClusterPolicy injects at pod + # admission. Kubescape scans this stored DRC-built spec (not the live + # pod), so the fields must be present here too — otherwise C-0017 + # flags it even though the running pod is already read-only. No + # writable /tmp is needed: provider-upjet-github's no-fork upjet + # runtime keeps Terraform state in memory / the resource status, + # never on disk (verified reconciling read-only in prod). + # runAsUser/runAsGroup are left to Crossplane's package-manager + # default (2000) — clearing them is the one unsupported DRC op. + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + seLinuxOptions: + level: s0 resources: requests: cpu: 10m