window fri 16:00 → mon 06:00 frozen for humans controllers keep running opinion, unless asked none

Kubernetes · k3s · OpenShift · change freeze

sabbath

On the seventh day, the cluster rests — from human hands.

A scheduled read-only window for your cluster. During a freeze, the API server itself declines mutating changes made by non-exempt people, then lifts automatically when the window ends. Reads are never touched, and controllers keep running — it freezes the ad-hoc human change, not the cluster.

$go install github.com/vikozs/sabbath/cmd/sabbath@latest
Source on GitHub Download a binary

Why

Friday afternoon is where weekend incidents are born.

Someone ships one more change before the weekend, it breaks at 18:00, and the people who could fix it are gone until Monday. Plenty of teams run a manual change freeze — as good as everyone's memory of it. sabbath makes the freeze a property of the cluster instead of a Slack reminder.

But "read-only cluster" cannot mean block every write. A cluster with all writes blocked is a dead cluster: leader-election Leases stop renewing, controllers can't write status, the autoscaler can't act, node heartbeats fail. So sabbath freezes human-initiated changes to workload and config resources, and by default exempts everything that isn't a person — every service account, every node. That one default is what keeps your operators, ArgoCD, cert-manager and kubelet working straight through the freeze.

The window

read-only · humans
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Mon
frozen
00:00Fri 16:00 → Mon 06:0006:00

$ sabbath status  →  FROZEN (window: weekend). Lifts Mon 06:00 CEST.

$ oc apply -f deploy-payments.yaml
Error from server (Forbidden): admission webhook/policy denied the request:
  sabbath: change freeze active (window: weekend). Gated changes
  blocked until Mon 06:00 CEST. Use the break-glass group or run:
  sabbath lift --reason <ID>.
 
✓ controllers reconciling   ✓ leases renewing   ✓ argocd health OK
✓ hpa scaling   ✓ status subresources writable   ✓ reads unaffected

How it works

Split the clock from the enforcement.

Enforcement is a ValidatingAdmissionPolicy — API-server-native, no webhook pod in the mutating path, so sabbath can't become a cluster-availability dependency. If everything sabbath runs falls over, the API server keeps enforcing the last known state.

The catch: CEL at admission has no access to wall-clock time — there is no now(). So a tiny controller (the same binary, sabbath reconcile --loop) is the clock: it works out whether the freeze is active from your windows and timezone, and writes active: true|false into a ConfigMap the policy reads. Preview it with sabbath simulate --at "Fri 18:00" — that uses the exact decision the policy enforces, so what you preview is what gets blocked.

Gate humans, not the system

$ sabbath simulate --at "Fri 18:00"during a freeze
RequestSubjectVerdictWhy
update deploymentalice (human)BLOCKEDa person changing workloads mid-freeze
update deploymentsystem:serviceaccount:…allowedservice accounts exempt by default
update leasekube-controller-managerallowedleader election must keep working
update /statusanyallowedstatus subresources are never gated
delete deploymentoncall (break-glass)allowedthe break-glass group always bypasses
create deploymentargocd controllerBLOCKEDonly if you opt in to freezing deploys

Defaults exempt every service account and node, plus a break-glass group and cluster-admins. The gated set is an allowlist of resource types — never *, never /status, Leases or Events.

Two things you'll want on day one

Warn before you enforce

Install in warn mode and sabbath reports what it would have blocked without blocking anything. Run it for a few Fridays, find the automation you forgot to exempt, then switch to enforce.

sabbath install --mode warn
# ...a few weekends later...
sabbath install --mode enforce

Break-glass is mandatory

A freeze you can't escape during a Friday-night incident is worse than none. A break-glass group always bypasses, and one command lifts the freeze now — the clock won't re-freeze before your --until.

sabbath lift --reason INC-4242 --until 22:00
sabbath resume   # hand control back to the schedule

Backends

ValidatingAdmissionPolicy — the default

GA on Kubernetes 1.30 and OpenShift 4.17+. No webhook in the request path, so no availability risk. This is the path to prefer wherever the version allows it.

  • Backend--backend vap
  • Needsk8s 1.30 / OCP 4.17+
  • Request pathno webhook pod

Webhook — the fallback

For older clusters, a validating webhook that reuses the identical decision logic — same freeze semantics, no second implementation. It sits in the request path, so it runs HA and fails open by default.

  • Backend--backend webhook
  • TLScert-manager, or bring your own
  • failurePolicyIgnore (fails open)

How it behaves

A guardrail, not a security control

sabbath stops routine, risky, human changes during a window. It is not an authorization boundary: a determined cluster-admin can remove it, and that is deliberate — break-glass must always be possible. Reads are never affected, because reads never reach admission.

  • Blockshuman mutations, while frozen
  • Never blocksreads, SAs, nodes, status, leases
  • Installone static binary
  • LicenceApache-2.0

Complementary to GitOps windows

ArgoCD sync windows already pause deploys. sabbath's distinct job is freezing everything a person might do by hand, cluster-wide — including the changes that never go through Git. Freeze deploys too if you want, by opting the CD service account into the gated set.

sabbath install --backend vap --mode enforce \
  --gate-serviceaccount \
  system:serviceaccount:argocd:argocd-application-controller