Skip to content

Configuration

All configuration goes through Helm values. Create a values.yaml with your overrides and apply it:

helm upgrade kubesentry kubesentry/kubesentry \
  --namespace kubesentry \
  -f values.yaml

Notifications

KubeSentry notifies you two ways:

  • Instant — a new Critical alert (Falco CRITICAL, EMERGENCY, or ALERT) fires every channel you've configured, in parallel, within seconds.
  • Daily digest — a 24-hour rollup, email only.

High and Warning alerts land on the dashboard and in the digest but never page you. See how severity works for the full mapping, and Verify your installation for a Critical trigger you can use to test delivery.

Every channel is off by default and enabled independently. Delivery failures are logged and never block an alert from being stored.

Email

Email is delivered through Resend. Because KubeSentry is self-hosted, it sends through your own Resend account — there is no default sender. Enabling email requires two things you provide:

  1. A Resend API key (resendApiKey for testing, or existingSecret in production).
  2. A from address on a domain you have verified in that same Resend account (resend.com/domains). Resend only accepts mail from domains you control, so no shared or default address will work for delivery to your team.

to is a list.

notifications:
  email:
    enabled: true
    from: "alerts@yourdomain.com"    # an address on a domain YOU verified in Resend
    to:
      - "you@your-team.com"
      - "security@your-team.com"
    resendApiKey: "re_xxxxxxxx"      # testing only — prefer existingSecret in production

If enabled: true but from (or a credential) is blank, the install still succeeds — detection and the dashboard always run — but no email is sent. The collector logs a loud warning at startup saying exactly what's missing, so check the collector logs if alerts aren't arriving.

403 from Resend

A resend returned 403 error in the collector logs means the from domain isn't verified on your Resend account. Verify the domain at resend.com/domains and set from to an address on it — never a domain you don't control.

Keep secrets out of values.yaml

Don't commit real API keys. Instead of inlining resendApiKey, create a Kubernetes Secret with the key resend-api-key and reference it:

notifications:
  email:
    enabled: true
    from: "alerts@yourdomain.com"
    to: ["you@your-team.com"]
    existingSecret: "kubesentry-email"   # Secret with key: resend-api-key

Slack / Discord / Teams

Each chat channel takes a webhook URL. Inline webhookUrl is fine for testing; in production reference an existingSecret instead (keys: slack-webhook-url, discord-webhook-url, teams-webhook-url).

notifications:
  slack:
    enabled: true
    webhookUrl: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
    # or: existingSecret: "kubesentry-slack"   # key: slack-webhook-url
  discord:
    enabled: false
    webhookUrl: ""
  teams:
    enabled: false
    webhookUrl: ""

Generic webhook

Any HTTP endpoint of your own — a SIEM, a ticketing system, an internal handler. Instead of a chat-platform card, it receives the full alert as raw KubeSentry JSON, including the remediation advice:

{
  "type": "kubesentry.alert",
  "version": "1.0",
  "timestamp": "2026-07-27T13:55:11Z",
  "cluster": "prod-us-east",
  "alert": {
    "id": 42,
    "rule": "KubeSentry: Sudoers or passwd modified in container",
    "priority": "Critical",
    "pod": "payments-api-7d8f",
    "namespace": "prod",
    "container": "payments-api",
    "image": "ghcr.io/acme/payments-api",
    "output": "13:55:11.144843182: Critical Sensitive auth file opened for writing …",
    "count": 1,
    "first_seen": "2026-07-27T13:55:11Z",
    "last_seen": "2026-07-27T13:55:11Z",
    "advice": {
      "what": "A process opened /etc/passwd, /etc/shadow, or /etc/sudoers for writing …",
      "triage": ["…"],
      "remediation": ["…"],
      "references": ["https://attack.mitre.org/techniques/T1136/"]
    }
  }
}

It's a POST with Content-Type: application/json and no authentication header — if your endpoint needs a credential, put it in the URL (a signed path or query token) or terminate the request behind a proxy that adds the header.

notifications:
  generic:
    enabled: true
    webhookUrl: "https://my-system.example.com/kubesentry"
    # or: existingSecret: "kubesentry-generic"   # key: generic-webhook-url

Microsoft Teams: check which webhook URL you have

KubeSentry posts a MessageCard, the format Office 365 incoming webhook connectors accept. Microsoft is replacing those connectors with Power Automate Workflows, whose URLs expect a different payload — a MessageCard sent to a Workflows URL won't render as a card unless the flow is built to parse one. This is a connectivity question, not a capability one: on a classic connector URL, Teams gets the same alert content as every other channel, remediation included.

If your tenant has already retired classic connectors, point a generic webhook at a Workflow you author yourself. You get the complete alert as JSON and full control over the card format.

Wiring a channel from the CLI

If you'd rather not put credentials in a values file at all, create the Secret yourself and point the chart at it. Reading the value with read -rs keeps it out of your shell history and off the process list.

Slack:

read -rs SLACK_URL
kubectl create secret generic kubesentry-slack -n kubesentry \
  --from-literal=slack-webhook-url="$SLACK_URL"
unset SLACK_URL

helm upgrade <release> kubesentry/kubesentry -n kubesentry --reuse-values \
  --set notifications.slack.enabled=true \
  --set notifications.slack.existingSecret=kubesentry-slack

Discord and Teams are identical — swap the channel name and the Secret key (discord-webhook-url, teams-webhook-url):

read -rs DISCORD_URL
kubectl create secret generic kubesentry-discord -n kubesentry \
  --from-literal=discord-webhook-url="$DISCORD_URL"
unset DISCORD_URL

helm upgrade <release> kubesentry/kubesentry -n kubesentry --reuse-values \
  --set notifications.discord.enabled=true \
  --set notifications.discord.existingSecret=kubesentry-discord

Generic webhook — same shape, key generic-webhook-url:

read -rs GENERIC_URL
kubectl create secret generic kubesentry-generic -n kubesentry \
  --from-literal=generic-webhook-url="$GENERIC_URL"
unset GENERIC_URL

helm upgrade <release> kubesentry/kubesentry -n kubesentry --reuse-values \
  --set notifications.generic.enabled=true \
  --set notifications.generic.existingSecret=kubesentry-generic

Email needs the API key in a Secret plus two plain values — the verified sender and the recipient list:

read -rs RESEND_KEY
kubectl create secret generic kubesentry-email -n kubesentry \
  --from-literal=resend-api-key="$RESEND_KEY"
unset RESEND_KEY

helm upgrade <release> kubesentry/kubesentry -n kubesentry --reuse-values \
  --set notifications.email.enabled=true \
  --set notifications.email.existingSecret=kubesentry-email \
  --set notifications.email.from=alerts@yourdomain.com \
  --set 'notifications.email.to={you@yourdomain.com,security@yourdomain.com}'

to is a list

Note the braces: --set 'notifications.email.to={a@x.com,b@x.com}' is how Helm sets an array on the command line. Quote the whole argument so your shell doesn't try to expand the braces.

<release> is whatever you named the Helm release — any name works, the collector's Service name is pinned. --reuse-values keeps the rest of your configuration as it is; without it, unspecified values fall back to chart defaults.

Daily digest

A once-a-day summary of the last 24 hours, with a comparison against the previous day. Enabled by default at 09:00 UTC.

The digest is email only — it uses the email channel configured above and is never sent to Slack, Discord, or Teams. If email isn't configured, no digest goes out regardless of this setting.

digest:
  enabled: true
  cron: "0 9 * * *"
  timezone: "UTC"        # e.g. "Europe/Zagreb"

Storage

Alert history is written to a PersistentVolume so it survives pod restarts and reinstalls.

collector:
  persistence:
    enabled: true
    size: 1Gi
    storageClass: ""     # empty = cluster default

The collector stores events in an embedded SQLite database on this volume. For most small teams the default size is plenty; increase it if you retain a lot of history.

Dashboard exposure

By default the dashboard Service is ClusterIP — reachable only in-cluster. The dashboard has no built-in authentication, so never expose it directly to the internet.

To make it reachable, front it with your own ingress or reverse proxy with authentication (e.g. an OAuth proxy or basic-auth annotations on your ingress controller), pointing at the kubesentry-collector Service on port 8080. So links in emails and webhooks use the right address, set the public URL:

dashboard:
  url: "https://kubesentry.internal.your-domain.com/dashboard"

Resource requests

The collector is lightweight (the image is a ~15 MB distroless build). Falco is the heavier component since it runs on every node. Defaults are sensible for small clusters; tune if needed:

collector:
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 500m
      memory: 512Mi

Cluster identity

Name shown in the dashboard top bar, email subjects, and webhook payloads:

cluster:
  name: "prod-us-east"

Tuning detection noise

Every cluster has legitimate activity that looks suspicious to default rules (init scripts, debugging, CI jobs). Falco rules are customizable — add exceptions or custom rules through the falco values block in the chart. See the Falco rules documentation for rule syntax.

Applying changes

Any config change is applied the same way:

helm upgrade kubesentry kubesentry/kubesentry -n kubesentry -f values.yaml

Pods roll automatically. Alert history on the PVC is preserved across upgrades.