diff --git a/voucher-vault/Chart.yaml b/voucher-vault/Chart.yaml index 63d58d0..86be85f 100644 --- a/voucher-vault/Chart.yaml +++ b/voucher-vault/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/l4rm4nd/VoucherVault/main/myapp/static/a type: application -version: 3.1.2+up1.30.1 +version: 4.0.0+up1.30.1 appVersion: "1.30.1" maintainers: diff --git a/voucher-vault/templates/voucher-vault.ingress.yaml b/voucher-vault/templates/voucher-vault.ingress.yaml index d2227e8..a9387ff 100644 --- a/voucher-vault/templates/voucher-vault.ingress.yaml +++ b/voucher-vault/templates/voucher-vault.ingress.yaml @@ -4,9 +4,12 @@ kind: Ingress metadata: 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 }} + {{- 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: @@ -22,4 +25,4 @@ spec: - hosts: - {{ required "A Domain/Host must be provided" .Values.ingress.domain }} secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress -{{- end }} \ No newline at end of file +{{- end }} diff --git a/voucher-vault/templates/voucher-vault.sfs.yaml b/voucher-vault/templates/voucher-vault.sfs.yaml index 22a303d..c495f23 100644 --- a/voucher-vault/templates/voucher-vault.sfs.yaml +++ b/voucher-vault/templates/voucher-vault.sfs.yaml @@ -19,6 +19,33 @@ spec: automountServiceAccountToken: false securityContext: {{- toYaml .Values.voucherVault.securityContext.pod | nindent 8 }} + initContainers: + # STATIC_ROOT (/opt/app/myapp/static) doubles as the app's own static + # source directory, so it cannot simply be shadowed by an empty + # volume: this init container seeds the static-files emptyDir with + # the assets shipped in the image (cp -a preserves timestamps, so the + # entrypoint's collectstatic only adds what is missing, e.g. the + # Django admin static files). See securityContext in values.yaml. + - name: init-static + image: "l4rm4nd/vouchervault:{{ .Chart.AppVersion }}" + imagePullPolicy: IfNotPresent + # Copies the children (not ".") so cp never tries to set metadata + # on the mountpoint itself, which the non-root uid may not own. + command: ["sh", "-c", "cp -a /opt/app/myapp/static/* /static-seed/"] + securityContext: + {{- toYaml .Values.voucherVault.securityContext.container | nindent 12 }} + volumeMounts: + - name: static-files + mountPath: /static-seed + resources: + requests: + cpu: 10m + memory: 16Mi + ephemeral-storage: 1Mi + limits: + cpu: 200m + memory: 64Mi + ephemeral-storage: 10Mi containers: - name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs image: "l4rm4nd/vouchervault:{{ .Chart.AppVersion }}" @@ -63,7 +90,15 @@ spec: securityContext: {{- toYaml .Values.voucherVault.securityContext.container | nindent 12 }} volumeMounts: - {{- if .Values.voucherVault.storage.volumeMounts }} + # Writable emptyDirs over the paths the image writes at runtime, + # so the root filesystem can stay read-only (see securityContext + # in values.yaml). + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /opt/app/logs + - name: static-files + mountPath: /opt/app/myapp/static {{- range $value := .Values.voucherVault.storage.volumeMounts }} - name: {{ tpl $value.name $ | quote }} mountPath: {{ tpl $value.mountPath $ | quote }} @@ -77,12 +112,16 @@ spec: readOnly: {{ tpl $value.readOnly $ | quote }} {{- end }} {{- end }} - {{- else }} - [] - {{- end }} restartPolicy: Always volumes: - {{- if .Values.voucherVault.storage.volumes }} + # Writable emptyDirs backing the read-only root filesystem (see + # securityContext in values.yaml). + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} + - name: static-files + emptyDir: {} {{- range $value := .Values.voucherVault.storage.volumes }} - name: {{ tpl $value.name $ | quote }} {{- if $value.persistentVolumeClaim }} @@ -127,7 +166,4 @@ spec: sizeLimit: {{ tpl $value.emptyDir.sizeLimit $ | quote }} {{- end }} {{- end }} - {{- end }} - {{- else }} - [] {{- end }} \ No newline at end of file diff --git a/voucher-vault/values.yaml b/voucher-vault/values.yaml index ff668fd..5da19e7 100644 --- a/voucher-vault/values.yaml +++ b/voucher-vault/values.yaml @@ -21,7 +21,10 @@ voucherVault: requests: cpu: 0.1 memory: 256Mi - ephemeral-storage: 10Mi + # The static-files emptyDir (~90Mi of static assets, see the + # securityContext comment) counts toward the pod's ephemeral-storage + # usage, so the request must cover it. + ephemeral-storage: 128Mi # "/" answers with a redirect for anonymous users, which httpGet treats as # success. The Host header must be set to 127.0.0.1 because Django's # ALLOWED_HOSTS only accepts 127.0.0.1 and the configured DOMAIN — probing @@ -57,6 +60,13 @@ voucherVault: periodSeconds: 5 timeoutSeconds: 5 failureThreshold: 30 + # The upstream image (l4rm4nd/vouchervault) runs as www-data (uid/gid 33). + # With readOnlyRootFilesystem the chart mounts emptyDirs over the paths the + # image writes at runtime: /tmp (upload/PDF scratch files), /opt/app/logs + # and STATIC_ROOT /opt/app/myapp/static (the entrypoint runs collectstatic + # at every start; an init container seeds the volume with the static files + # shipped in the image, because the directory doubles as the app's static + # source). Uploads and the database live on the PVC at /opt/app/database. securityContext: pod: runAsNonRoot: true @@ -66,7 +76,11 @@ voucherVault: fsGroupChangePolicy: Always container: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true runAsNonRoot: true + capabilities: + drop: + - ALL ports: - containerPort: 8000 protocol: TCP @@ -185,3 +199,7 @@ ingress: enabled: true clusterIssuer: null domain: null + className: nginx + # Extra annotations for the Ingress; merged with the cert-manager + # cluster-issuer annotation the chart always sets. + annotations: {}