yeke.io · docs · guardrail
Guardrail policies
Use policies to decide when an operation should stop or require extra approval. This guide covers CEL conditions, effects, examples and testing before production.
File mode (YEKE_POLICY_DIR) works on every tier. The signed
package mode, the dual-approval fields, and maintenance windows are Enterprise and are
marked in their own section below.
What a policy is, and where it runs in the chain
Policies can restrict existing permissions or require extra checks. They cannot grant access that Kubernetes RBAC does not allow.
Every write in YEKE goes through a single chain, and guardrail is one link in it. The order is fixed; the policy runs after dry-run, which means it decides while holding the apiserver's own answer to "what would this have done".
POST /operations → validation → prior state → collection expansion → classification → dry-run → guardrail → plan hash → approval card → apply
The practical consequence is what your condition gets to see: object is the
object dry-run returned (or the request body if there is none), and
step.dryRun.changedPaths is the list of paths that will actually change. A
policy looks at a measurement, not a guess.
What a policy cannot do
- It does not replace Kubernetes RBAC. The request reaches the apiserver under the user's own identity and the apiserver makes the final call. Guardrail cannot widen what RBAC granted, and it cannot approve what RBAC denied — it puts a gate in front, it does not stand in for it.
- There is no
alloworbypasseffect. There are four effects (deny,require,warn,redact) and none of them loosens anything. Guardrail only tightens. - The structural approval floor is independent of policy. Even with an entirely
empty policy set, every write asks for at least
standardapproval and every step classifieddestructiveasks for at leastelevated. "No destructive operation without approval" rests on code, not on policy hygiene — no policy can go below that floor. - A policy error does not mean "go ahead". If
matchthrows at evaluation time, that step counts asdenyand the card showsPolicy errortogether with the policy'sid. The cost is real: a broken policy stops operations — but it stops them visibly.
File mode and fields
In file mode, restart core to load policy changes.
YEKE_POLICY_DIR points at a directory; the *.yaml and
*.yml files inside it are loaded at startup, in name order. A file is
either a policy document (top-level policies:) or a freeze calendar (top-level
freezes:) — the two cannot live in the same file, the loader rejects it.
The distinction comes from the top-level key, not from the filename: renaming a file does not
change what it contains.
# /etc/yeke/policies/10-production.yaml
policies:
- id: org.production-delete
description: "Deletes in production namespaces"
enabled: true
match: 'step.action == "delete" && step.target.namespace == "production"'
effect: require
level: elevated| Field | Required | What it does |
|---|---|---|
id | yes | The policy's name (1–128 characters). Appears on the card and in the audit
trail. If the same id is defined in two sources core will not start —
which one is valid cannot remain ambiguous. |
description | no | Free-form description. With no messageCode and no
message, this is what the card shows as the reason; with none of
them, the card shows the policy's id. |
enabled | no (true) |
A policy set to false is still compiled but never
evaluated. Learning that a disabled policy is broken on the day you enable it
would defer the error to the worst possible moment. |
match | yes | A CEL expression (1–8192 characters) that must return a bool. If it does not compile, core will not start. |
effect | yes | deny · require · warn ·
redact. Meanings are in the table below. |
level | no | standard or elevated. Only valid with
effect: require; written with any other effect, core will not start.
Omitted, it is standard. |
message | no | The one-line reason for the match, in the operator's own words (≤2048
characters). If messageCode is also written, this field is
never read. |
messageCode | no | The code for that reason — a closed set (below). The code wins, because the code is the half that can be translated: the card shows it in the reader's own language. |
flags | no | Flags added to the classification. The irreversible flag is set
only by a policy; the card then draws the step as not revertible. |
caveats | no | Extra warnings on the card — the operator's own sentences. The interface draws them labelled as coming from the server and does not translate them. |
caveatCodes | no | The code for a warning — a closed set (below). On the card, codes come first and free text after. |
severity | no | Raises the step's destructiveness class. The only accepted value is
destructive; writing mutating is an attempt to lower it
and is rejected. Cannot be used with effect: redact. |
redactPaths | conditional | The field paths to mask. Only valid with effect: redact,
and there it is required — an empty list is rejected too. |
requireSecondApproval | no | Enterprise. When true, a matching plan asks for a second
approver's consent. Independent of the effect: a warn policy may also
want a second set of eyes. |
requiredApproverGroup | no | Enterprise. Narrows the approver set to one group. Cannot be written without
requireSecondApproval: true — narrowing an approval that is never
asked for would silently do nothing. |
It never swallows a key it does not know; it dies at startup
- A typo does not pass quietly. The schema is strict: a key that is not in the
list above (
levell,affect) is rejected. Swallowed, it would leave the operator with "I wrote it but it doesn't work". - A broken policy keeps core from starting. Unparseable YAML, a field that does
not match the schema, a
matchthat will not compile, a repeatedid— all four stop startup. Skipping the broken policy and carrying on would produce a rule the operator believes they wrote and the system never loaded. - Setting
YEKE_POLICY_DIRto something unreadable also stops startup. If the directory does not exist, do not set the variable at all: assuming it exists and failing to read it means policies silently fail to load. - A change is a file plus a restart. In file mode there is no write endpoint for policies; the loaded set stays fixed for the life of the process. The only source that takes effect without a restart is the signed package (below).
Writing match — the CEL context
The naming follows Kubernetes' own ValidatingAdmissionPolicy vocabulary:
object and oldObject mean here what they mean there.
The expression is evaluated per step — each expanded target appears separately —
and must return a bool. A non-bool result (writing just step.action, say) is an
error, not a "true".
| Variable | Contents |
|---|---|
op.source |
Who built the plan: "ui" (the interface), "nl"
(chat), "api" (a direct API caller; the shell is, in the schema's own
vocabulary, an API caller too). |
op.identity.userop.identity.groups |
The Kubernetes identity the request will reach the apiserver with, and its groups. |
op.cluster.idop.cluster.name |
The target cluster. |
op.cluster.execRecording |
The cluster's recording posture — independent of the license. For a configuration audit: "is this cluster set up for recording". |
op.cluster.recordingActive |
Posture and the session-recording license allowing
use — whether recording will actually start for this session.
When the license is removed, posture stays true but this field becomes
false; a rule that wants assurance should read this one. |
op.revertOf |
The id of the source plan if this one is a revert, otherwise
null. |
step.action |
create · update · patch ·
delete · stream. |
step.methodstep.path |
The step's HTTP verb and apiserver path — testable with
startsWith, contains, matches. |
step.target.schemaId |
The target's schema id: group.singular when there is a group
(apps.deployment), just the singular name in the core group
(pod). |
step.target.groupstep.target.versionstep.target.kindstep.target.resource |
API group ("" for the core group), version, kind, and the plural
resource name. |
step.target.namespaced |
Whether the type is namespaced (bool). |
step.target.namespacestep.target.namestep.target.subresource |
The target's namespace, name, and subresource. All three are always
present; absence is represented by "", never by a missing
key. |
step.classification.severity |
mutating or destructive. |
step.classification.flags |
The flag list: cluster-scoped, collection,
finalizers, owner-managed, orphan-delete,
dry-run-unsupported, irreversible, stream,
conflicting-declaration, plus type-specific flags shaped
subresource:<name>. |
step.dryRun.status |
ok · failed · unsupported ·
skipped · not-applicable. |
step.dryRun.changedPaths |
The list of field paths dry-run measured as actually changing. |
object |
The new state: the dry-run result, or the request body if there is none.
null on a delete step. |
oldObject |
The live prior state. null on a create step. |
Two classes of field — and the has() rule
Every field under op and step is a contract field and is
always present; you can read it directly. object and oldObject, by
contrast, are real Kubernetes objects and their fields may be missing — and reading a missing
field is an error in CEL, not a silent false. The error falls to the
fail-closed side, which means your policy can deny a plan it never meant to. The rule is
simple: guard every path under object / oldObject with
has().
# wrong — if the object has no spec the policy throws and the step is denied match: 'object.spec.replicas == 0' # right — has() does not throw on a null root either, it returns false match: 'has(object.spec) && has(object.spec.replicas) && object.spec.replicas == 0'
Supported syntax
The engine's semantics are checked at every startup against a conformance corpus of 54 expressions; if it fails, core does not start. Every construct below is verified in that corpus:
- Comparison and logic:
==,!=,&&,||,!, the ternary (? :). Short-circuiting works (false && <error>isfalse) but errors are not swallowed:true && <error>is an error. in: both list membership (step.target.resource in ["pods", "nodes"]) and map keys ("critical" in oldObject.metadata.labels).has(): does the field exist. Returnsfalsefor a missing field instead of throwing.- Indexing:
oldObject.metadata.labels["app"],step.classification.flags[0]. Careful: reaching for a map key or a list index that is not there is an error — ask withinorhas()before you index. - String functions:
startsWith(),endsWith(),contains(),matches()(regular expression). - Macros and size:
exists(),all(),size(). - Comparing against
null:object == null,op.revertOf != null. But reaching underneath anullroot (object.metadata.name) is an error. - Anything unrecognized raises a visible error: unknown variable, unknown field,
unknown function, unknown method, syntax error. None of them quietly become
false— that is exactly what the corpus is there to measure.
Examples
Eight complete policies. The names live in the organization's own
namespace (org.), which is all it takes to avoid colliding with the built-in
set.
1) Deletes in production namespaces need elevated approval
policies:
- id: org.production-delete
description: "Deletes in production namespaces"
match: >
step.action == "delete" &&
step.target.namespace in ["production", "production-data"]
effect: require
level: elevated
caveats:
- "Production namespace: open the change record before applying."Every delete step in those two namespaces raises the card to elevated — the
apply button stays disabled until the user types the target's name exactly. The
caveats line appears on the card as the operator's own sentence,
untranslated.
2) Writing to kube-system is denied
policies:
- id: org.system-namespace-write
description: "kube-system is not written through YEKE"
match: >
step.action in ["create", "update", "patch", "delete"] &&
step.target.namespace == "kube-system"
effect: deny
message: "kube-system belongs to cluster administration; this change does not go through YEKE."A single deny on a single step drops the whole plan; the card is drawn
as not applicable. The built-in builtin.system-namespace only raises the
approval level for the same target — this policy tightens it, because in guardrail the
combination always favors the most restrictive rule.
3) Scaling to zero raises a warning
policies:
- id: org.scale-to-zero-warning
description: "Remind the on-call when scaling to zero"
match: >
has(object.spec) && has(object.spec.replicas) &&
object.spec.replicas == 0
effect: warn
message: "Scaling to zero should be announced to the on-call rotation."warn does not touch the approval level; it adds a line to the card.
Scope note: this condition only sees scaling done through the main object. The
canonical path (the scale subresource) carries a Scale object where
a value of zero drops out of the body entirely — the built-in
builtin.scale-to-zero has a two-branch condition for exactly that reason.
Scaling to zero already asks for elevated approval through that built-in; this
policy only adds the organization's own sentence on top.
4) Mask a password field in your own type
policies:
- id: org.database-password-mask
description: "Password fields in our own database type are masked"
match: >
step.target.group == "example.io" &&
step.target.resource == "databases"
effect: redact
redactPaths:
- "spec.connection.password"
- "spec.connection.users[].password"
message: "Password fields are masked in the plan response and the audit trail."The path format descends with dots, and [] means "every element of this
array". The mask replaces the value with a keyed marker: the value becomes unreadable, but
whether two records carry the same value is still visible — "did this field change" stays
answerable after masking. A Secret's data / stringData are already
masked by a built-in; this example carries the same protection to your own type.
Write the condition around what the object carries, not around the
verb. Putting step.action in a redact policy drops the mask
exactly where it is needed most: on a delete there is no request body, and the only thing
the user sees on the card is the whole live object.
5) Every write in one CRD group opens a card
policies:
- id: org.example-group-approval
description: "Every write in the example.io group opens an approval card"
match: 'step.target.group == "example.io"'
effect: require
messageCode: SYSTEM_NAMESPACEWith no level, it is standard: one card, no name to type. The
structural floor still holds — a delete in the same group is
elevated anyway. messageCode is picked from a closed set and shows
on the card in the reader's language; when no code in the set fits, write your own sentence
with message.
6) Condition on what dry-run actually changes
policies:
- id: org.replica-change
description: "Elevated approval when dry-run changes the replica count"
match: >
step.dryRun.status == "ok" &&
"spec.replicas" in step.dryRun.changedPaths
effect: require
level: elevatedBecause guardrail runs after dry-run, changedPaths is a measurement rather
than a guess: even if the request appears to touch that field, the path is absent when the
value is unchanged. The status == "ok" clause is deliberate — when dry-run did
not run (unsupported, skipped) the list is empty, and a rule
reading an empty list would quietly never match.
7) A labelled object cannot be deleted
policies:
- id: org.critical-label-guard
description: "An object labelled critical=yes cannot be deleted through YEKE"
match: >
step.action == "delete" &&
has(oldObject.metadata) && has(oldObject.metadata.labels) &&
"critical" in oldObject.metadata.labels &&
oldObject.metadata.labels["critical"] == "yes"
effect: deny
message: "This object is labelled critical; deleting it goes through a separate process."Note why both guards are needed: has() asks whether the label map exists,
in asks whether the key exists in it. Without the second, an object that has
labels but no critical key would make the indexing throw and the step would
fail closed to deny — the policy would stop objects it never meant to.
8) No terminal in production while recording is off
policies:
- id: org.no-unrecorded-terminal-in-production
description: "No terminal session in production clusters while recording is off"
match: >
step.action == "stream" &&
op.cluster.name.startsWith("production") &&
!op.cluster.recordingActive
effect: deny
message: "A terminal session on this cluster can only open while recording is on."This is the built-in closed template ops.recording-required, copied into
your own namespace. The condition deliberately reads recordingActive, not
execRecording: when the license is removed the cluster's recording posture
stays true but recording does NOT start, so a rule reading posture alone would
quietly go unenforced on that installation.
Effects, levels, and codes
Multiple policies can match a step. Their effects are combined; policy order cannot weaken the checks.
| Effect | What happens |
|---|---|
deny |
The plan is rejected. One deny on one step drops the whole plan.
level cannot be written. |
require |
An approval card opens. level: standard is a single approval;
level: elevated asks the user to type the target's name
exactly — apply stays disabled until they do. |
warn |
Shows as a warning on the card; does not touch the approval level. This is how you try a new rule out in the field. |
redact |
The values at redactPaths are masked in the plan response
and in the audit trail. Such a policy never enters the card's policy list —
its message does not go over the wire, it stands there for the operator reading
the YAML. |
- Levels combine by maximum:
approvalLevel = max(matchedrequirelevels, the structural floor). A policy cannot lower the floor. - Flags, warnings, and class raises accumulate and cannot be taken back: once a
policy has pulled a step to
destructive, no other policy brings it down. - There is exactly one pass. A second round is never run so that another policy
could match on a flag a first one added (
irreversible, say) — the guarantee that order does not affect the outcome depends on this. - Two policies giving the same effect with different messages are both listed. The card does not hide which rule is speaking.
The messageCode values you can write
A closed set — the product's own sentences, translated in both languages:
IRREVERSIBLE_STORAGE · NAMESPACE_DELETE ·
CRD_DELETE · CLUSTER_SCOPED_DELETE ·
DRY_RUN_UNSUPPORTED · POD_EVICTION ·
SYSTEM_NAMESPACE · RBAC_BINDING_WRITE ·
SCALE_TO_ZERO · NL_STRICT · SHELL_STRICT
The caveatCodes values you can write
REVERT_DELETES_CREATED · REVERT_RESTORES_PRIOR ·
REVERT_RECREATES_OBJECT · SUBRESOURCE_CREATE_NO_TARGET ·
FINALIZERS_PENDING · FINALIZERS_ON_DELETING ·
OWNER_MANAGED · ORPHAN_PROPAGATION ·
FINALIZERS_DRY_RUN · SEQUENCED_IN_PLAN ·
STORAGE_DATA_LOST · NAMESPACE_CASCADE ·
CRD_INSTANCES_DELETED · DRY_RUN_UNVERIFIED ·
EVICTION_DELETES_POD · RBAC_GRANTS_AUTHORITY ·
SCALE_TO_ZERO_OUTAGE
For a warning the set has no equivalent for, write your own sentence with
caveats. Picking a code is always better — a code is translated, free text is
not.
Dual approval, maintenance windows, package mode
All three are Enterprise. The first two live inside the policy data; the third changes where the policy comes from.
The dual-approval fields
policies:
- id: org.dual-approval
description: "Deployment changes in production need a second set of eyes"
match: >
step.action in ["update", "patch"] &&
step.target.resource == "deployments" &&
step.target.namespace == "production"
effect: require
level: elevated
requireSecondApproval: true
requiredApproverGroup: "sre-approvers"- Who counts as an eligible approver: anyone other than the plan owner, whose account is active, and who holds an identity on that cluster through an identity rule. If the policy names a group, that group must also be among the user's resolved groups — the groups come from the identity rule; no second notion of a group is introduced.
- The set is never stored, it is recomputed every time. A stored list goes stale: accounts are deactivated, identity mappings are removed, groups narrow.
- The second approver only consents; the plan is still applied by its owner. Consent is bound to the plan's current hash; if the plan is refreshed and the hash changes, the consent falls (the record is not deleted, the gate simply stops counting it) and is asked for again.
- It is off by default. Until a rule is written, no plan asks for a second approval — a deliberate choice so that a single-admin install is never locked out.
requiredApproverGroupcannot stand alone. Narrowing an approval that is never asked for would silently do nothing; the loader rejects it at startup.
A maintenance window — a separate file
The calendar does not live in a policy file but in a separate file in the same
directory, under the top-level key freezes:. A file carrying both
policies: and freezes: is rejected — what a file contains should be
visible the moment it is opened.
# /etc/yeke/policies/90-calendar.yaml freezes: - id: weekend description: "Weekend change freeze" timezone: Europe/Istanbul # REQUIRED weekly: - { from: "Fri 17:00", to: "Mon 09:00" } dates: - { from: "2026-12-29T00:00:00", to: "2027-01-02T09:00:00" } scope: { clusters: ["*"] }
timezoneis required and is an IANA zone name. "Friday 17:00" is a real question in an organization; assuming UTC produces a silent error whose symptom looks like "the window shifted by two hours".weeklytakes the shape"Day HH:MM"— a three-letter weekday (Mon–Sun) on a 24-hour clock.datesare local wall-clock times (YYYY-MM-DDTHH:mm:ss) with no timezone suffix: a suffix would open a second source of time that could contradict the window's owntimezone.weeklyanddatescan both be left empty — a window that never matches is far cheaper than one that always does.scope.clustersis the set of clusters the window covers;["*"]means all of them. Anapplyinside the window is rejected and the plan record is left unmutated — the details are on the Governance page.- Freeze ids are a separate namespace from policy ids, but must still be unique among themselves: two windows sharing a name would make "which window rejected this" unanswerable in the trail.
Package mode — signed, effective immediately
With YEKE_POLICY_MODE=central the policy set comes from a signed package
rather than a directory. The body is written without the signature field,
and version must increase on every install:
{
"version": 3,
"policies": [
{
"id": "org.production-delete",
"match": "step.action == \"delete\" && step.target.namespace == \"production\"",
"effect": "require",
"level": "elevated"
}
],
"freezes": [
{
"id": "weekend",
"timezone": "Europe/Istanbul",
"weekly": [{ "from": "Fri 17:00", "to": "Mon 09:00" }],
"scope": { "clusters": ["*"] }
}
]
}The fields are identical to file mode — package and file go through the same compiler, so a policy rejected in file mode is rejected in a package too. Signing is done with the organization's own private key:
pnpm --filter @yeke/core exec tsx tools/policy-package/sign.ts \ --private-key policy-private.pem \ --package package.json \ --out package-signed.json
With an encrypted PEM (an ENCRYPTED PRIVATE KEY header, or the
older Proc-Type: 4,ENCRYPTED), the passphrase is read from the
YEKE_POLICY_KEY_PASSPHRASE environment variable or a secret prompt; a plain PEM
never asks for one.
The signed body is installed with PUT /api/policy/package under an admin
session; to upload it from a screen, use /admin/policy. The install takes effect
immediately and core does not restart — but it does not affect a plan already in flight: the
plan record carries the policy version it was classified under, and apply runs
with that version.
- The two modes cannot both be active. Giving
YEKE_POLICY_MODE=centraltogether withYEKE_POLICY_DIRstops core with an explicit error — merging two sources would silently answer the question "which of two policies with the sameidwins". - A bad signature or a stale version does not drop the package in force. The
install is rejected and the previous version stays. An equal or lower
versionis rejected too. - The built-in set is loaded in package mode as well. The package is the operator's set; it does not replace the product's own policies.
Key generation, the full install endpoint, and the license behavior are on the Governance page.
Trying a policy out
Start with a warning policy. Review the results before enabling denial or additional approval.
- Write it as
warnfirst. Ship a new rule witheffect: warn, watch it appear on the card for exactly the steps you meant, and only then turn it intorequireordeny. A badly writtendenystops correct operations too. - Build the plan, read the card, do not apply. Build a plan from the interface or
with
POST /api/clusters/:clusterId/operations; the card shows theids of the matched policies, their messages, the warnings, the approval level, and which fields were masked. Then cancel the plan withPOST /api/clusters/:clusterId/operations/:opId/cancel— building a plan applies nothing, and cancelling it leaves a trail entry. - The startup log counts what was loaded. As core starts it writes that the
conformance corpus passed and how many policies are active, by name; if freeze windows
exist it lists them with their zones. If you cannot find your policy's name there, it is
either
enabled: falseor its file was never read.
[guardrail] CEL conformance corpus passed (54 expressions) [guardrail] 12 active policies loaded (2 files): builtin.irreversible-storage, … [guardrail] 1 change-freeze windows loaded: weekend (Europe/Istanbul)
Errors that stop startup
The error always names the policy and the file it came from; core never opens for listening.
Policy file is not valid YAML (/etc/yeke/policies/10-production.yaml): … Policy file does not match the schema (/etc/yeke/policies/10-production.yaml): policies.0.effect: Invalid enum value Policy id defined twice: 'org.production-delete' (/etc/yeke/policies/10-production.yaml and /etc/yeke/policies/20-extra.yaml). Which one is valid cannot remain ambiguous. Policy 'org.example' (…) could not be compiled: …
- An
idcollision counts across files — and against the built-in set. Give your own policies a prefix. - A disabled policy is compiled too. A broken policy written
enabled: falsestill stops startup; it does not defer the error to the day you enable it. - A runtime error is not visible at startup. Compilation checks
match's syntax and its type; a missing-field error only surfaces once a matching plan is built, and that step becomesdeny. Which is why thehas()discipline matters.
Built-in policies
Seventeen entries, fourteen of them on. All of them ship as data — the engine knows no resource names at all.
| Policy | Effect | What it does |
|---|---|---|
builtin.irreversible-storage | require · elevated |
Deleting a PersistentVolume or PersistentVolumeClaim; adds the
irreversible flag. |
builtin.namespace-delete | require · elevated |
Deleting a namespace — everything inside cascades away with it. |
builtin.crd-delete | require · elevated |
Deleting a CustomResourceDefinition; every instance of that type goes with it. |
builtin.cluster-scoped-delete | require · elevated |
Deleting a cluster-scoped object. |
builtin.dry-run-unsupported | require · elevated |
An operation dry-run could not run on — an unverified change is being applied. |
builtin.pod-eviction | require · elevated |
The eviction subresource; raises the class to destructive, because
it deletes a pod despite being a create-only request. |
builtin.system-namespace | require · elevated |
A destructive operation inside kube-system,
kube-public, or kube-node-lease. |
builtin.rbac-binding-write | require · elevated |
Writing a RoleBinding or ClusterRoleBinding — the objects that hand out authority. YEKE's own "grant access" button goes through this gate too. |
builtin.scale-to-zero | require · elevated |
Bringing the replica count to zero; covers both the main object and the
scale subresource branch. |
builtin.node-cordon | warn |
Cordoning a node (closing it to new pods) while the cluster has another schedulable node. Warns without asking for approval. |
builtin.node-cordon-last-node | require · elevated |
Cordoning the cluster's last schedulable node; raises the class to
destructive, because new pods have no node to land on. |
builtin.node-cordon-topology-unknown | require · elevated |
Cordoning a node when the number of schedulable nodes could not be read — approval is raised because it is unknown whether this is the last one. |
builtin.redact-secrets | redact |
A Secret's data and stringData — on reads, writes,
and deletes alike. |
builtin.redact-env-values | redact |
Environment variable values, across the four places a PodSpec can live × the
three container lists in it. valueFrom is deliberately out of scope:
it holds a reference, not a value. |
ops.nl-strict | off | A template. Turned on, every write coming from chat
(op.source == "nl") asks for elevated approval. |
ops.shell-strict | off | A template. Turned on, every write from the shell opens its own approval card; the write path authorized by the session grant is disabled. |
ops.recording-required | off | A template. Turned on, denies a stream step while
op.cluster.recordingActive is false — a terminal session that will not
be recorded cannot open. |
Can they be turned off
The enabled field lives inside the built-in file itself, and that file
ships with the image. You cannot disable or override a built-in policy from
YEKE_POLICY_DIR: defining the same id in a second source stops core
at startup, and the built-in set is loaded in package mode as well. What the operator has is
addition — guardrail tightens, it does not loosen. If you want the behavior of the
three templates above (ops.nl-strict, ops.shell-strict,
ops.recording-required), write the same condition as your own policy in your
own namespace.