Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion k8s/bases/apps/actual-budget/helm-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions k8s/bases/apps/ascoachingogvaner/flux-kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 26 additions & 4 deletions k8s/bases/apps/backstage/helm-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.<hash>.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_<dotted_path> env keys as config overrides at
Expand Down
75 changes: 75 additions & 0 deletions k8s/bases/apps/crossview/helm-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 27 additions & 0 deletions k8s/bases/apps/homepage/helm-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
29 changes: 26 additions & 3 deletions k8s/bases/apps/umami/helm-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
resources:
requests:
cpu: 50m
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading