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 use gate is never checked, only configure is.

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 apply inside the window waits. It returns 409 APPROVAL_HOOK_DENIED plus params.source: "change-freeze" and params.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.

VariableDefaultWhat it does
YEKE_POLICY_MODEfile 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) and apply runs 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 as stream.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 gets 400 INVALID_FORMAT. The alternate format is ndjson.
  • The period cap is 366 days (a full leap year); from >= to is rejected with 400 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.

ItemFlagWithout a license
Dual approvaldual-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 windowschange-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 packagepolicy-central If central mode was requested, startup stops; if not, file mode (the default) runs normally and stays bit-for-bit unchanged.
Terminal session recordingsession-recording No new recording starts; existing recordings remain readable — data is never held hostage.
Compliance reportcompliance-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 >= 1 and 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 .cast download.

The product looks for the second person

A plan awaiting dual approval, or one blocked by a freeze window, is queued onto the existing notification hook — not a new mechanism, a new trigger on the one that already exists.