0110. The safety-check override is a flag, and its reason is optional
Status: Accepted · Date: 2026-11-03 · Design section: Trust, Validation · Touches: 0065, 0085, 0089, 0107, 0109
Decided: the override of a safety check is requested with a flag (--override, override on a node operation), and a reason is optional, recorded with --override-reason or overrideReason; the RBAC verb override stays the only gate, and an override with no reason is recorded as "no reason given".
Context
ADR 0065, as renamed by ADR 0109, made the override a string: --override "<reason>" on the CLI, Override{reason} on the API, and spec.node.overrideReason with MinLength=1 as the signal itself. A non-empty reason was what turned the override on, and the etcd restore spec required one. The user asked on 2026-11-03 that the override be usable without a reason.
The reason was never the gate. The gate is the Kubernetes verb override, checked as the person making the request. A typed reason adds friction in an incident without adding any authority, and it pushed people to type placeholders, which made the record no more useful. The decision to override, and who made it, still needs to be recorded; only the requirement to explain it goes.
Decision
- The request is a flag.
--overrideis a boolean on the CLI. A node operation carriesspec.node.override(boolean, appended afteroverrideReason). On the API theOverridemessage is sent when the caller wants an override, and its presence is the request. A request overrides whenoverrideis true, or when a reason is set inoverrideReasonor in the deprecatedbreakGlassReason. - The reason is optional and only recorded.
--override-reason <text>on the CLI,overrideReasonon the CRD (its MinLength=1 stays; an empty value is omitted), andOverride.reasonon the API. The CLI refuses--override-reasonwithout--override, so a reason is never dropped silently. - RBAC is the gate, with or without a reason. The same
SelfSubjectAccessReview(in the handlers) orSubjectAccessReview(in the webhook) asks for the verboverrideon the same resource and name, whenever the request overrides. The affected operations are unchanged: reset, reboot or shutdown past the last-worker guard, and the etcd restore, which is always an override. - The record says plainly what was given. The audit record's reason, the Warning Event (
under override: <reason>) and the reason stored on an etcd restore sayno reason givenwhen none was typed. The etcd restore's specreasonis required by the CRD, so it stores that text rather than an empty value; the schema does not change. - The UI offers "Override the safety check" as a checkbox that gates Start, with an optional reason box below it. The checkbox is the consent; the reason is not required.
Why this shape
- The verb already says who may override. Asking for a reason on top of it adds nothing a permission check does not already settle.
- The request still records who overrode and when, through the audit record, the Event and the API server's own audit log. The "no reason given" text marks the record for an auditor who wants a reason.
- A flag replaces a value because a value made the reason the switch. Keeping the reason separate lets the switch be one word.
Compatibility
- Existing operations. An operation written with
overrideReason, or with the deprecatedbreakGlassReason, still overrides and is still gated by the same verb.Overrides()counts a reason as an override, so nothing that was gated becomes ungated. - Existing CRD.
overrideis appended. No field is removed or renamed. An object written before it hasoverridefalse, and whatever reason it carries still gates it. - Old clients. An older CLI or UI sends
Override{reason}with a non-empty reason, which the new server reads as an override with that reason. The Connect message shape is unchanged; only the comments on it change. - New clients against an older server. A reason-less override is refused by a server built before this decision ("needs an override reason"). The fix is to add
--override-reasonand retry, or to update the server. - Scripts.
--override <reason>no longer takes a value. Moving the reason to--override-reasonis the one change needed. There is no alias, as ADR 0109 chose for the earlier flag.
Consequences
- The verb is the only gate for an override. Anyone holding it can override without giving a reason, and the record will say so.
- The reason stays available for an incident that needs one, and is recorded whenever it is typed.
- The etcd restore keeps a required
reasonin its spec, now filled with "no reason given" when none was typed, so the restore record always has a value. - The Connect messages keep their names; the proto comments and the generated code change only where their text does.
Amendment 2026-11-05 (ADR 0130): the optional reason is gone too. Decisions 2, 4 and 5 no longer hold: there is no --override-reason, Override.reason and overrideReason are deprecated and unused, the audit record and the Event no longer say no reason given (they say nothing about a reason), the etcd restore's spec.etcdRestore.reason is optional and not written, and the UI's override checkbox has no reason box. Decisions 1 and 3 (the flag, and the verb as the only gate) stand.