From 0481007cfefcbe4f6f679c1eb061361b572e83e1 Mon Sep 17 00:00:00 2001 From: jklein Date: Sat, 22 Aug 2026 15:47:52 +0200 Subject: [PATCH] Outline chart: security hardening, configurable probes, flexible ingress - Pod and container securityContext are now values-configurable (outline.securityContext.pod/container) with hardened defaults: runAsNonRoot (uid/gid 1001, the upstream image's nodejs user), readOnlyRootFilesystem, allowPrivilegeEscalation false, all capabilities dropped. /tmp is mounted as an emptyDir for Outline's import/export scratch files. - Probes moved to values (outline.livenessProbe/readinessProbe/ startupProbe); defaults are exactly the previously hardcoded ones. - Ingress raised to the repo standard: ingress.className (default nginx), ingress.annotations merged with the always-set annotations, ingress.enabled flag; domain/clusterIssuer stay required and the TLS secret name was already conform. - Additive outline.env block rendered through tpl with value/ secretKeyRef/configMapKeyRef forms; chart-managed env stays in the template. Major bump 3.1.1+up1.9.2 -> 4.0.0+up1.9.2 (runtime behavior changes through hardening). Part of #32 --- outline/Chart.yaml | 2 +- outline/templates/outline.ingress.yaml | 9 +++- outline/templates/outline.sfs.yaml | 60 ++++++++++++++------------ outline/values.yaml | 54 +++++++++++++++++++++++ 4 files changed, 95 insertions(+), 30 deletions(-) diff --git a/outline/Chart.yaml b/outline/Chart.yaml index 622e499..560bc7b 100644 --- a/outline/Chart.yaml +++ b/outline/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/outline/outline/main/public/images/icon- type: application -version: 3.1.1+up1.9.2 +version: 4.0.0+up1.9.2 appVersion: 1.9.2 maintainers: diff --git a/outline/templates/outline.ingress.yaml b/outline/templates/outline.ingress.yaml index 70eda21..6d73b9a 100644 --- a/outline/templates/outline.ingress.yaml +++ b/outline/templates/outline.ingress.yaml @@ -1,13 +1,17 @@ +{{- if .Values.ingress.enabled }} apiVersion: networking.k8s.io/v1 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 }} nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxUploadFileSize }} nginx.org/client-max-body-size: {{ .Values.ingress.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: @@ -23,3 +27,4 @@ spec: - hosts: - {{ required "A Domain/Host must be provided" .Values.ingress.domain }} secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress +{{- end }} diff --git a/outline/templates/outline.sfs.yaml b/outline/templates/outline.sfs.yaml index f2b91df..ef79983 100644 --- a/outline/templates/outline.sfs.yaml +++ b/outline/templates/outline.sfs.yaml @@ -18,8 +18,7 @@ spec: spec: automountServiceAccountToken: false securityContext: - fsGroup: 1001 - fsGroupChangePolicy: Always + {{- toYaml .Values.outline.securityContext.pod | nindent 8 }} containers: - name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs image: "outlinewiki/outline:{{ .Chart.AppVersion }}" @@ -76,45 +75,52 @@ spec: value: "{{ .Values.outline.maxUploadSize }}" - name: FORCE_HTTPS value: "{{ .Values.outline.forceHttps }}" + {{- range $value := .Values.outline.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 }} ports: - containerPort: 3000 protocol: TCP name: http - # Outline exposes a dedicated health endpoint at /_health (also used - # by the upstream Docker HEALTHCHECK). It checks Postgres and Redis; - # there is no dependency-free HTTP endpoint ("/" returns 500 too when - # the database is down), so all three probes use /_health. livenessProbe: - httpGet: - path: /_health - port: 3000 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 + {{- toYaml .Values.outline.livenessProbe | nindent 12 }} readinessProbe: - httpGet: - path: /_health - port: 3000 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - # The startup probe gates liveness/readiness. Outline runs database - # migrations on startup: budget 10s * 30 = 300s. + {{- toYaml .Values.outline.readinessProbe | nindent 12 }} startupProbe: - httpGet: - path: /_health - port: 3000 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 30 + {{- toYaml .Values.outline.startupProbe | nindent 12 }} resources: {{- include "outline.resources" .Values.outline.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.outline.securityContext.container | nindent 12 }} volumeMounts: - mountPath: "/outline-data/data" name: outline-data subPath: data + # Writable emptyDir so the root filesystem can stay read-only: + # Outline writes import/export scratch files to /tmp. + - mountPath: /tmp + name: tmp restartPolicy: Always volumes: - name: outline-data persistentVolumeClaim: - claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc \ No newline at end of file + claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc + - name: tmp + emptyDir: {} \ No newline at end of file diff --git a/outline/values.yaml b/outline/values.yaml index 8054736..dd2e7a9 100644 --- a/outline/values.yaml +++ b/outline/values.yaml @@ -19,6 +19,55 @@ outline: storage: size: 10Gi storageClass: null + # The upstream image (outlinewiki/outline) runs as its nodejs user + # (uid/gid 1001). Outline only writes to the upload storage (PVC) and to + # /tmp (import/export scratch files), which the chart mounts as an + # emptyDir, so the root filesystem stays read-only. + securityContext: + pod: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + fsGroupChangePolicy: Always + container: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + # Outline exposes a dedicated health endpoint at /_health (also used + # by the upstream Docker HEALTHCHECK). It checks Postgres and Redis; + # there is no dependency-free HTTP endpoint ("/" returns 500 too when + # the database is down), so all three probes use /_health. + livenessProbe: + httpGet: + path: /_health + port: 3000 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /_health + port: 3000 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + # The startup probe gates liveness/readiness. Outline runs database + # migrations on startup: budget 10s * 30 = 300s. + startupProbe: + httpGet: + path: /_health + port: 3000 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + # Extra environment variables, appended to the ones the chart always sets. + # Values are rendered through tpl; value, secretKeyRef and configMapKeyRef + # forms are supported. + env: [] resources: # Limits are optional: memory and ephemeral-storage limits default to # limitFactor x the request, an explicitly set limit always wins, and a @@ -30,6 +79,11 @@ outline: ephemeralStorage: 10Mi ingress: + enabled: true maxUploadFileSize: 25m clusterIssuer: letsencrypt 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: {}