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

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: 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.

ModeWhat 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-display flag, 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 to full would 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 a WARNING line 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 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

Six items, four 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. In file mode, a new or changed rule stops core from starting.
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. In file mode, a new or changed window stops core from starting.
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.
Secret displaysecret-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 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.