Five changes to the scaffold every future chart is copied from: 1. seccompProfile.type: RuntimeDefault in the pod security context, in line with the rest of the seccomp series (#84). Pod context, because from there it also covers init containers and any container added later without being repeated. 2. containerPort 80 -> 8080, clearing AVD-KSV-0117. Note that the usual justification for this does not hold: a non-root container CAN bind port 80 on current Kubernetes, because the kubelet sets net.ipv4.ip_unprivileged_port_start=0 in the pod network namespace. Measured, not assumed - see the values.yaml comment. The reason to stay above 1024 is that this is a runtime default rather than a guarantee, and that every real chart here already does. 3. The ConfigMap placeholder key is renamed from the literal "key" to "example-setting", clearing AVD-KSV-01010 (secret-scanner patterns match any key named "key"). 4. The image placeholder is fully qualified as ghcr.io/ORG/IMAGE, so the template stops teaching the unqualified form that produced the repo's unqualified Docker Hub images. 5. env values are rendered through "quote". Without it a value like "8080" renders as a YAML number and the API server rejects the whole manifest ("cannot unmarshal number into Go struct field EnvVar.value of type string"). Every real chart in the repo already quotes here; only the template did not. Found by instantiating the template and installing it, not by rendering it. Verified by instantiating the scaffold into a working chart per the README procedure and installing it in k3d: the workload becomes Ready, the Service routes to the app on 8080 and an ephemeral container added to the running pod inherits Seccomp: 2 from the pod context. Refs #84
131 lines
5.0 KiB
YAML
131 lines
5.0 KiB
YAML
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
|
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
|
|
# takes precedence over any per-workload replicas value. PVCs, Secrets and
|
|
# Services stay in place; scaleDown: false brings the workloads back up.
|
|
scaleDown: false
|
|
|
|
secrets:
|
|
xxxSecretRef: null
|
|
|
|
xxx:
|
|
# Replica count for this workload; 0 is allowed and scales it down.
|
|
# The chart-global scaleDown flag takes precedence.
|
|
replicas: 1
|
|
resources:
|
|
# Limits are optional: memory and ephemeral-storage limits default to
|
|
# limitFactor x the request, an explicitly set limit always wins, and a
|
|
# CPU limit is only set when configured explicitly under "limits".
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 0.1
|
|
memory: 512Mi
|
|
ephemeral-storage: 10Mi
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
# The startup probe gates liveness/readiness, so those need no initialDelaySeconds.
|
|
# Startup budget = periodSeconds * failureThreshold = 150s.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 5
|
|
failureThreshold: 30
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 20000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: Always
|
|
# RuntimeDefault applies the container runtime's default seccomp filter,
|
|
# which blocks the syscalls a normal workload never makes. It sits in the
|
|
# pod context on purpose: from here it covers every container in the pod
|
|
# including init containers, while a container-level seccompProfile would
|
|
# have to be repeated for each one (and silently missed on the next one
|
|
# added). A container that genuinely needs a different filter can still
|
|
# override it under securityContext.container, which wins over this value.
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
add: []
|
|
ports:
|
|
# An unprivileged port on purpose (AVD-KSV-0117 flags containerPort < 1024).
|
|
#
|
|
# Note what the reason is NOT: on current Kubernetes a non-root container
|
|
# can bind port 80 just fine. The kubelet sets
|
|
# net.ipv4.ip_unprivileged_port_start=0 in the pod network namespace, so
|
|
# the classic "ports below 1024 need NET_BIND_SERVICE" rule does not apply
|
|
# inside a pod. Measured in k3d (k3s v1.35.5): the sysctl reads 0, and a
|
|
# container running as runAsUser 20000 with capabilities.drop: [ALL] binds
|
|
# port 80 successfully. Anyone who "fixes" this back to 80 after testing it
|
|
# once will find that it works - which is exactly why the reason is written
|
|
# down here.
|
|
#
|
|
# The reason to stay above 1024 is that this is a runtime default, not a
|
|
# guarantee: a cluster that restores ip_unprivileged_port_start=1024 (node
|
|
# sysctl or a hardened distribution) breaks every workload that copied the
|
|
# 80, and the failure then appears in the copied chart, not here. Every
|
|
# real chart in this repo listens above 1024 (3000, 8080, 8123, 9998, ...);
|
|
# the one exception is samba, which needs 139/445 and adds NET_BIND_SERVICE
|
|
# with a documented justification.
|
|
#
|
|
# containerPort is declarative - it does not force the application onto the
|
|
# port. Point the application at the same port when copying this template.
|
|
- containerPort: 8080
|
|
protocol: TCP
|
|
name: http
|
|
env:
|
|
# Reference for guideline 1: ingress.domain is read HERE, outside the
|
|
# Ingress template, so it needs its own required(). Without it, switching
|
|
# the Ingress off via ingress.enabled would render an empty value and the
|
|
# workload would start with a broken address - silently. Copy this shape
|
|
# whenever a chart uses the domain for anything but the Ingress host.
|
|
- name: NORMAL_ENV
|
|
value: '{{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}'
|
|
- name: ENV_FROM_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: "{{ .Release.Name }}-{{ .Chart.Name }}-secret"
|
|
key: "secretKey"
|
|
- name: ENV_FROM_CONFIGMAP
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: "{{ .Release.Name }}-{{ .Chart.Name }}-config-map"
|
|
key: "configMapKey"
|
|
|
|
storage:
|
|
amount: 10Gi
|
|
storageClass: null
|
|
|
|
ingress:
|
|
# Gates the whole Ingress template. Note that ingress.domain is ALSO read by
|
|
# the env block above, so it stays required even with the Ingress switched
|
|
# off; only clusterIssuer becomes unnecessary. Check this per chart instead
|
|
# of assuming either way (guideline 1).
|
|
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: {}
|