Enforcement
¶
What this is¶
Three sidebar pages work together to stop risky AI behaviour at runtime:
| Page | Sidebar | What it is for |
|---|---|---|
| Enforcement | Govern | The rules: what to watch for and what to do when it happens. |
| Hits | Protect | Every time a rule fired, newest first. |
| Block List | Protect | Who is blocked right now, for how long — and lifting blocks. |
Rules act only on namespaces opted in with Enforce. In a Monitor namespace a rule records
what it would have done but blocks nothing — on any enforcement lane. See
Getting started.
Enforcement (rules)¶
- Application dropdown — All applications or one application.
- The rule list. Each rule shows its name, condition and action, a switch to turn it on or off,
edit and delete. Add rule creates one. - After you change rules, a bar shows the scanners picking up the new rule set.
Default rules on a new workspace¶
Every workspace starts with these rules, all switched on:
| Rule | Action |
|---|---|
| Block Prompt Injection | Block |
| Block Jailbreaks | Block |
| High Risk Session Block — after more than 3 blocks in a session | Block |
| Scanner-Flagged Session Block — sessions the scanner flagged for jailbreak or injection | Block |
| Non-AI Egress Block — personal data sent to non-AI destinations | Block |
| Unauthenticated MCP Block — MCP calls without authentication | Block |
| PII Exfiltration Alert — personal data in session traffic | Alert |
| Critical CVE Alert — sessions from applications with critical or high CVEs | Alert |
They block only in namespaces opted in with Enforce; in Monitor namespaces they record what
they would have done. To stop a default rule, switch it off rather than deleting it.
Non-AI Egress Block refuses a request that carries personal data to a destination outside the
AI providers, inline, for that request. When the session has no caller identity (for example a
service talking to its own database), a match is recorded as an Alert and no session block is
placed: without an identity, a session block would apply to the whole application and every
destination it uses.
If you delete every rule, the list says Click "Add rule" to create one.
Rule fields¶
| Field | What you set |
|---|---|
| Rule name, Description | Your label for the rule. |
| Category | Prompt security, Data protection, Runtime controls, Network egress, MCP tools, Supply chain. |
| Severity | How serious a hit is. |
| Source, Destination | Which traffic the rule applies to. |
| Condition Type and Condition Value | What triggers it — see below. Some types use Measured over the last (a time window). |
| Action | Block, Quarantine, Alert, Log; Redact (mask the matched content) on content conditions only. |
| Blast radius | Who is blocked: This identity, in this app, This identity, everywhere, This run only, This app pair only, This endpoint only. |
| Kill duration | How long a block lasts. Tenant default uses Settings → Preferences → Kill duration — automatic rule blocks. |
| Enabled | Rule is active and enforced when on. |
Condition types: Regex pattern, Findings in session >=, Injection detections >=,
PII items leaving >=, Blocked destination host, Unauthenticated MCP tool call, Scanner
flagged the session, Blocked requests in window >=, Supply-chain CVE severity, App must
not call app, Endpoint must not be served. Redact is offered for Regex pattern,
Blocked destination host and Unauthenticated MCP tool call.
When you delete a rule that still has active blocks, the dialog offers to lift them too.
Hits¶
Every time a rule fired. Filter by action: All actions, Blocked, Quarantined,
Alerted, Redacted, Monitor (recorded, not blocked), Lifted. Each row shows when it
happened and the flow it acted on. Expand a row for details, then open:
- the session it acted on, in Traffic;
- the user's entries on the Block List.
No enforcement events yet means no rule has fired: either there are no rules, or nothing has
matched them.
Block List¶
Everyone who is blocked right now. Blocks come from Kill Session in
Traffic and from rules whose action blocks.
- The count of active entries, and an application filter that lists only applications that hold
blocks. - Each entry: the identity, the application, the scope (Everywhere, This app, This
run, …; Whole source IP when no identity could be resolved), the reason, and when it
expires (for example in 47m, or Never for blocks set Until manually lifted). - Lift ends one block. When the list is filtered to one identity, Lift all for
ends all of theirs. Traffic is allowed again as soon as the scanner picks up the change.
How to use it¶
Create a rule¶
-
Open Enforcement → Add rule.
-
Pick a Category, a Condition Type and its Condition Value.
-
Pick an Action and a Blast radius. Start with Alert or Log to see what would
match, then switch to Block. -
Make sure Enabled is on, click Save.
Find out why something was blocked¶
-
Open Hits and filter by Blocked.
-
Expand the row and open the session to see the exact request.
Unblock someone¶
-
Open Block List and find the identity.
-
Click Lift.
Where to go next¶
| From | You land in |
|---|---|
| A hit → session | Traffic, at that session |
| A hit → block list entries | Block List, filtered to that user |
| A Block List entry → session | Traffic, at that session |
How you get here¶
- Sidebar → Govern → Enforcement; sidebar → Protect → Hits or Block List.
- Kill Session in Traffic adds the user to the Block List.
- Flow → Boundary → Block creates a rule here — see Traffic → Flow.
How it works¶
How session-level termination is implemented¶
The scanner attaches eBPF programs to TCP egress hooks on each instrumented pod. Every outbound connection is recorded in an in-kernel map keyed by connection tuple (source IP, source port, destination, PID), with the session ID the scanner has stitched for that flow attached. A second map carries the per-session verdict: allow (default) or block.
When the verdict for a session flips to block — because an operator pressed Kill Session in Traffic, or because an inline rule fired on a call inside that session — the eBPF program emits a TCP RST on the next packet of every matching connection instead of forwarding it. The connection is reset, not drained: the agent sees ECONNRESET on every active socket of that session within one round-trip. Other sessions on the same pod, and sessions of other pods on the same node, are untouched.
The same eBPF path handles two cases:
-
Block this session now (operator-initiated, from a session row).
-
Block calls matching this inline rule from now on (rule-initiated, after the proxy detects a match the scanner sets the verdict for that session).
The verdict map is queried by the local MITM proxy over a localhost endpoint (GET /api/policy?src_ip=…), so HTTPS sessions are caught at the proxy layer in addition to the kernel-level RST — the proxy refuses to relay the bytes onward and the kernel reset stops any retry attempt on the same socket.
Enforcement lanes¶
The scanner enforces on three independent lanes. Switching one off does not switch off the others —
in particular, proxy.enabled: false leaves the kernel lane on.
| Lane | What it does | Switch |
|---|---|---|
| Proxy | Refuses a request inline (HTTP 403 or a tool error), for exactly the matched caller and destination. | proxy.enabled |
| Kernel | eBPF drops and TCP resets on the scanner's node: an application's egress when a session block names no caller, and traffic back to a blocked caller's public address. | enforcement.kernelLane.enabled |
| Agent runtime policy | In-process checks of programs, files and libraries for Python and Node.js. | agents.runtimePolicy (off by default) |
The kernel lane is bounded on every node:
-
It acts only on pods in Enforce scope (
higgs.io/enforce=enabledon the pod or its
namespace). A Monitor pod is never blocked. A pod that exits is released at once; a pod
whosehiggs.io/enforcelabel is removed (from the pod or its namespace) is released within
about one minute. -
It never acts on a
hostNetworkpod or on the node's own interfaces. An enforce-labelled
hostNetworkpod gets the eventAIHKernelLaneRefusedinstead. -
It drops only a pod's traffic to public destinations — ones that pod was seen using, or a blocked caller's address. Private and
internal addresses (RFC 1918, carrier-grade NAT, IPv6 unique-local), link-local addresses such as
the cloud metadata server, and the node proxy are never dropped; the proxy lane enforces those. -
It never blocks a pod started after the scanner on that node received the block, for example
a recreated pod. One exception: when the scanner restarts and receives a block before it has
discovered the application's pods, that block covers every pod of the application on the node
until it is lifted or expires. -
If the scanner stops — uninstalled, crashed or evicted — its drops stop within two minutes, even
though the eBPF objects outlive the scanner process so that a restart keeps enforcing. -
While a pod has an active kernel drop, the scanner records less of that pod's outbound traffic:
the drop program handles the pod's sends, and the capture of sends does not see them. Inbound
traffic and other pods are not affected, and full capture resumes when the block is
lifted or expires.
What you see on the pod¶
Every pod the kernel lane acts on carries a Kubernetes event, so kubectl describe pod and
kubectl get events show it next to the pod's other events:
| Reason | Type | When |
|---|---|---|
AIHKernelEgressBlocked |
Warning | The kernel lane drops some of the pod's egress. The message names the block and the number of destinations. |
AIHKernelEgressReleased |
Normal | It stopped: the block was lifted, or the pod left Enforce scope. |
AIHKernelLaneRefused |
Warning | The pod is labelled for Enforce but uses hostNetwork; the kernel lane is not applied. |
To stop all kernel-level enforcement on a pod, remove higgs.io/enforce from the pod or its
namespace. To turn the lane off cluster-wide, set enforcement.kernelLane.enabled: false; on start
the scanner then also removes anything an earlier version left on the node.
What the agent sees when something is blocked¶
The exact failure surface depends on which primitive fired:
-
Namespace quarantine. Outbound connections to AI provider domains fail at connect time. Most SDKs surface this as
ECONNREFUSEDor DNS-resolution failure if the resolver itself is short-circuited. -
Inline block. The proxy returns a synthetic error response to the agent (HTTP 4xx with a structured body for OpenAI-shaped APIs,
tool_result.is_error=truefor MCP tool calls). The model provider never sees the call. -
Session termination. Every open connection of the session terminates with
ECONNRESET. SDKs surface this as a retryable network error; depending on retry behavior, the next attempt either matches an inline rule (and is rejected) or hits a fresh session ID.
Application code that retries unconditionally may still complete the action on a fresh session. For consistent enforcement of intent, pair the session kill with an inline rule that prevents the next attempt.
Auditability¶
Every enforcement decision is logged. The proxy emits one record per evaluated call (matched rule, action taken, latency); the kernel-side quarantine emits a record per first-blocked connection; session terminations emit a record per killed session. Records carry the rule identifier, the operator who applied the rule, and a hash of the matched field. They appear in Notifications, in the per-app finding feed, and in the audit trail packaged by Compliance.