yeke.io · docs · enterprise
Governance
Bring your change process into YEKE with dual approval, change freezes, central policies, terminal recordings, Secret display and compliance reports. This guide covers setup and limits for each feature.
Six items, six flags — all Enterprise: dual approval
dual-approval, maintenance windows change-freeze, centralized
policy policy-central, session recording session-recording, Secret
display secret-display, compliance reports compliance-reports. On
23.09.2026 Secret display moved its phase from E3 to E6 (Access and Secret management); its name and behavior are
unchanged.
Dual approval (four-eyes)
Not a role, a policy match — OFF by default, so it never locks out a single-admin install.
A matching requireSecondApproval policy requires a second approval. The approver must be active, differ from the owner and have a mapped cluster identity. If requiredApproverGroup is set, group membership is also checked. Eligibility is recalculated each time. Policy fields and examples →
policies:
- id: org.dual-approval
match: 'step.action == "update" && step.target.resource == "deployments"'
effect: require
level: elevated
requireSecondApproval: true
requiredApproverGroup: "sre-approvers"- The second approver GRANTS consent, the OWNER still applies the plan. The
actor-≠-owner gate is unchanged — there is no second applier. Consent endpoints:
POST/DELETE/GET /api/clusters/:clusterId/operations/:opId/consent. - Consent is tied to the plan's CURRENT hash. If the plan is refreshed and the hash changes, standing consent is dropped (the record isn't deleted, it simply stops counting) and must be given again.
- If no eligible approver exists, this is not left to apply time. The plan is
still created and its card says so AT PLAN TIME, with
SECOND_APPROVAL_REQUIRED/NO_ELIGIBLE_APPROVER. The TTL (30 min) is not stretched — instead, the existing notification hook is queued to reach a second person. - A configured rule stays in force when the license is removed — the only thing
that's cut off is defining a new rule; the
usegate is never checked, onlyconfigureis. - In file mode this gate is at startup. Core records the rules that go into force. On a start whose license doesn't cover the feature, if it finds a dual-approval rule that isn't in the record or has changed, it refuses to start and names the rule. Removing a rule from the files is always allowed.
- Upgrading: an installation that used dual-approval rules in file mode before 0.64.0 records them on its first start after the upgrade and keeps starting without a license. The record appears once in the audit trail.
Maintenance windows — change-freeze
The calendar is INTERNAL; it doesn't open an outbound hook.
Define windows under freezes:. File mode uses a separate file in YEKE_POLICY_DIR; central mode uses the package’s freezes field. Every window requires a timezone.
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: ["*"] }- An
applyinside the window waits. It returns409 APPROVAL_HOOK_DENIEDplusparams.source: "change-freeze"andparams.windowEndsAt; the plan record stays unmutated and the TTL is not stretched. - There is no break-glass path. v1 only "waits" — a half-built break-glass would be worse than the freeze itself.
- A configured window stays in force when the license is removed — the only thing that's cut off is defining a new window.
- In file mode, a new or changed window stops core from starting when the license
doesn't cover the feature; the same record and the same upgrade rule apply as for
dual-approval rules. Changing only the
descriptiondoesn't count as a new definition.
Centralized policy package
The policy set can also be loaded from a signed package instead of a file — the two can never be the source at the same time.
| Variable | Default | What it does |
|---|---|---|
YEKE_POLICY_MODE | file |
The source of the policy set: file or central. An
unrecognized value stops core at startup. central is Enterprise;
requesting it without a license stops startup, file mode runs fine unlicensed. |
YEKE_POLICY_DIR | — | The policy directory for file mode. Cannot be given TOGETHER with
YEKE_POLICY_MODE=central — giving both stops core with an explicit
error. |
YEKE_POLICY_PACKAGE_PUBLIC_KEYS | — | The Ed25519 keys that verify the package's signature (comma-separated PEM, or
its base64). REQUIRED in central mode. Separate from the license key
AND the air-gap release signing key — three claims, three keys. |
1) Key pair — the organization generates it itself, it never reaches YEKE
openssl genpkey -algorithm ed25519 -out policy-private.pem openssl pkey -in policy-private.pem -pubout -out policy-public.pem
policy-private.pem stays with the organization;
policy-public.pem is given to core via
YEKE_POLICY_PACKAGE_PUBLIC_KEYS.
2) Package body — without a signature field, version must
INCREASE on every upload
{
"version": 3,
"policies": [
{ "id": "prod-namespace-guard", "match": "...", "effect": "require" }
],
"freezes": [
{
"id": "weekend",
"description": "Weekend change freeze",
"timezone": "Europe/Istanbul",
"weekly": [{ "from": "Fri 17:00", "to": "Mon 09:00" }],
"scope": { "clusters": ["*"] }
}
]
}3) Signing — 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
The tool rejects an already-signed body (a filled signature field) as
input. The last line of its output prints the fingerprint — the value to compare against
the fingerprint that GET /api/policy/package returns after
upload.
4) Upload — with an authenticated admin session
curl -X PUT https://yeke.example.com/api/policy/package \ -H "Authorization: Bearer <the admin session's own token>" \ -H "Content-Type: application/json" \ --data @package-signed.json
- Loading takes effect immediately — core does not restart — but never affects a
plan already in flight: the plan record carries the policy version it was classified
under (
plan.policyVersion) andapplyruns with that version. - A package with a broken signature does NOT drop the previous version — it is rejected, and the previous version stays in force and keeps being read.
- Every change is logged:
policy.package_installed(who, version, policy count, signature fingerprint, a diff summary); the policy body itself is not written to the log.
Terminal session recording
Opt-in per cluster, OFF by default — no covert recording.
Metadata events (who, which pod, how many bytes) are always written, Community included.
Full pty recording is turned on with a separate switch: PUT
/api/clusters/:clusterId/exec-recording (admin + configure), logged with
a cluster.exec_recording_changed audit event.
- When recording is on, the terminal says so on its opening line. This isn't an oversight, it's a design decision: the user sees that full recording is active as the session starts.
- Recording is encrypted with AES-256-GCM, with a raw-output cap of 32 MiB per
session — past the cap it's marked
truncated, the session is not cut off. - The download format is asciinema v2
.cast(GET /api/clusters/:clusterId/recordings,GET …/recordings/:id/cast, admin). There is no inline playback in the browser; customers bring their own player. Every read is logged asstream.recording_read. - It has its own retention dial: System settings → Data retention → “Shell recordings” (default 30 days) — deliberately separate from the operation retention period, and it does not enter E2's archive plane.
- When the license is removed, no new recording starts, but existing recordings remain readable — data is never held hostage.
- Can be enforced with a policy. The guardrail CEL context sees the cluster's
recording posture and whether it is actually active; a closed template ships in the repo
(
ops.recording-required) — how to turn on the rule "a terminal session that will not be recorded cannot open" in your own namespace is on the Policies page.
Secret display
One setting, three modes — in a restricted mode, no one sees a Secret value through YEKE, owner included.
The owner picks the mode in Administration → System → System settings → Sessions & access → “Secret values”; it is identity-independent. Kubernetes authorization and permission groups still decide who can reach which key — this ceiling changes what is SHOWN for a key you can reach.
| Mode | What it does |
|---|---|
Full (full, default) |
Values are shown. |
Masked (masked) |
Replaces the value, at every egress path, with a marker that stays the same across equal values. |
Hidden (hidden) |
Removes the marker too, leaving only key names. |
If the deployment sets YEKE_SECRET_DISPLAY (full,
masked or hidden), the field shows as locked in the UI; an
unrecognized value stops core at startup.
- Blind write. In a restricted mode the Secret screen draws no "Show values"; instead it shows a mask chip and a per-key "New value" field — the new value is written without the old one ever being shown.
- Enforced at every egress path. The read proxy (browser + shell), the watch stream, the shell write-echo, plan/audit records and GitOps reflection all go through the same rule; writing a mask marker back (plain, base64-decoded, or inside a JSON patch) is denied in every mode.
- Combines with a permission group's key rule, doesn't replace it. Effective
visibility is the stricter of this ceiling and the permission group's key rule — even if a group
marks a key "View", the value stays masked while the ceiling is
masked/hidden. - The same ceiling applies to Secret versions history. In a restricted mode the diff screen keeps values masked; a "blind restore" is possible only by knowing which key changed.
- Restricted modes need a license. Without the Enterprise
secret-displayflag, System settings won't save Masked or Hidden, and a restricted mode given by the deployment keeps an unlicensed first install from starting — silently falling back tofullwould show every value meant to stay hidden. If the license is later removed, core still starts — the stricter of the previously enforced mode and the setting applies, with aWARNINGline in the startup log and a warning in the UI (a "Secret display" badge on the License page). - Boundary: this is a display boundary through YEKE, not a Kubernetes one. Reading from inside a pod via exec, a workload that logs the value, and cluster access outside YEKE are all out of scope.
Compliance report
A cluster × period change report — from a single endpoint, with a single template.
The report is a derivative: it writes no new table or event. Its sources are the
audit trail, admin approvals (admin_actions), and — if the period has fallen
into archive — E2's archive. One endpoint:
GET /api/clusters/:clusterId/reports/change?from=&to=&format=&lang=
(admin); to download it from the screen, /admin/reports.
- An absent format falls back to
html(a single file that opens even in an air-gapped install); a misspelled one gets400 INVALID_FORMAT. The alternate format isndjson. - The period cap is 366 days (a full leap year);
from >= tois rejected with400 INVALID_PERIOD. - The report carries its own integrity: it includes the start/end canonical hash
of the chain and the event count for the range it covers, and can be verified with the
existing
verify-tool— turning the report from a claim into evidence. - Scope statement: a single template is fixed as the product ("cluster × period change report") — there is no report-template editor or user-defined report. For terminal session recording the statement now gives counts: sessions opened in the period, how many started recording, how many opened with recording off.
- Without a license the report endpoint returns
403 LICENSE_FEATURE_LOCKED; raw trail reads (GET /api/audit) and NDJSON export (SIEM) are unaffected.
pnpm --filter @yeke/core exec tsx tools/audit-export/verify.ts \ --input yeke-change-report.ndjson --format ndjson
The report's NDJSON is unsigned — --pubkey is not needed; the
tool still checks the chain's start/end values and the event count.
Without a license
Six items, four different classes — a single "403" would not describe any of them correctly.
| Item | Flag | Without a license |
|---|---|---|
| Dual approval | dual-approval |
The use gate is never checked; a configured rule stays in force
— the only thing that's cut off is defining a new rule. In file mode, a new or changed rule
stops core from starting. |
| Maintenance windows | change-freeze |
The use gate is never checked; a configured window stays in
force — the only thing that's cut off is defining a new window. In file mode, a new or changed
window stops core from starting. |
| Centralized policy package | policy-central |
If central mode was requested, startup stops; if not, file mode
(the default) runs normally and stays bit-for-bit unchanged. |
| Terminal session recording | session-recording |
No new recording starts; existing recordings remain readable — data is never held hostage. |
| Secret display | secret-display
(phase E6) |
If a restricted mode was never before enforced on this install, startup stops. If it was, core still starts: the stricter of the previous mode and the setting applies, and a warning is logged. |
| Compliance report | compliance-reports |
The report endpoint returns 403 LICENSE_FEATURE_LOCKED; raw
trail reads and NDJSON export are unaffected. |
Never held hostage: removing a configured rule is always possible —
taking it out of the policy file or package is not the configure gate's
concern, it's the source's own, and the operator can always reach it.
Limits
Unsupported cases and known limitations.
- No quorum (N>2 approvers) in dual approval. The schema already supports
multiple rows at no cost, but today's gate says
count >= 1and there is no setting for it in the interface. - No break-glass / freeze-bypass path. It's waiting for its own round: who can break it, with what record, who gets notified, what the way back is — a half-built break-glass would be worse than the freeze itself.
- The report template is SINGLE today: "cluster × period change report". There is no template editor or user-defined report.
- No inline recording playback in the browser, only the
.castdownload.