Compare commits

..
3 Commits
Author SHA1 Message Date
Cédric Verstraeten 371408e5eb Merge pull request #207 from kerberos-io/security/image-hardening-rollout
Add opt-in hardened Hub worker runtime contexts
2026-10-27 14:43:33 +01:00
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
14 changed files with 428 additions and 2 deletions
+145
View File
@@ -73,6 +73,151 @@ 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
Users and sub-users can create and manage their own passkeys in **Profile >
+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 }}
@@ -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."