lookout stab drift
Find spec fields of Deployments/StatefulSets/DaemonSets owned by a manager other than the GitOps controller (managedFields) — out-of-band kubectl edits and rogue co-managers. Reports manager strings (tool names, not people); —identity additionally resolves each drift write to the audited principal via the cloud provider’s audit trail (GKE Cloud Audit Logs), reporting an explicit unavailable on clusters without one. Default scope: all namespaces; scanned counts workload objects examined.
MCP tool: k8s_gitops_drift
lookout stab drift [flags]| Flag | Type | Default | Meaning |
|---|---|---|---|
--manager | string | — | the declared GitOps manager (e.g. argocd-controller); empty auto-detects it as the manager owning a strict majority (>50%) of the spec leaf fields summed across the scanned objects AND recognized as a GitOps controller (Argo CD, Flux, Helm, Config Sync, Fleet, kapp, Terraform, Pulumi). No manager clears both bars — the usual shape of a cluster with no GitOps controller at all — and the scan resolves to detection=none and emits nothing rather than measuring drift against a guess; the summary then names the leading candidate (ties to the lexicographically smallest) and its share, to pass back here if it is in fact the GitOps controller. A declared manager skips both bars: the operator knows their cluster |
--identity | bool | — | resolve each finding’s last drift write to the audited principal (who ran it) via the cloud provider’s audit trail; requires a provider with the audit capability (GKE: Cloud Audit Logs admin-activity read), otherwise the summary line reports an explicit unavailable |
Common flags (every lookout command)
Section titled “Common flags (every lookout command)”| Flag | Type | Default | Meaning |
|---|---|---|---|
--namespace | string | — | limit the scan to one namespace |
-A | bool | — | scan all namespaces |
--workload | string | — | target one workload as <Kind>/<namespace>/<name>, e.g. Deployment/prod/api |
--since | duration | — | how far back to look (0 = command default) |
--format | string | logfmt | output format: logfmt|json (one record per line either way) |
--timeout | duration | 10s | abort the invocation after this long (exit 1) |
--kubeconfig | string | — | path to a kubeconfig file, instead of $KUBECONFIG / ~/.kube/config |
--context | string | — | kubeconfig context to read, instead of its current-context. Selects a cluster for THIS invocation only — nothing is written back — so concurrent invocations can target different clusters. Reported as context=<name> in the summary line |
--exemptions | string | — | path to a git-reviewed exemption file (YAML); covered findings are ANNOTATED with their reason and expiry and counted as exempt=<n> in the summary, never dropped |
Finding kinds
Section titled “Finding kinds”Every kind= this command can emit, and the severities it carries them at. Nothing else appears in its output; a kind absent from a run means the check looked and found nothing. See the finding-kind glossary for the whole vocabulary.
| Kind | Severity | Claim |
|---|---|---|
drift.manual_edit | critical, warning | a manager other than the GitOps controller owns spec fields on this object; critical when one of them is high blast radius (image, replicas, env) |
Output fields
Section titled “Output fields”Beyond the shared envelope fields (kind, severity, namespace, kind_of_object, name, reason, message, fingerprint, exempt_reason, exempt_expires):
| Field | Meaning |
|---|---|
manager | on findings: the foreign manager string from managedFields (a tool name like kubectl-edit — never a user identity; see —identity); on the summary line: the resolved GitOps manager |
detection | summary note: how the GitOps manager was resolved — declared (—manager), majority (auto-detected recognized GitOps controller owning >50% of the spec leaf fields in scope), or none (no manager resolved; nothing emitted) |
detection_reason | summary note on detection=none, naming why: no-spec-fields-in-scope (nothing in scope owns a spec field), no-majority-manager (a leading candidate exists but owns 50% or less), or not-a-gitops-manager (the majority owner is not a recognized GitOps controller — e.g. kubeadm or a kubectl manager on a cluster with no GitOps at all) |
candidate | summary note on detection=none: the leading manager that fell short (of the majority, or of being a recognized GitOps controller) — pass it to —manager if it is in fact the GitOps controller |
share | summary note: the resolved manager’s (or, on detection=none, the candidate’s) percentage of every spec leaf field owned across the scanned objects, rounded. A declared manager with a low share means most findings are other legitimate owners |
unmanaged | summary note, omitted at zero: scanned objects the resolved GitOps manager owns no spec field on. Nothing is reported for them — an object the manager never applied cannot have drifted from it — so a high count next to zero findings means the manager’s scope is narrower than the scan’s |
operation | managedFields operation of the foreign manager’s last write: Apply or Update |
tool | client tool recognized from the manager string (kubectl for kubectl-edit/kubectl-patch/kubectl-*) |
fields | compact spec paths the foreign manager owns (e.g. spec.template.spec.containers[app].image), capped at 8 with a +N more tail |
field_count | total spec leaf fields the foreign manager owns on this object (uncapped) |
age | how long ago the foreign manager last wrote (managedFields time); omitted when the API server recorded no time |
principal | —identity: the audited principal of the write nearest the drift time (GKE: principalEmail), or the explicit sentinel none-in-audit-window / no-write-time-anchor when the trail cannot answer |
principal_agent | —identity: the caller-supplied client string of that write (a kubectl or controller user-agent), when the trail records one; caller-controlled text, display-only |
other_principals | —identity: other distinct principals that wrote the object inside the audit window, capped at 8 with a +N more tail |
identity | summary note when —identity could not be served: the unavailable marker naming why (no provider / audit capability absent) |
Output contract
Section titled “Output contract”Output: one finding per line (logfmt; —format=json for one JSON object
per line), keys in fixed order; healthy resources emit nothing. The final
line is always the summary: scanned=
Examples
Section titled “Examples”lookout stab driftlookout stab drift --namespace=prod --manager=argocd-controllerlookout stab drift --workload=Deployment/prod/api --identitylookout stab drift --workload=Deployment/prod/api --format=json