Compare commits

..
Author SHA1 Message Date
KilianandClaude Opus 5.5 f0b79bea0c docs(hub): plain wording for audit destination credentials (0.172.1) (#224)
Describe inline audit destination credentials on their own terms in the
values comments and the README.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:44:59 +01:00
KilianandClaude Opus 5.5 535ddc847d hub: inline credentials for every audit destination (0.172.0) (#223)
Audit destinations now take credentials as plain values, like the other
Hub services: s3.accessKey / s3.secretKey on S3 destinations, bearerToken
and secretHeaders.<name>.value on webhooks. The chart stores them in a
Secret hub-audit-<id>-credentials and mounts it where hub-audit already
reads credential files, so no hub-audit change is needed and changed
values are picked up without a restart.

A shared hub.audit.destinations helper resolves inline values for the
ConfigMap, the Deployment volumes and the Secret template. Referencing an
existing Secret and credentialsSecret.create keep working and render
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 09:26:05 +01:00
KilianandClaude Opus 5.5 1c1593a8a0 hub: optionally create the hub-audit S3 credentials Secret (0.171.0) (#222)
credentialsSecret.create: true makes the chart render the Secret from
credentialsSecret.accessKeyId / secretAccessKey, so the audit S3 keys can
live in values like the chart's other credentials. The default stays
"bring your own Secret".

Inline keys without create: true now fail instead of being ignored, two
destinations creating the same Secret fail, and the docs spell out that
accessKeyIdKey / secretAccessKeyKey name keys inside the Secret.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 08:33:48 +01:00
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
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
20 changed files with 930 additions and 81 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.3
version: 0.172.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
+137 -13
View File
@@ -31,12 +31,41 @@ 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.
@@ -73,6 +102,26 @@ 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`
@@ -128,6 +177,57 @@ 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
@@ -260,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` |
@@ -278,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
@@ -607,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"` |
@@ -716,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"` |
@@ -786,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`, and credentials: `bearerToken` and `secretHeaders.<name>.value`. S3 destinations add `s3.bucket`, `s3.region`, optional `s3.prefix`, `s3.endpoint` and `s3.forcePathStyle` for S3-compatible stores, and credentials `s3.accessKey` / `s3.secretKey`; without credentials the pod's AWS identity is used (AWS S3 only). Credentials are set inline: the chart stores them in a Secret named `hub-audit-<id>-credentials` and mounts it into hub-audit, and changed values are picked up within a minute without a restart. To keep credentials out of the values, reference an existing Secret instead: `bearerTokenSecret` (`name`, `key`), `secretHeaders.<name>` (`secretName`, `secretKey`) or `credentialsSecret` (`name`, `accessKeyIdKey`, `secretAccessKeyKey`; the key fields name keys inside the Secret, not credentials). `credentialsSecret.create: true` with `accessKeyId` / `secretAccessKey` (chart 0.171.0) still works and is equivalent to `s3.accessKey` / `s3.secretKey`. Helm does not adopt resources it did not create, so delete a hand-made Secret before letting the chart create one with the same name. 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. | `[]` |
@@ -802,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"` |
@@ -1004,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). |
@@ -1027,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;
+64
View File
@@ -106,3 +106,67 @@ Usage: {{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
{{- end }}
{{- end -}}
{{/*
hub.audit.destinations resolves the audit destinations into JSON. Inline
credentials (s3.accessKey / s3.secretKey on S3 destinations, bearerToken and
secretHeaders.<name>.value on webhooks) are turned into a chart-created Secret named
hub-audit-<id>-credentials, so every template below reads one shape:
credentialsSecret / bearerTokenSecret / secretHeaders with create: true and the
values to store.
Use: fromJsonArray (include "hub.audit.destinations" .)
*/}}
{{- define "hub.audit.destinations" -}}
{{- $resolved := list -}}
{{- range $index, $original := .Values.kerberoshub.audit.destinations -}}
{{- $destination := deepCopy $original -}}
{{- $s3 := default dict $destination.s3 -}}
{{- if or $s3.accessKey $s3.secretKey -}}
{{- $id := required (printf "id is required for audit destination %d" $index) $destination.id -}}
{{- if not (and $s3.accessKey $s3.secretKey) -}}
{{- fail (printf "audit destination %s must set both s3.accessKey and s3.secretKey" $id) -}}
{{- end -}}
{{- if $destination.credentialsSecret -}}
{{- fail (printf "audit destination %s sets s3.accessKey/secretKey and credentialsSecret; use one of them" $id) -}}
{{- end -}}
{{- $_ := set $destination "credentialsSecret" (dict
"name" (printf "hub-audit-%s-credentials" $id)
"create" true
"accessKeyId" $s3.accessKey
"secretAccessKey" $s3.secretKey) -}}
{{- $_ := unset $s3 "accessKey" -}}
{{- $_ := unset $s3 "secretKey" -}}
{{- end -}}
{{- if hasKey $destination "bearerToken" -}}
{{- $id := required (printf "id is required for audit destination %d" $index) $destination.id -}}
{{- if not $destination.bearerToken -}}
{{- fail (printf "audit destination %s sets an empty bearerToken" $id) -}}
{{- end -}}
{{- if $destination.bearerTokenSecret -}}
{{- fail (printf "audit destination %s sets bearerToken and bearerTokenSecret; use one of them" $id) -}}
{{- end -}}
{{- $_ := set $destination "bearerTokenSecret" (dict
"name" (printf "hub-audit-%s-credentials" $id)
"key" "bearer-token"
"create" true
"value" $destination.bearerToken) -}}
{{- $_ := unset $destination "bearerToken" -}}
{{- end -}}
{{- range $header, $secret := (default dict $destination.secretHeaders) -}}
{{- if and (kindIs "map" $secret) (hasKey $secret "value") -}}
{{- $id := required (printf "id is required for audit destination %d" $index) $destination.id -}}
{{- if not $secret.value -}}
{{- fail (printf "audit destination %s header %s sets an empty value" $id $header) -}}
{{- end -}}
{{- if or $secret.secretName $secret.secretKey -}}
{{- fail (printf "audit destination %s header %s sets value and secretName/secretKey; use one of them" $id $header) -}}
{{- end -}}
{{- $_ := set $secret "secretName" (printf "hub-audit-%s-credentials" $id) -}}
{{- $_ := set $secret "secretKey" (printf "header-%s" (sha256sum $header | trunc 12)) -}}
{{- $_ := set $secret "create" true -}}
{{- end -}}
{{- end -}}
{{- $resolved = append $resolved $destination -}}
{{- end -}}
{{- toJson $resolved -}}
{{- end -}}
@@ -1,7 +1,7 @@
{{- if .Values.audit.enabled -}}
{{- $destinations := list -}}
{{- $destinationIDs := dict -}}
{{- range $index, $destination := .Values.kerberoshub.audit.destinations -}}
{{- range $index, $destination := fromJsonArray (include "hub.audit.destinations" .) -}}
{{- $id := required (printf "id is required for audit destination %d" $index) $destination.id -}}
{{- if not (regexMatch "^[a-z][a-z0-9-]{0,62}$" $id) -}}
{{- fail (printf "audit destination id %q must match ^[a-z][a-z0-9-]{0,62}$" $id) -}}
@@ -10,33 +10,63 @@
{{- 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, bearerToken, 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 -}}
{{- if and (not $credentials.create) (or $credentials.accessKeyId $credentials.secretAccessKey) -}}
{{- fail (printf "audit destination %s sets credentialsSecret.accessKeyId/secretAccessKey without create: true; set create: true to let the chart create Secret %s from them, or remove them and create the Secret yourself" $id $secretName) -}}
{{- end -}}
{{- $_ := 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,54 @@
{{- if .Values.audit.enabled }}
{{- /*
Secrets the chart creates for audit destinations: inline credentials
(s3.accessKey / s3.secretKey, bearerToken, secretHeaders.<name>.value) and credentialsSecret.create: true.
Destinations that reference an existing Secret are not rendered here.
*/}}
{{- $created := dict }}
{{- range $index, $destination := fromJsonArray (include "hub.audit.destinations" .) }}
{{- $data := dict }}
{{- $name := "" }}
{{- $credentials := default dict $destination.credentialsSecret }}
{{- if $credentials.create }}
{{- $name = required (printf "credentialsSecret.name is required for destination %s" $destination.id) $credentials.name }}
{{- $accessKeyIdKey := default "access-key-id" $credentials.accessKeyIdKey }}
{{- $secretAccessKeyKey := default "secret-access-key" $credentials.secretAccessKeyKey }}
{{- if eq $accessKeyIdKey $secretAccessKeyKey }}
{{- fail (printf "credentialsSecret.accessKeyIdKey and secretAccessKeyKey must differ for destination %s" $destination.id) }}
{{- end }}
{{- $_ := set $data $accessKeyIdKey (required (printf "credentialsSecret.accessKeyId is required for destination %s when credentialsSecret.create is true" $destination.id) $credentials.accessKeyId) }}
{{- $_ := set $data $secretAccessKeyKey (required (printf "credentialsSecret.secretAccessKey is required for destination %s when credentialsSecret.create is true" $destination.id) $credentials.secretAccessKey) }}
{{- end }}
{{- $bearer := default dict $destination.bearerTokenSecret }}
{{- if $bearer.create }}
{{- $name = $bearer.name }}
{{- $_ := set $data $bearer.key $bearer.value }}
{{- end }}
{{- range $header, $secret := (default dict $destination.secretHeaders) }}
{{- if $secret.create }}
{{- $name = $secret.secretName }}
{{- $_ := set $data $secret.secretKey $secret.value }}
{{- end }}
{{- end }}
{{- if $name }}
{{- if hasKey $created $name }}
{{- fail (printf "audit destinations %s and %s both create Secret %q; give each its own credentialsSecret.name, or set create: false on one and reuse the Secret" (get $created $name) $destination.id $name) }}
{{- end }}
{{- $_ := set $created $name $destination.id }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
app: hub-audit
audit-destination: {{ $destination.id }}
type: Opaque
stringData:
{{- range $key, $value := $data }}
{{ $key | quote }}: {{ toString $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -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 }}
@@ -38,13 +43,25 @@ spec:
- name: destinations
configMap:
name: hub-audit
{{- range $index, $destination := .Values.kerberoshub.audit.destinations }}
{{- range $index, $destination := fromJsonArray (include "hub.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 }}
@@ -95,10 +112,11 @@ spec:
- name: destinations
mountPath: /etc/hub-audit
readOnly: true
{{- range $index, $destination := .Values.kerberoshub.audit.destinations }}
{{- range $index, $destination := fromJsonArray (include "hub.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
@@ -110,7 +110,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:
@@ -241,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
@@ -365,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
+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."
}
}
}
+133 -34
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:
@@ -339,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).
@@ -375,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
@@ -419,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:
@@ -477,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).
@@ -655,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'
@@ -764,7 +810,7 @@ kerberoshub:
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).
@@ -812,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"
@@ -826,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
@@ -840,19 +897,60 @@ kerberoshub:
# startAt: latest # latest | beginning, applied only on first creation
# headers:
# X-Source: kerberos-hub
# bearerTokenSecret:
# name: audit-primary-siem
# key: token
# # Inline credentials. The chart stores them in Secret
# # hub-audit-<id>-credentials and mounts them.
# bearerToken: "xxx"
# secretHeaders:
# X-Api-Key:
# secretName: audit-primary-siem
# secretKey: api-key
# value: "xxx"
# # Or reference an existing Secret instead of the inline values:
# # bearerTokenSecret:
# # name: audit-primary-siem
# # key: token
# # secretHeaders:
# # X-Api-Key:
# # secretName: audit-primary-siem
# # secretKey: api-key
# filters:
# organisationIds: []
# projectIds: []
# 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
# # Inline credentials. The chart stores them in Secret
# # hub-audit-<id>-credentials and mounts them.
# accessKey: "xxx"
# secretKey: "xxx"
# # Or reference an existing Secret instead of s3.accessKey/secretKey.
# # accessKeyIdKey/secretAccessKeyKey name keys inside that Secret.
# # credentialsSecret:
# # name: audit-immutable-archive
# # accessKeyIdKey: access-key-id
# # secretAccessKeyKey: secret-access-key
# # Without either, hub-audit uses the pod's AWS identity (IRSA via
# # kerberoshub.audit.serviceAccount, or an instance profile): AWS S3 only.
# resources:
# requests:
# memory: 32Mi
@@ -1089,13 +1187,14 @@ kerberoshub:
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:
@@ -1176,7 +1275,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).
@@ -1197,7 +1296,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).
@@ -1218,7 +1317,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).
@@ -1236,12 +1335,12 @@ kerberospipeline:
throttler:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-throttler
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).
@@ -1264,7 +1363,7 @@ kerberospipeline:
# 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
@@ -1284,9 +1383,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:
@@ -1307,7 +1406,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).
@@ -1345,7 +1444,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).
@@ -1371,7 +1470,7 @@ kerberospipeline:
# 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).
@@ -1399,12 +1498,12 @@ kerberospipeline:
counting:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-counting
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).
@@ -1426,7 +1525,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).
@@ -1453,7 +1552,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
+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."
+14 -5
View File
@@ -60,10 +60,19 @@ assert_line() {
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")"
if grep -q 'securityContext:' <<< "$defaults"; then
echo "Default $mode mode unexpectedly enables security settings" >&2
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
@@ -111,8 +120,8 @@ for mode in all pipeline ui; do
fi
done
expected="${#names[@]}"
[[ "$(grep -c 'runAsUser: 65532' <<< "$hardened")" == "$expected" ]]
[[ "$(grep -c 'fsGroup: 65532' <<< "$hardened")" == "$expected" ]]
[[ "$(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
@@ -179,7 +188,7 @@ for mode in all ui pipeline; do
[[ "$(resource Service oauth2-proxy-admin <<< "$legacy")" == "$(resource Service oauth2-proxy-admin <<< "$configured")" ]]
[[ "$(grep 'image:' <<< "$legacy")" == "$(grep 'image:' <<< "$configured")" ]]
# No other workload inherits Admin's container port or security settings.
[[ "$(grep -c 'runAsUser: 65532' <<< "$configured")" == 1 ]]
[[ "$(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