Compare commits

..
Author SHA1 Message Date
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
14 changed files with 609 additions and 56 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.168.2
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
+122 -11
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
@@ -614,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"` |
@@ -795,12 +896,12 @@ 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` | 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). 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`. | `{}` |
@@ -815,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"` |
@@ -1017,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). |
@@ -1040,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;
@@ -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 }}
@@ -553,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
@@ -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:
@@ -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:
+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."
}
}
}
+80 -36
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).
@@ -771,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).
@@ -825,7 +864,7 @@ kerberoshub:
audit:
repository: ghcr.io/uug-ai/hub-audit
pullPolicy: IfNotPresent
tag: "v1.0.1"
tag: "v1.0.10"
replicas: 1
dispatchInterval: "5s"
leaseDuration: "2m"
@@ -871,27 +910,31 @@ kerberoshub:
# actions: []
# categories: []
# outcomes: [] # success | failure
# S3 Object Lock destination (requires a hub-audit release with S3 support).
# 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.
# 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://minio.example.com # S3-compatible stores only
# forcePathStyle: true # usually needed for MinIO
# 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 with #16.
# # 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).
# # 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
@@ -1132,13 +1175,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:
@@ -1219,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).
@@ -1240,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).
@@ -1261,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).
@@ -1279,12 +1323,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).
@@ -1307,7 +1351,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
@@ -1327,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:
@@ -1350,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).
@@ -1388,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).
@@ -1414,7 +1458,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).
@@ -1442,12 +1486,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).
@@ -1469,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).
@@ -1496,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
+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
+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