Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,9 @@ jobs:
- 'scripts/tests/test-guard-helm-post-renderers.sh'
- 'scripts/tests/test-kubescape-storage-placement.sh'
- 'scripts/tests/test-kubescape-runtime-rules-switch.sh'
- 'scripts/generate-kubescape-rule-schema.sh'
- 'scripts/tests/kubescape-rule-schema/**'
- 'schemas/**'
- 'scripts/build-controller-helm.sh'
- 'scripts/prepare-helm-render-input-capture.sh'
- 'scripts/render-helm-chart/**'
Expand Down Expand Up @@ -4131,7 +4134,7 @@ jobs:
${{ github.event.pull_request.base.sha ||
github.event.merge_group.base_sha }}
run: |
shellcheck scripts/tests/test-kubescape-runtime-rules-switch.sh
shellcheck scripts/tests/test-kubescape-runtime-rules-switch.sh scripts/generate-kubescape-rule-schema.sh
changed_inputs="$(git diff --name-only "$BASE_SHA" HEAD -- \
.github/workflows/ci.yaml \
k8s/bases/infrastructure/controllers/kubescape \
Expand All @@ -4140,6 +4143,7 @@ jobs:
k8s/providers/hetzner/infrastructure/controllers/kubescape \
k8s/bases/components ksail.prod.yaml \
scripts/tests/test-kubescape-runtime-rules-switch.sh \
scripts/generate-kubescape-rule-schema.sh schemas \
scripts/build-controller-helm.sh \
scripts/render-helm-chart/go.mod scripts/render-helm-chart/go.sum)"
if [[ -n "$changed_inputs" ]]; then
Expand Down
28 changes: 19 additions & 9 deletions docs/runtime-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,28 @@ alone.
What it does **not** do: it is **detection-only**. It will tell you a process
misbehaved; it will not stop it.

**Detection needs rules, and production does not load them yet.** The node-agent
**Detection needs definitions and an enabled binding.** The node-agent
checks only the rules it finds in `Rules` and `RuntimeRuleAlertBinding` objects.
The chart installs its default rule set and a binding over every scanned namespace
only when `alertCRD.installDefault` is true. Production declares that switch,
**off**, in
only when `alertCRD.installDefault` is true. Production declares that switch on in
[`install-default-runtime-rules.yaml`](../k8s/providers/hetzner/infrastructure/controllers/kubescape/patches/install-default-runtime-rules.yaml),
so the agent keeps building profiles but raises no alerts: an empty alert route
means nothing was checked. Turning the switch on adds exactly those two objects
and sends alerts through the existing Alertmanager (Slack) and stdout
(Coroot) routes. `scripts/tests/test-kubescape-runtime-rules-switch.sh` renders
the pinned chart in both states and refuses an undeclared switch, an on state
that binds no enabled rule, and an on state that changes anything else (#4968).
and appends a post-renderer that retains only the vendor's **R2001** definition,
pod port-forward detection, and binds its stable ID. The pinned rule does not require a learned profile, so incomplete
profiles do not prevent this initial baseline. The scanned namespace scope and the
existing Alertmanager and stdout (Coroot) routes are unchanged. Installing the
definitions and binding changes no RBAC, workload or CRD.

This baseline is a first stage, not complete runtime coverage. Anomaly rules and
other currently unbound rules need separate execution and alert-volume evidence
before expanding the binding (#4968, #4978). A successful render is not evidence
that an alert reached its destination: rollout acceptance needs non-zero live rule
and binding readback, a controlled port-forward event, and observed delivery.

`scripts/tests/test-kubescape-runtime-rules-switch.sh` renders the pinned chart
in both switch states, as installs and upgrades. It rejects an enabled detector
with no installed baseline, unknown or additional bound rules, a profile-dependent
or changed port-forward definition, changed namespace scope, and changes to other
objects. Renaming a rule while retaining its ID must preserve the binding.

### Tetragon — *enforcement and precise kernel observability*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,17 @@ spec:
interval: 10m
values:
alertCRD:
# Release switch for the node-agent's default detection rules (#4968). It is
# OFF: production runs runtime detection with NO rules, so the agent profiles
# workloads but checks nothing, and an empty alert route means nothing was
# checked, not that nothing happened.
# Install the chart's rule definitions for the initial runtime-detection
# baseline (#4968). The appended runtime-rule-baseline post-renderer retains
# and binds only R2001, the profile-independent pod port-forward rule. It does not
# activate the chart's anomaly bindings while profiles remain incomplete.
#
# The chart installs its `default-rules` Rules object and the cluster-wide
# `all-rules-all-pods` RuntimeRuleAlertBinding only when this is true, and
# defaults it to false. Turning it on adds exactly those two objects and
# nothing else (no RBAC, workload or CRD change). The binding covers every
# pod outside `excludeNamespaces`, and alerts then flow to the existing
# Alertmanager (Slack) and stdout (Coroot) routes.
#
# It stays off until the rollout is decided: the anomaly rules depend on
# complete application profiles, several workloads have only partial ones,
# and the alert volume on the Slack route has never been measured.
# The switch adds only the Rules and binding objects: RBAC, workloads,
# CRDs, scanned namespaces and existing alert routes stay unchanged.
# Binding by ID also avoids the chart's mismatched "Port forward" name
# (#4978). Broader rule coverage still needs its own measured rollout.
#
# Keep the value explicit. scripts/tests/test-kubescape-runtime-rules-switch.sh
# fails when it is absent, renders the pinned chart in both states, and
# requires the on state to bind at least one enabled rule.
installDefault: false
# requires the on state to bind exactly the supported R2001 baseline.
installDefault: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Append rather than replace the release's existing hardening post-renderers.
- op: add
path: /spec/postRenderers/-
value:
kustomize:
patches:
- target:
group: kubescape.io
version: v1
kind: Rules
name: default-rules
# Retain the vendor's definition without copying its implementation.
# The index is pinned to chart 1.40.5; the test fails closed on a move.
patch: |-
- op: test
path: /spec/rules/27/id
value: R2001
- op: copy
from: /spec/rules/27
path: /spec/portForwardBaseline
- op: replace
path: /spec/rules
value: []
- op: move
from: /spec/portForwardBaseline
path: /spec/rules/-
- target:
group: kubescape.io
version: v1
kind: RuntimeRuleAlertBinding
name: all-rules-all-pods
patch: |-
- op: test
path: /metadata/name
value: all-rules-all-pods
- op: add
path: /metadata/labels/devantler.tech~1runtime-rule-stage
value: port-forward-baseline
- op: replace
path: /spec/rules
value:
- ruleID: R2001
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,15 @@ patches:
# in-cluster Alertmanager (alertmanager/ above) + keep the stdout exporter on,
# fanning them out to Slack and Coroot — see file.
- path: kubescape/patches/route-runtime-detection-alerts.yaml
# Prod-only release switch for the node-agent's default detection rules
# (#4968). OFF: runtime detection runs with no rules until it is turned on.
# Prod-only initial detection baseline (#4968): install the definitions, then
# bind only the profile-independent port-forward rule by its stable ID.
- path: kubescape/patches/install-default-runtime-rules.yaml
- path: kubescape/patches/runtime-rule-baseline.yaml
target:
group: helm.toolkit.fluxcd.io
version: v2
kind: HelmRelease
name: kubescape
# NB: the Coroot CR moved to the `infrastructure` layer (so the operator's
# CRD is installed first), so its hetzner patch now lives in
# ../coroot/patches/ and is applied by ../kustomization.yaml, not here.
2 changes: 2 additions & 0 deletions ksail.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -388,4 +388,6 @@ spec:
# compile, which it reports as a missing schema. So stay on the revision
# just before it, in both configs. Remove once the catalogue compiles again.
schemaLocations:
# Generated one-field correction from the pinned Kubescape Rules CRD.
- schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
- https://raw.githubusercontent.com/datreeio/CRDs-catalog/6ec4c804fcb9b0a90fec9dd468aa57b984be150a/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
2 changes: 2 additions & 0 deletions ksail.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ spec:
# compile, which it reports as a missing schema. So stay on the revision
# just before it, in both configs. Remove once the catalogue compiles again.
schemaLocations:
# Generated one-field correction from the pinned Kubescape Rules CRD.
- schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
- https://raw.githubusercontent.com/datreeio/CRDs-catalog/6ec4c804fcb9b0a90fec9dd468aa57b984be150a/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
18 changes: 18 additions & 0 deletions schemas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Validation schema corrections

`kubescape.io/rules_v1.json` is generated by
`bash scripts/generate-kubescape-rule-schema.sh --write`. Do not edit it directly.

The pinned CRDs-catalog revision `6ec4c804fcb9b0a90fec9dd468aa57b984be150a`
omits `profileDataRequired`, which the pinned Kubescape operator chart 1.40.5
ships in its Rules CRD and uses in its default rule definitions. The generator
verifies both input hashes, copies that one property from the shipped CRD, and
proves every existing catalog constraint is unchanged. Both KSail configurations
consult this local correction before their unchanged catalog fallback. No kind
is skipped and other CRD schemas are unchanged.

Run `bash scripts/generate-kubescape-rule-schema.sh --check` to verify provenance,
`go test ./scripts/tests/kubescape-rule-schema` for valid and invalid resource
controls, and both normal KSail validation commands for the actual manifests.
The runtime-rule chart regression also checks that this file is reproducible.
A chart-pin change requires auditing and regenerating the correction.
153 changes: 153 additions & 0 deletions schemas/kubescape.io/rules_v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
{
"properties": {
"apiVersion": {
"type": "string"
},
"kind": {
"type": "string"
},
"metadata": {
"type": "object"
},
"spec": {
"additionalProperties": false,
"properties": {
"rules": {
"items": {
"additionalProperties": false,
"properties": {
"agentVersionRequirement": {
"description": "Agent version requirement to evaluate this rule (supports semver ranges like ~1.0, >=1.2.0, etc.)",
"type": "string"
},
"description": {
"description": "Description of the rule",
"type": "string"
},
"enabled": {
"description": "Whether the rule is enabled",
"type": "boolean"
},
"expressions": {
"additionalProperties": false,
"properties": {
"message": {
"description": "Message expression",
"type": "string"
},
"ruleExpression": {
"items": {
"additionalProperties": false,
"properties": {
"eventType": {
"description": "Type of event this expression handles",
"type": "string"
},
"expression": {
"description": "The rule expression string",
"type": "string"
}
},
"required": [
"eventType",
"expression"
],
"type": "object"
},
"type": "array"
},
"uniqueId": {
"description": "Unique identifier expression",
"type": "string"
}
},
"required": [
"message",
"uniqueId",
"ruleExpression"
],
"type": "object"
},
"id": {
"description": "Unique identifier for the rule",
"type": "string"
},
"isTriggerAlert": {
"default": true,
"description": "Whether the rule is a trigger alert",
"type": "boolean"
},
"mitreTactic": {
"description": "MITRE tactic associated with the rule",
"type": "string"
},
"mitreTechnique": {
"description": "MITRE technique associated with the rule",
"type": "string"
},
"name": {
"description": "Name of the rule",
"type": "string"
},
"profileDataRequired": {
"description": "Per-rule profile fields required for rule-aware projection.",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"profileDependency": {
"description": "Profile dependency level (0=Required, 1=Optional, 2=NotRequired)",
"enum": [
0,
1,
2
],
"type": "integer"
},
"severity": {
"description": "Severity level of the rule",
"type": "integer"
},
"state": {
"additionalProperties": true,
"description": "State information for the rule",
"type": "object"
},
"supportPolicy": {
"default": false,
"description": "Whether the rule supports rule policy enforcement",
"type": "boolean"
},
"tags": {
"description": "Tags associated with the rule",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"enabled",
"id",
"name",
"description",
"expressions",
"profileDependency",
"severity",
"supportPolicy",
"isTriggerAlert",
"mitreTechnique",
"mitreTactic"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"rules"
],
"type": "object"
}
},
"type": "object"
}
Loading
Loading