Harden pod and container security contexts across all workloads

Add securityContext hardening (runAsNonRoot, readOnlyRootFilesystem,
capabilities drop ALL, allowPrivilegeEscalation false) to the 10 workloads
that lacked it: apache-tika, cyberchef, gotenberg, homeassistant,
matrix-delegation-nginx, paperless-server, paperless-consume cronjob
(plus automountServiceAccountToken false), patchmon-frontend,
urlaubsverwaltung and zipline. All contexts are configurable via
securityContext.pod / securityContext.container in values.yaml; writable
paths are provided as emptyDirs. Image-forced deviations (Home Assistant
root start for s6-overlay + NET_RAW for discovery, paperless root init
container for /run ownership) are documented in the charts' values.yaml.
Major version bump for every changed published chart.
This commit is contained in:
jklein committed 2026-08-21 21:49:05 +02:00
1 parent 6dd80e3874
commit b5eba4cbf1
31 files changed
+354 -13

No files matched your search

+1 -1
View File
@@ -51,6 +51,6 @@ Copy `template-chart/` and replace every `xxx` placeholder — in file names (`t
- Secrets are 1Password `OnePasswordItem` CRs: values expose `secrets.<name>SecretRef` holding an `op://` item path; the operator materializes the Kubernetes Secret. - Secrets are 1Password `OnePasswordItem` CRs: values expose `secrets.<name>SecretRef` holding an `op://` item path; the operator materializes the Kubernetes Secret.
- Ingress assumes `ingressClassName: nginx` and cert-manager: `ingress.clusterIssuer` and `ingress.domain` are `required` values, TLS secret named `tls-<release>-<chart>-ingress`. - Ingress assumes `ingressClassName: nginx` and cert-manager: `ingress.clusterIssuer` and `ingress.domain` are `required` values, TLS secret named `tls-<release>-<chart>-ingress`.
- `env` entries in values support Helm templating — the deployment renders them through `tpl`, and supports `value`, `secretKeyRef`, and `configMapKeyRef` forms. - `env` entries in values support Helm templating — the deployment renders them through `tpl`, and supports `value`, `secretKeyRef`, and `configMapKeyRef` forms.
- Hardened defaults: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped; probes and resource requests/limits always set. - Hardened defaults: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped; probes and resource requests/limits always set. Security contexts are values-configurable (`securityContext.pod` / `securityContext.container`); image-forced deviations are documented with a comment in the chart's `values.yaml`.
- Stateful apps use a StatefulSet in a `<app>.sfs.yaml` template instead of a deployment. - Stateful apps use a StatefulSet in a `<app>.sfs.yaml` template instead of a deployment.
- No chart has dependencies; `charts/` directories are empty. - No chart has dependencies; `charts/` directories are empty.
+1 -1
View File
@@ -62,5 +62,5 @@ For runtime verification (do the workloads actually start, do probes/security se
- Secrets are **1Password `OnePasswordItem` CRs**: values expose `secrets.<name>SecretRef` holding an `op://` item path; the 1Password operator materializes the Kubernetes Secret in-cluster. - Secrets are **1Password `OnePasswordItem` CRs**: values expose `secrets.<name>SecretRef` holding an `op://` item path; the 1Password operator materializes the Kubernetes Secret in-cluster.
- Ingress assumes ingress-nginx (`ingressClassName: nginx`) and cert-manager: `ingress.clusterIssuer` and `ingress.domain` are required values; the TLS secret is named `tls-<release>-<chart>-ingress`. - Ingress assumes ingress-nginx (`ingressClassName: nginx`) and cert-manager: `ingress.clusterIssuer` and `ingress.domain` are required values; the TLS secret is named `tls-<release>-<chart>-ingress`.
- `env` entries in `values.yaml` are rendered through `tpl`, so values may contain Helm templating; `value`, `secretKeyRef` and `configMapKeyRef` forms are supported. - `env` entries in `values.yaml` are rendered through `tpl`, so values may contain Helm templating; `value`, `secretKeyRef` and `configMapKeyRef` forms are supported.
- Hardened defaults: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped, probes and resource requests/limits always set. - Hardened defaults: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped, probes and resource requests/limits always set. Pod and container contexts are configurable via `securityContext.pod` / `securityContext.container` in each chart's values; writable paths (e.g. `/tmp`, nginx cache dirs, s6-overlay's `/run`) are emptyDirs. Where an image cannot satisfy the full standard (e.g. Home Assistant must start as root for s6-overlay, paperless needs a root init container to hand over `/run`), the deviation is minimal and justified with a comment in that chart's `values.yaml`.
- Charts have no dependencies (`charts/` directories are empty). - Charts have no dependencies (`charts/` directories are empty).
+1 -1
View File
@@ -4,5 +4,5 @@ description: A Helm chart to deploy Apache Tika on Kubernetes
type: application type: application
version: 5.0.0+up3.2.3.0.full version: 6.0.0+up3.2.3.0.full
appVersion: "3.2.3.0-full" appVersion: "3.2.3.0-full"
+12
View File
@@ -16,10 +16,19 @@ spec:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.tika.securityContext.pod | nindent 8 }}
containers: containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment - name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
image: "apache/tika:{{ .Chart.AppVersion }}" image: "apache/tika:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.tika.securityContext.container | nindent 12 }}
volumeMounts:
# Tika writes temporary files during parsing; with a read-only
# root filesystem /tmp must be a writable emptyDir.
- name: tmp
mountPath: /tmp
ports: ports:
- containerPort: 9998 - containerPort: 9998
protocol: TCP protocol: TCP
@@ -39,4 +48,7 @@ spec:
memory: "{{ .Values.tika.resources.limits.memory }}" memory: "{{ .Values.tika.resources.limits.memory }}"
cpu: "{{ .Values.tika.resources.limits.cpu }}" cpu: "{{ .Values.tika.resources.limits.cpu }}"
ephemeral-storage: "{{ .Values.tika.resources.limits.ephemeralStorage }}" ephemeral-storage: "{{ .Values.tika.resources.limits.ephemeralStorage }}"
volumes:
- name: tmp
emptyDir: {}
restartPolicy: Always restartPolicy: Always
+14
View File
@@ -1,4 +1,18 @@
tika: tika:
# The official image runs as the dedicated user 35002:35002. /tmp is a
# writable emptyDir because Tika writes temporary files while parsing.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 35002
runAsGroup: 35002
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
# Tika answers "GET /" on its server port with HTTP 200. # Tika answers "GET /" on its server port with HTTP 200.
livenessProbe: livenessProbe:
httpGet: httpGet:
+1 -1
View File
@@ -4,7 +4,7 @@ description: A Helm chart to deploy Cyberchef on Kubernetes
type: application type: application
version: 2.0.0+up10.22.1 version: 3.0.0+up10.22.1
appVersion: "10.22.1" appVersion: "10.22.1"
maintainers: maintainers:
@@ -16,10 +16,21 @@ spec:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.cyberchef.securityContext.pod | nindent 8 }}
containers: containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment - name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
image: "ghcr.io/gchq/cyberchef:{{ .Chart.AppVersion }}" image: "ghcr.io/gchq/cyberchef:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.cyberchef.securityContext.container | nindent 12 }}
volumeMounts:
# nginx needs writable cache/temp dirs and a writable pid file
# location when the root filesystem is read-only.
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /run
ports: ports:
- containerPort: 80 - containerPort: 80
protocol: TCP protocol: TCP
@@ -39,4 +50,9 @@ spec:
memory: "{{ .Values.cyberchef.resources.limits.memory }}" memory: "{{ .Values.cyberchef.resources.limits.memory }}"
cpu: "{{ .Values.cyberchef.resources.limits.cpu }}" cpu: "{{ .Values.cyberchef.resources.limits.cpu }}"
ephemeral-storage: "{{ .Values.cyberchef.resources.limits.ephemeralStorage }}" ephemeral-storage: "{{ .Values.cyberchef.resources.limits.ephemeralStorage }}"
volumes:
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
restartPolicy: Always restartPolicy: Always
+19
View File
@@ -1,4 +1,23 @@
cyberchef: cyberchef:
# The upstream image is stock nginx (root, listening on port 80). To still
# run fully non-root, the pod runs as the image's nginx user (uid 101) and
# the safe sysctl net.ipv4.ip_unprivileged_port_start=80 lets the unprivileged
# nginx bind port 80 without NET_BIND_SERVICE.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
sysctls:
- name: net.ipv4.ip_unprivileged_port_start
value: "80"
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: /
+1 -1
View File
@@ -4,7 +4,7 @@ description: A Helm chart to deploy Gotenberg on Kubernetes
type: application type: application
version: 2.0.0+up8.26.0 version: 3.0.0+up8.26.0
appVersion: "8.26.0" appVersion: "8.26.0"
maintainers: maintainers:
+17
View File
@@ -16,10 +16,22 @@ spec:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.gotenberg.securityContext.pod | nindent 8 }}
containers: containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment - name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
image: "gotenberg/gotenberg:{{ .Chart.AppVersion }}" image: "gotenberg/gotenberg:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.gotenberg.securityContext.container | nindent 12 }}
volumeMounts:
# Gotenberg (and the Chromium/LibreOffice it drives) needs writable
# /tmp and home directories; with a read-only root filesystem these
# are provided as emptyDirs.
- name: tmp
mountPath: /tmp
- name: gotenberg-home
mountPath: /home/gotenberg
ports: ports:
- containerPort: 3000 - containerPort: 3000
protocol: TCP protocol: TCP
@@ -39,4 +51,9 @@ spec:
memory: "{{ .Values.gotenberg.resources.limits.memory }}" memory: "{{ .Values.gotenberg.resources.limits.memory }}"
cpu: "{{ .Values.gotenberg.resources.limits.cpu }}" cpu: "{{ .Values.gotenberg.resources.limits.cpu }}"
ephemeral-storage: "{{ .Values.gotenberg.resources.limits.ephemeralStorage }}" ephemeral-storage: "{{ .Values.gotenberg.resources.limits.ephemeralStorage }}"
volumes:
- name: tmp
emptyDir: {}
- name: gotenberg-home
emptyDir: {}
restartPolicy: Always restartPolicy: Always
+15
View File
@@ -1,4 +1,19 @@
gotenberg: gotenberg:
# The official image runs as the dedicated gotenberg user (uid 1001). /tmp
# and /home/gotenberg are writable emptyDirs so Chromium/LibreOffice keep
# working with a read-only root filesystem.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
# Gotenberg exposes a documented health endpoint at /health. # Gotenberg exposes a documented health endpoint at /health.
livenessProbe: livenessProbe:
httpGet: httpGet:
+1 -1
View File
@@ -3,7 +3,7 @@ name: homeassistant
description: A Helm chart to deploy Home Assistant on Kubernetes description: A Helm chart to deploy Home Assistant on Kubernetes
type: application type: application
version: 2.0.0+up2026.2.3 version: 3.0.0+up2026.2.3
appVersion: "2026.2.3" appVersion: "2026.2.3"
@@ -17,10 +17,14 @@ spec:
name: "{{ .Release.Name }}-{{ .Chart.Name }}-sfs" name: "{{ .Release.Name }}-{{ .Chart.Name }}-sfs"
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.securityContext.pod | nindent 8 }}
containers: containers:
- name: "{{ .Release.Name }}-{{ .Chart.Name }}" - name: "{{ .Release.Name }}-{{ .Chart.Name }}"
image: "ghcr.io/home-assistant/home-assistant:{{ .Chart.AppVersion }}" image: "ghcr.io/home-assistant/home-assistant:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.securityContext.container | nindent 12 }}
env: env:
- name: TZ - name: TZ
value: "{{ .Values.core.timezone }}" value: "{{ .Values.core.timezone }}"
@@ -56,7 +60,17 @@ spec:
- mountPath: /config - mountPath: /config
name: hass-volume name: hass-volume
subPath: hass-config subPath: hass-config
# Writable emptyDirs so the root filesystem can stay read-only:
# s6-overlay needs /run, Home Assistant writes temp files to /tmp.
- mountPath: /tmp
name: tmp
- mountPath: /run
name: run
volumes: volumes:
- name: tmp
emptyDir: {}
- name: run
emptyDir: {}
- name: core-config - name: core-config
secret: secret:
secretName: {{ .Release.Name }}-{{ .Chart.Name }}-config-secret secretName: {{ .Release.Name }}-{{ .Chart.Name }}-config-secret
+20
View File
@@ -22,6 +22,26 @@ ingress:
# hosts: # hosts:
# - chart-example.local # - chart-example.local
# Documented deviation from the repo-wide hardening standard: the official
# Home Assistant container must start as root — it boots via the s6-overlay
# init system, which performs its setup as uid 0 (the image supports no
# non-root user), so runAsNonRoot/runAsUser are deliberately not set.
# Everything else is hardened: no privilege escalation, all capabilities
# dropped and a read-only root filesystem with writable emptyDirs for
# /tmp and /run (s6-overlay state). NET_RAW is added back because Home
# Assistant's device discovery (DHCP watcher, ping/arp integrations) uses
# raw sockets and logs "Operation not permitted" without it.
securityContext:
pod: {}
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_RAW
resources: resources:
limits: limits:
cpu: 1 cpu: 1
+1 -1
View File
@@ -2,7 +2,7 @@ apiVersion: v2
name: matrix-synapse name: matrix-synapse
description: A Helm chart to deploy a Matrix Synapse server on Kubernetes description: A Helm chart to deploy a Matrix Synapse server on Kubernetes
type: application type: application
version: 2.0.0+upv1.149.1 version: 3.0.0+upv1.149.1
appVersion: "v1.149.1" appVersion: "v1.149.1"
maintainers: maintainers:
- name: jklein - name: jklein
@@ -6,14 +6,24 @@ metadata:
data: data:
nginx.conf: | nginx.conf: |
worker_processes 1; worker_processes 1;
# The container runs as the unprivileged nginx user with a read-only root
# filesystem: pid file and temp paths live in the /tmp emptyDir, and the
# listen port is unprivileged (8080).
pid /tmp/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
server { server {
listen 80; listen 8080;
location /.well-known/matrix/server { location /.well-known/matrix/server {
return 200 '{"m.server": "{{ .Values.ingress.domain }}:443"}'; return 200 '{"m.server": "{{ .Values.ingress.domain }}:443"}';
@@ -16,16 +16,25 @@ spec:
app: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx app: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.matrix.delegation.securityContext.pod | nindent 8 }}
containers: containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx - name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx
image: "nginx:1.28.3" image: "nginx:1.28.3"
securityContext:
{{- toYaml .Values.matrix.delegation.securityContext.container | nindent 12 }}
ports: ports:
- containerPort: 80 - containerPort: 8080
name: http name: http
volumeMounts: volumeMounts:
- name: nginx-config - name: nginx-config
mountPath: /etc/nginx/nginx.conf mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf subPath: nginx.conf
# The mounted nginx.conf points the pid file and all temp paths to
# /tmp, so a single writable emptyDir suffices with a read-only
# root filesystem.
- name: tmp
mountPath: /tmp
resources: resources:
limits: limits:
memory: "{{ .Values.matrix.delegation.resources.limits.memory }}" memory: "{{ .Values.matrix.delegation.resources.limits.memory }}"
@@ -61,6 +70,8 @@ spec:
timeoutSeconds: 3 timeoutSeconds: 3
failureThreshold: 30 failureThreshold: 30
volumes: volumes:
- name: tmp
emptyDir: {}
- name: nginx-config - name: nginx-config
configMap: configMap:
name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx-config name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx-config
+15
View File
@@ -5,6 +5,21 @@ matrix:
delegation: delegation:
enabled: false enabled: false
serverUrl: null serverUrl: null
# The delegation nginx runs as the image's unprivileged nginx user
# (uid 101) and listens on the unprivileged port 8080; pid file and temp
# paths are redirected to the /tmp emptyDir by the mounted nginx.conf.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
resources: resources:
limits: limits:
memory: "128Mi" memory: "128Mi"
+1 -1
View File
@@ -4,7 +4,7 @@ description: A Helm chart to deploy Paperless-NGX on Kubernetes
type: application type: application
version: 5.0.0+up2.20.13 version: 6.0.0+up2.20.13
appVersion: 2.20.13 appVersion: 2.20.13
@@ -9,10 +9,15 @@ spec:
spec: spec:
template: template:
spec: spec:
automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.smbConnection.cron.securityContext.pod | nindent 12 }}
containers: containers:
- name: paperless-consume-cronjob - name: paperless-consume-cronjob
image: busybox:1.28 image: busybox:1.28
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.smbConnection.cron.securityContext.container | nindent 16 }}
command: command:
- /bin/sh - /bin/sh
- -c - -c
@@ -17,10 +17,49 @@ spec:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.paperless.securityContext.pod | nindent 8 }}
initContainers:
# s6-overlay refuses to start unless /run is owned by the runtime uid,
# but emptyDirs are always created root-owned. This minimal init
# container hands /run over to the paperless uid (see
# paperless.securityContext in values.yaml) so the main container can
# run fully hardened (non-root, no privilege escalation).
- name: init-run-ownership
image: busybox:1.28
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- chown {{ .Values.paperless.securityContext.pod.runAsUser }}:{{ .Values.paperless.securityContext.pod.runAsGroup }} /run
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- CHOWN
volumeMounts:
- mountPath: /run
name: run
resources:
requests:
cpu: 10m
memory: 16Mi
ephemeral-storage: 1Mi
limits:
cpu: 100m
memory: 32Mi
ephemeral-storage: 10Mi
containers: containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs - name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
image: "ghcr.io/paperless-ngx/paperless-ngx:{{ .Chart.AppVersion }}" image: "ghcr.io/paperless-ngx/paperless-ngx:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.paperless.securityContext.container | nindent 12 }}
ports: ports:
- containerPort: 8000 - containerPort: 8000
protocol: TCP protocol: TCP
@@ -153,6 +192,12 @@ spec:
subPath: export/ subPath: export/
- mountPath: "/usr/src/paperless/consume" - mountPath: "/usr/src/paperless/consume"
name: "paperless-consume-volume" name: "paperless-consume-volume"
# Writable emptyDirs so the root filesystem can stay read-only:
# s6-overlay needs /run, OCR/consumption uses /tmp for scratch files.
- mountPath: /tmp
name: tmp
- mountPath: /run
name: run
resources: resources:
requests: requests:
memory: "{{ .Values.paperless.resources.requests.memory }}" memory: "{{ .Values.paperless.resources.requests.memory }}"
@@ -189,6 +234,10 @@ spec:
timeoutSeconds: 5 timeoutSeconds: 5
restartPolicy: Always restartPolicy: Always
volumes: volumes:
- name: tmp
emptyDir: {}
- name: run
emptyDir: {}
- name: paperless-volume - name: paperless-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-volume-claim claimName: {{ .Release.Name }}-{{ .Chart.Name }}-volume-claim
+38
View File
@@ -42,11 +42,49 @@ paperless:
cpu: 1 cpu: 1
memory: 2048Mi memory: 2048Mi
ephemeralStorage: 400Mi ephemeralStorage: 400Mi
# Paperless-ngx supports rootless operation: the pod runs as uid/gid 1000
# (the image's default paperless user), fsGroup makes the PVCs writable and
# /tmp + /run are emptyDirs so the root filesystem stays read-only.
# Documented deviation: s6-overlay requires /run to be OWNED by the runtime
# uid (group-write is not enough), so a minimal root init container chowns
# the /run emptyDir to runAsUser/runAsGroup before the hardened main
# container starts.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: Always
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
smbConnection: smbConnection:
enabled: false enabled: false
server: null server: null
cronJob: "0 0,12 * * *" cronJob: "0 0,12 * * *"
cron: cron:
# The consume mover only shuffles files between the SMB share (mounted
# with 0777 modes) and the consume PVC, so it runs as the same non-root
# uid/gid as paperless itself with a fully hardened context.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: Always
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
+1 -1
View File
@@ -4,7 +4,7 @@ description: A Helm chart to deploy Patchmon on Kubernetes
type: application type: application
version: 2.0.0+up1.4.2 version: 3.0.0+up1.4.2
appVersion: "1.4.2" appVersion: "1.4.2"
maintainers: maintainers:
@@ -16,10 +16,14 @@ spec:
app: {{ .Release.Name }}-{{ .Chart.Name }}-frontend app: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.patchmon.frontend.securityContext.pod | nindent 8 }}
containers: containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend - name: {{ .Release.Name }}-{{ .Chart.Name }}-frontend
image: "ghcr.io/patchmon/patchmon-frontend:{{ .Chart.AppVersion }}" image: "ghcr.io/patchmon/patchmon-frontend:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.patchmon.frontend.securityContext.container | nindent 12 }}
ports: ports:
- containerPort: 3000 - containerPort: 3000
protocol: TCP protocol: TCP
@@ -59,8 +63,19 @@ spec:
- name: patchmon-volume - name: patchmon-volume
mountPath: /usr/share/nginx/html/assets mountPath: /usr/share/nginx/html/assets
subPath: patchmon-assets subPath: patchmon-assets
# nginx-unprivileged keeps its pid file and temp dirs in /tmp, and
# its entrypoint renders config templates into /etc/nginx/conf.d;
# both must be writable with a read-only root filesystem.
- name: tmp
mountPath: /tmp
- name: nginx-conf-d
mountPath: /etc/nginx/conf.d
restartPolicy: Always restartPolicy: Always
volumes: volumes:
- name: tmp
emptyDir: {}
- name: nginx-conf-d
emptyDir: {}
- name: patchmon-volume - name: patchmon-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
+18
View File
@@ -3,6 +3,24 @@ patchmon:
size: 10Gi size: 10Gi
storageClass: null storageClass: null
frontend: frontend:
# The frontend image is based on nginx-unprivileged; the pod runs as its
# nginx user (uid 101). The image's root-only init script (it would chown
# the shared assets volume) is replaced by fsGroup 1000: the same group
# the backend runs with, so both can read/write the shared assets.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 1000
fsGroupChangePolicy: Always
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
resources: resources:
requests: requests:
cpu: 0.1 cpu: 0.1
+1 -1
View File
@@ -3,5 +3,5 @@ name: urlaubsverwaltung
description: A Helm chart to deploy urlaubsverwaltung on Kubernetes description: A Helm chart to deploy urlaubsverwaltung on Kubernetes
type: application type: application
version: 2.0.0+up6.5.0 version: 3.0.0+up6.5.0
appVersion: 6.5.0 appVersion: 6.5.0
@@ -16,10 +16,19 @@ spec:
app: {{ .Chart.Name }}-{{ .Release.Name }}-uv-deployment app: {{ .Chart.Name }}-{{ .Release.Name }}-uv-deployment
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.urlaubsverwaltung.securityContext.pod | nindent 8 }}
containers: containers:
- name: {{ .Chart.Name }}-{{ .Release.Name }}-uv-container - name: {{ .Chart.Name }}-{{ .Release.Name }}-uv-container
image: "urlaubsverwaltung/urlaubsverwaltung:{{ .Chart.AppVersion }}" image: "urlaubsverwaltung/urlaubsverwaltung:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
{{- toYaml .Values.urlaubsverwaltung.securityContext.container | nindent 12 }}
volumeMounts:
# The JVM (Tomcat work dir, temp files) needs a writable /tmp with
# a read-only root filesystem.
- name: tmp
mountPath: /tmp
ports: ports:
- containerPort: 8080 - containerPort: 8080
name: http name: http
@@ -134,4 +143,7 @@ spec:
periodSeconds: 10 periodSeconds: 10
failureThreshold: 30 failureThreshold: 30
timeoutSeconds: 5 timeoutSeconds: 5
volumes:
- name: tmp
emptyDir: {}
restartPolicy: Always restartPolicy: Always
+15
View File
@@ -7,6 +7,21 @@ ingress:
domain: null domain: null
urlaubsverwaltung: urlaubsverwaltung:
# The official image is built with Cloud Native Buildpacks and runs as the
# non-root cnb user (uid 1002, gid 1001). /tmp is a writable emptyDir for
# the JVM's temp/work files.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1002
runAsGroup: 1001
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
resources: resources:
requests: requests:
cpu: 0.1 cpu: 0.1
+1 -1
View File
@@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 2.0.0+up4.6.4 version: 3.0.0+up4.6.4
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
+10
View File
@@ -13,9 +13,13 @@ spec:
labels: labels:
app: "{{ .Release.Name }}-{{ .Chart.Name }}-sfs" app: "{{ .Release.Name }}-{{ .Chart.Name }}-sfs"
spec: spec:
securityContext:
{{- toYaml .Values.zipline.securityContext.pod | nindent 8 }}
containers: containers:
- name: "{{ .Release.Name }}-{{ .Chart.Name }}-sfs" - name: "{{ .Release.Name }}-{{ .Chart.Name }}-sfs"
image: "ghcr.io/diced/zipline:{{ .Chart.AppVersion }}" image: "ghcr.io/diced/zipline:{{ .Chart.AppVersion }}"
securityContext:
{{- toYaml .Values.zipline.securityContext.container | nindent 12 }}
env: env:
- name: DATABASE_URL - name: DATABASE_URL
valueFrom: valueFrom:
@@ -60,8 +64,14 @@ spec:
- name: zipline-volume - name: zipline-volume
mountPath: /zipline/themes mountPath: /zipline/themes
subPath: themes/ subPath: themes/
# Zipline writes temporary chunks/files below /tmp; with a
# read-only root filesystem this must be a writable emptyDir.
- name: tmp
mountPath: /tmp
automountServiceAccountToken: false automountServiceAccountToken: false
volumes: volumes:
- name: tmp
emptyDir: {}
- name: zipline-volume - name: zipline-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-vc claimName: {{ .Release.Name }}-{{ .Chart.Name }}-vc
+16
View File
@@ -7,6 +7,22 @@ config:
domain: null domain: null
maxUploadFileSize: 30m maxUploadFileSize: 30m
zipline: zipline:
# The upstream image ships no dedicated user, but the Node.js server runs
# fine as an arbitrary non-root uid; fsGroup makes the uploads PVC writable.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: Always
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
# Zipline v4 exposes /api/healthcheck (checks server and database health); # Zipline v4 exposes /api/healthcheck (checks server and database health);
# the endpoint is enabled by default (config.features.healthcheck). Since it # the endpoint is enabled by default (config.features.healthcheck). Since it
# includes the database, it is used for readiness/startup only; liveness # includes the database, it is used for readiness/startup only; liveness