Follow-up to the template-chart hardening fixes in #133, which landed while this branch was open. The env block now quotes names, values and keyRef fields. Without it a value like "5" renders as a YAML number and the API server rejects the whole manifest. RSS-Bridge takes numeric settings that way (RSSBRIDGE_http_timeout), so this was reachable with the first env entry anyone adds. The port-80 question is settled by measurement rather than by the "it works in k3d" reasoning #133 warns about. Same pod spec in k3d (k3s v1.35.5), only ip_unprivileged_port_start changed: 1024, capabilities.drop [ALL] nginx: bind() to 0.0.0.0:80 failed (13: Permission denied) 1024, plus NET_BIND_SERVICE identical failure - for a non-root process the capability lands in the permitted but not the effective set, so it buys nothing 0 binds, HTTP 200 So the chart cannot buy safety with a capability the way samba does, and port 80 is not negotiable either: nginx is configured for it inside the image and the entrypoint's only lever is a sed against a read-only /etc/nginx. What it can do is stop depending on the node default - the pod securityContext now pins the sysctl to 0, so the bind no longer relies on cluster configuration. Verified: the chart-installed pod reports 0 and answers 200 through the Service. Worth recording how that failure looks without probes: the entrypoint backgrounds nginx and then execs php-fpm, so a failed bind leaves a container reporting Running with no web server at all. Both test pods above were Running. The probes on /?action=health are what turn it into a pod that never becomes Ready. trivy config still reports 0 to act on; the KSV-0117 statement is updated to the measured reasoning.
141 lines
6.8 KiB
YAML
141 lines
6.8 KiB
YAML
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
spec:
|
|
replicas: {{ include "rss-bridge.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.rssBridge.replicas) }}
|
|
selector:
|
|
matchLabels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
template:
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
annotations:
|
|
# Hash of the rendered ConfigMap: a changed whitelist changes the pod
|
|
# template and thus rolls the pods. Without it a config change stays
|
|
# invisible to the running container - whitelist.txt is a subPath
|
|
# mount, which the kubelet never updates, and RSS-Bridge reads the file
|
|
# once at request bootstrap from the path it was started with.
|
|
checksum/config: {{ include (print $.Template.BasePath "/rss-bridge.configmap.yaml") . | sha256sum }}
|
|
spec:
|
|
automountServiceAccountToken: false
|
|
securityContext:
|
|
{{- include "rss-bridge.podSecurityContext" .Values.rssBridge.securityContext | nindent 8 }}
|
|
containers:
|
|
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
# Registry written out in full on purpose: an unqualified Docker Hub
|
|
# reference puts "rssbridge" in the registry position, which says
|
|
# nothing about where the image comes from and is reported as an
|
|
# untrusted registry (AVD-KSV-0125). See ci/trivy/data/ksv0125.yaml.
|
|
image: "docker.io/rssbridge/rss-bridge:{{ .Chart.AppVersion }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
{{- range $value := .Values.rssBridge.env }}
|
|
{{- if $value.value }}
|
|
- name: {{ $value.name | quote }}
|
|
# quote is required, not cosmetic: a value like "5" or "true"
|
|
# renders unquoted as a YAML number/bool, and the API server then
|
|
# rejects the whole manifest with "cannot unmarshal number into
|
|
# Go struct field EnvVar.value of type string". RSS-Bridge takes
|
|
# numeric settings this way (RSSBRIDGE_http_timeout), so this is
|
|
# reachable with the very first env entry someone adds.
|
|
value: {{ tpl $value.value $ | quote }}
|
|
{{- else if $value.valueFrom }}
|
|
{{- if $value.valueFrom.secretKeyRef }}
|
|
- name: {{ $value.name | quote }}
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
|
|
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
|
|
{{- else if $value.valueFrom.configMapKeyRef }}
|
|
- name: {{ $value.name | quote }}
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
|
|
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
ports:
|
|
# The port NAME is load-bearing and must not be renamed: the
|
|
# Service selects it by name (targetPort: rss-bridge-port), and so
|
|
# do the probes.
|
|
- containerPort: 80
|
|
protocol: TCP
|
|
name: rss-bridge-port
|
|
# Probes, resources and both security contexts go through the
|
|
# null-safe helpers (#99): an erased block means "chart defaults
|
|
# apply" instead of a vanished probe or an unhardened container.
|
|
# Never wrap these in an "if" - a guard hides the erased case
|
|
# completely instead of rendering a visible null.
|
|
livenessProbe:
|
|
{{- include "rss-bridge.livenessProbe" .Values.rssBridge.livenessProbe | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "rss-bridge.readinessProbe" .Values.rssBridge.readinessProbe | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "rss-bridge.startupProbe" .Values.rssBridge.startupProbe | nindent 12 }}
|
|
resources:
|
|
{{- include "rss-bridge.effectiveResources" .Values.rssBridge.resources | nindent 12 }}
|
|
securityContext:
|
|
{{- include "rss-bridge.containerSecurityContext" .Values.rssBridge.securityContext | nindent 12 }}
|
|
volumeMounts:
|
|
- name: whitelist
|
|
mountPath: /app/whitelist.txt
|
|
subPath: whitelist.txt
|
|
readOnly: true
|
|
# The four writable paths below are what keeps
|
|
# readOnlyRootFilesystem: true possible. They are image facts
|
|
# rather than user configuration, so they are not values.
|
|
#
|
|
# They were found empirically, not guessed: the image was run with
|
|
# a writable root filesystem, exercised across every whitelisted
|
|
# bridge, and its diff layer read afterwards. Three of the four
|
|
# showed up that way. /tmp did NOT - PHP's opcache creates its lock
|
|
# file there and unlinks it immediately, so it never appears in a
|
|
# filesystem diff and only surfaced when the hardened container
|
|
# died at startup with "Unable to create lock file: Read-only file
|
|
# system". Stopping at the first writable path found would have
|
|
# shipped a chart that cannot start.
|
|
- name: cache
|
|
# RSS-Bridge's file cache. Not optional: the app checks the
|
|
# directory with is_writable() and throws on every feed request
|
|
# if it is not - while /?action=health still answers 200, so the
|
|
# probes would report a healthy pod that serves no feeds.
|
|
mountPath: /app/cache
|
|
- name: tmp
|
|
# PHP opcache lock file (created and immediately unlinked) and
|
|
# PHP's default upload temp dir.
|
|
mountPath: /tmp
|
|
- name: run
|
|
# nginx.pid
|
|
mountPath: /run
|
|
- name: run-php
|
|
# php-fpm's pid file and its fastcgi socket. Nested inside /run
|
|
# on purpose: the emptyDir above does not contain the php
|
|
# subdirectory the pool config expects, and php-fpm does not
|
|
# create it.
|
|
mountPath: /run/php
|
|
- name: nginx-lib
|
|
# nginx creates its body/fastcgi/proxy/scgi/uwsgi temp
|
|
# directories here at startup.
|
|
mountPath: /var/lib/nginx
|
|
volumes:
|
|
- name: whitelist
|
|
configMap:
|
|
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
|
|
- name: cache
|
|
emptyDir: {}
|
|
- name: tmp
|
|
emptyDir: {}
|
|
- name: run
|
|
emptyDir: {}
|
|
- name: run-php
|
|
emptyDir: {}
|
|
- name: nginx-lib
|
|
emptyDir: {}
|
|
restartPolicy: Always
|