From f6de7ffa801c2d073451aea8568e99c24b42315f Mon Sep 17 00:00:00 2001 From: jklein Date: Sat, 22 Aug 2026 18:46:07 +0200 Subject: [PATCH] Calibre-Web-Automated chart: naming scheme, security hardening (conventions batch 3) All audit findings for the calibre-web-automated chart (#32) in one major bump, 3.1.1+upv4.0.6 -> 4.0.0+upv4.0.6: - Rename StatefulSet, Service, OnePasswordItem secret and Ingress from the inverted --cwa- scheme to --; spec.serviceName and all references follow. Both PVCs deliberately keep their historical names (documented naming exception; renaming would orphan the data). - TLS secret renamed to the standard tls---ingress. - Ingress on the repo standard: ingress.className (default nginx), ingress.annotations merged with the chart-managed annotations, ingress.clusterIssuer now required (was silently defaulted to "letsencrypt-issuer"). - Security hardening, values-configurable via securityContext.pod / securityContext.container: allowPrivilegeEscalation false, all capabilities dropped except the minimal set the s6-overlay init needs (CHOWN, DAC_OVERRIDE, FOWNER, KILL, SETGID, SETUID). Documented deviations: the image must start as root (s6 drops to PUID 1000) and readOnlyRootFilesystem stays false (read-only mode disables PUID/PGID remapping upstream and breaks the ImageMagick policy setup). - Additive env value rendered through tpl (value / secretKeyRef / configMapKeyRef forms). - CI fixtures: secret renamed to the new scheme, clusterIssuer added to test-values. Part of #32. --- calibre-web-automated/Chart.yaml | 2 +- .../calibre-web-automated-consume.pvc.yaml | 5 +- .../calibre-web-automated.ingress.yaml | 13 +++-- .../templates/calibre-web-automated.pvc.yaml | 3 ++ .../calibre-web-automated.secret.yaml | 2 +- .../calibre-web-automated.service.yaml | 4 +- .../templates/calibre-web-automated.sfs.yaml | 52 ++++++++++++++----- calibre-web-automated/values.yaml | 42 ++++++++++++++- ci/calibre-web-automated/fixtures.yaml | 6 ++- ci/calibre-web-automated/test-values.yaml | 1 + 10 files changed, 103 insertions(+), 27 deletions(-) diff --git a/calibre-web-automated/Chart.yaml b/calibre-web-automated/Chart.yaml index b8b3df2..45f2f3b 100644 --- a/calibre-web-automated/Chart.yaml +++ b/calibre-web-automated/Chart.yaml @@ -5,7 +5,7 @@ name: calibre-web-automated description: A Helm chart to deploy calibere-web-automated icon: https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/main/README_images/cwa-logo-round-dark.png -version: 3.1.1+upv4.0.6 +version: 4.0.0+upv4.0.6 appVersion: v4.0.6 maintainers: diff --git a/calibre-web-automated/templates/calibre-web-automated-consume.pvc.yaml b/calibre-web-automated/templates/calibre-web-automated-consume.pvc.yaml index 9c6dbca..991db56 100644 --- a/calibre-web-automated/templates/calibre-web-automated-consume.pvc.yaml +++ b/calibre-web-automated/templates/calibre-web-automated-consume.pvc.yaml @@ -1,6 +1,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: + # Deliberate naming exception (see README): this PVC keeps its historical + # --cwa-consume-pvc name — renaming a PVC would provision + # a new, empty volume and orphan the existing data. name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-consume-pvc spec: accessModes: @@ -8,4 +11,4 @@ spec: resources: requests: storage: 1Gi - storageClassName: {{ .Values.cwa.storageClass }} \ No newline at end of file + storageClassName: {{ .Values.cwa.storageClass }} diff --git a/calibre-web-automated/templates/calibre-web-automated.ingress.yaml b/calibre-web-automated/templates/calibre-web-automated.ingress.yaml index ed54cd8..364c19f 100644 --- a/calibre-web-automated/templates/calibre-web-automated.ingress.yaml +++ b/calibre-web-automated/templates/calibre-web-automated.ingress.yaml @@ -1,20 +1,23 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-ingress + name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress annotations: - cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer}} + cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }} nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.cwa.maxUploadFileSize }} nginx.org/client-max-body-size: {{ .Values.cwa.maxUploadFileSize }} + {{- with .Values.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: - ingressClassName: nginx + ingressClassName: {{ .Values.ingress.className }} rules: - host: {{ required "A Domain/Host must be provided" .Values.ingress.domain }} http: paths: - backend: service: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-service + name: {{ .Release.Name }}-{{ .Chart.Name }}-service port: name: http path: / @@ -22,4 +25,4 @@ spec: tls: - hosts: - {{ required "A Domain/Host must be provided" .Values.ingress.domain }} - secretName: tls-{{ .Chart.Name }}-{{ .Release.Name }}-ingress + secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress diff --git a/calibre-web-automated/templates/calibre-web-automated.pvc.yaml b/calibre-web-automated/templates/calibre-web-automated.pvc.yaml index 22c36c5..967ea86 100644 --- a/calibre-web-automated/templates/calibre-web-automated.pvc.yaml +++ b/calibre-web-automated/templates/calibre-web-automated.pvc.yaml @@ -1,6 +1,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: + # Deliberate naming exception (see README): this PVC keeps its historical + # --cwa-pvc name — renaming a PVC would provision a new, + # empty volume and orphan the existing data. name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-pvc spec: accessModes: diff --git a/calibre-web-automated/templates/calibre-web-automated.secret.yaml b/calibre-web-automated/templates/calibre-web-automated.secret.yaml index 7717d3a..07d933c 100644 --- a/calibre-web-automated/templates/calibre-web-automated.secret.yaml +++ b/calibre-web-automated/templates/calibre-web-automated.secret.yaml @@ -1,6 +1,6 @@ apiVersion: onepassword.com/v1 kind: OnePasswordItem metadata: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-secret + name: {{ .Release.Name }}-{{ .Chart.Name }}-secret spec: itemPath: {{ .Values.secrets.cwaSecretRef }} diff --git a/calibre-web-automated/templates/calibre-web-automated.service.yaml b/calibre-web-automated/templates/calibre-web-automated.service.yaml index 8d22239..a5cfa09 100644 --- a/calibre-web-automated/templates/calibre-web-automated.service.yaml +++ b/calibre-web-automated/templates/calibre-web-automated.service.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: Service metadata: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-service + name: {{ .Release.Name }}-{{ .Chart.Name }}-service spec: selector: - app: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-sfs + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs ports: - protocol: TCP port: 80 diff --git a/calibre-web-automated/templates/calibre-web-automated.sfs.yaml b/calibre-web-automated/templates/calibre-web-automated.sfs.yaml index 3cfc14e..c9f1d0d 100644 --- a/calibre-web-automated/templates/calibre-web-automated.sfs.yaml +++ b/calibre-web-automated/templates/calibre-web-automated.sfs.yaml @@ -1,23 +1,26 @@ apiVersion: apps/v1 kind: StatefulSet metadata: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-sfs + name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs labels: - app: {{ .Chart.Name }}-{{ .Release.Name }} + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs spec: - serviceName: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-service + serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service replicas: {{ include "calibre-web-automated.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.cwa.replicas) }} selector: matchLabels: - app: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-sfs + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs template: metadata: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-sfs + name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs labels: - app: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-sfs + app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs spec: + automountServiceAccountToken: false + securityContext: + {{- toYaml .Values.cwa.securityContext.pod | nindent 8 }} containers: - - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-container + - name: {{ .Release.Name }}-{{ .Chart.Name }}-container image: "ghcr.io/crocodilestick/calibre-web-automated:{{ .Chart.AppVersion }}" imagePullPolicy: IfNotPresent ports: @@ -34,12 +37,32 @@ spec: - name: HARDCOVER_TOKEN valueFrom: secretKeyRef: - name: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-secret + name: {{ .Release.Name }}-{{ .Chart.Name }}-secret key: hardcover-api-token - name: CALIBRE_CONFIG_DIR value: "/config/.config/calibre" - name: NETWORK_SHARE_MODE value: "false" + {{- range $value := .Values.cwa.env }} + {{- if $value.value }} + - name: {{ $value.name | quote }} + value: {{ tpl $value.value $ | quote }} + {{- else if $value.valueFrom }} + {{- if $value.valueFrom.secretKeyRef }} + - name: {{ $value.name }} + valueFrom: + secretKeyRef: + name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }} + key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }} + {{- else if $value.valueFrom.configMapKeyRef }} + - name: {{ $value.name }} + valueFrom: + configMapKeyRef: + name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }} + key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }} + {{- end }} + {{- end }} + {{- end }} volumeMounts: - mountPath: "/config" name: cwa @@ -63,12 +86,13 @@ spec: {{- toYaml .Values.cwa.startupProbe | nindent 12 }} resources: {{- include "calibre-web-automated.resources" .Values.cwa.resources | nindent 12 }} - automountServiceAccountToken: false - securityContext: - fsGroup: 1000 - fsGroupChangePolicy: Always + securityContext: + {{- toYaml .Values.cwa.securityContext.container | nindent 12 }} restartPolicy: Always volumes: + # Deliberate naming exception (see README): both PVCs keep their + # historical --cwa-* names — renaming a PVC would + # provision a new, empty volume and orphan the existing data. - name: cwa persistentVolumeClaim: claimName: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-pvc @@ -77,8 +101,8 @@ spec: claimName: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-consume-pvc - name: cwa-plugins-config secret: - secretName: {{ .Chart.Name }}-{{ .Release.Name }}-cwa-secret + secretName: {{ .Release.Name }}-{{ .Chart.Name }}-secret items: - key: calibre-plugins path: customize.py.json - mode: 0644 \ No newline at end of file + mode: 0644 diff --git a/calibre-web-automated/values.yaml b/calibre-web-automated/values.yaml index 5bc7277..ae92346 100644 --- a/calibre-web-automated/values.yaml +++ b/calibre-web-automated/values.yaml @@ -46,7 +46,47 @@ cwa: cpu: 0.1 memory: 512Mi ephemeralStorage: 100Mi + # Documented deviation from the repo-wide hardening standard: the CWA image + # (linuxserver.io-style) must start as root — it boots via the s6-overlay + # init system, whose stage-1/2 setup runs as uid 0 and then drops the app + # to the PUID/PGID user (1000), so runAsNonRoot/runAsUser are deliberately + # not set (s6 supervisors stay root, the application runs as uid 1000). + # readOnlyRootFilesystem is deliberately false: in read-only mode the image + # disables the PUID/PGID remapping (the runtime uid would silently change + # from 1000 to the built-in 911 and break existing volume data) and its + # init fails to write the ImageMagick policy under /etc. Everything else + # is hardened: no privilege escalation and all capabilities dropped except + # the minimal set the s6 init/supervision needs (verified against v4.0.6): + # CHOWN/DAC_OVERRIDE/FOWNER/SETGID/SETUID for the ownership fix-up of + # /config etc. and the root->PUID drop, KILL so the root supervisor can + # signal the uid-1000 services (without it the pod cannot shut down + # cleanly and gets SIGKILLed). + securityContext: + pod: + fsGroup: 1000 + fsGroupChangePolicy: Always + container: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + add: + - CHOWN + - DAC_OVERRIDE + - FOWNER + - KILL + - SETGID + - SETUID + # Extra environment variables, appended to the ones the chart always sets. + # Rendered through tpl; value, secretKeyRef and configMapKeyRef forms are + # supported (see template-chart). + env: [] ingress: - clusterIssuer: "letsencrypt-issuer" + clusterIssuer: null domain: null + className: nginx + # Extra annotations for the Ingress; merged with the cert-manager + # cluster-issuer and proxy-body-size annotations the chart always sets. + annotations: {} diff --git a/ci/calibre-web-automated/fixtures.yaml b/ci/calibre-web-automated/fixtures.yaml index ffe8ebd..d1b7a50 100644 --- a/ci/calibre-web-automated/fixtures.yaml +++ b/ci/calibre-web-automated/fixtures.yaml @@ -1,7 +1,9 @@ # Fixtures for the calibre-web-automated install-test: the cwa secret the # 1Password operator would otherwise materialize and static hostPath PVs # (the consume PVC is ReadWriteMany, which local-path cannot provision). -# This chart names resources --..., release name is ci. +# Release name is ci; resources are named --..., except the +# PVCs, which keep their historical --cwa-* names +# (documented naming exception, see README). apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: @@ -74,7 +76,7 @@ spec: apiVersion: v1 kind: Secret metadata: - name: calibre-web-automated-ci-cwa-secret + name: ci-calibre-web-automated-secret stringData: hardcover-api-token: ci-dummy-token # Mounted as /config/.config/calibre/customize.py.json (calibre plugin diff --git a/ci/calibre-web-automated/test-values.yaml b/ci/calibre-web-automated/test-values.yaml index 2be27b3..aaf7198 100644 --- a/ci/calibre-web-automated/test-values.yaml +++ b/ci/calibre-web-automated/test-values.yaml @@ -9,4 +9,5 @@ cwa: storage: 2Gi ingress: + clusterIssuer: ci domain: cwa.ci.local