yeke.io · docs · enterprise
Governance
Bring your change process into YEKE with dual approval, change freezes, central policies, terminal recordings and compliance reports. This guide covers setup and limits for each feature.
Five items, five flags — all Enterprise: dual approval
dual-approval, maintenance windows change-freeze, centralized
policy policy-central, session recording session-recording,
compliance reports compliance-reports.
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.
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.
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:
YEKE_EXEC_RECORDING_RETENTION_DAYS(default 30 days) — deliberately separate from the operation retention duration (YEKE_OPS_RETENTION_DAYS), 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.
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
Five items, three 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. |
| 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. |
| 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. |
| 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.