The version described here is policy.pahlevan.io/v1beta1, which is what
the API server stores. v1alpha1 is still served so existing objects and
clients keep working, and the API server converts between the two. The
v1alpha1 fields with no v1beta1 counterpart are listed, with the reason
each one is gone, in IntentionallyDropped in
pkg/apis/policy/v1alpha1/conversion.go.
That matters more than it sounds. The previous version of this file was
hand-written and described an API that had never existed - learning:
instead of learningConfig:, enforcement: instead of
enforcementConfig:, plus whole subsystems the CRD has never had. A
Kubernetes API server does not reject an unknown field in a custom
resource; it prunes it. So anyone who copied from it got a policy that
applied cleanly and did a fraction of what they asked for, with no error
anywhere.
Fields marked inert are accepted by the API and acted on by nothing.
They are listed rather than hidden, because finding one in a cluster and
not knowing is worse than being told. pahlevan policy explain -f names
every one in a given policy.
PahlevanPolicy
is the Schema for the pahlevanpolicies API
| Field | Type | Required | Description |
|---|---|---|---|
metadata |
metav1.ObjectMeta |
||
spec |
PahlevanPolicySpec |
||
status |
PahlevanPolicyStatus |
PahlevanPolicySpec
defines the desired state of PahlevanPolicy
| Field | Type | Required | Description |
|---|---|---|---|
selector |
WorkloadSelector |
yes | Selector specifies the target workloads for this policy |
learningConfig |
LearningConfig |
LearningConfig controls the learning phase behavior | |
enforcementConfig |
EnforcementConfig |
EnforcementConfig controls enforcement behavior | |
syscallPolicy |
*SyscallPolicy |
SyscallPolicy defines syscall-specific policies | |
networkPolicy |
*NetworkPolicy |
NetworkPolicy defines network-specific policies | |
filePolicy |
*FilePolicy |
FilePolicy defines file access policies | |
selfHealing |
SelfHealingConfig |
SelfHealing enables automatic policy rollback on failures | |
observabilityConfig |
ObservabilityConfig |
Inert - the agent's flags configure telemetry, per node rather than per policy. ObservabilityConfig controls observability exports |
LearningConfig
controls the learning phase
| Field | Type | Required | Description |
|---|---|---|---|
duration |
*metav1.Duration |
Duration specifies how long to run in learning mode | |
windowSize |
*metav1.Duration |
Inert - the controller observes continuously, not in sampling windows. WindowSize specifies the minimum learning window size | |
minSamples |
*int32 |
MinSamples specifies minimum number of samples before transitioning. Bounded below by 1: zero means "transition with no evidence at all", which is a policy that enforces an empty baseline and kills the workload on its first syscall. Negative was accepted and compared against a count that can never be less than it, so it silently meant the same thing. | |
autoTransition |
bool |
AutoTransition enables automatic transition to enforcement | |
lifecycleAware |
bool |
Inert - stored and displayed; nothing acts on it. LifecycleAware enables lifecycle-based learning transitions | |
requireReview |
bool |
RequireReview holds a container in learning once its window and grace period have elapsed, until ReviewedAt is set. A workload already compromised when learning starts has its malicious behaviour baselined; this is the point at which an operator can look at the learned baseline before it becomes the thing that is enforced. Checked once, at the moment the container would otherwise transition: a baseline that changes after review is not re-reviewed. | |
reviewedAt |
*metav1.Time |
ReviewedAt is set by an operator after reviewing the learned baseline, clearing the RequireReview hold. Nil means not yet reviewed. Ignored when RequireReview is false. | |
expectedBehavior |
*ExpectedBehavior |
ExpectedBehavior declares operations the operator knows the workload performs but which may not happen during the learning window. Learning is a window of wall-clock time, so anything the workload does once a day is simply absent from the baseline: a nightly batch, a weekly certificate renewal, a log rotation, a backup that opens a path nothing else opens. Under Blocking the kernel then refuses it, and from the kernel's side that refusal is correct - the only evidence against the operation is that the workload has never done it before, which is exactly what an attacker produces too. Without this field the operator's only options are to guess a longer duration, or to let self-healing roll enforcement back after the job has already been denied at 03:00. Declarations are additive. Every entry is merged into the allow-set alongside what was learned, none can remove a learned entry, and an entry that cannot be represented exactly is refused with a warning naming the field rather than widened into something broader. |
EnforcementConfig
controls enforcement behavior
| Field | Type | Required | Description |
|---|---|---|---|
mode |
EnforcementMode |
Mode specifies the enforcement mode. The enum is load-bearing rather than decorative. Without it the API server accepts any string and the controller maps whatever it does not recognize onto Monitoring, so a typo produces a policy that looks applied and enforces nothing. The trap that found this: mode: Off unquoted is a YAML 1.1 boolean, so it arrives as false and silently became Monitoring - the opposite of switching a policy off. Quote it, or the API server now says so. One of: Off, Monitoring, Blocking. |
|
gracePeriod |
*metav1.Duration |
GracePeriod specifies grace period before strict enforcement | |
alertOnly |
bool |
AlertOnly enables alert-only mode for testing. It downgrades Blocking to Monitoring and is kept as its own field because it is a temporary override an operator flips during an incident and flips back, without losing the mode the policy is meant to run in. | |
blockUnknown |
*bool |
BlockUnknown blocks behavior outside the learned baseline. Nil means "the default for the mode", which is true under Blocking: default-deny of unlearned behavior is the only enforcement the data plane performs, so a Blocking policy that did not block it would enforce nothing. Explicitly false downgrades the policy to Monitoring. A bare bool could not express the difference between unset and false. | |
exceptions |
[]EnforcementException |
Exceptions defines enforcement exceptions |
EnforcementException
defines enforcement exceptions. v1alpha1 had both temporary: true and expiresAt, and enforcement applied an expiry only when both were set. So expiresAt on its own was accepted, displayed, and never acted on: an operator who wrote a deadline got a permanent hole in the policy and no warning, which is the worst possible outcome for a field whose entire purpose is to close itself. The expiry is now the only statement of the fact, and an exception is temporary exactly when it has one.
| Field | Type | Required | Description |
|---|---|---|---|
type |
ExceptionType |
yes | Type specifies exception type One of: Syscall, Network, File. |
patterns |
[]string |
yes | Patterns specifies patterns to match |
reason |
string |
Reason provides human-readable reason | |
expiresAt |
*metav1.Time |
ExpiresAt is when this exception stops being applied. Unset means the exception is permanent. |
FilePolicy
defines file access policies
| Field | Type | Required | Description |
|---|---|---|---|
allowedPaths |
[]string |
AllowedPaths explicitly allows specific paths. Paths must be fully resolved and exact. Enforcement keys on the path the kernel resolves, which follows symlinks, so "/etc/os-release" grants nothing where it links to /usr/lib/os-release. Wildcards are not supported and are matched literally. | |
deniedPaths |
[]string |
DeniedPaths explicitly denies specific paths, removing them from the learned baseline. The same resolution rules as AllowedPaths apply. | |
defaultAction |
PolicyAction |
Inert - redundant: default-deny is what enforcement is. DefaultAction specifies default action for unknown paths One of: Allow, Deny, Alert, Audit. |
|
readOnlyPaths |
[]string |
ReadOnlyPaths specifies read-only paths | |
writeAllowedPaths |
[]string |
WriteAllowedPaths specifies write-allowed paths | |
executableFilter |
*ExecutableFilter |
ExecutableFilter controls executable access |
ExecutableFilter
defines executable access controls
| Field | Type | Required | Description |
|---|---|---|---|
allowedExecutables |
[]string |
AllowedExecutables specifies allowed executables | |
deniedExecutables |
[]string |
DeniedExecutables specifies denied executables | |
requireSignature |
bool |
Inert - nothing verifies executable signatures. RequireSignature requires signed executables |
SyscallPolicy
defines syscall enforcement policies
| Field | Type | Required | Description |
|---|---|---|---|
allowedSyscalls |
[]string |
AllowedSyscalls explicitly allows specific syscalls | |
deniedSyscalls |
[]string |
DeniedSyscalls explicitly denies specific syscalls | |
defaultAction |
PolicyAction |
Inert - redundant: default-deny is what enforcement is. DefaultAction specifies default action for unknown syscalls One of: Allow, Deny, Alert, Audit. |
|
capabilityFilter |
[]string |
CapabilityFilter filters based on Linux capabilities | |
processFilter |
*ProcessFilter |
ProcessFilter filters based on process attributes |
ProcessFilter
defines process-based filtering
| Field | Type | Required | Description |
|---|---|---|---|
commands |
[]string |
Commands specifies allowed command patterns | |
users |
[]string |
Users specifies allowed users | |
groups |
[]string |
Groups specifies allowed groups | |
parentProcesses |
[]string |
ParentProcesses specifies allowed parent processes |
NetworkPolicy
defines network enforcement policies
| Field | Type | Required | Description |
|---|---|---|---|
egressRules |
[]NetworkRule |
EgressRules defines allowed egress traffic | |
ingressRules |
[]NetworkRule |
IngressRules defines allowed ingress traffic | |
defaultAction |
PolicyAction |
Inert - redundant: default-deny is what enforcement is. DefaultAction specifies default action for unknown connections One of: Allow, Deny, Alert, Audit. |
|
allowLoopback |
bool |
AllowLoopback allows loopback traffic | |
allowDNS |
bool |
AllowDNS allows DNS traffic |
NetworkRule
defines a network access rule
| Field | Type | Required | Description |
|---|---|---|---|
protocols |
[]string |
Protocols specifies allowed protocols | |
ports |
[]NetworkPort |
Ports specifies allowed ports | |
peers |
[]NetworkPeer |
Peers specifies allowed peers | |
action |
PolicyAction |
Action specifies the action to take One of: Allow, Deny, Alert, Audit. |
NetworkPeer
defines network peer specifications
| Field | Type | Required | Description |
|---|---|---|---|
ipBlock |
*IPBlock |
IPBlock specifies IP CIDR blocks | |
namespaceSelector |
*LabelSelector |
NamespaceSelector selects namespaces by their labels. | |
podSelector |
*LabelSelector |
PodSelector selects pods by their labels. |
NetworkPort
defines port specifications. The bounds are not decoration. A port is a uint16 on the wire and this field is an int32, so v1alpha1 accepted 0, -1 and 70000 and the translation truncated them into whatever the low sixteen bits happened to be. Port 70000 became port 4464, and the rule looked applied.
| Field | Type | Required | Description |
|---|---|---|---|
port |
*int32 |
Port specifies the port number | |
startPort |
*int32 |
StartPort specifies start of port range | |
endPort |
*int32 |
EndPort specifies end of port range | |
protocol |
string |
Protocol specifies the protocol |
IPBlock
defines IP CIDR block
| Field | Type | Required | Description |
|---|---|---|---|
cidr |
string |
yes | CIDR specifies the IP range |
except |
[]string |
Except specifies exceptions within the CIDR |
SelfHealingConfig
controls self-healing behavior
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
bool |
Enabled enables self-healing | |
rollbackThreshold |
int32 |
RollbackThreshold specifies failure threshold for rollback | |
rollbackWindow |
*metav1.Duration |
RollbackWindow specifies time window for failure counting | |
recoveryStrategy |
RecoveryStrategy |
RecoveryStrategy specifies recovery strategy One of: Rollback, Relax, Maintenance. |
ObservabilityConfig
controls observability exports
| Field | Type | Required | Description |
|---|---|---|---|
metrics |
MetricsConfig |
Metrics controls metrics export | |
tracing |
TracingConfig |
Tracing controls distributed tracing | |
logging |
LoggingConfig |
Logging controls structured logging | |
visualization |
VisualizationConfig |
Visualization controls attack surface visualization |
PahlevanPolicyStatus
defines the observed state of PahlevanPolicy
| Field | Type | Required | Description |
|---|---|---|---|
phase |
PolicyPhase |
Phase indicates the current phase of the policy One of: Initializing, Learning, Transition, Enforcing, Failed, RollingBack. |
|
conditions |
[]PolicyCondition |
Conditions represents the latest available observations. Declared a map list keyed by type, which is what the controller has always meant: updateCondition searches for the entry with a matching type and replaces it. The schema did not say so, so server-side apply merged the array by position instead, and two writers updating different conditions overwrote each other's entries rather than merging them. | |
learningStatus |
*LearningStatus |
LearningStatus provides learning phase status | |
enforcementStatus |
*EnforcementStatus |
EnforcementStatus provides enforcement status | |
attackSurface |
*AttackSurfaceStatus |
AttackSurface provides current attack surface analysis | |
targetWorkloads |
[]WorkloadReference |
TargetWorkloads lists target workloads | |
lastUpdated |
*metav1.Time |
LastUpdated indicates when status was last updated |
LearningStatus
provides learning phase status
| Field | Type | Required | Description |
|---|---|---|---|
startTime |
*metav1.Time |
StartTime indicates when learning started | |
endTime |
*metav1.Time |
EndTime indicates when learning ended | |
samplesCollected |
int64 |
SamplesCollected indicates samples collected | |
syscallsLearned |
int32 |
SyscallsLearned indicates unique syscalls learned | |
networkFlowsLearned |
int32 |
NetworkFlowsLearned indicates network flows learned | |
filePathsLearned |
int32 |
FilePathsLearned indicates file paths learned | |
progress |
*int32 |
Progress indicates learning progress percentage. Bounded, because it is printed verbatim in the Learning column of kubectl get pahlevanpolicy. An unbounded int32 there reads as "413" next to a header that says a percentage, and nothing rejects it. |
EnforcementStatus
provides enforcement status. v1alpha1 carried a blockedSyscalls counter that was always zero by construction: syscalls are confined by the generated seccomp profile, whose denials the kernel does not report back to this agent, and the BPF syscall program is observation only. A counter that can only read zero answers "are we blocking anything?" with "no" forever, so it is gone rather than documented again.
| Field | Type | Required | Description |
|---|---|---|---|
startTime |
*metav1.Time |
StartTime indicates when enforcement started | |
blockedNetworkConnections |
int64 |
BlockedNetworkConnections indicates connect() calls denied in-kernel. | |
blockedFileAccess |
int64 |
BlockedFileAccess indicates file opens denied in-kernel. | |
blockedExecs |
int64 |
BlockedExecs indicates execve calls denied in-kernel. | |
blockedCapabilities |
int64 |
BlockedCapabilities indicates capability checks denied in-kernel. | |
blockedTotal |
int64 |
BlockedTotal is the sum of every in-kernel denial across the containers this policy governs. It exists so the printed column reports the whole picture rather than one signal. | |
enforcingContainers |
int32 |
EnforcingContainers and TotalContainers describe how many of the containers this policy selects have actually reached enforcement. | |
totalContainers |
int32 |
||
alertsGenerated |
int64 |
AlertsGenerated indicates alerts generated count | |
rollbackCount |
int32 |
RollbackCount indicates number of rollbacks performed |
AttackSurfaceStatus
provides attack surface analysis
| Field | Type | Required | Description |
|---|---|---|---|
exposedSyscalls |
[]string |
ExposedSyscalls lists exposed syscalls | |
exposedPorts |
[]int32 |
ExposedPorts lists exposed network ports | |
writableFiles |
[]string |
WritableFiles lists writable file paths | |
capabilities |
[]string |
Capabilities lists effective capabilities | |
riskScore |
*int32 |
RiskScore provides overall risk score, 0 to 100. Bounded for the same reason as Progress: it is the Risk printer column, and the analyzer's arithmetic has no upper clamp of its own. | |
lastAnalysis |
*metav1.Time |
LastAnalysis indicates when analysis was last performed |
Other types
Reachable from the types above; listed so a new one cannot go undocumented by omission.
ExpectedBehaviorExpectedDestinationExpectedFileLabelSelectorLabelSelectorRequirementLogOutputLoggingConfigMetricsConfigMetricsExporterNamespaceSelectorPahlevanPolicyListPolicyConditionTracingConfigTracingExporterVisualizationConfigVisualizationExporterWorkloadReferenceWorkloadSelector
Metrics, events and the CLI
The sections below are not generated: they describe surfaces outside the CRD.
Metrics API
Pahlevan exposes Prometheus metrics on port 8080.
Metrics Endpoint
GET /metrics
Available Metrics
Policy Metrics
| Metric | Type | Description |
|---|---|---|
pahlevan_policies_total |
Counter | Total number of policies |
pahlevan_policies_by_phase |
Gauge | Policies by phase |
pahlevan_policy_violations_total |
Counter | Total policy violations |
pahlevan_enforcement_actions_total |
Counter | Total enforcement actions |
Learning Metrics
| Metric | Type | Description |
|---|---|---|
pahlevan_learning_progress_ratio |
Gauge | Learning progress (0.0-1.0) |
pahlevan_learning_samples_total |
Counter | Total learning samples |
pahlevan_learning_duration_seconds |
Histogram | Learning duration |
eBPF Metrics
| Metric | Type | Description |
|---|---|---|
pahlevan_ebpf_programs_loaded |
Gauge | Number of eBPF programs loaded |
pahlevan_ebpf_events_total |
Counter | Events decoded from the ring buffers, by kind |
pahlevan_ebpf_denials_total |
Counter | Operations denied in-kernel with EPERM, by kind |
pahlevan_ebpf_decode_errors_total |
Counter | Ring-buffer records that could not be decoded, by kind |
pahlevan_ebpf_handler_errors_total |
Counter | Event handlers that returned an error, by kind |
pahlevan_ebpf_breakouts_total |
Counter | Execs whose working directory was outside the process's own mount namespace |
pahlevan_ebpf_program_load_errors_total |
Counter | eBPF program load errors |
pahlevan_ebpf_handler_errors_total is the one to alert on if you export
events. Enforcement happens in the kernel and is unaffected by a handler
failure, so the event and denial counters keep climbing normally while an
export file, webhook or gRPC subscriber quietly receives nothing.
System Metrics
| Metric | Type | Description |
|---|---|---|
pahlevan_containers_monitored |
Gauge | Containers being monitored |
pahlevan_operator_info |
Info | Operator version and build info |
Metric Labels
Common labels across metrics:
| Label | Description |
|---|---|
policy |
Policy name |
namespace |
Policy namespace |
container |
Container ID |
violation_type |
Type of violation |
action |
Enforcement action taken |
Event stream
Security events leave the agent in four ways, all carrying the same versioned
envelope (pahlevan.io/v1alpha1):
| Route | Flag | Notes |
|---|---|---|
| JSON lines to a file | --export-file |
What pahlevan events reads by default. Size-based rotation. |
| HTTP webhook | --export-webhook |
Batched POSTs, retried on 5xx/408/429. |
| OTLP log records | --otlp-endpoint |
Reaches Loki through the same collector as the metrics and traces. |
| gRPC stream | --grpc-bind-address |
pahlevan.v1alpha1.EventService; server-side filtering. Requires TLS, mTLS or a bearer token: a plaintext, unauthenticated listener refuses to start unless --grpc-insecure is passed deliberately. |
Those four carry the envelope, which is the right shape for a collector and the wrong shape for a person. For getting a denial in front of somebody who can act on it there are three more, which send formatted messages rather than JSON:
| Route | Flag | Notes |
|---|---|---|
| Slack | --notify-slack-webhook |
One Block Kit message per batch, with a fallback text so a phone notification says something useful. |
| PagerDuty | --notify-pagerduty-key |
Events API v2. One incident per distinct finding, keyed so the same problem re-triggers rather than opening another. --notify-pagerduty-severity sets critical/error/warning/info. |
| Anything else | --notify-template-url + --notify-template |
A Go text/template rendered into the POST body: Teams, Discord, Opsgenie, an internal ticket API. Helpers: summary, workload, subject, json, truncate, upper, lower, join. |
All three send denials only by default, because an observation stream is not
an alert stream and a channel receiving one message per file open gets muted
within the hour. --notify-all-events opts in.
All three deduplicate by finding over --notify-dedupe-window (five minutes
by default, negative to disable). A finding is the workload, the kind of
operation and the subject - deliberately not the pid or the timestamp, which are
exactly what make two occurrences of one problem look different. It keys on the
owning workload rather than the pod, so a Deployment rolling out does not
produce one message per replica for a single misconfiguration.
A template that fails to parse is a startup error naming the mistake, not an error logged once per batch while notifications silently never arrive:
--notify-template-url=https://example.webhook.office.com/... --notify-template='{"text":{{ .Summary | json }}}'
The envelope is defined in pkg/export/event.go and mirrored on the wire by
api/v1alpha1/events.proto; the two are asserted to agree in
pkg/grpcapi/convert_test.go.
Seeing what a policy does before applying it
pahlevan policy explain -f policy.yaml [--strict]
Translates the policy offline, prints what will be written into the kernel
allow-sets, and names every part that will not be enforced - an ingress rule, a
CIDR too wide to enumerate, a glob, an inert field. --strict exits non-zero,
so a policy that quietly does less than it says can fail a CI gate.
It also rejects a field the CRD does not have, which the API server would otherwise prune without an error.