Compare commits

...
Author SHA1 Message Date
Cédric Verstraeten 5dfaaf0363 Merge pull request #221 from kerberos-io/fix/hub-api-security-context
Add hardened Hub API security contexts (0.170.1)
2026-11-04 15:47:29 +01:00
Cédric VerstraetenandCopilot 56ea65c6e7 Add hardened Hub API security contexts and writable scratch (0.170.1)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-04 14:05:22 +00:00
uug.ai af4e1c36a9 A new public release - 1793786033 (#219) 2026-11-04 11:19:24 +01:00
Cédric VerstraetenandCopilot 166b4a31e6 Default Hub frontends to hardened images on port 8080 (0.170.0) (#220)
Coordinate frontend v1.19.28, port 8080, non-root/read-only security defaults and writable scratch for primary and demo. Preserve Service port 80, Admin legacy defaults, and explicit legacy rollback settings. Validate real image startup, health and generated configuration in Kind.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-04 11:16:48 +01:00
Cédric VerstraetenandCopilot e938cb01b5 Release Hub chart 0.169.1 (#218)
Publish HTTP port recovery guidance from #217 without changing application images or runtime defaults.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-04 10:48:17 +01:00
Cédric VerstraetenandCopilot bd4e118a7a Add safe recovery for hardened HTTP port upgrades (#217)
Add guarded, dry-run-first HTTP port migration and rollback recovery for Admin and both frontends. Preserve unrelated ports and containers; validate strategic-merge duplicates and repaired specifications against Kubernetes in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-04 10:19:05 +01:00
KilianandClaude Opus 5.5 fe658cce05 feat(hub): configure audit export events per file (0.169.0) (#216)
* feat(hub): configure the audit export limit (0.169.0)

kerberoshub.api.auditExportMaxEvents sets AUDIT_EXPORT_MAX_EVENTS on the
Hub API only: the most audit events one export file holds. Empty keeps
the API default of 100000; the schema accepts 1 to 1000000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(hub): name the audit export setting per file

kerberoshub.api.auditExportMaxEventsPerFile sets
AUDIT_EXPORT_MAX_EVENTS_PER_FILE: it sizes each export file and does not
refuse larger exports, which the Audit page splits into parts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:00:17 +01:00
KilianandClaude Opus 5.5 1288266666 hub: default hub-audit v1.0.10 and provider-neutral audit docs (0.168.4) (#215)
The default kerberoshub.audit.tag was v1.0.1, which predates S3
destinations, so following the chart docs without overriding the tag
gave a rejected config. Defaults to v1.0.10 (S3 + allowUnlockedBucket).
The values example drops PR references for versions, adds maxAttempts,
names Ceph RGW/MinIO/Garage for path-style, notes that pod identity is
AWS-only, and links the per-provider settings on learn.uug.ai.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:06:49 +01:00
KilianandClaude Opus 5.5 9c1e37aba4 docs(hub): grouped device alerts as of hub-monitor-device v1.4.10 (0.168.3) (#214)
- custom-layout device_group.html: use the status-neutral sentence the
  built-in template got in hub-pipeline-notification#150.
- values.yaml / README: the grouping hold is 20 s
  (DEVICE_GROUPING_HOLD_SECONDS, monitor v1.4.10), no longer one scan.
- README: device {{text}} per change and the new grouped sections
  (monitor v1.4.9).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:44:05 +01:00
KilianandClaude Opus 5.5 b186ce94ba hub: pass allowUnlockedBucket through to hub-audit s3 destinations (#213)
The audit ConfigMap copies an allowlist of s3 fields, so the new
hub-audit allowUnlockedBucket option (uug-ai/hub-audit#16) was dropped.
Chart 0.168.2.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:32:29 +01:00
Cédric Verstraeten 584ea33ce6 Merge pull request #212 from kerberos-io/fix/hub-publish-0.168.1-20261030
Publish hub chart 0.168.1 (passkey flag, S3 Object Lock audit)
2026-10-30 13:55:18 +01:00
Cedric VerstraetenandClaude Opus 5.5 4442c9c201 fix(hub): publish 0.168.1 with passkey flag and S3 Object Lock audit
#211, #210 and #209 all bumped the chart to 0.168.0. The release was
cut from #211, so chart-releaser skipped the later merges and the
passkey feature flag (#210) and S3 Object Lock audit destinations (#209)
were never published. Bump to 0.168.1 to release them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:40:12 +00:00
KilianandClaude Opus 5.5 c1e15368c7 feat(hub): S3 Object Lock audit destinations (0.168.0) (#209)
Render kerberoshub.audit.destinations per type: webhook as before, or s3
with bucket, region, prefix, endpoint and forcePathStyle. An optional
credentialsSecret mounts the access key files hub-audit reads; without it
the pod's AWS identity is used. kerberoshub.audit.serviceAccount adds an
optional ServiceAccount for hub-audit alone, so IRSA access to the audit
bucket isn't granted to every Hub workload. Invalid combinations fail at
render time. Webhook-only output is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:28 +01:00
Cédric Verstraeten 8d86eefba5 Merge pull request #210 from kerberos-io/feat/hub-passkeys-feature-flag-20261030
Set FEATURE_PASSKEYS_ENABLED from passkeys.enabled (0.168.0)
2026-10-30 13:22:48 +01:00
KilianandClaude Opus 5.5 78d83e368f feat(hub): frontend flag for the media detail Analysis block (0.168.0) (#211)
kerberoshub.frontend.features.media.detail.analysis.enabled sets
FEATURE_MEDIA_DETAIL_ANALYSIS_ENABLED on hub-frontend and
hub-frontend-demo. Defaults to "true", matching the frontend default.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:21:02 +01:00
Cedric VerstraetenandClaude Opus 5.5 e899f070f8 feat(hub): set FEATURE_PASSKEYS_ENABLED from passkeys.enabled (0.168.0)
Hub API and frontend now gate passkeys behind FEATURE_PASSKEYS_ENABLED.
kerberoshub.api.passkeys.enabled sets it to "true" on Hub API alongside
the passkey configuration, and passes the same value to the frontend and
demo frontend, so one switch turns passkeys on or off everywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:01:05 +00:00
Cédric Verstraeten 4a643aa5c2 Merge pull request #203 from kerberos-io/feat/media-forward-flag
feat(hub): frontend flag for the Forward media action (0.167.0)
2026-10-28 19:36:04 +01:00
Cédric Verstraeten fdca7b1196 Merge branch 'main' into feat/media-forward-flag 2026-10-28 19:35:47 +01:00
Cédric VerstraetenandCopilot 5a130b7301 Merge pull request #208 from kerberos-io/fix/admin-runtime-release
Publish Hub chart 0.167.3 for the hardened Admin deployment rollout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-28 19:30:32 +01:00
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
Kilian BouteandClaude Opus 5.5 e228fd36fb feat(hub): frontend flag for the Forward media action (0.167.0)
Add kerberoshub.frontend.features.media.forward.enabled (default "true"),
rendered as FEATURE_MEDIA_FORWARD_ENABLED on hub-frontend and the demo
frontend. Setting it to "false" hides the bulk Forward media action in
the Recordings Actions menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:52:02 +00:00
30 changed files with 1332 additions and 73 deletions
@@ -0,0 +1,28 @@
name: HTTP port migration
on:
pull_request:
workflow_dispatch:
jobs:
migration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v4.3.1
- uses: helm/kind-action@v1
- name: Validate migration against Kubernetes
env:
SERVER_VALIDATION: "true"
run: bash scripts/check-http-port-migration.sh
- name: Validate chart runtime compatibility
run: |
helm lint charts/hub
bash scripts/check-admin-runtime.sh
bash scripts/check-hardened-workers.sh
bash scripts/check-frontend-runtime.sh
bash scripts/check-hub-api-runtime.sh
- name: Smoke test hardened frontend images and Service routing
env:
CLUSTER_SMOKE: "true"
run: bash scripts/check-frontend-runtime.sh
+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.170.1
# 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
+305 -15
View File
@@ -31,12 +31,43 @@ Uninstall the Kerberos Hub chart
helm uninstall hub -n kerberos-hub
### Hardened Hub API
Starting with chart **`0.170.1`**, `kerberoshub.api.securityContext` is rendered
on the Hub API container and `kerberoshub.api.podSecurityContext` on its pod.
The current image runs as `65532:65532` with a read-only root filesystem,
privilege escalation disabled, all capabilities dropped and `RuntimeDefault`
seccomp. A disk-backed `hub-api-tmp` emptyDir mounted at `/tmp`, with pod
`fsGroup: 65532`, keeps attachment uploads and multipart parsing writable.
API Service, ingress and port settings are unchanged.
Append custom mounts and volumes to the default lists rather than replacing
`hub-api-tmp`. Helm replaces lists in overrides. Existing MongoDB TLS, API TLS,
catalog and workflow-configuration mounts remain supported.
For a legacy image that requires its original identity or writable root
filesystem, explicitly set both `kerberoshub.api.securityContext: null` and
`kerberoshub.api.podSecurityContext: null`. Custom security settings are
per-API; other workloads do not inherit them. When upgrading, check retained
values (`--reuse-values` or GitOps overrides) so the security context and
writable scratch mount stay aligned.
Run `bash scripts/check-hub-api-runtime.sh` to validate defaults, overrides,
legacy opt-out, TLS/configuration mounts and deployment modes.
### 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`.
Starting with chart **`0.170.0`**, both frontends default to shell-free
`ghcr.io/uug-ai/hub-frontend:v1.19.28`, running as `65532:65532` on port `8080`
with a read-only root filesystem and writable `/tmp`. The Service and ingress
remain on port `80`. Admin retains its legacy image and port `80`; its hardened
runtime remains opt-in.
The following frontend runtime settings are now the defaults. When upgrading,
remove stale port `80` overrides and coordinate image/runtime values; Helm
`--reuse-values` and GitOps overrides may retain the old settings.
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 +97,243 @@ 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.
For legacy frontend images or a rollback, explicitly restore **all** runtime
settings together (including removing inherited security defaults):
```yaml
kerberoshub:
frontend:
tag: v1.13.9
containerPort: 80
securityContext: null
volumes: []
volumeMounts: []
```
Keep any application-specific branding mounts when adjusting these lists.
Chart upgrades do not rewrite separately managed NodePort Services: ensure they
target the named `http` port, not numeric `80`. Follow the duplicate-port recovery
instructions below if the existing Deployment cannot be patched to `8080`.
Run `bash scripts/check-frontend-runtime.sh` for default, demo, legacy override,
and deployment-values regression coverage.
### 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.
### Recovering a duplicate HTTP port during upgrades
Chart `0.169.1` publishes this recovery guidance. The helper is available in the
repository's `scripts/` directory, not in the chart archive. This release does
not automatically migrate live container ports or change runtime defaults.
An upgrade changing a container's HTTP port from `80` to `8080` can fail with
`spec.template.spec.containers[0].ports[1].name: Duplicate value: "http"`.
Kubernetes strategic merges identify container ports by number, not name:
depending on the live state and apply mechanism, the old `80` entry can survive
beside the new `8080` entry. Fresh chart renders contain only one HTTP port.
This is not a reason to rename `http`: Services and probes reference that name.
Inspect the failing Deployment and confirm the image's listening port first:
```sh
kubectl -n kerberos-hub get deployment hub-frontend -o yaml
```
In a checkout of this repository, use the targeted migration helper to replace
the named HTTP entry (not strategically merge it). It supports `admin`,
`hub-frontend` and `hub-frontend-demo`, preserves unrelated ports and containers,
and rejects unfamiliar HTTP configurations. It requires `kubectl` and `jq` and
uses the current kubeconfig context. The default is a **server dry run**:
```sh
bash scripts/migrate-http-port.sh kerberos-hub hub-frontend 8080
# Apply only after inspecting the dry run and confirming the target/context.
bash scripts/migrate-http-port.sh kerberos-hub hub-frontend 8080 --apply
```
Run this only for affected Deployments, using the port selected in your upgrade
values. Then retry the Helm upgrade or GitOps sync with the coordinated image,
port, security context and scratch-volume settings above. Pause GitOps auto-sync
during manual recovery so it cannot race or undo the patch, and resume it after
the desired state has synced. The helper uses resource-version/name tests to
reject concurrent changes; re-inspect and retry if those tests fail.
Changing the pod template triggers a rollout, and setting `8080` while the old
image still listens on `80` can temporarily interrupt Service traffic. Schedule
the recovery in a maintenance window and complete the matching image upgrade
immediately. The helper does not change images, mount scratch volumes, or repair
unrelated readiness failures. For a legacy-image rollback use port `80` instead,
restore the matching runtime settings, and retry the rollback. Do not use blanket
`helm upgrade --force` or resource replacement for unrelated workloads.
Run `bash scripts/check-http-port-migration.sh` to reproduce the strategic-merge
failure and test targeted replacement, idempotency, and reverse migration for all
three Deployments. CI also checks API rejection of duplicate names and acceptance
of the repaired spec against a disposable Kind cluster.
### 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
@@ -94,7 +360,7 @@ kerberoshub:
| Value | Default | Purpose |
| --- | --- | --- |
| `kerberoshub.api.passkeys.enabled` | `false` | Inject passkey configuration into Hub API pods only |
| `kerberoshub.api.passkeys.enabled` | `false` | Turns passkeys on: sets `FEATURE_PASSKEYS_ENABLED` on Hub API and the frontend, and injects the passkey configuration into Hub API pods |
| `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` |
@@ -112,7 +378,14 @@ HTTPS and a compatible browser are required. Local development may use
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.
Since chart `0.168.1`, Hub API and frontend images that include the passkey
feature flag also require `FEATURE_PASSKEYS_ENABLED=true`. `enabled` sets it on
both, so the API answers `501 FEATURE_NOT_SUPPORTED` and the frontend hides
passkeys when it is `false`. If you configure passkeys through
`kerberoshub.extraEnv` instead, add `FEATURE_PASSKEYS_ENABLED=true` there and in
the frontend environment too.
Apply values with a Helm upgrade or GitOps sync; changes trigger an API and frontend 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
@@ -441,6 +714,7 @@ kerberospipeline:
| `kerberoshub.api.mfaIssuer` | When enabling the MFA access, this is the name that will be shown in the MFA app. | `"yourdomain.com"` |
| `kerberoshub.api.apiKey` | API key for `kerberoshub.api`. | `"a-random-admin-api-key"` |
| `kerberoshub.api.defaultTaskRetentionDays` | Default retention (in days) applied to tasks without an explicit `retention_days`. New tasks are stamped with this value. Set to `"0"` or a negative value to keep tasks indefinitely. Must match `kerberoshub.cleanup.defaultTaskRetentionDays`. | `"0"` |
| `kerberoshub.api.auditExportMaxEventsPerFile` | Most audit events one export file holds (`AUDIT_EXPORT_MAX_EVENTS_PER_FILE`). It does not refuse larger exports: the Audit page downloads them as several files of at most this size. An integer from 1 to 1000000; empty uses the API default of 100000. | `""` |
| `kerberoshub.api.tls` | Bring your own TLS certificates for Kerberos Hub API ingress. | `<list>` |
| `kerberoshub.api.tls.secretName` | Kubernetes Secret name used by `kerberoshub.api.tls`. | `""` |
| `kerberoshub.api.language` | The language of Kerberos Hub API responses, error messages will be communicated in the specified language. | `"english"` |
@@ -550,6 +824,8 @@ kerberospipeline:
| `kerberoshub.frontend.features.devices.analyticsEnabled` | Enable or disable the Analytics tab on device detail pages. | `"true"` |
| `kerberoshub.frontend.features.devices.developerToolsEnabled` | Enable or disable owner-only live Agent logs and terminal access on device detail pages. | `"true"` |
| `kerberoshub.frontend.features.dashboard.sitesFullscreenEnabled` | Enable or disable the fullscreen Sites map on the Dashboard. | `"true"` |
| `kerberoshub.frontend.features.media.forward.enabled` | Enable or disable the bulk Forward media action in the Recordings Actions menu. | `"true"` |
| `kerberoshub.frontend.features.media.detail.analysis.enabled` | Enable or disable the Analysis block (dominant colors, thumbnail, sprite, motion) on the media detail page. | `"true"` |
| `kerberoshub.frontend.features.media.filter.date.enabled` | Enable or disable `kerberoshub.frontend.features.media.filter.date`. | `"true"` |
| `kerberoshub.frontend.features.media.filter.sites.enabled` | Enable or disable `kerberoshub.frontend.features.media.filter.sites`. | `"true"` |
| `kerberoshub.frontend.features.media.filter.groups.enabled` | Enable or disable `kerberoshub.frontend.features.media.filter.groups`. | `"true"` |
@@ -620,12 +896,16 @@ kerberospipeline:
| `kerberoshub.cleanup.resources.requests.cpu` | CPU request for `kerberoshub.cleanup`. | `"10m"` |
| `kerberoshub.audit.repository` | Hub audit service container image repository. | `"ghcr.io/uug-ai/hub-audit"` |
| `kerberoshub.audit.pullPolicy` | Hub audit service image pull policy. | `"IfNotPresent"` |
| `kerberoshub.audit.tag` | Hub audit service image tag. | `"v1.0.1"` |
| `kerberoshub.audit.tag` | Hub audit service image tag. S3 destinations need v1.0.9 or later, and `s3.allowUnlockedBucket` needs v1.0.10. | `"v1.0.10"` |
| `kerberoshub.audit.replicas` | Number of audit service replicas. Per-destination MongoDB leases prevent concurrent delivery. | `1` |
| `kerberoshub.audit.dispatchInterval` | How often the mounted destination configuration is reloaded and eligible destinations are polled. | `"5s"` |
| `kerberoshub.audit.leaseDuration` | Per-destination lease duration. It must exceed every destination timeout by at least 30 seconds. | `"2m"` |
| `kerberoshub.audit.terminationGracePeriodSeconds` | Pod termination grace period. Keep this longer than `leaseDuration` so an in-flight cycle can finish. | `150` |
| `kerberoshub.audit.destinations` | Webhook destinations. Each entry supports `id`, `enabled`, `url`, delivery limits, filters, public `headers`, `bearerTokenSecret`, and arbitrary `secretHeaders`. Destination IDs retain independent checkpoints. | `[]` |
| `kerberoshub.audit.destinations` | Audit destinations. Every entry supports `id`, `type` (`webhook` by default, or `s3`), `enabled`, delivery limits and filters. Webhooks add `url`, public `headers`, `bearerTokenSecret` and arbitrary `secretHeaders`. S3 destinations add `s3.bucket`, `s3.region`, optional `s3.prefix`, `s3.endpoint` and `s3.forcePathStyle` for S3-compatible stores, and an optional `credentialsSecret` (`name`, `accessKeyIdKey`, `secretAccessKeyKey`); without it the pod's AWS identity is used. The bucket must have Object Lock with a `COMPLIANCE` default retention unless `s3.allowUnlockedBucket` is `true` (stores without Object Lock such as Garage; the archive is then deletable). Every field, the delivery semantics and per-provider S3 settings are documented at https://learn.uug.ai/docs/hub/audit-events/#export-audit-events. Destination IDs retain independent checkpoints. | `[]` |
| `kerberoshub.audit.serviceAccount.create` | Create a ServiceAccount used only by hub-audit, for example for S3 access through IRSA. | `false` |
| `kerberoshub.audit.serviceAccount.name` | Name of the hub-audit ServiceAccount; with `create: false`, an existing ServiceAccount to use. Defaults to `hub-audit` when created; when empty and not created, `kerberoshub.serviceAccount` applies. | `""` |
| `kerberoshub.audit.serviceAccount.annotations` | Annotations for the hub-audit ServiceAccount, such as `eks.amazonaws.com/role-arn`. | `{}` |
| `kerberoshub.audit.serviceAccount.labels` | Labels for the hub-audit ServiceAccount. | `{}` |
| `kerberoshub.audit.serviceMonitor.enabled` | Create a Prometheus `ServiceMonitor` for audit service metrics. | `true` |
| `kerberoshub.audit.serviceMonitor.interval` | Audit service metrics scrape interval. | `"15s"` |
| `kerberoshub.audit.volumes` | Additional pod volumes, for example a private webhook CA bundle. | `[]` |
@@ -636,7 +916,7 @@ kerberospipeline:
| `kerberoshub.monitordevice.tag` | The Docker image tag/version. | `"v1.4.0"` |
| `kerberoshub.monitordevice.replicas` | Number of replicas for `kerberoshub.monitordevice`. | `1` |
| `kerberoshub.monitordevice.logLevel` | Log verbosity level for `kerberoshub.monitordevice`. | `"info"` |
| `kerberoshub.monitordevice.scanIntervalSeconds` | Seconds between device status scans. Sets how quickly a camera going offline or online is detected and, for accounts with "Group notifications" on, how long notifications are held to collect other cameras that change status at the same time. | `60` |
| `kerberoshub.monitordevice.scanIntervalSeconds` | Seconds between device status scans. Sets how quickly a camera going offline or online is detected. For accounts with "Group notifications" on, `hub-monitor-device` v1.4.10 and later then hold a change 20 seconds (`DEVICE_GROUPING_HOLD_SECONDS`) to collect other cameras that change status at the same time; older releases hold it one scan. | `60` |
| `kerberoshub.monitordevice.resources.requests.memory` | Memory request for `kerberoshub.monitordevice`. | `"10Mi"` |
| `kerberoshub.monitordevice.resources.requests.cpu` | CPU request for `kerberoshub.monitordevice`. | `"10m"` |
| `kerberoshub.reactivate.repository` | Container image repository for `kerberoshub.reactivate`. | `"uugai/hub-reactivatesubscriptions"` |
@@ -838,22 +1118,32 @@ Sent by `hub-monitor-device` when a camera goes online, offline or idle. The sub
|---|---|
| `{{user}}` | Username of the account the camera belongs to. |
| `{{title}}` | The email subject. |
| `{{text}}` | The status change, for example "`<camera>` is online, but the camera stream is not connected." |
| `{{text}}` | The change, worded from the previous status (see below). |
| `{{status}}` | `active` (online), `inactive` (offline) or `idle` (online, camera stream not connected). |
| `{{devicename}}`, `{{deviceid}}` | Camera name and camera key. |
| `{{eventdate}}`, `{{eventtime}}`, `{{eventdatetime}}` | When the status changed, in the server's timezone (normally UTC), not the account's. |
| `{{link}}` | Not set; replaced with an empty value. |
`{{text}}` per change (webhooks and IFTTT get the same text):
| Change | `{{text}}` |
|---|---|
| Goes offline | "`<camera>` is offline, it hasn't responded for 5 minutes." |
| Loses its camera stream while online | "`<camera>` is online, but the camera stream is not connected." |
| Comes back from offline without its camera stream | "`<camera>` is back online, but the camera stream is not connected." |
| Gets its camera stream back | "`<camera>`'s camera stream is connected again." |
| Comes back from offline | "`<camera>` is back online." |
#### Grouped camera status: `device_group`
Sent by `hub-monitor-device` instead of `device` when an account has **Group notifications** switched on (Alerts → Devices, stored as `notificationSettings.devices.groupNotifications`) and **several** of its cameras change status in the same scan. A change is held for one scan (`kerberoshub.monitordevice.scanIntervalSeconds`, default 60 seconds) so that cameras of the same outage arrive in one email. When only one camera changed, the usual `device` email is sent. The subject is `email.templates.deviceGroupTitle`; the template name is `email.templates.deviceGroup` (default `device_group`, so custom files are `device_group.html` and `device_group.txt`).
Sent by `hub-monitor-device` instead of `device` when an account has **Group notifications** switched on (Alerts → Devices, stored as `notificationSettings.devices.groupNotifications`) and **several** of its cameras change status at the same time. Changes are detected on every scan (`kerberoshub.monitordevice.scanIntervalSeconds`, default 60 seconds); a change is then held 20 seconds and the account checked again, so that cameras of the same outage arrive in one email. The hold is the monitor's `DEVICE_GROUPING_HOLD_SECONDS` (default `20`; `0`, or at least the scan interval, holds until the next scan, which is also what releases before v1.4.10 do). When only one camera changed, the usual `device` email is sent. The subject is `email.templates.deviceGroupTitle`; the template name is `email.templates.deviceGroup` (default `device_group`, so custom files are `device_group.html` and `device_group.txt`).
| Placeholder | Value |
|---|---|
| `{{user}}` | Name of the account the cameras belong to: first and last name, or the username when both are empty. |
| `{{title}}` | The email subject. |
| `{{text}}` | One-line summary, for example "6 cameras changed status: 4 offline, 1 lost their camera stream, 1 back online." |
| `{{text}}` | One-line summary, for example "6 cameras changed status: 4 offline, 1 lost their camera stream, 1 back online." The other changes read "back online without camera stream" and "camera stream reconnected". |
| `{{count}}` | Number of cameras in the email. |
| `{{status}}` | `active`, `inactive` or `idle` when all cameras have the same new status, otherwise `mixed`. |
| `{{sections}}` | The cameras as HTML, one section per status and site, group or "Other cameras" (see below). |
@@ -861,7 +1151,7 @@ Sent by `hub-monitor-device` instead of `device` when an account has **Group not
| `{{link}}` | The cameras page, `<tenant URL>/devices`, built from `kerberoshub.frontend.schema` and `kerberoshub.frontend.url` (see *Links* above). |
| `{{eventdate}}`, `{{eventtime}}`, `{{eventdatetime}}` | When the email was sent, in the server's timezone (normally UTC). |
**Sections.** Sections are ordered by status (offline, camera stream lost, back online), then sites, groups and "Other cameras". Each camera is listed once: under the site or group that holds the most cameras of the email with the same status (a site wins a tie), or under "Other cameras" when it is in no site or group. A section title reads `Site Gate · Offline · all 3 cameras`, `Group Perimeter · Back online · 2 cameras` or `Other cameras · Camera stream lost · 1 camera`; "all" is added when every camera of the site or group is in the section. Each camera line has its name and a detail: `no response for 12 min`, `agent online, camera stream not connected` or `online`.
**Sections.** There is one section per change and per site, group or "Other cameras". Sections are ordered by change (offline, camera stream lost, back online without camera stream, camera stream back, back online), then sites, groups and "Other cameras". Each camera is listed once: under the site or group that holds the most cameras of the email with the same change (a site wins a tie), or under "Other cameras" when it is in no site or group. A section title reads `Site Gate · Offline · all 3 cameras`, `Group Perimeter · Back online without camera stream · 2 cameras` or `Other cameras · Camera stream back · 1 camera`; the change labels are `Offline`, `Camera stream lost`, `Back online without camera stream`, `Camera stream back` and `Back online`, and "all" is added when a section of two or more cameras holds every camera of the site or group. Each camera line has its name and a detail: `no response for 12 min`, `agent online, camera stream not connected`, `agent back online, camera stream not connected`, `camera stream connected` or `online`. A camera that changes again during the hold keeps the status from before its first change: one that went offline and came back reads `back online after a brief interruption`, the other reversals `offline again after briefly coming back, no response for 4 min` and `camera stream lost again after briefly reconnecting`.
`{{sections}}` is generated HTML with inline styles (email clients ignore stylesheets): per section a `<p>` with the title, followed by a `<table role="presentation" width="100%">` with one row per camera (name left, detail right). Place it where the camera list should appear; its look can't be changed from the template. For full control over the layout, use `{{sectionsText}}`. Camera, site and group names are HTML-escaped.
@@ -276,7 +276,7 @@
letter-spacing: 0em;
text-align: left;
color: #6D6666;
margin-top: 12px;">Cameras that go offline together usually share a cause, such as a site losing its internet connection or power. Have a look into your Kerberos Hub account for the latest recordings and the status of your cameras.</p>
margin-top: 12px;">Cameras that change status together usually share a cause, such as a site losing or regaining its internet connection or power. Have a look into your Kerberos Hub account for the latest recordings and the status of your cameras.</p>
<a style="text-decoration: none;color: none;" href="{{link}}">
<p style="font-family: Inter;
+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
@@ -10,33 +10,60 @@
{{- fail (printf "duplicate audit destination id %q" $id) -}}
{{- end -}}
{{- $_ := set $destinationIDs $id true -}}
{{- $url := required (printf "url is required for audit destination %s" $id) $destination.url -}}
{{- $type := default "webhook" $destination.type -}}
{{- $entry := dict
"id" $id
"type" (default "webhook" $destination.type)
"type" $type
"enabled" $destination.enabled
"url" $url
"timeoutSeconds" (default 10 $destination.timeoutSeconds)
"batchSize" (default 100 $destination.batchSize)
"maxAttempts" (default 10 $destination.maxAttempts)
"startAt" (default "latest" $destination.startAt)
"headers" (default dict $destination.headers)
"filters" (default dict $destination.filters) -}}
{{- $secretRoot := printf "/var/run/secrets/audit-destinations/%s" $id -}}
{{- $bearer := default dict $destination.bearerTokenSecret -}}
{{- if gt (len $bearer) 0 -}}
{{- $secretName := required (printf "bearerTokenSecret.name is required for destination %s" $id) $bearer.name -}}
{{- $secretKey := default "token" $bearer.key -}}
{{- $_ := set $entry "bearerTokenFile" (printf "%s/bearer-%s" $secretRoot (sha256sum (printf "%s:%s" $secretName $secretKey) | trunc 12)) -}}
{{- end -}}
{{- $headerFiles := dict -}}
{{- range $headerName, $secret := (default dict $destination.secretHeaders) -}}
{{- $secretName := required (printf "secretName is required for destination %s header %s" $id $headerName) $secret.secretName -}}
{{- $secretKey := required (printf "secretKey is required for destination %s header %s" $id $headerName) $secret.secretKey -}}
{{- $_ := set $headerFiles $headerName (printf "%s/header-%s" $secretRoot (sha256sum (printf "%s:%s:%s" $headerName $secretName $secretKey) | trunc 12)) -}}
{{- end -}}
{{- if gt (len $headerFiles) 0 -}}
{{- $_ := set $entry "headerFiles" $headerFiles -}}
{{- if eq $type "webhook" -}}
{{- if or $destination.s3 $destination.credentialsSecret -}}
{{- fail (printf "audit destination %s is a webhook and cannot set s3 or credentialsSecret" $id) -}}
{{- end -}}
{{- $_ := set $entry "url" (required (printf "url is required for audit destination %s" $id) $destination.url) -}}
{{- $_ := set $entry "headers" (default dict $destination.headers) -}}
{{- $bearer := default dict $destination.bearerTokenSecret -}}
{{- if gt (len $bearer) 0 -}}
{{- $secretName := required (printf "bearerTokenSecret.name is required for destination %s" $id) $bearer.name -}}
{{- $secretKey := default "token" $bearer.key -}}
{{- $_ := set $entry "bearerTokenFile" (printf "%s/bearer-%s" $secretRoot (sha256sum (printf "%s:%s" $secretName $secretKey) | trunc 12)) -}}
{{- end -}}
{{- $headerFiles := dict -}}
{{- range $headerName, $secret := (default dict $destination.secretHeaders) -}}
{{- $secretName := required (printf "secretName is required for destination %s header %s" $id $headerName) $secret.secretName -}}
{{- $secretKey := required (printf "secretKey is required for destination %s header %s" $id $headerName) $secret.secretKey -}}
{{- $_ := set $headerFiles $headerName (printf "%s/header-%s" $secretRoot (sha256sum (printf "%s:%s:%s" $headerName $secretName $secretKey) | trunc 12)) -}}
{{- end -}}
{{- if gt (len $headerFiles) 0 -}}
{{- $_ := set $entry "headerFiles" $headerFiles -}}
{{- end -}}
{{- else if eq $type "s3" -}}
{{- if or $destination.url $destination.headers $destination.bearerTokenSecret $destination.secretHeaders -}}
{{- fail (printf "audit destination %s is s3 and cannot set url, headers, bearerTokenSecret or secretHeaders" $id) -}}
{{- end -}}
{{- $s3 := required (printf "s3 is required for audit destination %s" $id) $destination.s3 -}}
{{- $settings := dict
"bucket" (required (printf "s3.bucket is required for audit destination %s" $id) $s3.bucket)
"region" (required (printf "s3.region is required for audit destination %s" $id) $s3.region) -}}
{{- range $key := list "prefix" "endpoint" "forcePathStyle" "allowUnlockedBucket" -}}
{{- if hasKey $s3 $key -}}
{{- $_ := set $settings $key (get $s3 $key) -}}
{{- end -}}
{{- end -}}
{{- $credentials := default dict $destination.credentialsSecret -}}
{{- if gt (len $credentials) 0 -}}
{{- $secretName := required (printf "credentialsSecret.name is required for destination %s" $id) $credentials.name -}}
{{- $_ := set $settings "accessKeyIdFile" (printf "%s/access-key-id-%s" $secretRoot (sha256sum (printf "%s:%s" $secretName (default "access-key-id" $credentials.accessKeyIdKey)) | trunc 12)) -}}
{{- $_ := set $settings "secretAccessKeyFile" (printf "%s/secret-access-key-%s" $secretRoot (sha256sum (printf "%s:%s" $secretName (default "secret-access-key" $credentials.secretAccessKeyKey)) | trunc 12)) -}}
{{- end -}}
{{- $_ := set $entry "s3" $settings -}}
{{- else -}}
{{- fail (printf "audit destination %s has unsupported type %q (webhook or s3)" $id $type) -}}
{{- end -}}
{{- $destinations = append $destinations $entry -}}
{{- end -}}
@@ -140,6 +140,10 @@ spec:
labels:
app: hub-api
spec:
{{- with .Values.kerberoshub.api.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 }}
@@ -229,6 +233,10 @@ spec:
- name: hub-api
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.api.repository }}:{{ .Values.kerberoshub.api.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.api.pullPolicy }}
{{- with .Values.kerberoshub.api.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.kerberoshub.api.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -437,6 +445,8 @@ spec:
value: "{{ .Values.kerberoshub.api.mfaIssuer }}"
{{- if .Values.kerberoshub.api.passkeys.enabled }}
- name: FEATURE_PASSKEYS_ENABLED
value: "true"
- 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
@@ -551,6 +561,10 @@ spec:
value: "{{ .Values.kerberoshub.api.defaultTaskRetentionDays }}"
- name: CASES_MAX_RETENTION_DAYS
value: "{{ .Values.kerberoshub.api.casesMaxRetentionDays }}"
{{- with .Values.kerberoshub.api.auditExportMaxEventsPerFile }}
- name: AUDIT_EXPORT_MAX_EVENTS_PER_FILE
value: {{ . | int64 | toString | quote }}
{{- end }}
# SMTP
- name: SMTP_SERVER
@@ -0,0 +1,16 @@
{{- if and .Values.audit.enabled (default dict .Values.kerberoshub.audit.serviceAccount).create }}
{{- $serviceAccount := .Values.kerberoshub.audit.serviceAccount }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ default "hub-audit" $serviceAccount.name }}
namespace: {{ .Release.Namespace }}
{{- with $serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $serviceAccount.labels }}
labels:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -26,7 +26,12 @@ spec:
app: hub-audit
spec:
terminationGracePeriodSeconds: {{ .Values.kerberoshub.audit.terminationGracePeriodSeconds }}
{{- if .Values.kerberoshub.serviceAccount.create }}
{{- $auditServiceAccount := default dict .Values.kerberoshub.audit.serviceAccount }}
{{- if $auditServiceAccount.create }}
serviceAccountName: {{ default "hub-audit" $auditServiceAccount.name }}
{{- else if $auditServiceAccount.name }}
serviceAccountName: {{ $auditServiceAccount.name }}
{{- else if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
{{- with .Values.imagePullSecrets }}
@@ -41,10 +46,22 @@ spec:
{{- range $index, $destination := .Values.kerberoshub.audit.destinations }}
{{- $bearer := default dict $destination.bearerTokenSecret }}
{{- $secretHeaders := default dict $destination.secretHeaders }}
{{- if or (gt (len $bearer) 0) (gt (len $secretHeaders) 0) }}
{{- $credentials := default dict $destination.credentialsSecret }}
{{- if or (gt (len $bearer) 0) (gt (len $secretHeaders) 0) (gt (len $credentials) 0) }}
- name: audit-dest-{{ sha256sum $destination.id | trunc 12 }}
projected:
sources:
{{- if gt (len $credentials) 0 }}
{{- $accessKeyIdKey := default "access-key-id" $credentials.accessKeyIdKey }}
{{- $secretAccessKeyKey := default "secret-access-key" $credentials.secretAccessKeyKey }}
- secret:
name: {{ required (printf "credentialsSecret.name is required for destination %s" $destination.id) $credentials.name | quote }}
items:
- key: {{ $accessKeyIdKey | quote }}
path: access-key-id-{{ sha256sum (printf "%s:%s" $credentials.name $accessKeyIdKey) | trunc 12 }}
- key: {{ $secretAccessKeyKey | quote }}
path: secret-access-key-{{ sha256sum (printf "%s:%s" $credentials.name $secretAccessKeyKey) | trunc 12 }}
{{- end }}
{{- if gt (len $bearer) 0 }}
- secret:
name: {{ required (printf "bearerTokenSecret.name is required for destination %s" $destination.id) $bearer.name | quote }}
@@ -98,7 +115,8 @@ spec:
{{- range $index, $destination := .Values.kerberoshub.audit.destinations }}
{{- $bearer := default dict $destination.bearerTokenSecret }}
{{- $secretHeaders := default dict $destination.secretHeaders }}
{{- if or (gt (len $bearer) 0) (gt (len $secretHeaders) 0) }}
{{- $credentials := default dict $destination.credentialsSecret }}
{{- if or (gt (len $bearer) 0) (gt (len $secretHeaders) 0) (gt (len $credentials) 0) }}
- name: audit-dest-{{ sha256sum $destination.id | trunc 12 }}
mountPath: /var/run/secrets/audit-destinations/{{ $destination.id }}
readOnly: true
@@ -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 8080 }}
name: http
{{- with .Values.kerberoshub.frontend.volumeMounts}}
volumeMounts:
@@ -237,6 +241,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.workflows.enabled }}"
- name: FEATURE_AUDIT_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.audit.enabled }}"
- name: FEATURE_PASSKEYS_ENABLED
value: "{{ .Values.kerberoshub.api.passkeys.enabled }}"
- name: FEATURE_DAY_OVERVIEW_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.dayOverview.enabled }}"
- name: FEATURE_DEVICE_ANALYTICS_ENABLED
@@ -361,6 +367,10 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.faceRedaction.classifierTracksEnabled }}"
# features > media
- name: FEATURE_MEDIA_FORWARD_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.media.forward.enabled }}"
- name: FEATURE_MEDIA_DETAIL_ANALYSIS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.media.detail.analysis.enabled }}"
- name: FEATURE_MEDIA_FILTER_DATE_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.media.filter.date.enabled }}"
- name: FEATURE_MEDIA_FILTER_SITES_ENABLED
@@ -191,7 +191,7 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- containerPort: {{ .Values.kerberoshub.frontend.containerPort | default 80 }}
- containerPort: {{ .Values.kerberoshub.frontend.containerPort | default 8080 }}
name: http
{{- with .Values.kerberoshub.frontend.volumeMounts}}
volumeMounts:
@@ -320,6 +320,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.workflows.enabled }}"
- name: FEATURE_AUDIT_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.audit.enabled }}"
- name: FEATURE_PASSKEYS_ENABLED
value: "{{ .Values.kerberoshub.api.passkeys.enabled }}"
- name: FEATURE_DAY_OVERVIEW_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.dayOverview.enabled }}"
- name: FEATURE_DEVICE_ANALYTICS_ENABLED
@@ -457,6 +459,10 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.faceRedaction.classifierTracksEnabled }}"
# features > media
- name: FEATURE_MEDIA_FORWARD_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.media.forward.enabled }}"
- name: FEATURE_MEDIA_DETAIL_ANALYSIS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.media.detail.analysis.enabled }}"
- name: FEATURE_MEDIA_FILTER_DATE_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.media.filter.date.enabled }}"
- name: FEATURE_MEDIA_FILTER_SITES_ENABLED
@@ -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 }}
+7
View File
@@ -100,6 +100,13 @@
"type": "string",
"pattern": "^https?://[^/?#]+$"
}
},
"auditExportMaxEventsPerFile": {
"type": ["integer", "string"],
"minimum": 1,
"maximum": 1000000,
"pattern": "^([1-9][0-9]{0,5}|1000000)?$",
"description": "Most audit events one export file holds (AUDIT_EXPORT_MAX_EVENTS_PER_FILE). Empty uses the API default of 100000."
}
}
}
+138 -29
View File
@@ -50,7 +50,6 @@ mongodb:
# hub-pipeline-monitor, ...) instead of the one in uri, so the Atlas query
# profiler and currentOp show which service issued an operation.
appNamePerService: true
# Self-hosted mongodb, you could also use the uri property above.
host: mongodb.mongodb
adminDatabase: admin
@@ -73,7 +72,6 @@ mongodb:
caFileName: ""
mountPath: "/etc/mongodb/tls"
insecureSkipVerify: false
# Classifications supported by Hub filters, alerts, markers, and other
# downstream processing. Classifier output keys must match these values.
# Disable this to use the legacy classification catalog stored in MongoDB.
@@ -115,7 +113,6 @@ deviceTags:
# - key: anpr-pilot
# label: ANPR pilot
# description: Runs licence plate recognition on this camera
# Configuration packaging only: consuming contracts requires a compatible
# API/frontend. Enabling this mounts files but does not register stage workers.
workflowConfiguration:
@@ -268,6 +265,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
@@ -334,8 +336,27 @@ kerberoshub:
api:
repository: ghcr.io/uug-ai/hub-api
pullPolicy: IfNotPresent
tag: "v1.9.51"
tag: "v1.11.31"
replicas: 2
podSecurityContext:
fsGroup: 65532
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
# Keep writable scratch space for attachment uploads and multipart parsing.
volumes:
- name: hub-api-tmp
emptyDir: {}
volumeMounts:
- name: hub-api-tmp
mountPath: /tmp
audit:
enabled: true
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
@@ -370,6 +391,7 @@ kerberoshub:
memory: 100Mi
cpu: 250m
# E-mail templates
# Append custom mounts/volumes to the lists above; retain hub-api-tmp.
#volumeMounts:
# - name: custom-email-templates
# mountPath: /mail
@@ -414,6 +436,11 @@ kerberoshub:
# beyond this cap, the value is clamped down to `now + casesMaxRetentionDays`.
# Set to "0" (or a negative value) to disable the cap (no maximum).
casesMaxRetentionDays: "0"
# Most audit events one export file holds (AUDIT_EXPORT_MAX_EVENTS_PER_FILE).
# It does not refuse larger exports: the Audit page downloads them as
# several files of at most this size. An integer from 1 to 1000000; leave
# empty to use the API default of 100000.
auditExportMaxEventsPerFile: ""
## Certificates
tls:
- hosts:
@@ -472,11 +499,28 @@ kerberoshub:
frontend:
repository: ghcr.io/uug-ai/hub-frontend
pullPolicy: IfNotPresent
tag: "v1.13.9"
tag: "v1.19.28"
replicas: 2
# Hardened frontend images listen on 8080; retain 80 for existing images.
containerPort: 80
securityContext: {}
containerPort: 8080
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
# Keep these entries when adding custom branding/translation mounts.
volumes:
- name: frontend-tmp
emptyDir:
medium: Memory
sizeLimit: 16Mi
volumeMounts:
- name: frontend-tmp
mountPath: /tmp
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).
@@ -650,6 +694,13 @@ kerberoshub:
sitesFullscreenEnabled: "true" # Enable or disable the fullscreen Sites map 'true' or 'false'
# Media page
media:
# Bulk "Forward media" action in the Recordings Actions menu (on-demand forwarding of metadata-only recordings).
forward:
enabled: "true" # Enable or disable the Forward media action in media 'true' or 'false'
# Analysis block (dominant colors, thumbnail, sprite, motion) on the media detail page.
detail:
analysis:
enabled: "true" # Enable or disable the Analysis block on the media detail page 'true' or 'false'
filter:
date:
enabled: "true" # Enable or disable date filter in media 'true' or 'false'
@@ -754,9 +805,12 @@ 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"
tag: "v1.4.26"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -804,12 +858,13 @@ kerberoshub:
requests:
memory: 10Mi
cpu: 10m
# Dispatches canonical audit_events to one or more external webhook sinks.
# Dispatches canonical audit_events to one or more destinations: webhook
# sinks, or S3 Object Lock buckets for immutable storage.
# Credentials are always read from existing Secrets and never stored here.
audit:
repository: ghcr.io/uug-ai/hub-audit
pullPolicy: IfNotPresent
tag: "v1.0.1"
tag: "v1.0.10"
replicas: 1
dispatchInterval: "5s"
leaseDuration: "2m"
@@ -818,6 +873,16 @@ kerberoshub:
volumes: []
volumeMounts: []
extraEnv: []
# Optional ServiceAccount for hub-audit alone, for example to grant S3 access
# through IRSA without giving it to every Hub workload. When create is false
# and name is set, that existing ServiceAccount is used; otherwise the pod
# falls back to kerberoshub.serviceAccount.
serviceAccount:
create: false
name: ""
annotations: {}
# eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/hub-audit-archive
labels: {}
serviceMonitor:
enabled: true
interval: 15s
@@ -845,6 +910,35 @@ kerberoshub:
# actions: []
# categories: []
# outcomes: [] # success | failure
# S3 Object Lock destination (hub-audit v1.0.9 or later). The bucket needs
# versioning, Object Lock and a COMPLIANCE default retention; hub-audit
# refuses to deliver otherwise. Each batch is written as
# <prefix>/<id>/yyyy/mm/dd/<firstId>-<lastId>.ndjson.gz. Per-provider
# region/endpoint values (AWS, Wasabi, B2, Ceph RGW, MinIO, Garage):
# https://learn.uug.ai/docs/hub/audit-events/#s3-providers
# - id: immutable-archive
# type: s3
# enabled: true
# timeoutSeconds: 30
# batchSize: 500
# maxAttempts: 100 # keep high: exhausted batches are dead-lettered, not archived
# startAt: beginning
# s3:
# bucket: hub-audit-archive
# region: eu-west-1
# prefix: hub
# endpoint: https://s3.example.com # S3-compatible stores only; omit for AWS S3
# forcePathStyle: true # usually needed for Ceph RGW, MinIO and Garage
# # Only for stores without Object Lock (e.g. Garage): skips the lock
# # check, so archived batches are deletable. Needs hub-audit v1.0.10.
# allowUnlockedBucket: false
# # Omit credentialsSecret to use the pod's AWS identity (IRSA via
# # kerberoshub.audit.serviceAccount, or an instance profile). That only
# # works for AWS S3; every other store needs credentialsSecret.
# credentialsSecret:
# name: audit-immutable-archive
# accessKeyIdKey: access-key-id
# secretAccessKeyKey: secret-access-key
# resources:
# requests:
# memory: 32Mi
@@ -967,6 +1061,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,15 +1171,18 @@ kerberoshub:
# memory: 128Mi
# cpu: 100m
monitordevice:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-monitor-device
pullPolicy: IfNotPresent
tag: "v1.4.2"
tag: "v1.4.10"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
logLevel: "info" # possible values: trace, debug, info, warn, error
# Seconds between device status scans. Sets how quickly a camera going
# offline/online is detected and, with "Group notifications" on, how long
# notifications are held to collect other cameras that change at the same time.
# offline/online is detected. With "Group notifications" on, hub-monitor-device
# v1.4.10+ holds a change 20 seconds (DEVICE_GROUPING_HOLD_SECONDS) to collect
# other cameras that change at the same time; older releases hold one scan.
scanIntervalSeconds: 60
resources:
requests:
@@ -1164,7 +1263,7 @@ kerberospipeline:
# argocd.argoproj.io/sync-wave: "10"
repository: ghcr.io/uug-ai/hub-pipeline-event
pullPolicy: IfNotPresent
tag: "v1.3.1"
tag: "v1.3.9"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1185,7 +1284,7 @@ kerberospipeline:
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "70"
pullPolicy: IfNotPresent
tag: "v1.3.13"
tag: "v1.3.29"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1206,7 +1305,7 @@ kerberospipeline:
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "60"
pullPolicy: IfNotPresent
tag: "v1.6.26"
tag: "v1.7.14"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1222,12 +1321,14 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
throttler:
repository: uugai/hub-pipeline-throttler
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-throttler
deploymentAnnotations: {}
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "30"
pullPolicy: IfNotPresent
tag: "v1.2.1"
tag: "v1.2.10"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1243,12 +1344,14 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
notify:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-notification
deploymentAnnotations: {}
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "20"
pullPolicy: IfNotPresent
tag: "v1.3.18"
tag: "v1.3.46"
replicas: 1 # Number of pods for the service.
audit:
enabled: true
@@ -1268,9 +1371,9 @@ kerberospipeline:
# persistentVolumeClaim:
# claimName: custom-layout-claim
notifyTest:
repository: uugai/hub-pipeline-notification-test
repository: ghcr.io/uug-ai/hub-pipeline-notification-test
pullPolicy: IfNotPresent
tag: "v1.2.2"
tag: "v1.2.6"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
resources:
@@ -1291,7 +1394,7 @@ kerberospipeline:
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "40"
pullPolicy: IfNotPresent
tag: "v1.8.22"
tag: "v1.8.29"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1318,6 +1421,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.
@@ -1327,7 +1432,7 @@ kerberospipeline:
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "50"
pullPolicy: IfNotPresent
tag: "v2.0.3"
tag: "v2.0.9"
replicas: 3 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1346,12 +1451,14 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
thumbnail:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-thumbnail
deploymentAnnotations: {}
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "50"
pullPolicy: IfNotPresent
tag: "v1.3.15"
tag: "v1.3.22"
replicas: 2 # Number of pods for the service.
prefetchCount: 0 # Max unacknowledged RabbitMQ messages per pod; 0 = service default (5).
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
@@ -1377,12 +1484,14 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
counting:
repository: uugai/hub-pipeline-counting
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-counting
deploymentAnnotations: {}
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "50"
pullPolicy: IfNotPresent
tag: "v2.0.0"
tag: "v2.0.6"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1404,7 +1513,7 @@ kerberospipeline:
# argocd.argoproj.io/sync-wave: "50"
repository: ghcr.io/uug-ai/hub-pipeline-sprite
pullPolicy: IfNotPresent
tag: "v1.1.21"
tag: "v1.1.27"
replicas: 5 # Number of pods for the service.
prefetchCount: 0 # Max unacknowledged RabbitMQ messages per pod; 0 = service default (5).
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
@@ -1431,7 +1540,7 @@ kerberospipeline:
export:
repository: ghcr.io/uug-ai/hub-pipeline-export
pullPolicy: IfNotPresent
tag: "v1.2.10"
tag: "v1.2.20"
replicas: 2 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
logLevel: "info" # possible values: trace, debug, info, warn, error
+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."
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
values=()
if [[ $# -ge 2 ]]; then values=(--values "$2"); fi
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_contains() {
if ! grep -Eq -- "$2" <<< "$1"; then
printf 'Missing expected frontend setting: %s\n' "$2" >&2
exit 1
fi
}
for mode in all ui; do
rendered="$(helm template hub "$CHART_DIR" "${values[@]}" --set "mode=$mode" \
--set kerberoshub.frontend.demoEnabled=true)"
for name in hub-frontend hub-frontend-demo; do
deployment="$(resource Deployment "$name" <<< "$rendered")"
assert_contains "$deployment" 'image: "ghcr.io/uug-ai/hub-frontend:v1.19.28"$'
assert_contains "$deployment" 'containerPort: 8080$'
[[ "$(grep -c 'containerPort:' <<< "$deployment")" == 1 ]]
for setting in 'name: http' 'runAsNonRoot: true' 'runAsUser: 65532' \
'runAsGroup: 65532' 'readOnlyRootFilesystem: true' \
'allowPrivilegeEscalation: false' 'type: RuntimeDefault' 'drop:' '- ALL' \
'medium: Memory' 'sizeLimit: 16Mi' 'mountPath: /tmp' 'name: frontend-tmp'; do
assert_contains "$deployment" "^[[:space:]]*(- )?$setting$"
done
service="$(resource Service "$name-svc" <<< "$rendered")"
assert_contains "$service" 'port: 80$'
assert_contains "$service" 'targetPort: http$'
echo "OK ($mode): $name hardened image, security, scratch and named routing"
done
admin="$(resource Deployment admin <<< "$rendered")"
assert_contains "$admin" 'containerPort: 80$'
legacy="$(helm template hub "$CHART_DIR" "${values[@]}" --set "mode=$mode" \
--set kerberoshub.frontend.demoEnabled=true \
--set kerberoshub.frontend.tag=v1.13.9 \
--set kerberoshub.frontend.containerPort=80 \
--set kerberoshub.frontend.securityContext=null \
--set-json 'kerberoshub.frontend.volumes=[]' \
--set-json 'kerberoshub.frontend.volumeMounts=[]')"
for name in hub-frontend hub-frontend-demo; do
deployment="$(resource Deployment "$name" <<< "$legacy")"
assert_contains "$deployment" 'containerPort: 80$'
assert_contains "$deployment" 'image: "ghcr.io/uug-ai/hub-frontend:v1.13.9"$'
if grep -Eq 'securityContext:|mountPath: /tmp' <<< "$deployment"; then
echo "Legacy frontend retains incompatible hardened runtime settings" >&2
exit 1
fi
done
done
if [[ "${CLUSTER_SMOKE:-false}" == true ]]; then
namespace=frontend-runtime-test
kubectl create namespace "$namespace"
helm template runtime "$CHART_DIR" "${values[@]}" --namespace "$namespace" \
--set mode=ui --set ingress="" --set kerberoshub.frontend.demoEnabled=true \
--show-only templates/kerberos-hub/hub-frontend.yaml \
--show-only templates/kerberos-hub/hub-frontend-demo.yaml | kubectl apply -f -
pid=""
cleanup() {
if [[ -n "$pid" ]]; then kill "$pid"; fi
if [[ "$1" != 0 ]]; then
kubectl -n "$namespace" get pods
kubectl -n "$namespace" describe pods
kubectl -n "$namespace" logs -l app=hub-frontend --all-containers=true
kubectl -n "$namespace" logs -l app=hub-frontend-demo --all-containers=true
fi
}
trap 'cleanup "$?"' EXIT
for name in hub-frontend hub-frontend-demo; do
kubectl -n "$namespace" rollout status "deployment/$name" --timeout=180s
kubectl -n "$namespace" port-forward "service/$name-svc" 18080:80 &
pid=$!
for attempt in {1..30}; do
if curl --fail --silent http://127.0.0.1:18080/healthz > /dev/null; then break; fi
sleep 1
done
[[ "$(curl --fail --silent http://127.0.0.1:18080/healthz)" == ok ]]
curl --fail --silent http://127.0.0.1:18080/assets/env.js | grep 'window' > /dev/null
kill "$pid"
wait "$pid" || [[ "$?" == 143 ]]
pid=""
echo "OK: $name serves health and generated runtime configuration through Service port 80"
done
fi
echo "Frontend runtime checks passed."
+194
View File
@@ -0,0 +1,194 @@
#!/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
}
without_frontends() {
awk '
/^---$/ { skip=0 }
/^# Source: hub\/templates\/kerberos-hub\/(hub-frontend(-demo)?|hub-api)\.yaml$/ { skip=1 }
!skip { print }
'
}
for mode in all pipeline ui; do
defaults="$(helm template hub "$CHART_DIR" --set "mode=$mode")"
worker_defaults="$(without_frontends <<< "$defaults")"
if grep -q 'securityContext:' <<< "$worker_defaults"; then
echo "Default $mode mode unexpectedly enables worker 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[@]}"
[[ "$(without_frontends <<< "$hardened" | grep -c 'runAsUser: 65532')" == "$expected" ]]
[[ "$(without_frontends <<< "$hardened" | grep -c 'fsGroup: 65532')" == "$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.
[[ "$(without_frontends <<< "$configured" | grep -c 'runAsUser: 65532')" == 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."
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REAL_KUBECTL="$(command -v kubectl)"
export REAL_KUBECTL
# Substitute only transport: exercise the migration against Kubernetes' real patch engine.
kubectl() {
if [[ "$*" == *" get deployment "* ]]; then
printf '%s\n' "$LIVE"
else
local patch="" previous="" argument
for argument in "$@"; do
if [[ "$previous" == -p ]]; then patch="$argument"; fi
previous="$argument"
done
if [[ "${EXPECT_APPLY:-false}" == true ]]; then
[[ "$*" != *"--dry-run=server"* ]] || return 1
else
[[ "$*" == *"--dry-run=server"* ]] || return 1
fi
local current="$LIVE"
if [[ "${SIMULATE_RACE:-false}" == true ]]; then
current="$(jq '.metadata.resourceVersion = "2"' <<< "$LIVE")"
fi
"$REAL_KUBECTL" patch --local -f <(printf '%s\n' "$current") \
--type=json -p "$patch" -o json
fi
}
export -f kubectl
for deployment in admin hub-frontend hub-frontend-demo; do
for old in 80 8080; do
target=8080
if [[ "$old" == 8080 ]]; then target=80; fi
LIVE="$(jq -nc --arg name "$deployment" --argjson port "$old" '{
apiVersion: "apps/v1", kind: "Deployment",
metadata: {name: $name, resourceVersion: "1"},
spec: {selector: {matchLabels: {app: $name}}, template: {
metadata: {labels: {app: $name}}, spec: {containers: [
{name: "sidecar", image: "example/sidecar:unchanged"},
{name: $name, image: "example/frontend:unchanged", ports: [
{name: "http", containerPort: $port},
{name: "metrics", containerPort: 2112}
]}
]}
}}
}')"
export LIVE
merge="$(jq -nc --arg name "$deployment" --argjson port "$target" \
'{spec:{template:{spec:{containers:[{name:$name,ports:[{name:"http",containerPort:$port}]}]}}}}')"
merged="$("$REAL_KUBECTL" patch --local -f <(printf '%s\n' "$LIVE") \
--type=strategic -p "$merge" -o json)"
jq -e '[.spec.template.spec.containers[] | .ports[]? |
select(.name == "http")] | length == 2' <<< "$merged" > /dev/null
fixed="$(bash "$ROOT/scripts/migrate-http-port.sh" test "$deployment" "$target")"
jq -e --argjson port "$target" '
[.spec.template.spec.containers[] | .ports[]? | select(.name == "http")] |
length == 1 and .[0].containerPort == $port
' <<< "$fixed" > /dev/null
[[ "$(jq -Sc '.spec.template.spec.containers[0]' <<< "$fixed")" == \
"$(jq -Sc '.spec.template.spec.containers[0]' <<< "$LIVE")" ]]
[[ "$(jq -Sc '.spec.template.spec.containers[1] | del(.ports)' <<< "$fixed")" == \
"$(jq -Sc '.spec.template.spec.containers[1] | del(.ports)' <<< "$LIVE")" ]]
jq -e '.spec.template.spec.containers[1].ports |
any(.name == "metrics" and .containerPort == 2112)' <<< "$fixed" > /dev/null
LIVE="$fixed"
again="$(EXPECT_APPLY=true bash "$ROOT/scripts/migrate-http-port.sh" test "$deployment" "$target" --apply)"
[[ "$(jq -Sc . <<< "$again")" == "$(jq -Sc . <<< "$fixed")" ]]
LIVE="$merged"
repaired="$(EXPECT_APPLY=true bash "$ROOT/scripts/migrate-http-port.sh" test "$deployment" "$target" --apply)"
jq -e '[.spec.template.spec.containers[] | .ports[]? |
select(.name == "http")] | length == 1' <<< "$repaired" > /dev/null
if [[ "${SERVER_VALIDATION:-false}" == true ]]; then
if "$REAL_KUBECTL" create --dry-run=server \
-f <(jq 'del(.metadata.resourceVersion)' <<< "$merged") > /dev/null 2>&1; then
echo "Expected API rejection of duplicate http ports" >&2
exit 1
fi
"$REAL_KUBECTL" create --dry-run=server \
-f <(jq 'del(.metadata.resourceVersion)' <<< "$repaired") > /dev/null
fi
echo "OK: $deployment $old -> $target; duplicate reproduced, replacement and rollback safe"
done
done
if SIMULATE_RACE=true bash "$ROOT/scripts/migrate-http-port.sh" test hub-frontend-demo 8080 > /dev/null 2>&1; then
echo "Expected rejection of concurrent Deployment changes" >&2
exit 1
fi
LIVE="$(jq '.spec.template.spec.containers[1].ports = [{name:"http",containerPort:9000}]' <<< "$LIVE")"
if bash "$ROOT/scripts/migrate-http-port.sh" test hub-frontend-demo 8080 > /dev/null 2>&1; then
echo "Expected rejection of an unknown HTTP port" >&2
exit 1
fi
echo "HTTP port migration checks passed."
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
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() {
if ! grep -Eq -- "$2" <<< "$1"; then
printf 'Missing expected Hub API setting: %s\n' "$2" >&2
exit 1
fi
}
fixtures=(
--set workflowConfiguration.enabled=true
--set mongodb.tls.enabled=true
--set mongodb.tls.existingSecret=mongodb-ca
--set kerberoshub.api.serverTLS.enabled=true
--set kerberoshub.api.serverTLS.secretName=api-tls
)
for mode in all ui pipeline; do
defaults="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}")"
legacy="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}" \
--set kerberoshub.api.securityContext=null \
--set kerberoshub.api.podSecurityContext=null)"
configured="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}" \
--set kerberoshub.api.securityContext.runAsUser=1000 \
--set kerberoshub.api.securityContext.runAsGroup=1000 \
--set kerberoshub.api.podSecurityContext.fsGroup=1000)"
helm lint "$CHART_DIR" --quiet --set "mode=$mode" "${fixtures[@]}"
if [[ "$mode" == pipeline ]]; then
[[ -z "$(resource Deployment hub-api <<< "$defaults")" ]]
[[ "$defaults" == "$legacy" && "$defaults" == "$configured" ]]
echo "OK (pipeline): no Hub API resources or unrelated changes"
continue
fi
api="$(resource Deployment hub-api <<< "$defaults")"
for pattern in \
'^[ ]{6}securityContext:$' '^[ ]{8}fsGroup: 65532$' \
'^[ ]{10}securityContext:$' '^[ ]{12}runAsUser: 65532$' \
'^[ ]{12}runAsGroup: 65532$' '^[ ]{12}runAsNonRoot: true$' \
'^[ ]{12}readOnlyRootFilesystem: true$' \
'^[ ]{12}allowPrivilegeEscalation: false$' '^[ ]{12}capabilities:$' \
'^[ ]{14}drop:$' '^[ ]{14}- ALL$' \
'^[ ]{12}seccompProfile:$' '^[ ]{14}type: RuntimeDefault$' \
'^[ ]+(- )?name: hub-api-tmp$' '^[ ]+(- )?emptyDir: \{\}$' \
'^[ ]+(- )?mountPath: /tmp$' '^[ ]+secretName: mongodb-ca$' \
'^[ ]+secretName: api-tls$' '^[ ]+mountPath: /etc/mongodb/tls$' \
'^[ ]+mountPath: /etc/hub-api/tls$' \
'^[ ]+mountPath: /etc/kerberos/configuration$' \
'^[ ]+mountPath: /etc/kerberos/classifications$'; do
assert_line "$api" "$pattern"
done
[[ "$(grep -c 'securityContext:' <<< "$api")" == 2 ]]
old_api="$(resource Deployment hub-api <<< "$legacy")"
if grep -q 'securityContext:' <<< "$old_api"; then
echo "Legacy Hub API opt-out still renders a security context" >&2
exit 1
fi
custom_api="$(resource Deployment hub-api <<< "$configured")"
for field in runAsUser runAsGroup fsGroup; do
assert_line "$custom_api" "^[ ]+$field: 1000$"
done
# Only security settings change; preserve env, mounts, images and routing.
for pattern in '^[ ]+image:' '^[ ]+(port|targetPort|containerPort):' \
'^[ ]+(- )?(name|mountPath|secretName|value):'; do
[[ "$(grep -E "$pattern" <<< "$defaults")" == "$(grep -E "$pattern" <<< "$legacy")" ]]
[[ "$(grep -E "$pattern" <<< "$defaults")" == "$(grep -E "$pattern" <<< "$configured")" ]]
done
echo "OK ($mode): API security defaults, overrides, opt-out, scratch and TLS/config mounts"
done
+33 -1
View File
@@ -33,6 +33,30 @@ assert_passkeys() {
fi
}
extract_feature() {
awk '
/^---/ { deployment=0; name=""; flag=0 }
/^kind: Deployment$/ { deployment=1 }
deployment && /^ name:/ { name=$2 }
deployment && /- name: FEATURE_PASSKEYS_ENABLED$/ { flag=1; next }
flag && /value:/ {
value=$0
sub(/^[[:space:]]*value: /, "", value)
print name, value
flag=0
}
' | sort
}
assert_feature() {
local rendered="$1" expected="$2" actual
actual="$(printf '%s\n' "$rendered" | extract_feature)"
if [[ "$actual" != "$expected" ]]; then
printf 'Unexpected FEATURE_PASSKEYS_ENABLED:\n%s\nExpected:\n%s\n' "$actual" "$expected" >&2
return 1
fi
}
CONFIGURED_FLAGS=(
--set-json kerberoshub.api.passkeys.enabled=true
--set-json 'kerberoshub.api.passkeys.rpId="example.com"'
@@ -50,14 +74,22 @@ for mode in all ui pipeline; do
configured_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${CONFIGURED_FLAGS[@]}")"
if [[ "$mode" == pipeline ]]; then
assert_passkeys "$configured_render" ""
assert_feature "$default_render" ""
assert_feature "$configured_render" ""
else
assert_passkeys "$configured_render" "$expected"
assert_feature "$default_render" 'hub-frontend "false"'
assert_feature "$configured_render" "$(printf '%s\n' 'hub-api "true"' 'hub-frontend "true"')"
fi
echo "OK ($mode): defaults opt out; enabled settings affect Hub API only"
echo "OK ($mode): defaults opt out; enabled settings configure Hub API and flag the frontend"
done
disabled_render="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" --set kerberoshub.api.passkeys.enabled=false)"
assert_passkeys "$disabled_render" ""
assert_feature "$disabled_render" 'hub-frontend "false"'
demo_render="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" --set kerberoshub.frontend.demoEnabled=true)"
assert_feature "$demo_render" "$(printf '%s\n' 'hub-api "true"' 'hub-frontend "true"' 'hub-frontend-demo "true"')"
localhost_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set kerberoshub.api.passkeys.enabled=true \
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 3 || $# -gt 4 ]]; then
echo "Usage: bash scripts/migrate-http-port.sh NAMESPACE DEPLOYMENT PORT [--apply]" >&2
exit 1
fi
namespace="$1" deployment="$2" port="$3"
case "$deployment" in
admin|hub-frontend|hub-frontend-demo) ;;
*) echo "Unsupported Deployment: $deployment" >&2; exit 1 ;;
esac
case "$port" in
80|8080) ;;
*) echo "Expected HTTP port 80 or 8080, got: $port" >&2; exit 1 ;;
esac
dry_run=(--dry-run=server)
if [[ $# -eq 4 ]]; then
if [[ "$4" != --apply ]]; then
echo "Unknown option: $4" >&2
exit 1
fi
dry_run=()
fi
live="$(kubectl -n "$namespace" get deployment "$deployment" -o json)"
patch="$(jq -ce --arg container "$deployment" --argjson port "$port" '
. as $deployment |
[.spec.template.spec.containers | to_entries[] |
select(.value.name == $container)] |
if length != 1 then error("Expected exactly one matching container") else .[0] end |
.key as $index | .value.ports as $ports |
if ($ports | type) != "array" then error("Container ports must be an array") else . end |
[$ports[] | select(.name == "http")] as $http |
if ($http | length) == 0 then error("No named http port to migrate") else . end |
if any($http[]; (.containerPort != 80 and .containerPort != 8080) or
((.protocol // "TCP") != "TCP") or has("hostPort") or has("hostIP"))
then error("Unsupported http port configuration; migrate manually") else . end |
if any($ports[]; .name != "http" and .containerPort == $port and
((.protocol // "TCP") == "TCP"))
then error("Target port is already used by another named port") else . end |
([$ports[] | select(.name != "http")] +
[($http[0] + {containerPort: $port})]) as $replacement |
[
{op: "test", path: "/metadata/resourceVersion", value: $deployment.metadata.resourceVersion},
{op: "test", path: ("/spec/template/spec/containers/" + ($index | tostring) + "/name"),
value: $container},
{op: "replace", path: ("/spec/template/spec/containers/" + ($index | tostring) + "/ports"),
value: $replacement}
]
' <<< "$live")"
kubectl -n "$namespace" patch deployment "$deployment" --type=json \
-p "$patch" "${dry_run[@]}" -o yaml