Compare commits

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 13:33:22 +00:00
KilianandClaude Opus 5.5 5af6472826 feat(hub): grouped device alerts — scan interval and device_group email (0.166.0) (#201)
* feat(hub): configurable device scan interval for hub-monitor-device (0.166.0)

kerberoshub.monitordevice.scanIntervalSeconds (default 60) is passed as
DEVICE_SCAN_INTERVAL_SECONDS: the pause between device status scans,
which also sets how long grouped device notifications are held.

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

* feat(hub): device_group email for grouped camera status notifications

- custom-layout/templates/device_group.html|.txt: the email sent instead of
  device when several cameras of an account change status in one scan and
  the account groups device notifications.
- email.templates.deviceGroup / deviceGroupTitle, passed to
  hub-monitor-device as DEVICE_GROUP_TEMPLATE / DEVICE_GROUP_TITLE, plus
  PUBLIC_URL for the link to the cameras page.
- README: full reference for the device_group email (placeholders, how
  sections are built and rendered, custom templates, webhook
  device_statuses) and the correct {{status}} values of the device email.

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

* docs(hub): device_group webhook data keys and whole-group section titles

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

* docs(hub): device_sections on grouped device webhooks

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

* docs(hub): name the device grouping toggle Group notifications

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

* docs(hub): drop device_statuses/device_sections webhook fields

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

* docs(hub): grouped device notifications skip webhooks and IFTTT; SMTP-only email

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:54:16 +02:00
10 changed files with 762 additions and 4 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.165.0
version: 0.167.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
+136 -1
View File
@@ -31,6 +31,99 @@ Uninstall the Kerberos Hub chart
helm uninstall hub -n kerberos-hub
### Hardened frontend images
New shell-free frontend images run as `65532:65532` and listen on port `8080`.
Before selecting such an image tag, set the following values together. Defaults
remain compatible with legacy images on port `80`; this chart does not upgrade
the frontend image automatically. The Service and ingress remain on port `80`.
```yaml
kerberoshub:
frontend:
containerPort: 8080
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumes:
- name: frontend-tmp
emptyDir:
medium: Memory
sizeLimit: 16Mi
volumeMounts:
- name: frontend-tmp
mountPath: /tmp
```
Merge the temporary volume into any existing branding/translation mounts rather
than replacing them. Custom files must be readable by UID `65532`; mount them
read-only. Only `/tmp` needs writes for generated public runtime configuration
and nginx's PID/temp files. The web root stays read-only. Health checks can use
`GET /healthz` on the named `http` container port. This setting applies to the
primary frontend, not the separately configured legacy demo deployment.
### Passkeys
Users and sub-users can create and manage their own passkeys in **Profile >
Passkeys**, then use **Sign in with a passkey** for passwordless authentication
with device PIN/biometric verification. Existing password/MFA sign-in remains
available. Enrollment and removal require the current password and an
authenticator-app code when MFA is enabled.
Deploy Hub API and frontend images that support passkeys before enabling this
configuration; the chart version alone does not update the service image tags.
Passkeys are disabled by default. Configure their trust explicitly:
```yaml
kerberoshub:
api:
passkeys:
enabled: true
rpId: "hub.example.com"
origins:
- "https://hub.example.com"
rpName: "Kerberos Hub"
```
| Value | Default | Purpose |
| --- | --- | --- |
| `kerberoshub.api.passkeys.enabled` | `false` | Inject passkey configuration into Hub API pods only |
| `kerberoshub.api.passkeys.rpId` | `""` | Required when enabled; sets `PASSKEY_RP_ID` |
| `kerberoshub.api.passkeys.origins` | `[]` | Required when enabled; exact trusted frontend origins, joined with commas into `PASSKEY_ORIGINS` |
| `kerberoshub.api.passkeys.rpName` | `"Kerberos Hub"` | Display name; sets `PASSKEY_RP_NAME` |
Use the **frontend's public hostname and origins**, not the API URL. Origins
must have no path or trailing slash. Each origin hostname must equal the RP ID
or be its subdomain. Include only trusted origins, especially when sharing an
RP ID across tenant subdomains. Keep the RP ID stable: changing it makes
existing passkeys unusable. The API validates the RP/origin relationship at
runtime; missing/invalid configuration makes passkeys unavailable without
affecting password/MFA sign-in.
HTTPS and a compatible browser are required. Local development may use
`rpId: "localhost"` with `origins: ["http://localhost:4200"]`.
The frontend displays translated cancellation, verification and unavailable
messages when a ceremony cannot complete.
Apply values with a Helm upgrade or GitOps sync; changes trigger an API rollout.
If these variables were previously configured through `kerberoshub.extraEnv`,
remove those entries when enabling the dedicated settings to avoid duplicate
environment variables. Setting `enabled: false` omits the chart-managed
variables; any passkey variables supplied through `extraEnv` still take effect.
The API's MongoDB identity needs reads on `users`, CRUD on `passkeys`, and
insert/find-and-delete on `passkey_challenges`. It attempts to create the
`passkey_challenge_expiry_ttl` index on `expiresAt` with
`expireAfterSeconds: 0`; expiration and single-use enforcement work without the
index, but the index is needed for automatic expired-challenge cleanup.
### Sequence logging
Set `kerberospipeline.sequence.logLevel` to control the Sequence service's
@@ -543,6 +636,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.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"` |
@@ -690,6 +784,8 @@ kerberospipeline:
| `email.templates.device` | The template which is send when a camera goes online or offline, by default `'device'`. | `"device"` |
| `email.templates.alertTitle` | Email subject title for the `alert` template. | `"[Alert] Kerberos Hub detected something an event"` |
| `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.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. | `""` |
@@ -743,11 +839,50 @@ 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." |
| `{{status}}` | `online`, `offline` or `idle`. |
| `{{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. |
#### 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`).
| 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." |
| `{{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). |
| `{{sectionsText}}` | The same sections as plain text, for `device_group.txt`. |
| `{{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}}` 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.
`{{sectionsText}}` example:
```text
Site Gate · Offline · all 3 cameras
- Gate camera 1 (no response for 4 min)
- Gate camera 2 (no response for 4 min)
- Gate camera 3 (no response for 4 min)
Other cameras · Camera stream lost · 1 camera
- Lobby camera (agent online, camera stream not connected)
```
**Custom templates** for this email are read by `hub-monitor-device`, so mount the `custom-email-templates` volume on `kerberoshub.monitordevice` as well (see the commented example in `values.yaml`).
**Which channels are grouped.** Grouping applies to the channels people read: email, Slack, Telegram, Pushover, Pushbullet, SMS and Alexa. Webhooks, IFTTT and the real-time feed are never grouped: they keep receiving one notification per camera, with the same fields and at the same moment as without grouping, so existing integrations keep working.
**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.
#### Account and sharing emails
Sent by the API (`kerberoshub.api`). The subject of each email is the matching `email.templates.<name>Title` value.
@@ -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;">Several of your cameras changed status</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">What changed</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;">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>
<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,22 @@
Kerberos.io
------------
Hey, {{user}}
Several of your cameras changed status
{{text}}
{{sectionsText}}
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.
@@ -436,6 +436,15 @@ spec:
- name: MFA_ISSUER
value: "{{ .Values.kerberoshub.api.mfaIssuer }}"
{{- if .Values.kerberoshub.api.passkeys.enabled }}
- name: PASSKEY_RP_ID
value: {{ required "kerberoshub.api.passkeys.rpId is required when passkeys are enabled" .Values.kerberoshub.api.passkeys.rpId | quote }}
- name: PASSKEY_ORIGINS
value: {{ required "kerberoshub.api.passkeys.origins is required when passkeys are enabled" .Values.kerberoshub.api.passkeys.origins | join "," | quote }}
- name: PASSKEY_RP_NAME
value: {{ .Values.kerberoshub.api.passkeys.rpName | quote }}
{{- end }}
# AWS (Legacy, use Kerberos Vault instead)
- name: AWS_REGION
value: "{{ .Values.kerberoshub.api.aws.region }}"
@@ -11,7 +11,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: hub-frontend
@@ -182,12 +182,16 @@ spec:
- name: hub-frontend
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.frontend.repository }}:{{ .Values.kerberoshub.frontend.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.frontend.pullPolicy }}
{{- with .Values.kerberoshub.frontend.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberoshub.frontend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- containerPort: 80
- containerPort: {{ .Values.kerberoshub.frontend.containerPort | default 80 }}
name: http
{{- with .Values.kerberoshub.frontend.volumeMounts}}
volumeMounts:
@@ -67,6 +67,8 @@ spec:
{{- include "hub.mongodb.appUriEnv" (dict "root" $ "app" "hub-monitor-device") | nindent 12 }}
- name: LOG_LEVEL
value: "{{ .Values.kerberoshub.monitordevice.logLevel }}"
- name: DEVICE_SCAN_INTERVAL_SECONDS
value: "{{ .Values.kerberoshub.monitordevice.scanIntervalSeconds | default 60 }}"
- name: READ_ONLY
value: "{{ .Values.readonly }}"
# Mail settings
@@ -82,6 +84,13 @@ spec:
value: "{{ .Values.email.templates.device }}"
- name: DEVICE_TITLE
value: "{{ .Values.email.templates.deviceTitle }}"
- name: DEVICE_GROUP_TEMPLATE
value: "{{ .Values.email.templates.deviceGroup | default "device_group" }}"
- name: DEVICE_GROUP_TITLE
value: "{{ .Values.email.templates.deviceGroupTitle | default "[Device] Several cameras changed status" }}"
# Base URL for the link in grouped device notifications.
- name: PUBLIC_URL
value: "{{ .Values.kerberoshub.frontend.schema }}://{{ .Values.kerberoshub.frontend.url }}"
# - Plain SMTP
- name: SMTP_SERVER
+32
View File
@@ -62,6 +62,38 @@
"api": {
"type": "object",
"properties": {
"passkeys": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean"},
"rpId": {
"type": "string",
"description": "Stable WebAuthn relying-party hostname, without scheme, port or path. Must match each frontend origin's hostname or be its parent domain."
},
"origins": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^https://[^/?#]+$|^http://localhost(:[0-9]+)?$"
},
"description": "Exact trusted frontend origins, with no path or trailing slash. HTTPS is required except for HTTP localhost development."
},
"rpName": {"type": "string"}
},
"if": {
"required": ["enabled"],
"properties": {"enabled": {"const": true}}
},
"then": {
"required": ["rpId", "origins"],
"properties": {
"rpId": {"minLength": 1},
"origins": {"minItems": 1}
}
}
},
"corsAllowedOrigins": {
"type": "array",
"items": {
+18
View File
@@ -392,6 +392,13 @@ kerberoshub:
# MFA issuer name
mfaIssuer: "yourdomain.com"
# WebAuthn passkeys for users and sub-users; requires compatible API/frontend images.
# Use the public frontend hostname/origins, not the API URL. Keep rpId stable.
passkeys:
enabled: false
rpId: "" # e.g. "hub.example.com", without scheme, port or path.
origins: [] # e.g. ["https://hub.example.com"]; exact origins, no trailing slash.
rpName: "Kerberos Hub"
# Admin API's are made available for automation of Kerberos Hub.
# To access those API's (e.g. creation of owner users), an API key needs to be provided.
apiKey: "a-random-admin-api-key"
@@ -467,6 +474,9 @@ kerberoshub:
pullPolicy: IfNotPresent
tag: "v1.13.9"
replicas: 2
# Hardened frontend images listen on 8080; retain 80 for existing images.
containerPort: 80
securityContext: {}
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Set to false to skip rendering the hub-frontend Service (e.g. when an
# external/shared Service is managed elsewhere).
@@ -1071,6 +1081,10 @@ kerberoshub:
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.
scanIntervalSeconds: 60
resources:
requests:
memory: 10Mi
@@ -1470,5 +1484,9 @@ email:
disabled: "disable"
highupload: "highupload"
device: "device"
# Sent instead of "device" when several cameras of an account change status
# in the same scan and the account groups device notifications.
deviceGroup: "device_group"
alertTitle: "[Alert] Kerberos Hub detected something an event"
deviceTitle: "[Device] A Kerberos Agent's status has been changed"
deviceGroupTitle: "[Device] Several cameras changed status"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
extract_passkeys() {
awk '
/^---/ { deployment=0; name=""; variable="" }
/^kind: Deployment$/ { deployment=1 }
deployment && /^ name:/ { name=$2 }
deployment && /- name: PASSKEY_/ { variable=$3; next }
variable != "" && /value:/ {
value=$0
sub(/^[[:space:]]*value: /, "", value)
print name, variable, value
variable=""
}
' | sort
}
assert_passkeys() {
local rendered="$1" expected="$2" actual count expected_count
actual="$(printf '%s\n' "$rendered" | extract_passkeys)"
if [[ "$actual" != "$expected" ]]; then
printf 'Unexpected passkey environment:\n%s\nExpected:\n%s\n' "$actual" "$expected" >&2
return 1
fi
count="$(printf '%s\n' "$rendered" | awk '/- name: PASSKEY_/ { count++ } END { print count+0 }')"
expected_count="$(printf '%s\n' "$expected" | awk 'NF { count++ } END { print count+0 }')"
if [[ "$count" != "$expected_count" ]]; then
echo "Passkey variables leaked outside the expected Deployment or were duplicated" >&2
return 1
fi
}
CONFIGURED_FLAGS=(
--set-json kerberoshub.api.passkeys.enabled=true
--set-json 'kerberoshub.api.passkeys.rpId="example.com"'
--set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com","https://legacy.example.com:8443"]'
--set-json 'kerberoshub.api.passkeys.rpName="Example Hub"'
)
expected="$(printf '%s\n' \
'hub-api PASSKEY_ORIGINS "https://hub.example.com,https://legacy.example.com:8443"' \
'hub-api PASSKEY_RP_ID "example.com"' \
'hub-api PASSKEY_RP_NAME "Example Hub"' | sort)"
for mode in all ui pipeline; do
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode")"
assert_passkeys "$default_render" ""
configured_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${CONFIGURED_FLAGS[@]}")"
if [[ "$mode" == pipeline ]]; then
assert_passkeys "$configured_render" ""
else
assert_passkeys "$configured_render" "$expected"
fi
echo "OK ($mode): defaults opt out; enabled settings affect Hub API only"
done
disabled_render="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" --set kerberoshub.api.passkeys.enabled=false)"
assert_passkeys "$disabled_render" ""
localhost_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set kerberoshub.api.passkeys.enabled=true \
--set-string kerberoshub.api.passkeys.rpId=localhost \
--set-json 'kerberoshub.api.passkeys.origins=["http://localhost:4200"]')"
assert_passkeys "$localhost_render" "$(printf '%s\n' \
'hub-api PASSKEY_ORIGINS "http://localhost:4200"' \
'hub-api PASSKEY_RP_ID "localhost"' \
'hub-api PASSKEY_RP_NAME "Kerberos Hub"' | sort)"
legacy_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set-json 'kerberoshub.extraEnv=[{"name":"PASSKEY_RP_ID","value":"example.com"}]')"
printf '%s\n' "$legacy_render" | grep -q -- '- name: PASSKEY_RP_ID'
expect_invalid() {
local output
if output="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" "$@" 2>&1)"; then
echo "Invalid passkey settings unexpectedly rendered: $*" >&2
return 1
fi
if [[ "$output" != *"passkeys"* ]]; then
printf 'Rendering failed for an unexpected reason:\n%s\n' "$output" >&2
return 1
fi
}
expect_invalid --set-string kerberoshub.api.passkeys.rpId=
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=[]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com/"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["http://hub.example.com"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com","https://hub.example.com"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins="https://hub.example.com"'
expect_invalid --set-json 'kerberoshub.api.passkeys.enabled="true"'
expect_invalid --set-json 'kerberoshub.api.passkeys.rpId=123'
expect_invalid --set-json 'kerberoshub.api.passkeys.rpName=123'
expect_invalid --set-string kerberoshub.api.passkeys.origin=https://hub.example.com
echo "All Hub passkey configuration checks passed."