Compare commits

...
Author SHA1 Message Date
Copilot 0eff4bf2ee feat(hub): support opt-in hardened Admin and notification images
Retain default Admin port 80 while allowing 8080 and explicit container security settings. Add notification-only pod and container contexts without changing its provider or FFmpeg contract. Cover routing, tmpfs, TLS, identity, default preservation and per-component isolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 22:35:20 +00:00
Copilot 18e456d490 feat(hub): add opt-in hardened worker runtime contexts
Preserve legacy image and deployment defaults while supporting verified per-worker container and pod security contexts alongside existing writable volumes. Document draft image contracts and workload-sized scratch storage; validate all/pipeline/ui renders and TLS compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 22:21:31 +00:00
Cédric Verstraeten 374d40aa41 Merge pull request #206 from kerberos-io/fix/hardened-frontend-demo-routing
Fix hardened frontend runtime settings for demo deployments
2026-10-26 21:37:24 +01:00
Copilot 073c12f8eb Fix hardened frontend runtime settings for demo deployments
Apply the shared port and security context to the demo frontend so both deployments support the shell-free v1.19.21 image. Release chart 0.167.2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 20:33:49 +00:00
Cédric VerstraetenandCopilot 001c835068 Merge pull request #205 from kerberos-io/security/frontend-image-runtime
Add opt-in frontend port and security context support for hardened images.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 20:18:36 +01:00
16 changed files with 441 additions and 7 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.167.1
version: 0.167.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
+151 -2
View File
@@ -37,6 +37,8 @@ New shell-free frontend images run as `65532:65532` and listen on port `8080`.
Before selecting such an image tag, set the following values together. Defaults
remain compatible with legacy images on port `80`; this chart does not upgrade
the frontend image automatically. The Service and ingress remain on port `80`.
Use chart `0.167.2` or newer when the demo frontend is enabled; `0.167.1`
supports these settings only on the primary frontend.
```yaml
kerberoshub:
@@ -66,8 +68,155 @@ Merge the temporary volume into any existing branding/translation mounts rather
than replacing them. Custom files must be readable by UID `65532`; mount them
read-only. Only `/tmp` needs writes for generated public runtime configuration
and nginx's PID/temp files. The web root stays read-only. Health checks can use
`GET /healthz` on the named `http` container port. This setting applies to the
primary frontend, not the separately configured legacy demo deployment.
`GET /healthz` on the named `http` container port. These settings apply to both
the primary and demo frontends, which share the frontend image tag. Update the
live Argo CD Application's chart pin as well as its values: an image-only
upgrade leaves older charts forwarding to port `80` and causes gateway errors.
### Hardened Admin image (opt-in, unreleased chart support)
`charts/hub` deploys Admin in `all` and `ui` modes. The shell-free image proposed
by `uug-ai/admin#59` runs as `65532:65532` and listens on `8080`, unlike the legacy
image on `80`. When selecting a verified compatible image, set these values
together; the chart does not select or upgrade that image automatically:
```yaml
admin:
containerPort: 8080
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumes:
- name: admin-tmp
emptyDir:
medium: Memory
sizeLimit: 16Mi # Example; size for your runtime configuration.
volumeMounts:
- name: admin-tmp
mountPath: /tmp
```
Merge the scratch volume with any existing mounts. Only `/tmp` is writable;
application files remain read-only. Memory-backed scratch counts against the
pod memory limit. The `admin-svc` Service and Admin ingress still use port `80`;
only the Service target and container port change. OAuth2-proxy is unchanged.
Defaults remain port `80` with no security context or added volumes. Coordinate
the image, values and a chart revision containing these settings: published
`0.167.2` does not contain Admin runtime support.
### Hardened Hub workers (opt-in, unreleased chart support)
The worker templates accept **per-component** `securityContext` (container) and
`podSecurityContext` (pod) maps. Both default to `{}` and are omitted from the
rendered Deployment. Image tags, ports, existing mounts and legacy permissions
are unchanged. Support covers:
| Values path | Deployment |
| --- | --- |
| `kerberospipeline.counting` | `pipe-counting` |
| `kerberospipeline.throttler` | `pipe-throttler` |
| `kerberospipeline.notify` | `pipe-notify` |
| `kerberospipeline.thumbnail` | `pipe-thumbnail` |
| `kerberospipeline.dominantColor` | `pipe-dominantcolor` |
| `kerberoshub.cleanup` | `hub-cleanup` |
| `kerberoshub.monitordevice` | `hub-monitor-device` |
| `kerberoshub.services.workflows` | `hub-workflows` |
| `kerberoshub.services.<stage>` | Enabled generic workflow workers, e.g. loitering, ANPR or forwarder |
These application hardening changes are proposed in draft PRs, not release
clearance. First select and validate a compatible hardened image for each
component, then deploy a chart revision containing this support and its matching
values together. Published chart `0.167.2` does not contain these worker settings.
Do not enable these settings for an unverified legacy, Python/GPU or custom image.
There is no global switch that hardens every worker.
For a verified `65532:65532` Go worker, explicitly configure its own values path:
```yaml
kerberospipeline:
counting:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
```
Apply that same container map independently to other verified workers; it is not
inherited by them. The confirmed loitering image processes trajectories entirely
in memory and needs no writable mount. Worker metrics Services remain on `8080`;
`GET /metrics` is process liveness, not RabbitMQ/dependency readiness.
The notification image proposed by `uug-ai/hub-pipeline-notification#149` retains
its existing worker/provider/FFmpeg behavior. Configure its own
`kerberospipeline.notify` security maps only after selecting that compatible
image; retain provider settings and merge existing mounts/resources. Mount
workload-sized writable scratch at `/tmp` (the default temporary directory);
the worker creates `/tmp/hub-pipeline-thumbnail` for generated media. Keep
`/mail/templates` override mounts read-only and custom CA mounts readable by UID
`65532`. The application working directory need not be writable. MongoDB TLS
secret mounts remain read-only. Final native image proof is still required.
Thumbnail and dominant-colour workers need writable **`/tmp`**, including legacy
debug image output. Merge a disk-backed `emptyDir` and its mount into each
component's existing `volumes`/`volumeMounts`. For example, after sizing for the
workload:
```yaml
kerberospipeline:
thumbnail:
# Also configure the container securityContext above for this worker.
podSecurityContext:
fsGroup: 65532
volumes:
- name: media-scratch
emptyDir:
sizeLimit: 4Gi # Illustrative only, NOT a safe universal limit.
volumeMounts:
- name: media-scratch
mountPath: /tmp
resources:
requests:
ephemeral-storage: 4Gi
limits:
ephemeral-storage: 6Gi
```
Size scratch for the sum of concurrently downloaded full recordings, extracted
JPEG/PNG frames, debug files and headroom. Align ephemeral-storage requests/limits
with scratch and other pod usage; retain suitable CPU/memory resources.
Memory-backed scratch counts against the pod memory limit and can double-count
media already buffered in Go. Small CI fixture volumes are not production sizing.
Only opt in to `fsGroup` after reviewing **all** mounts on that pod: persistent
volumes can be affected by Kubernetes/CSI ownership handling. Do not add a
recursive chmod/chown init container or mount over the root-owned application.
The proposed ANPR image uses a workload-sized writable directory such as `/work`
with `kerberoshub.services.anpr.env.TMPDIR: /work`; configure matching
`volumes`/`volumeMounts` and container security settings on that stage only.
Optional dataset mode also requires its own writable `ANPR_DATASET_DIR` mount.
Models and application directories stay read-only.
This repository has **no Vault server or design-system chart**. The files named
`vault-proxy` and `vault-forwarder` deploy different legacy applications; their
ports are not the Vault server's port. Do not change them for `uug-ai/vault#119`.
That proposed Vault image requires port `8080` (legacy `80`), UID/GID `65532`,
read-only `/home/vault`, workload-sized writable `/run/vault`, and compatible
ownership on existing optional `LOCAL_CACHE_DIRECTORY` mounts. Its liveness
endpoint is `GET /api/health`. Wire those settings in the actual Vault deployment
only when a compatible image release is selected; no migration is required.
### Passkeys
+5 -1
View File
@@ -26,6 +26,10 @@ spec:
- name: admin
image: "{{ .Values.global.imageRegistry }}{{ .Values.admin.repository }}:{{ .Values.admin.tag }}"
imagePullPolicy: {{ .Values.admin.pullPolicy }}
{{- with .Values.admin.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.admin.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -36,7 +40,7 @@ spec:
{{- end }}
ports:
- name: http
containerPort: 80
containerPort: {{ .Values.admin.containerPort }}
protocol: TCP
env:
- name: LOG_LEVEL
+1 -1
View File
@@ -10,7 +10,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: {{ .Values.admin.containerPort }}
name: http
selector:
app: admin
@@ -22,6 +22,10 @@ spec:
labels:
app: hub-cleanup
spec:
{{- with .Values.kerberoshub.cleanup.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -47,6 +51,10 @@ spec:
- name: hub-cleanup
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.cleanup.repository }}:{{ .Values.kerberoshub.cleanup.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.cleanup.pullPolicy }}
{{- with .Values.kerberoshub.cleanup.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.kerberoshub.cleanup.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -10,7 +10,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: hub-frontend-demo
@@ -101,12 +101,16 @@ spec:
- name: hub-frontend-demo
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.frontend.repository }}:{{ .Values.kerberoshub.frontend.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.frontend.pullPolicy }}
{{- with .Values.kerberoshub.frontend.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberoshub.frontend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- containerPort: 80
- containerPort: {{ .Values.kerberoshub.frontend.containerPort | default 80 }}
name: http
{{- with .Values.kerberoshub.frontend.volumeMounts}}
volumeMounts:
@@ -22,6 +22,10 @@ spec:
labels:
app: hub-monitor-device
spec:
{{- with .Values.kerberoshub.monitordevice.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -47,6 +51,10 @@ spec:
- name: hub-monitor-device
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.monitordevice.repository }}:{{ .Values.kerberoshub.monitordevice.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.monitordevice.pullPolicy }}
{{- with .Values.kerberoshub.monitordevice.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.kerberoshub.monitordevice.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -50,6 +50,10 @@ spec:
labels:
app: hub-{{ $name }}
spec:
{{- with $svc.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if $root.Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" $root.Release.Name $root.Chart.Name | trunc 63 | trimSuffix "-") $root.Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -69,6 +73,10 @@ spec:
- name: hub-{{ $name }}
image: "{{ $root.Values.global.imageRegistry }}{{ $svc.repository }}:{{ $svc.tag }}"
imagePullPolicy: {{ $svc.pullPolicy | default "IfNotPresent" }}
{{- with $svc.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with $svc.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -23,6 +23,10 @@ spec:
labels:
app: hub-workflows
spec:
{{- with .Values.kerberoshub.services.workflows.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -48,6 +52,10 @@ spec:
- name: hub-workflows
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.services.workflows.repository }}:{{ .Values.kerberoshub.services.workflows.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.services.workflows.pullPolicy }}
{{- with .Values.kerberoshub.services.workflows.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberoshub.services.workflows.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -24,6 +24,10 @@ spec:
labels:
app: pipe-counting
spec:
{{- with .Values.kerberospipeline.counting.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -43,6 +47,10 @@ spec:
- name: pipe-counting
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.counting.repository }}:{{ .Values.kerberospipeline.counting.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.counting.pullPolicy }}
{{- with .Values.kerberospipeline.counting.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.counting.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -24,6 +24,10 @@ spec:
labels:
app: pipe-dominantcolor
spec:
{{- with .Values.kerberospipeline.dominantColor.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -43,6 +47,10 @@ spec:
- name: pipe-dominantcolor
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.dominantColor.repository }}:{{ .Values.kerberospipeline.dominantColor.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.dominantColor.pullPolicy }}
{{- with .Values.kerberospipeline.dominantColor.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.dominantColor.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -26,6 +26,10 @@ spec:
labels:
app: pipe-notify
spec:
{{- with .Values.kerberospipeline.notify.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -51,6 +55,10 @@ spec:
- name: pipe-notify
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.notify.repository }}:{{ .Values.kerberospipeline.notify.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.notify.pullPolicy }}
{{- with .Values.kerberospipeline.notify.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.notify.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -26,6 +26,10 @@ spec:
labels:
app: pipe-throttler
spec:
{{- with .Values.kerberospipeline.throttler.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -51,6 +55,10 @@ spec:
- name: pipe-throttler
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.throttler.repository }}:{{ .Values.kerberospipeline.throttler.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.throttler.pullPolicy }}
{{- with .Values.kerberospipeline.throttler.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.throttler.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -24,6 +24,10 @@ spec:
labels:
app: pipe-thumbnail
spec:
{{- with .Values.kerberospipeline.thumbnail.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -43,6 +47,10 @@ spec:
- name: pipe-thumbnail
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.thumbnail.repository }}:{{ .Values.kerberospipeline.thumbnail.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.thumbnail.pullPolicy }}
{{- with .Values.kerberospipeline.thumbnail.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.thumbnail.resources }}
resources:
{{- toYaml . | nindent 12 }}
+20
View File
@@ -268,6 +268,9 @@ kerberosvault:
###########################################################################
admin:
repository: uugai/admin
# Opt in to 8080 and a securityContext only with a compatible hardened image.
containerPort: 80
securityContext: {}
pullPolicy: IfNotPresent
tag: "v1.3.0"
replicas: 2
@@ -754,6 +757,9 @@ kerberoshub:
organization: "github-organization"
team: "github-team"
cleanup:
# Explicit opt-in for a selected hardened image; legacy defaults are unchanged.
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-cleanup
pullPolicy: IfNotPresent
tag: "v1.4.19"
@@ -967,6 +973,8 @@ kerberoshub:
# `enabled` here.
workflows:
repository: ghcr.io/uug-ai/hub-workflows
securityContext: {}
podSecurityContext: {}
pullPolicy: IfNotPresent
tag: "v1.0.0"
replicas: 1 # Number of pods for the service.
@@ -1075,6 +1083,8 @@ kerberoshub:
# memory: 128Mi
# cpu: 100m
monitordevice:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-monitor-device
pullPolicy: IfNotPresent
tag: "v1.4.2"
@@ -1222,6 +1232,8 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
throttler:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-throttler
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1243,6 +1255,8 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
notify:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-notification
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1318,6 +1332,8 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
dominantColor:
securityContext: {}
podSecurityContext: {}
# Opt-in: deploys pipe-dominantcolor and tells the analysis stage to schedule
# it (DOMINANTCOLOR_ENABLED). Requires an analysis image that reads the flag;
# older images always publish to the dominantcolor queue.
@@ -1346,6 +1362,8 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
thumbnail:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-thumbnail
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1377,6 +1395,8 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
counting:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-counting
deploymentAnnotations: {}
# deploymentAnnotations:
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
components=(
kerberospipeline.counting kerberospipeline.throttler
kerberospipeline.notify
kerberospipeline.thumbnail kerberospipeline.dominantColor
kerberoshub.cleanup kerberoshub.monitordevice
kerberoshub.services.workflows kerberoshub.services.loitering
)
fixtures=(
--set kerberospipeline.dominantColor.enabled=true
--set kerberoshub.workflows.enabled=true
--set kerberoshub.services.loitering.enabled=true
--set kerberoshub.services.loitering.repository=example/loitering
--set kerberoshub.services.loitering.tag=test
--set kerberoshub.services.loitering.queue=loitering
--set mongodb.tls.enabled=true
--set mongodb.tls.existingSecret=mongodb-ca
--set mongodb.tls.caFileName=ca.crt
)
security=()
for component in "${components[@]}"; do
security+=(
--set "$component.securityContext.runAsNonRoot=true"
--set "$component.securityContext.runAsUser=65532"
--set "$component.securityContext.runAsGroup=65532"
--set "$component.securityContext.readOnlyRootFilesystem=true"
--set "$component.securityContext.allowPrivilegeEscalation=false"
--set "$component.securityContext.capabilities.drop[0]=ALL"
--set "$component.securityContext.seccompProfile.type=RuntimeDefault"
--set "$component.podSecurityContext.fsGroup=65532"
--set "$component.volumes[0].name=scratch"
--set "$component.volumes[0].emptyDir.sizeLimit=4Gi"
--set "$component.volumeMounts[0].name=scratch"
--set "$component.volumeMounts[0].mountPath=/tmp"
--set "$component.resources.requests.ephemeral-storage=4Gi"
--set "$component.resources.limits.ephemeral-storage=6Gi"
)
done
resource() {
local kind="$1" name="$2"
awk -v kind="$kind" -v name="$name" '
function emit() { if (matchedKind && matchedName) printf "%s", block }
/^---$/ { emit(); block=""; matchedKind=0; matchedName=0 }
{ block=block $0 "\n" }
$0 == "kind: " kind { matchedKind=1 }
$0 == " name: " name { matchedName=1 }
END { emit() }
'
}
assert_line() {
local block="$1" pattern="$2"
if ! grep -Eq "$pattern" <<< "$block"; then
printf 'Missing expected pattern: %s\n%s\n' "$pattern" "$block" >&2
exit 1
fi
}
for mode in all pipeline ui; do
defaults="$(helm template hub "$CHART_DIR" --set "mode=$mode")"
if grep -q 'securityContext:' <<< "$defaults"; then
echo "Default $mode mode unexpectedly enables security settings" >&2
exit 1
fi
legacy="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}")"
hardened="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}" "${security[@]}")"
helm lint "$CHART_DIR" --quiet --set "mode=$mode" "${fixtures[@]}" "${security[@]}"
names=()
if [[ "$mode" != ui ]]; then
names+=(pipe-counting pipe-throttler pipe-notify pipe-thumbnail pipe-dominantcolor hub-workflows hub-loitering)
fi
if [[ "$mode" != pipeline ]]; then
names+=(hub-cleanup hub-monitor-device)
fi
for name in "${names[@]}"; do
block="$(resource Deployment "$name" <<< "$hardened")"
for pattern in \
'^[ ]{6}securityContext:$' '^[ ]{8}fsGroup: 65532$' \
'^[ ]+(runAsUser|runAsGroup): 65532$' \
'^[ ]+runAsNonRoot: true$' '^[ ]+readOnlyRootFilesystem: true$' \
'^[ ]+allowPrivilegeEscalation: false$' '^[ ]+capabilities:$' \
'^[ ]+drop:$' '^[ ]+- ALL$' '^[ ]+seccompProfile:$' \
'^[ ]+type: RuntimeDefault$' '^[ ]+(- )?mountPath: /tmp$' \
'^[ ]+sizeLimit: 4Gi$' '^[ ]+ephemeral-storage: 6Gi$' \
'^[ ]+ephemeral-storage: 4Gi$'; do
assert_line "$block" "$pattern"
done
# Verify both identity axes independently.
assert_line "$block" '^[ ]+runAsUser: 65532$'
assert_line "$block" '^[ ]+runAsGroup: 65532$'
assert_line "$block" '^[ ]{8,10}securityContext:$'
if [[ "$name" == hub-cleanup || "$name" == hub-monitor-device || "$name" == hub-workflows || "$name" == pipe-notify ]]; then
assert_line "$block" '^[ ]+secretName: mongodb-ca$'
assert_line "$block" '^[ ]+mountPath: /etc/mongodb/tls$'
assert_line "$block" '^[ ]+readOnly: true$'
fi
if [[ "$name" == pipe-notify ]]; then
# Provider, queue, FFmpeg and all other worker settings are unchanged.
old_block="$(resource Deployment pipe-notify <<< "$legacy")"
[[ "$(sed -n '/^ env:/,$p' <<< "$old_block")" == "$(sed -n '/^ env:/,$p' <<< "$block")" ]]
fi
if [[ "$name" != hub-monitor-device ]]; then
service="$(resource Service "$name" <<< "$hardened")"
assert_line "$service" '^[ ]+port: 8080$'
assert_line "$service" '^[ ]+targetPort: 8080$'
fi
done
expected="${#names[@]}"
[[ "$(grep -c 'runAsUser: 65532' <<< "$hardened")" == "$expected" ]]
[[ "$(grep -c 'fsGroup: 65532' <<< "$hardened")" == "$expected" ]]
# Image selection, Services, env and TLS mounts must not change.
for pattern in '^[ ]+image:' '^[ ]+(port|targetPort|containerPort):' 'mongodb-tls'; do
[[ "$(grep -E "$pattern" <<< "$legacy")" == "$(grep -E "$pattern" <<< "$hardened")" ]]
done
echo "OK ($mode): defaults unchanged; $expected independent hardened workers preserve images, ports and MongoDB TLS"
done
# Hardening one worker must not opt in another, even on the same pod mode.
isolated="$(helm template hub "$CHART_DIR" --set mode=pipeline \
--set kerberospipeline.counting.securityContext.runAsUser=65532)"
[[ "$(grep -c 'runAsUser: 65532' <<< "$isolated")" == 1 ]]
assert_line "$(resource Deployment pipe-counting <<< "$isolated")" '^[ ]+runAsUser: 65532$'
echo "All hardened worker checks passed."
admin_flags=(
--set admin.containerPort=8080
--set admin.securityContext.runAsNonRoot=true
--set admin.securityContext.runAsUser=65532
--set admin.securityContext.runAsGroup=65532
--set admin.securityContext.readOnlyRootFilesystem=true
--set admin.securityContext.allowPrivilegeEscalation=false
--set admin.securityContext.capabilities.drop[0]=ALL
--set admin.securityContext.seccompProfile.type=RuntimeDefault
--set admin.volumes[0].name=admin-tmp
--set admin.volumes[0].emptyDir.medium=Memory
--set admin.volumes[0].emptyDir.sizeLimit=16Mi
--set admin.volumeMounts[0].name=admin-tmp
--set admin.volumeMounts[0].mountPath=/tmp
--set admin.oauth2Proxy.enabled=true
)
for mode in all ui pipeline; do
legacy="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set admin.oauth2Proxy.enabled=true)"
configured="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${admin_flags[@]}")"
helm lint "$CHART_DIR" --quiet --set "mode=$mode" "${admin_flags[@]}"
if [[ "$mode" == pipeline ]]; then
[[ -z "$(resource Deployment admin <<< "$configured")" ]]
[[ -z "$(resource Service admin-svc <<< "$configured")" ]]
[[ "$legacy" == "$configured" ]]
echo "OK (pipeline): Admin settings render no Admin resources or unrelated changes"
continue
fi
old="$(resource Deployment admin <<< "$legacy")"
block="$(resource Deployment admin <<< "$configured")"
assert_line "$old" '^[ ]+containerPort: 80$'
[[ "$old" != *securityContext:* && "$old" != *admin-tmp* ]]
for pattern in \
'^[ ]+containerPort: 8080$' '^[ ]{10}securityContext:$' \
'^[ ]+runAsUser: 65532$' '^[ ]+runAsGroup: 65532$' \
'^[ ]+runAsNonRoot: true$' '^[ ]+readOnlyRootFilesystem: true$' \
'^[ ]+allowPrivilegeEscalation: false$' '^[ ]+capabilities:$' \
'^[ ]+drop:$' '^[ ]+- ALL$' '^[ ]+seccompProfile:$' \
'^[ ]+type: RuntimeDefault$' '^[ ]+medium: Memory$' \
'^[ ]+sizeLimit: 16Mi$' '^[ ]+(- )?mountPath: /tmp$'; do
assert_line "$block" "$pattern"
done
old_service="$(resource Service admin-svc <<< "$legacy")"
service="$(resource Service admin-svc <<< "$configured")"
assert_line "$old_service" '^[ ]+targetPort: 80$'
assert_line "$service" '^[ ]+port: 80$'
assert_line "$service" '^[ ]+targetPort: 8080$'
[[ "$(resource Ingress admin-ingress <<< "$legacy")" == "$(resource Ingress admin-ingress <<< "$configured")" ]]
[[ "$(resource Deployment oauth2-proxy-admin <<< "$legacy")" == "$(resource Deployment oauth2-proxy-admin <<< "$configured")" ]]
[[ "$(resource Service oauth2-proxy-admin <<< "$legacy")" == "$(resource Service oauth2-proxy-admin <<< "$configured")" ]]
[[ "$(grep 'image:' <<< "$legacy")" == "$(grep 'image:' <<< "$configured")" ]]
# No other workload inherits Admin's container port or security settings.
[[ "$(grep -c 'runAsUser: 65532' <<< "$configured")" == 1 ]]
echo "OK ($mode): Admin opt-in port/security/tmpfs; legacy image, Service port, ingress and OAuth2-proxy preserved"
done
echo "All hardened Admin checks passed."