Repository navigation
feat(operator): report whether the Secret/ConfigMap write policy is in force (#2500) - #2580
Open
AhmedElBanna80 wants to merge 8 commits into
Open
AhmedElBanna80 wants to merge 8 commits into
AhmedElBanna80 wants to merge 8 commits into
Conversation
…in force (#2500) The operator write ValidatingAdmissionPolicy can be off with nothing reporting it: a server older than 1.30, an install under another namespace or service account name, a deleted policy or binding. The operator now reads the policy and binding (get, by name) and its own identity (SelfSubjectReview), publishes knext_operator_write_policy_enforced (0/1, 0 when unverifiable), warns once per change and re-checks every five minutes. It never refuses to start. knext doctor mirrors the verdict (shared fixture), the alert fires on a 0, and the new role grants get on exactly the two named objects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
…ives (#2500) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
…lace K03 with a reachable row (#2500) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
…00-write-policy-off
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ons; alert on a missing gauge (#2500) Round-1 review. The gauge read 1 while the policy was not enforcing: - matchConstraints narrowing (excludeResourceRules, resourceNames, Cluster scope, another apiVersion) was not detected: PolicyScope. - the validation body was never checked, so validations: [true] was Enforced. The variables and validations are now compared with a canonical copy of the shipped CEL, held in lockstep by a test in each language: PolicyModified. - the == 0 alert was blind when the gauge is absent: add KnextOperatorWritePolicyUnreported, keyed on a discovered operator target. - doctor: an unclassifiable read or unparseable output is a WARN, not a skip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
…nion-alert detections (#2500) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
…t lands (#2500) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2500
What this does
The operator's Secret/ConfigMap write policy (a
ValidatingAdmissionPolicyplus binding) can be off with nothing reporting it: a server older than Kubernetes 1.30, an install under another namespace or ServiceAccount name than the bundle rendered, or a deleted policy or binding. This adds a read-only self-check and reports it three ways.internal/writepolicy). At start-up and every 5 minutes (--write-policy-check-interval) itgets the policy and binding by name, asks the API server who it is (SelfSubjectReview), and publishesknext_operator_write_policy_enforced(0/1). It logs one warning per change. It does not refuse to start (that stays a founder call). It runs on every replica, leader or not. An unreadable object or identity is 0, never 1.failurePolicyis notIgnore; it covers create/update/delete on Secrets and ConfigMaps with no namespace or object narrowing; its matchCondition is an exact equality on the operator's own username; the binding names it, includesDeny, and is not scoped.KnextOperatorWritePolicyNotEnforced(== 0, 15m, warning).knext doctorrowwrite-policy: same verdict, plus the server version against the 1.30 floor. WARN when not enforced, never FAIL.internal/writepolicy/testdata/verdict-cases.json(20 cases).RBAC (exactly
geton the two named objects)A dedicated
write-policy-readerClusterRole (config/rbac/write_policy_reader_role.yaml):getonvalidatingadmissionpoliciesandvalidatingadmissionpolicybindings,resourceNamesthe policy name. It is a hand-written role rather than a+kubebuilder:rbacmarker onmanager-rolebecause a marker'sresourceNamesis a literal the bundle's name prefix does not reach;config/defaultrewrites the role'sresourceNamesand the manager's newWRITE_POLICY_NAMEenv from the policy's real prefixed name.SelfSubjectReviewneeds no rule (system:basic-user). Pinned byTestWritePolicyReaderRoleIsExactlyGetOnTheTwoNamedObjects(controller) and the rendered-bundle test; the existingTestOperatorCannotEditItsOwnWritePolicynow covers the new file too.Tests
TestSelfCheckAgainstARealApiserver): matching -> 1; policy deleted, binding deleted, wrong username, Audit-only binding -> 0; an identity holding exactly the shipped role runs the check, cannot list, cannot read another policy, cannot delete; a forbidden read is Unverifiable and 0.resourceNames, and binding all follow the prefixed policy name.runDoctor: pass, policy missing, binding missing, renamed ServiceAccount, audit-only, 1.29 server, configured name, RBAC-denied, unreachable, no operator, read-only).scripts/mutation-prove-2500-write-policy-selfcheck.mjs26/26 killed (includes "check forced to true" reds the deleted-policy case on a real apiserver);scripts/mutation-prove-2500-write-policy-ts.mjs11/11 killed (alert + doctor). Registered in the prover lane and shard weights.Not done in this PR: a kind-cluster lane assertion. The live behaviour (a real apiserver, a real SelfSubjectReview, real RBAC) is covered by the envtest above; there is no pre-1.30 server in CI to drive the version-floor path, so that path is covered by the doctor unit test only.
Docs summary
User-visible: new
knext doctorrow, new metric and alert, a new operator flag/env. Docs updated:apps/docs/content/docs/security.mdx(new section "Knowing whether the policy is in force"),cli.mdx(doctor),slos-alerting.mdx(metric + alert); internaldocs/security/threat-model.md; changeset for@getknext/core. No issue, PR or ADR numbers in the user docs.Round 2 (code review: the gauge read 1 while the policy was not enforcing)
Cases first in the shared
verdict-cases.json(20 -> 37 cases), so Go and TS pin each one; seen red, then fixed.PolicyScope(gauge 0, doctor WARN). AnyexcludeResourceRules; a covering rule withresourceNames; a covering rule scopedCluster; a covering rule whoseapiVersionslacksv1/*.Namespaced/*scope and an unrestricted rule beside a restricted one still pass (table cases).PolicyModified(gauge 0, doctor WARN). The policy's variables (name + expression) and each validation'sexpression+messageExpressionare compared with a canonical copy of the shipped CEL (canonical.go,CANONICAL_POLICY_CELinwrite-policy.ts).messageandreasonare cosmetic and not compared. The shared fixture's base policy now carries the real shipped CEL instead oftrue. Drift is closed by tests in both languages that fail ifconfig/admissionchanges without the copy (TestCanonicalPolicyCELEqualsTheShippedPolicy,TestSharedTableBasePolicyCarriesTheShippedCEL, and the TS twins); a real apiserver round-trips the shipped policy unchanged (envtest still green). Consequence, documented: an operator newer than the installed policy reads 0 until the bundle is reapplied.KnextOperatorWritePolicyUnreported:up{job=~".*controller-manager-metrics-service"} unless on (namespace, pod) knext_operator_write_policy_enforced, 15m, warning. Keyed on a discovered target, not a bareabsent()(which would fire on every cluster with no operator). The test checks the job selector against the rendered Service name (namePrefix+metrics_service.yaml) so it cannot select nothing. Side effect to review: the metric-contractknext.operatorgroup now also allows theexternalemitter soupresolves, which also lets other externally emitted names pass in that group.DEFAULT_POLICY_NAMEcomment fixed; the TS fallback (the bundle's rendered, prefixed name) and the Go flag default (unprefixed base name; the bundle always sets the env) differ on purpose, now documented and pinned by a test.Docs:
security.mdx,slos-alerting.mdx,cli.mdx, the threat model (Silent-off detection bullet) and the changeset now state exactly what is verified.Mutation provers extended, run by exit code from a committed green tree with exact-once anchors: Go prover 33/33 killed (+7: exclude, resourceNames, Cluster scope, apiVersion, body comparison removed, canonical copy drift, shipped CEL changed); TS prover 22/22 killed (+3 alert rows for the companion alert, +5 doctor scope/body rows, +3 on the canonical spec for the WARN-not-skip nits and copy drift). A first run of the new rows had mutants whose residue comment swallowed the rest of the line; I fixed the rows and re-ran everything, so the counts above are from the fixed provers.
Escalation
Escalation trigger acknowledged: security invariant — adds a ClusterRole/ClusterRoleBinding (
geton two named admission objects), an operator flag/env (--write-policy-name,WRITE_POLICY_NAME,--write-policy-check-interval), and a new CLI doctor row; no mutating endpoint, no secret handling, no CRD field change, and the operator still holds nothing that can write the policy.Escalation trigger acknowledged: CLI surface —
knext doctorgains awrite-policyrow (read-onlykubectl gets, WARN-only).Escalation trigger acknowledged: CRD — none touched (
api/v1alpha1unchanged); listed only becauseconfig/andcmd/main.goare in the diff.🤖 Generated with Claude Code
https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx