Compare commits

..
Author SHA1 Message Date
Cédric VerstraetenandCopilot 6f1dcf8f00 Release Hub 0.167.3 with hardened Admin routing and probes
Publish opt-in hardened runtime support with named Admin Service routing, configurable health probes, legacy defaults and runtime regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-28 18:27:10 +00:00
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
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
Cédric VerstraetenandCopilot b520e2ecc1 Support hardened frontend ports and security context
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 18:20:57 +00:00
Cédric Verstraeten c42388b96f Merge pull request #204 from kerberos-io/feature/hub-passkey-configuration
Add Hub API passkey configuration and bump chart to 0.167.0
2026-10-26 14:47:35 +01:00
Cédric VerstraetenandCopilot 04d6990460 feat(hub): configure API passkeys and bump chart to 0.167.0
Add opt-in API-only WebAuthn configuration, value validation, deployment documentation and rendering checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 13:33:22 +00:00
21 changed files with 796 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.166.0
version: 0.167.3
# 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
+259
View File
@@ -31,6 +31,265 @@ Uninstall the Kerberos Hub chart
helm uninstall hub -n kerberos-hub
### Hardened frontend images
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:
frontend:
containerPort: 8080
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumes:
- name: frontend-tmp
emptyDir:
medium: Memory
sizeLimit: 16Mi
volumeMounts:
- name: frontend-tmp
mountPath: /tmp
```
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. 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, chart 0.167.3)
`charts/hub` deploys Admin in `all` and `ui` modes. Shell-free Admin `v1.4.3`
runs as `65532:65532` and listens on `8080`, unlike the legacy image on `80`.
Set these values together; the chart does not upgrade the image automatically:
```yaml
admin:
repository: ghcr.io/uug-ai/admin
tag: v1.4.3
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
readinessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
```
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`;
the Service targets the named `http` container port. OAuth2-proxy is unchanged.
Defaults remain port `80` with no security context, added volumes, or probes. Coordinate
the image, values and a chart revision containing these settings: published
`0.167.2` does not contain Admin runtime support.
Rollbacks to legacy images must restore port `80`, remove the hardened
security context, and disable `/healthz` probes together. Run
`bash scripts/check-admin-runtime.sh` from the repository root to verify
legacy defaults, hardened runtime/probes, and unchanged OAuth2 routing.
### Hardened Hub workers (opt-in, chart 0.167.3)
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 >
Passkeys**, then use **Sign in with a passkey** for passwordless authentication
with device PIN/biometric verification. Existing password/MFA sign-in remains
available. Enrollment and removal require the current password and an
authenticator-app code when MFA is enabled.
Deploy Hub API and frontend images that support passkeys before enabling this
configuration; the chart version alone does not update the service image tags.
Passkeys are disabled by default. Configure their trust explicitly:
```yaml
kerberoshub:
api:
passkeys:
enabled: true
rpId: "hub.example.com"
origins:
- "https://hub.example.com"
rpName: "Kerberos Hub"
```
| Value | Default | Purpose |
| --- | --- | --- |
| `kerberoshub.api.passkeys.enabled` | `false` | Inject passkey configuration into Hub API pods only |
| `kerberoshub.api.passkeys.rpId` | `""` | Required when enabled; sets `PASSKEY_RP_ID` |
| `kerberoshub.api.passkeys.origins` | `[]` | Required when enabled; exact trusted frontend origins, joined with commas into `PASSKEY_ORIGINS` |
| `kerberoshub.api.passkeys.rpName` | `"Kerberos Hub"` | Display name; sets `PASSKEY_RP_NAME` |
Use the **frontend's public hostname and origins**, not the API URL. Origins
must have no path or trailing slash. Each origin hostname must equal the RP ID
or be its subdomain. Include only trusted origins, especially when sharing an
RP ID across tenant subdomains. Keep the RP ID stable: changing it makes
existing passkeys unusable. The API validates the RP/origin relationship at
runtime; missing/invalid configuration makes passkeys unavailable without
affecting password/MFA sign-in.
HTTPS and a compatible browser are required. Local development may use
`rpId: "localhost"` with `origins: ["http://localhost:4200"]`.
The frontend displays translated cancellation, verification and unavailable
messages when a ceremony cannot complete.
Apply values with a Helm upgrade or GitOps sync; changes trigger an API rollout.
If these variables were previously configured through `kerberoshub.extraEnv`,
remove those entries when enabling the dedicated settings to avoid duplicate
environment variables. Setting `enabled: false` omits the chart-managed
variables; any passkey variables supplied through `extraEnv` still take effect.
The API's MongoDB identity needs reads on `users`, CRUD on `passkeys`, and
insert/find-and-delete on `passkey_challenges`. It attempts to create the
`passkey_challenge_expiry_ttl` index on `expiresAt` with
`expireAfterSeconds: 0`; expiration and single-use enforcement work without the
index, but the index is needed for automatic expired-challenge cleanup.
### Sequence logging
Set `kerberospipeline.sequence.logLevel` to control the Sequence service's
+13 -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,8 +40,16 @@ spec:
{{- end }}
ports:
- name: http
containerPort: 80
containerPort: {{ .Values.admin.containerPort | default 80 }}
protocol: TCP
{{- with .Values.admin.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.admin.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
env:
- name: LOG_LEVEL
value: "{{ .Values.admin.logLevel }}"
+1 -1
View File
@@ -10,7 +10,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: admin
@@ -436,6 +436,15 @@ spec:
- name: MFA_ISSUER
value: "{{ .Values.kerberoshub.api.mfaIssuer }}"
{{- if .Values.kerberoshub.api.passkeys.enabled }}
- name: PASSKEY_RP_ID
value: {{ required "kerberoshub.api.passkeys.rpId is required when passkeys are enabled" .Values.kerberoshub.api.passkeys.rpId | quote }}
- name: PASSKEY_ORIGINS
value: {{ required "kerberoshub.api.passkeys.origins is required when passkeys are enabled" .Values.kerberoshub.api.passkeys.origins | join "," | quote }}
- name: PASSKEY_RP_NAME
value: {{ .Values.kerberoshub.api.passkeys.rpName | quote }}
{{- end }}
# AWS (Legacy, use Kerberos Vault instead)
- name: AWS_REGION
value: "{{ .Values.kerberoshub.api.aws.region }}"
@@ -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:
@@ -11,7 +11,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: hub-frontend
@@ -182,12 +182,16 @@ spec:
- name: hub-frontend
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 }}
+32
View File
@@ -62,6 +62,38 @@
"api": {
"type": "object",
"properties": {
"passkeys": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean"},
"rpId": {
"type": "string",
"description": "Stable WebAuthn relying-party hostname, without scheme, port or path. Must match each frontend origin's hostname or be its parent domain."
},
"origins": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^https://[^/?#]+$|^http://localhost(:[0-9]+)?$"
},
"description": "Exact trusted frontend origins, with no path or trailing slash. HTTPS is required except for HTTP localhost development."
},
"rpName": {"type": "string"}
},
"if": {
"required": ["enabled"],
"properties": {"enabled": {"const": true}}
},
"then": {
"required": ["rpId", "origins"],
"properties": {
"rpId": {"minLength": 1},
"origins": {"minItems": 1}
}
}
},
"corsAllowedOrigins": {
"type": "array",
"items": {
+32
View File
@@ -268,6 +268,11 @@ kerberosvault:
###########################################################################
admin:
repository: uugai/admin
# Opt in to 8080 and a securityContext only with a compatible hardened image.
containerPort: 80
securityContext: {}
readinessProbe: {}
livenessProbe: {}
pullPolicy: IfNotPresent
tag: "v1.3.0"
replicas: 2
@@ -392,6 +397,13 @@ kerberoshub:
# MFA issuer name
mfaIssuer: "yourdomain.com"
# WebAuthn passkeys for users and sub-users; requires compatible API/frontend images.
# Use the public frontend hostname/origins, not the API URL. Keep rpId stable.
passkeys:
enabled: false
rpId: "" # e.g. "hub.example.com", without scheme, port or path.
origins: [] # e.g. ["https://hub.example.com"]; exact origins, no trailing slash.
rpName: "Kerberos Hub"
# Admin API's are made available for automation of Kerberos Hub.
# To access those API's (e.g. creation of owner users), an API key needs to be provided.
apiKey: "a-random-admin-api-key"
@@ -467,6 +479,9 @@ kerberoshub:
pullPolicy: IfNotPresent
tag: "v1.13.9"
replicas: 2
# Hardened frontend images listen on 8080; retain 80 for existing images.
containerPort: 80
securityContext: {}
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Set to false to skip rendering the hub-frontend Service (e.g. when an
# external/shared Service is managed elsewhere).
@@ -744,6 +759,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"
@@ -957,6 +975,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.
@@ -1065,6 +1085,8 @@ kerberoshub:
# memory: 128Mi
# cpu: 100m
monitordevice:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-monitor-device
pullPolicy: IfNotPresent
tag: "v1.4.2"
@@ -1212,6 +1234,8 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
throttler:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-throttler
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1233,6 +1257,8 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
notify:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-notification
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1308,6 +1334,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.
@@ -1336,6 +1364,8 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
thumbnail:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-thumbnail
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1367,6 +1397,8 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
counting:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-counting
deploymentAnnotations: {}
# deploymentAnnotations:
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
resource() {
local source="$1"
awk -v source="# Source: hub/templates/admin/$source.yaml" '
/^---/ { selected=0 }
/^# Source:/ { selected=($0 == source); next }
selected { print }
'
}
assert_contains() {
local rendered="$1" pattern="$2"
if ! grep -Eq -- "$pattern" <<< "$rendered"; then
printf 'Missing expected Admin setting: %s\n' "$pattern" >&2
return 1
fi
}
assert_absent() {
local rendered="$1" pattern="$2"
if grep -Eq -- "$pattern" <<< "$rendered"; then
printf 'Unexpected Admin setting: %s\n' "$pattern" >&2
return 1
fi
}
HARDENED_FLAGS=(
--set admin.containerPort=8080
--set-json 'admin.securityContext={"runAsNonRoot":true,"runAsUser":65532,"runAsGroup":65532,"readOnlyRootFilesystem":true,"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"seccompProfile":{"type":"RuntimeDefault"}}'
--set-json 'admin.volumes=[{"name":"admin-tmp","emptyDir":{"medium":"Memory","sizeLimit":"16Mi"}}]'
--set-json 'admin.volumeMounts=[{"name":"admin-tmp","mountPath":"/tmp"}]'
--set-json 'admin.readinessProbe={"httpGet":{"path":"/healthz","port":"http"},"periodSeconds":10}'
--set-json 'admin.livenessProbe={"httpGet":{"path":"/healthz","port":"http"},"initialDelaySeconds":10,"periodSeconds":10}'
)
for mode in all ui; do
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set admin.oauth2Proxy.enabled=true)"
default_deployment="$(resource deployment <<< "$default_render")"
default_service="$(resource service <<< "$default_render")"
default_ingress="$(resource ingress <<< "$default_render")"
default_oauth="$(resource oauth2-proxy <<< "$default_render")"
assert_contains "$default_deployment" 'containerPort: 80$'
assert_absent "$default_deployment" 'securityContext:|readinessProbe:|livenessProbe:'
assert_contains "$default_service" 'port: 80$'
assert_contains "$default_service" 'targetPort: http$'
hardened_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set admin.oauth2Proxy.enabled=true "${HARDENED_FLAGS[@]}")"
hardened_deployment="$(resource deployment <<< "$hardened_render")"
assert_contains "$hardened_deployment" 'containerPort: 8080$'
assert_absent "$hardened_deployment" 'containerPort: 80$'
for setting in 'runAsNonRoot: true' 'runAsUser: 65532' 'runAsGroup: 65532' \
'readOnlyRootFilesystem: true' 'allowPrivilegeEscalation: false' \
'type: RuntimeDefault' 'drop:' '- ALL' 'medium: Memory' \
'sizeLimit: 16Mi' 'mountPath: /tmp' 'name: admin-tmp'; do
assert_contains "$hardened_deployment" "^[[:space:]]*(- )?$setting$"
done
for probe in readinessProbe livenessProbe; do
probe_render="$(awk -v probe="$probe:" '
/^ [a-zA-Z]/ { selected=($1 == probe) }
selected { print }
' <<< "$hardened_deployment")"
assert_contains "$probe_render" 'path: /healthz$'
assert_contains "$probe_render" 'port: http$'
done
[[ "$(resource service <<< "$hardened_render")" == "$default_service" ]]
[[ "$(resource ingress <<< "$hardened_render")" == "$default_ingress" ]]
[[ "$(resource oauth2-proxy <<< "$hardened_render")" == "$default_oauth" ]]
assert_contains "$default_ingress" 'nginx.ingress.kubernetes.io/auth-url:'
assert_contains "$default_ingress" 'number: 80$'
echo "OK ($mode): legacy defaults; hardened runtime/probes; stable Service, ingress and OAuth2"
done
pipeline_render="$(helm template hub "$CHART_DIR" --set mode=pipeline "${HARDENED_FLAGS[@]}")"
assert_absent "$pipeline_render" '^# Source: hub/templates/admin/'
echo "All Admin runtime checks passed."
+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: http$'
assert_line "$service" '^[ ]+port: 80$'
assert_line "$service" '^[ ]+targetPort: http$'
[[ "$(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."
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
extract_passkeys() {
awk '
/^---/ { deployment=0; name=""; variable="" }
/^kind: Deployment$/ { deployment=1 }
deployment && /^ name:/ { name=$2 }
deployment && /- name: PASSKEY_/ { variable=$3; next }
variable != "" && /value:/ {
value=$0
sub(/^[[:space:]]*value: /, "", value)
print name, variable, value
variable=""
}
' | sort
}
assert_passkeys() {
local rendered="$1" expected="$2" actual count expected_count
actual="$(printf '%s\n' "$rendered" | extract_passkeys)"
if [[ "$actual" != "$expected" ]]; then
printf 'Unexpected passkey environment:\n%s\nExpected:\n%s\n' "$actual" "$expected" >&2
return 1
fi
count="$(printf '%s\n' "$rendered" | awk '/- name: PASSKEY_/ { count++ } END { print count+0 }')"
expected_count="$(printf '%s\n' "$expected" | awk 'NF { count++ } END { print count+0 }')"
if [[ "$count" != "$expected_count" ]]; then
echo "Passkey variables leaked outside the expected Deployment or were duplicated" >&2
return 1
fi
}
CONFIGURED_FLAGS=(
--set-json kerberoshub.api.passkeys.enabled=true
--set-json 'kerberoshub.api.passkeys.rpId="example.com"'
--set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com","https://legacy.example.com:8443"]'
--set-json 'kerberoshub.api.passkeys.rpName="Example Hub"'
)
expected="$(printf '%s\n' \
'hub-api PASSKEY_ORIGINS "https://hub.example.com,https://legacy.example.com:8443"' \
'hub-api PASSKEY_RP_ID "example.com"' \
'hub-api PASSKEY_RP_NAME "Example Hub"' | sort)"
for mode in all ui pipeline; do
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode")"
assert_passkeys "$default_render" ""
configured_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${CONFIGURED_FLAGS[@]}")"
if [[ "$mode" == pipeline ]]; then
assert_passkeys "$configured_render" ""
else
assert_passkeys "$configured_render" "$expected"
fi
echo "OK ($mode): defaults opt out; enabled settings affect Hub API only"
done
disabled_render="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" --set kerberoshub.api.passkeys.enabled=false)"
assert_passkeys "$disabled_render" ""
localhost_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set kerberoshub.api.passkeys.enabled=true \
--set-string kerberoshub.api.passkeys.rpId=localhost \
--set-json 'kerberoshub.api.passkeys.origins=["http://localhost:4200"]')"
assert_passkeys "$localhost_render" "$(printf '%s\n' \
'hub-api PASSKEY_ORIGINS "http://localhost:4200"' \
'hub-api PASSKEY_RP_ID "localhost"' \
'hub-api PASSKEY_RP_NAME "Kerberos Hub"' | sort)"
legacy_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set-json 'kerberoshub.extraEnv=[{"name":"PASSKEY_RP_ID","value":"example.com"}]')"
printf '%s\n' "$legacy_render" | grep -q -- '- name: PASSKEY_RP_ID'
expect_invalid() {
local output
if output="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" "$@" 2>&1)"; then
echo "Invalid passkey settings unexpectedly rendered: $*" >&2
return 1
fi
if [[ "$output" != *"passkeys"* ]]; then
printf 'Rendering failed for an unexpected reason:\n%s\n' "$output" >&2
return 1
fi
}
expect_invalid --set-string kerberoshub.api.passkeys.rpId=
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=[]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com/"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["http://hub.example.com"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com","https://hub.example.com"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins="https://hub.example.com"'
expect_invalid --set-json 'kerberoshub.api.passkeys.enabled="true"'
expect_invalid --set-json 'kerberoshub.api.passkeys.rpId=123'
expect_invalid --set-json 'kerberoshub.api.passkeys.rpName=123'
expect_invalid --set-string kerberoshub.api.passkeys.origin=https://hub.example.com
echo "All Hub passkey configuration checks passed."