Compare commits

...
Author SHA1 Message Date
KilianandClaude Opus 5.5 8b96a8c7b8 feat(hub): recording check email template (0.176.0) (#228)
hub-monitor-device's recording check (a continuously recording camera that
is online but uploads no recordings) sends the new recording email template.
email.templates.recording / recordingTitle set RECORDING_TEMPLATE and
RECORDING_TITLE on the monitor when given; empty keeps the monitor defaults,
so SendGrid and Mailgun keep their device template id. The README documents
the email and its placeholders, and custom-layout/templates gets
recording.html / recording.txt.

custom-layout/templates/device.txt was a copy of the welcome email; it now
describes the device change like device.html.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:03:19 +01:00
KilianandClaude Opus 5.5 2e018357d8 hub: audit destination credentials inline only (0.175.0) (#227)
Destinations take credentials only as inline values: s3.accessKey /
s3.secretKey, bearerToken and secretHeaders.<name>.value. credentialsSecret,
bearerTokenSecret and secretHeaders secretName/secretKey are removed from the
values and README and fail the render with the inline field to use.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:03:44 +01:00
KilianandClaude Opus 5.5 0b9d07339f hub: audit destination field reference, enabled by default (0.174.0) (#226)
- README documents every destination field in its own row: required or
  optional, default and limits. Adds kerberoshub.audit.resources and
  topologySpreadConstraints.
- values.yaml examples mark each field required/optional with its default;
  kerberoshub.audit.resources is a real key next to replicas.
- A destination without `enabled` is now enabled (it rendered null, which
  hub-audit reads as false). `enabled: false` still pauses it.
- Empty or one-sided s3.accessKey/s3.secretKey fail the render instead of
  falling back to the pod's AWS identity, like an empty bearerToken.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:29:59 +01:00
KilianandClaude Opus 5.5 78cf3c7e2b hub: flag for the Users (preview) organisation tab (0.173.0) (#225)
Adds kerberoshub.frontend.features.organisations.membersAccessPreviewEnabled
(default "false"), setting FEATURE_MEMBERS_ACCESS_PREVIEW_ENABLED on the
frontend and the demo frontend.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:45:38 +01:00
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
18 changed files with 1034 additions and 92 deletions
@@ -20,3 +20,9 @@ jobs:
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.169.0
version: 0.176.0
# 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
+112 -5
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`
@@ -130,6 +179,10 @@ 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:
@@ -739,6 +792,7 @@ kerberospipeline:
| `kerberoshub.frontend.features.organisations.switcherEnabled` | Enable or disable the organisation dropdown and switching. The current organisation remains visible when disabled. | `"false"` |
| `kerberoshub.frontend.features.organisations.creationEnabled` | Enable or disable organisation creation. Requires organisation switching to be enabled. | `"false"` |
| `kerberoshub.frontend.features.organisations.settingsEnabled` | Enable or disable the organisation identity link to organisation settings. | `"false"` |
| `kerberoshub.frontend.features.organisations.membersAccessPreviewEnabled` | Show the "Users (preview)" tab in organisation settings to organisation owners: a mockup of project, role and resource assignments with mocked users and data, nothing saved. Sets `FEATURE_MEMBERS_ACCESS_PREVIEW_ENABLED`. | `"false"` |
| `kerberoshub.frontend.features.projects.enabled` | Fallback project group switch used when the organisations umbrella is unset. | `""` |
| `kerberoshub.frontend.features.projects.switcherEnabled` | Enable or disable the read-only project dropdown. | `"false"` |
| `kerberoshub.frontend.features.projects.creationEnabled` | Reserved for the project creation UI. | `"false"` |
@@ -848,7 +902,26 @@ kerberospipeline:
| `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). 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.destinations` | Audit destinations: webhooks that receive batches as JSON, or S3 buckets that archive them. Every entry is validated when the chart renders. Credentials (`bearerToken`, `secretHeaders.<name>.value`, `s3.accessKey` / `s3.secretKey`) are set inline; the chart stores them in Secret `hub-audit-<id>-credentials`, and changes take effect within a few minutes without a restart. Delivery semantics and per-provider S3 settings: https://learn.uug.ai/docs/hub/audit-events/#export-audit-events. | `[]` |
| `kerberoshub.audit.destinations[].id` | Required. Unique name, `^[a-z][a-z0-9-]{0,62}$`. The delivery checkpoint is stored under it, so renaming starts a new destination. | |
| `kerberoshub.audit.destinations[].type` | Optional. `webhook` or `s3`. | `webhook` |
| `kerberoshub.audit.destinations[].enabled` | Optional. `false` pauses delivery and keeps the checkpoint. | `true` |
| `kerberoshub.audit.destinations[].timeoutSeconds` | Optional. Timeout for one delivery, at most 60. | `10` |
| `kerberoshub.audit.destinations[].batchSize` | Optional. Events per batch, at most 1000. | `100` |
| `kerberoshub.audit.destinations[].maxAttempts` | Optional. Attempts before a batch is dead-lettered. Keep it high for S3 archives. | `10` |
| `kerberoshub.audit.destinations[].startAt` | Optional. `latest` or `beginning`; only applies to a new `id`. | `latest` |
| `kerberoshub.audit.destinations[].filters` | Optional. Lists `organisationIds`, `projectIds` (ObjectIds), `actions`, `categories` and `outcomes` (`success`, `failure`). An event must match every list that is set. | all events |
| `kerberoshub.audit.destinations[].url` | Required for webhooks. `http` or `https` endpoint that receives each batch. | |
| `kerberoshub.audit.destinations[].headers` | Optional, webhooks. Public request headers. `Authorization` and `Idempotency-Key` are reserved. | `{}` |
| `kerberoshub.audit.destinations[].bearerToken` | Optional, webhooks. Sent as `Authorization: Bearer <token>`. | |
| `kerberoshub.audit.destinations[].secretHeaders.<name>.value` | Optional, webhooks. Header with a secret value, such as an API key. | |
| `kerberoshub.audit.destinations[].s3.bucket` | Required for S3. Bucket with versioning, Object Lock and a `COMPLIANCE` default retention. | |
| `kerberoshub.audit.destinations[].s3.region` | Required for S3, also for S3-compatible stores. | |
| `kerberoshub.audit.destinations[].s3.prefix` | Optional. Key prefix; objects are written as `<prefix>/<id>/yyyy/mm/dd/<firstId>-<lastId>.ndjson.gz`. | |
| `kerberoshub.audit.destinations[].s3.endpoint` | Optional. Base URL of an S3-compatible store, without the bucket. Omit for AWS S3. | |
| `kerberoshub.audit.destinations[].s3.forcePathStyle` | Optional. Path-style URLs, usually needed for Ceph RGW, MinIO and Garage. | `false` |
| `kerberoshub.audit.destinations[].s3.allowUnlockedBucket` | Optional. Skips the Object Lock check for stores without Object Lock, such as Garage; archived batches are then deletable. | `false` |
| `kerberoshub.audit.destinations[].s3.accessKey` / `kerberoshub.audit.destinations[].s3.secretKey` | Optional, both or neither, not empty. Without them the pod's AWS identity is used (AWS S3 only). | |
| `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`. | `{}` |
@@ -858,6 +931,8 @@ kerberospipeline:
| `kerberoshub.audit.volumes` | Additional pod volumes, for example a private webhook CA bundle. | `[]` |
| `kerberoshub.audit.volumeMounts` | Additional audit service container volume mounts. | `[]` |
| `kerberoshub.audit.extraEnv` | Additional audit service environment variables. | `[]` |
| `kerberoshub.audit.resources` | Resource requests and limits for the audit service container. | `{}` |
| `kerberoshub.audit.topologySpreadConstraints` | Topology spread constraints for audit service pods. | `[]` |
| `kerberoshub.monitordevice.repository` | The monitoring microservice, following up the status of your cameras and Kerberos Agents. | `"ghcr.io/uug-ai/hub-monitor-device"` |
| `kerberoshub.monitordevice.pullPolicy` | The Docker registry pull policy. | `"IfNotPresent"` |
| `kerberoshub.monitordevice.tag` | The Docker image tag/version. | `"v1.4.0"` |
@@ -1013,6 +1088,8 @@ kerberospipeline:
| `email.templates.deviceTitle` | Email subject title for the `device` template. | `"[Device] A Kerberos Agent's status has been changed"` |
| `email.templates.deviceGroup` | The template sent instead of `device` when several cameras of an account change status in the same scan and the account groups device notifications ("Group notifications"). SMTP only; see *Grouped camera status* below. | `"device_group"` |
| `email.templates.deviceGroupTitle` | Email subject title for the `device_group` template. | `"[Device] Several cameras changed status"` |
| `email.templates.recording` | The template for recording check alerts (a continuously recording camera that is online but uploads no recordings, and again when it records). Empty uses `recording` with SMTP; for SendGrid or Mailgun set the template id, otherwise they use `device`. See *Camera recordings* below. | `""` |
| `email.templates.recordingTitle` | Email subject for recording check alerts. Empty uses "[Device] A camera stopped recording", "[Device] A camera is recording again" and their forms for several cameras. | `""` |
| `email.mailgun.apiKey` | The Mailgun API key linked to your Mailgun domain. | `""` |
| `imagePullSecrets.name` | Docker registry secret name, which is also granted with the license. This allows you to download the Docker images. | `""` |
| `kerberoshub.forwarder.pullPolicy` | The Docker registry pull policy. | `""` |
@@ -1120,6 +1197,36 @@ Other cameras · Camera stream lost · 1 camera
**When email stays per camera.** The grouped email is only sent with the SMTP mail provider. With SendGrid or Mailgun the template name is a template id in that account, so those keep one `device` email per camera. With SMTP, a deployment that mounts its own `device` template but no `device_group` template also keeps one email per camera, so a white-labelled device email never falls back to the default layout. Add a `device_group` template next to your `device` template to get grouped emails.
#### Camera recordings: `recording`
Sent by `hub-monitor-device` for the **recording check** (Alerts → Devices → Recording check, stored as `notificationSettings.devices.recordingCheck` and `recordingCheckHours`). It alerts when a selected camera that records continuously is online but has not uploaded a recording for the account's period (1 to 72 hours, default 2), and again once it records. Cameras that record on motion only, cameras whose Agent does not report its recording mode, and offline cameras are not checked. The check uses the device alert channels, cameras and **Group notifications**: with grouping on, cameras that stop recording at the same time arrive in one email. The same template is used for one camera and for several.
The template name is `email.templates.recording` (default `recording`, so custom files are `recording.html` and `recording.txt`); the subject is `email.templates.recordingTitle`.
| 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}}` | For one camera, "`<camera>` is online, but hasn't uploaded a recording for 2 hours." (with "The account reached its upload limit, so new recordings are not stored." when it has) or "`<camera>` is recording again."; for several, "3 cameras stopped recording.", "2 cameras are recording again." or "3 cameras: 2 stopped recording, 1 recording again." |
| `{{count}}` | Number of cameras in the email. |
| `{{status}}` | `no_recordings` or `recording` when all cameras have the same change, otherwise `mixed`. |
| `{{sections}}` | The cameras as HTML, as for `device_group`. The change labels are `No recordings` and `Recording again`, and the details `online, no recording for 3 hours` (plus `, upload limit reached`) and `recording again`. |
| `{{sectionsText}}` | The same sections as plain text, for `recording.txt`. |
| `{{link}}` | The cameras page, `<tenant URL>/devices` (see *Links* above). |
| `{{eventdate}}`, `{{eventtime}}`, `{{eventdatetime}}` | When the email was sent, in the server's timezone (normally UTC). |
`{{sectionsText}}` example:
```text
Site Gate · No recordings · all 2 cameras
- Gate camera 1 (online, no recording for 2 hours)
- Gate camera 2 (online, no recording for 2 hours)
```
**Custom templates** for this email are read by `hub-monitor-device`; mount the `custom-email-templates` volume on `kerberoshub.monitordevice`. With SMTP, a deployment that mounts its own `device` template but no `recording` template keeps getting its `device` template (one email per camera), so a white-labelled email never falls back to the default layout. Add a `recording` template next to your `device` template to get the recording email. [`custom-layout/templates`](custom-layout/templates) has one to start from.
**Integrations.** Webhooks, IFTTT and the real-time feed get one message per camera with `data.status` set to `no_recordings` or `recording`, also with grouping on.
#### Account and sharing emails
Sent by the API (`kerberoshub.api`). The subject of each email is the matching `email.templates.<name>Title` value.
@@ -2,15 +2,15 @@ Kerberos.io
------------
Hey, {{user}}
Welcome to Kerberos Hub
Activate your account ->
One of your Kerberos Agents changed
{{text}}
Agents go offline due to a variety of reasons. The machine, node, micro controller on which your Agent runs, gets corrupted or disconnected from the internet. The camera itself is broken, damaged or in the worst case tampered. Have a look into your Kerberos Hub account for the latest recordings and/or verify the connection and status of your Kerberos Agent.
Go to Kerberos Hub ->
{{link}}
Kerberos Hub in a nutshell
With Kerberos Hub you can access your surveillance media remotely. By subscribing to a plan, you get access to a set of features, from advanced filtering, notifications to machine learning.
However before you get started this amazing applications, please activate your account.
Get in touch
------------
support@kerberos.io
@@ -0,0 +1,431 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<meta name="description" content="Kerberos.io Mailing">
<style type="text/css">
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("https://kerberos.io/dist/fonts/Inter-Regular.woff?v=/dist/fonts/Inter-Regular.woff2?v=3.183.18") format("woff2"),
url("https://kerberos.io/dist/fonts/Inter-Regular.woff?v=/dist/fonts/Inter-Regular.woff2?v=3.183.18") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("https://kerberos.io/dist/fonts/Inter-Medium.woff2?v=3.18") format("woff2"),
url("https://kerberos.io/dist/fonts/Inter-Medium.woff?v=3.18") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("https://kerberos.io/dist/fonts/Inter-SemiBold.woff2?v=3.18") format("woff2"),
url("https://kerberos.io/dist/fonts/Inter-SemiBold.woff?v=3.18") format("woff");
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: 'Regular';
src: url("https://kerberos.io/dist/fonts/Inter-roman.var.woff2?v=3.18") format("woff2");
}
body{
background: #E5E5E5;
margin-top:0;
margin-bottom: 0;
margin-right: 0;
margin-left: 0;
padding-top: 0;
padding-left: 0;
padding-right: 0;
padding-bottom: 0;
font-family: 'Inter';
}
a, a:hover, a:active {
color: #262424;
text-decoration: none;
}
.corner-td{
width: 60px;
}
table {border-collapse:separate;max-width: 850px; margin: 0 auto; width: 100%;}
.ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td {line-height: 100%;}
.ExternalClass {width: 100%;}
@media screen and (max-width:500px){
.tab-td{
padding-left: 10px!important;
}
.tab-td a h4{
font-size: 14px!important;
}
.corner-td{
width: 20px!important;
}
.company-name-td h3{
font-size: 16px!important;
}
table.header-table{
padding-top: 8px!important;
padding-right: 0px!important;
padding-bottom: 24px!important;
padding-left: 0px!important;
}
.colored-card-td h4{
font-size: 14px!important;
}
.colored-card-td h2{
font-size: 20px!important;
}
.colored-card-td a p{
font-size: 12px!important;
width: 143px!important;
}
.colored-card-td{
padding-top: 24px!important;
padding-right: 24px!important;
padding-bottom: 24px!important;
padding-left: 24px!important;
}
.colorless-card-td{
padding-top: 24px!important;
padding-right: 24px!important;
padding-bottom: 24px!important;
padding-left: 24px!important;
}
.colorless-card-td h3{
font-size: 18px!important;
}
.colorless-card-td p{
font-size: 14px!important;
}
.colorless-card-table{
margin-left: 0px!important;
margin-right: 0px!important;
margin-top: 24px!important;
margin-bottom: 24px!important;
}
.footer-td{
display: table-row!important;
}
}
@media screen and (max-width:600px) {
.footer-td{
display: table-row!important;
}
}
@media screen and (max-width:650px) {
.footer-table{
margin-left: 0px!important;
margin-right: 0px!important;
margin-top: 0px!important;
margin-bottom: 36px!important;
}
}
</style>
</head>
<body height="100%" width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="36" class="header-table" style="padding-top: 36px ;padding-right: 0;padding-bottom: 36px;padding-left: 0;border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<td width="48" height="36" align="left"><img alt="Kerberos.io" width="36" height="36" src="https://kerberos.io/images/email/kerberos.png"/></td>
<td height="36" align="left" class="company-name-td">
<h3 width="36" height="36" style=" font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #262424;">Kerberos.io</h3>
</td>
<td height="36" width="36" style="padding-left: 36px;" class="tab-td" align="right">
<a style="text-decoration: none;color: none;" href={{tab1_href}}>
<h4 style="font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: right;
color: #6D6666;">{{tab1_title}}</h4>
</a>
</td>
<td height="36" width="36" style="padding-left: 36px;" class="tab-td" align="right" >
<a style="text-decoration: none;color: none;" href={{tab2_href}}>
<h4 style="font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: right;
color: #6D6666;">{{tab2_title}}</h4>
</a>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;" >
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<td class="colored-card-td" bgcolor="#57356B" style="padding-left: 48px;padding-right: 48px;padding-top: 48px;padding-bottom: 48px;border-radius: 4px;background-color:#57356B;">
<h4 style="font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#b09fb9;">Hey, {{user}}</h4>
<h2 style=" font-family: Inter;
font-size: 24px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#FFFFFF;
padding-top: 12px;
margin-bottom: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;">Camera recordings</h2>
<h2 style=" font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#FFFFFF;
padding-top: 0;
padding-bottom: 24px;
padding-left: 0;
padding-right: 0;
color:#b09fb9;">{{text}}</h2>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="colorless-card-table" style="margin-top: 36px;margin-bottom: 36px;margin-left: 0;margin-right: 0;border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;" >
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<td class="colorless-card-td" bgcolor="#FFFFFF" style="background-color:#FFFFFF;padding-top: 36px;padding-right: 43px;padding-bottom: 25px;padding-left: 43px;border-radius: 4px;">
<h3 style=" font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #262424;
width: 480px">Cameras</h3>
{{sections}}
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #6D6666;
margin-top: 12px;">These cameras record continuously, so a new recording normally arrives every few minutes. When a camera is online but its recordings stop arriving, check that the account has not reached its upload limit, that the storage the recordings are sent to is reachable and not full, and that the Agent's cloud settings and credentials are still valid. Cameras that are recording again need no action.</p>
<a style="text-decoration: none;color: none;" href="{{link}}">
<p style="font-family: Inter;
font-size: 14px;
font-style: normal;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#FFFFFF;
background-color: #84559F;
padding-top: 6px;
padding-bottom: 6px;
padding-right: 16px;
padding-left: 16px;
width: 160px;
border-radius: 4px;
text-align: center;
cursor: pointer;">Go to Kerberos Hub -></p>
</a>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="footer-table" style="margin-top: 0;border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;" >
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;display:table-row !important">
<![endif]-->
<td height="146" width="190" class="footer-td" style="margin-bottom: 12px;" valign="top" align="left">
<table style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr>
<td>
<h4 style=" font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#6D6666;">Get in touch</h4>
<a style="text-decoration: none;color: none;" href="mailto:support@verstraeten.io">
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 16px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">support@kerberos.io</p>
</a>
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 16px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">9000 Ghent, BE</p>
<a style="text-decoration: none;color: none;" href="https://kerberos.io/">
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">https://kerberos.io</p>
</a>
</td>
</tr>
</tbody>
</table>
</td>
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;display:table-row !important">
<![endif]-->
<td class="footer-td" style="border-radius: 4px;padding-left: 0;padding-right: 0;padding-top: 0;padding-bottom: 0; margin-bottom: 12px;" valign="top" align="left">
<table style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr>
<td>
<h4 style=" font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#6D6666;">About Kerberos</h4>
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.</p>
<p style="margin-top: 12px;">
<a href="https://twitter.com/kerberosio" style="text-decoration: none;color: none;">
<img width="24" height="24" alt="Twitter" src="https://kerberos.io/images/email/twitter.png"/>
</a>
<a href="https://reddit.com/r/kerberos_io" style="text-decoration: none;color: none;">
<img g width="24" height="24" alt="Reddit" src="https://kerberos.io/images/email/reddit.png"/>
</a>
<a href="https://www.youtube.com/channel/UCnd9q7iRNNw4W95eQwQuECA" style="text-decoration: none;color: none;">
<img g width="24" height="24" alt="Youtube" src="https://kerberos.io/images/email/youtube.png"/>
</a>
<a href="https://github.com/kerberos-io" style="text-decoration: none;color: none;">
<img g width="24" height="24" alt="Github" src="https://kerberos.io/images/email/github.png"/>
</a>
</p>
</td>
</tr>
</tbody>
</table>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
<table style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr style="height: 50px">
<td></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,24 @@
Kerberos.io
------------
Hey, {{user}}
Camera recordings
{{text}}
{{sectionsText}}
These cameras record continuously, so a new recording normally arrives every few minutes. When a camera is online but its recordings stop arriving, check that the account has not reached its upload limit, that the storage the recordings are sent to is reachable and not full, and that the Agent's cloud settings and credentials are still valid. Cameras that are recording again need no action.
Go to Kerberos Hub ->
{{link}}
Get in touch
------------
support@kerberos.io
9000 Ghent, BE
https://kerberos.io
About Kerberos
------------
Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.
+63
View File
@@ -106,3 +106,66 @@ 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 the only way to pass credentials.
They are stored in a chart-created Secret named hub-audit-<id>-credentials;
the internal credentialsSecret / bearerTokenSecret / secretHeaders entries
below describe that Secret for the other templates.
Use: fromJsonArray (include "hub.audit.destinations" .)
*/}}
{{- define "hub.audit.destinations" -}}
{{- $resolved := list -}}
{{- range $index, $original := .Values.kerberoshub.audit.destinations -}}
{{- $destination := deepCopy $original -}}
{{- $label := default (printf "%d" $index) $destination.id -}}
{{- if hasKey $destination "credentialsSecret" -}}
{{- fail (printf "audit destination %s: credentialsSecret is not supported; set s3.accessKey and s3.secretKey" $label) -}}
{{- end -}}
{{- if hasKey $destination "bearerTokenSecret" -}}
{{- fail (printf "audit destination %s: bearerTokenSecret is not supported; set bearerToken" $label) -}}
{{- end -}}
{{- $s3 := default dict $destination.s3 -}}
{{- if or (hasKey $s3 "accessKey") (hasKey $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, and neither may be empty" $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 -}}
{{- $_ := 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 or (not (kindIs "map" $secret)) (not (hasKey $secret "value")) (hasKey $secret "secretName") (hasKey $secret "secretKey") -}}
{{- fail (printf "audit destination %s header %s: set secretHeaders.%s.value" $label $header $header) -}}
{{- end -}}
{{- $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 -}}
{{- $_ := set $secret "secretName" (printf "hub-audit-%s-credentials" $id) -}}
{{- $_ := set $secret "secretKey" (printf "header-%s" (sha256sum $header | trunc 12)) -}}
{{- $_ := set $secret "create" true -}}
{{- 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) -}}
@@ -14,7 +14,7 @@
{{- $entry := dict
"id" $id
"type" $type
"enabled" $destination.enabled
"enabled" (ternary $destination.enabled true (hasKey $destination "enabled"))
"timeoutSeconds" (default 10 $destination.timeoutSeconds)
"batchSize" (default 100 $destination.batchSize)
"maxAttempts" (default 10 $destination.maxAttempts)
@@ -44,7 +44,7 @@
{{- end -}}
{{- else if eq $type "s3" -}}
{{- if or $destination.url $destination.headers $destination.bearerTokenSecret $destination.secretHeaders -}}
{{- fail (printf "audit destination %s is s3 and cannot set url, headers, bearerTokenSecret or secretHeaders" $id) -}}
{{- 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
@@ -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 }}
@@ -0,0 +1,53 @@
{{- if .Values.audit.enabled }}
{{- /*
Secret the chart creates per audit destination from its inline credentials
(s3.accessKey / s3.secretKey, bearerToken, secretHeaders.<name>.value).
*/}}
{{- $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" (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 }}
@@ -43,7 +43,7 @@ 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 }}
{{- $credentials := default dict $destination.credentialsSecret }}
@@ -112,7 +112,7 @@ 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 }}
{{- $credentials := default dict $destination.credentialsSecret }}
@@ -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:
@@ -259,6 +259,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.organisations.creationEnabled }}"
- name: FEATURE_ORGANISATION_SETTINGS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.organisations.settingsEnabled }}"
- name: FEATURE_MEMBERS_ACCESS_PREVIEW_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.organisations.membersAccessPreviewEnabled }}"
- name: FEATURE_PROJECTS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.projects.enabled }}"
- name: FEATURE_PROJECT_SWITCHER_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:
@@ -338,6 +338,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.organisations.creationEnabled }}"
- name: FEATURE_ORGANISATION_SETTINGS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.organisations.settingsEnabled }}"
- name: FEATURE_MEMBERS_ACCESS_PREVIEW_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.organisations.membersAccessPreviewEnabled }}"
- name: FEATURE_PROJECTS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.projects.enabled }}"
- name: FEATURE_PROJECT_SWITCHER_ENABLED
@@ -96,6 +96,14 @@ spec:
value: "{{ .Values.email.templates.deviceGroup | default "device_group" }}"
- name: DEVICE_GROUP_TITLE
value: "{{ .Values.email.templates.deviceGroupTitle | default "[Device] Several cameras changed status" }}"
{{- with .Values.email.templates.recording }}
- name: RECORDING_TEMPLATE
value: {{ . | quote }}
{{- end }}
{{- with .Values.email.templates.recordingTitle }}
- name: RECORDING_TITLE
value: {{ . | quote }}
{{- end }}
# Base URL for the link in grouped device notifications.
- name: PUBLIC_URL
value: "{{ .Values.kerberoshub.frontend.schema }}://{{ .Values.kerberoshub.frontend.url }}"
+108 -67
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
@@ -482,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).
@@ -621,6 +655,7 @@ kerberoshub:
switcherEnabled: "false" # Enable or disable organisation switching 'true' or 'false'
creationEnabled: "false" # Enable or disable organisation creation; requires switcherEnabled 'true' or 'false'
settingsEnabled: "false" # Enable or disable the organisation settings link 'true' or 'false'
membersAccessPreviewEnabled: "false" # Show the mocked "Users (preview)" tab in organisation settings to owners 'true' or 'false'
projects:
enabled: "" # Fallback project group switch used only when organisations.enabled is empty
switcherEnabled: "false" # Enable or disable the read-only project dropdown 'true' or 'false'
@@ -776,7 +811,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).
@@ -826,12 +861,17 @@ kerberoshub:
cpu: 10m
# 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.
# Destination credentials are set inline on each destination.
audit:
repository: ghcr.io/uug-ai/hub-audit
pullPolicy: IfNotPresent
tag: "v1.0.10"
replicas: 1
resources: {}
# resources:
# requests:
# memory: 32Mi
# cpu: 10m
dispatchInterval: "5s"
leaseDuration: "2m"
terminationGracePeriodSeconds: 150 # Keep above leaseDuration so an in-flight cycle can finish.
@@ -852,63 +892,57 @@ kerberoshub:
serviceMonitor:
enabled: true
interval: 15s
# Each destination is optional; see the README for every field. Webhook:
destinations: []
# - id: primary-siem
# type: webhook
# enabled: true
# url: https://siem.example.com/api/audit
# timeoutSeconds: 10
# batchSize: 100
# maxAttempts: 10
# startAt: latest # latest | beginning, applied only on first creation
# headers:
# - id: primary-siem # required; ^[a-z][a-z0-9-]{0,62}$, keeps the checkpoint
# type: webhook # optional: webhook (default) | s3
# enabled: true # optional, default true; false pauses delivery
# url: https://siem.example.com/api/audit # required, http or https
# timeoutSeconds: 10 # optional, default 10, at most 60
# batchSize: 100 # optional, default 100, at most 1000
# maxAttempts: 10 # optional, default 10; then the batch is dead-lettered
# startAt: latest # optional: latest (default) | beginning, for a new id only
# headers: # optional; not Authorization or Idempotency-Key
# X-Source: kerberos-hub
# bearerTokenSecret:
# name: audit-primary-siem
# key: token
# # Optional inline credentials. The chart stores them in Secret
# # hub-audit-<id>-credentials and mounts them.
# bearerToken: "xxx" # sent as Authorization: Bearer <token>
# secretHeaders:
# X-Api-Key:
# secretName: audit-primary-siem
# secretKey: api-key
# filters:
# value: "xxx"
# filters: # optional, default all events; an event must match every list set
# 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):
# S3 Object Lock destination. 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
# - id: immutable-archive # required
# type: s3 # required for S3
# enabled: true # optional, default 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.
# bucket: hub-audit-archive # required
# region: eu-west-1 # required, also for S3-compatible stores
# prefix: hub # optional, default none
# endpoint: https://s3.example.com # optional; S3-compatible stores only, omit for AWS S3
# forcePathStyle: true # optional, default false; usually true for Ceph RGW, MinIO and Garage
# # Optional, default false. Only for stores without Object Lock (e.g. Garage):
# # skips the lock check, so archived batches are deletable.
# allowUnlockedBucket: false
# # Omit credentialsSecret to use the pod's AWS identity (IRSA via
# # kerberoshub.audit.serviceAccount, or an instance profile). That only
# # works for AWS S3; every other store needs credentialsSecret.
# credentialsSecret:
# name: audit-immutable-archive
# accessKeyIdKey: access-key-id
# secretAccessKeyKey: secret-access-key
# resources:
# requests:
# memory: 32Mi
# cpu: 10m
# # Optional inline credentials, both or neither. The chart stores them
# # in Secret hub-audit-<id>-credentials and mounts them.
# accessKey: "xxx"
# secretKey: "xxx"
# # Without them, hub-audit uses the pod's AWS identity (IRSA via
# # kerberoshub.audit.serviceAccount, or an instance profile): AWS S3 only.
# hub-workflows is the standalone, queue-driven workflow engine. It consumes
# pipeline events and dispatches the stages declared in its workflow
# definitions, tracking each run in its own `workflow_runs` collection. It shares events
@@ -1141,7 +1175,7 @@ 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
@@ -1229,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).
@@ -1250,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).
@@ -1271,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).
@@ -1289,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).
@@ -1317,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
@@ -1337,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:
@@ -1360,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).
@@ -1398,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).
@@ -1424,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).
@@ -1452,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).
@@ -1479,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).
@@ -1506,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
@@ -1565,3 +1599,10 @@ email:
alertTitle: "[Alert] Kerberos Hub detected something an event"
deviceTitle: "[Device] A Kerberos Agent's status has been changed"
deviceGroupTitle: "[Device] Several cameras changed status"
# Recording check alerts (Alerts → Devices → Recording check). Empty uses
# the "recording" template with SMTP. For SendGrid or Mailgun, set the
# template id; empty uses the device template there.
recording: ""
# Empty uses "[Device] A camera stopped recording", "[Device] A camera is
# recording again" and their forms for several cameras.
recordingTitle: ""
+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."
+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