Repository navigation
fix(kubectl): unknown namespace and unread manifests escalate instead of silently not matching - #38
Conversation
… of silently not matching
- namespace-unresolved: a namespace-scoped rule that would match a
kubectl/helm intent with no namespace (or -A) escalates, as env did;
cluster-scoped kinds unaffected
- --resolve-current-context fills the namespace from the kubeconfig
context ('default' when unset), for the current or an explicit context
- manifest-not-inspected: apply/create/replace/delete -f/-k escalates
rules for the same action written on kinds or objects; manifests are
still never read
- benchmark accuracy unchanged on rerun
Reported by a reader on dev.to (Chris Sellers).
moneytool
left a comment
There was a problem hiding this comment.
Not ready to merge: I reproduced two namespace-scoped policy bypasses introduced by the new opt-in namespace resolution. The 564 focused existing tests pass, but two additional regression tests return ALLOW where the prod namespace rule should BLOCK or ESCALATE. Please add coverage for explicit kubeconfig selection and manifest-provided namespaces alongside the fixes below.
| environ = os.environ if environ is None else environ | ||
| raw = environ.get("KUBECONFIG", "") | ||
| first = raw.split(os.pathsep)[0].strip() if raw.strip() else "" | ||
| path = Path(first).expanduser() if first else Path(home or Path.home()) / ".kube" / "config" |
There was a problem hiding this comment.
[P1] Resolve the namespace from the command-selected kubeconfig
This lookup ignores an explicit --kubeconfig and reads only the environment/default file. With context ctx using namespace dev in ~/.kube/config, but namespace prod in production.yaml, kubectl --kubeconfig production.yaml --context ctx rollout restart deploy/web gets annotated as namespace=dev. With current-context resolution enabled, a deployment rollout-restart BLOCK rule scoped to namespace=prod then returns ALLOW even though kubectl targets prod; I reproduced this. Pass the selected kubeconfig through to the namespace resolver, respecting explicit flag precedence and effective KUBECONFIG configuration, or retain an unresolved namespace when the effective configuration cannot be established. Add a regression using the same context name in two files with different namespaces.
There was a problem hiding this comment.
Fixed in b2512c6.
- Root cause: the kubectl parser discarded
--kubeconfig, so resolution always read$KUBECONFIGor~/.kube/config. That affected the context too, not only the namespace. - Parser: an explicit
--kubeconfigis now recorded on the intent (metadata.kubeconfig). - Resolution: with an explicit kubeconfig, that file is the only one read, for both the context and the namespace. If it cannot be read, nothing is filled and the namespace stays unresolved, so namespace rules escalate. There is no fallback.
- Without the flag: the files in
$KUBECONFIGare now merged as kubectl merges them (the first file to setcurrent-contextor define a context wins), not just the first file read. - Regression tests: context
ctxis in both~/.kube/config(namespacedev) andproduction.yaml(namespaceprod).--kubeconfig production.yaml, with and without--context ctx, now resolvesprod, and the prod rule BLOCKs. Also tested: an unreadable explicit kubeconfig gives ESCALATEnamespace-unresolved, and the$KUBECONFIGmerge order. These tests fail on the previous commit.
| from aegis_core.store import _kind_is_cluster_scoped # local: avoids a cycle | ||
|
|
||
| if not _kind_is_cluster_scoped(intent.resource): | ||
| fill("namespace", kubeconfig_context_namespace(md["context"], environ, home)) |
There was a problem hiding this comment.
[P1] Keep unread manifest namespaces unresolved
A context namespace is only a default for manifest operations without an explicit -n; objects can supply their own metadata.namespace. This fill converts kubectl apply -f - under a dev context into a known namespace=dev, so a secret/apply BLOCK rule scoped to prod returns ALLOW even when the unread stdin manifest creates a prod Secret. I reproduced the ALLOW verdict despite manifest_not_inspected. Kubernetes explicitly permits namespaces in object specs to override the default when namespace enforcement is disabled (resource builder contract). Skip this inference for unread manifest intents, or distinguish a default namespace from an enforced namespace so namespace-scoped rules still ESCALATE. Add a regression with a dev context, no -n, an unread manifest, and a prod-scoped kind rule.
There was a problem hiding this comment.
Fixed in b2512c6. The context namespace is no longer inferred for intents with manifest_not_inspected, because it is only a default the objects can override. The namespace stays unresolved, so a prod-scoped kind rule escalates (manifest-not-inspected, and namespace-unresolved where it applies) instead of being decided as dev.
- Explicit
-n: still decides normally, since kubectl enforces it on every object. - Regression test: a dev context, no
-n,kubectl apply -f -, and a prod-scopedsecret/*apply rule now give ESCALATE, where the previous commit gave ALLOW. The docs (docs/constraints.md) say both things.
… never for unread manifests - --kubeconfig is now recorded on kubectl intents; with it, it is the only file read for context and namespace, and an unreadable one resolves nothing (no fallback to $KUBECONFIG or ~/.kube/config) - $KUBECONFIG files merge as kubectl merges them (first definition wins) - an unread manifest never takes the context's namespace, which is only a default its objects can override; the namespace stays unresolved Review of #38.
This applies
build/promo/SPEC-kubectl-context-and-stdin.md: two kubectl gaps reported by Chris Sellers on dev.to. It changes decisions, which is why it's separate from the docs PR (#37).Gap 1: namespace left to the kubeconfig context
namespace-unresolved: <id>: a rule scoped onnamespacethat would otherwise match now escalates when the kubectl or helm command names no namespace, or uses-A(which coversprodtoo). Before, such a rule silently did not apply.env-unresolved, using the same mechanism (ConstraintStore.get_scope_unresolved).node,namespace, …, from the compiler's kind registry) are never affected.--resolve-current-contextnow also fills a missing namespace from the context'snamespacefield, ordefaultwhen the context sets none, as kubectl does. That works for the current context and for an explicit--context. It is listed inresolved_from_environment, and never applied to cluster-scoped kinds or with-A.Gap 2: objects that live in a manifest
manifest-not-inspected: <id>:kubectl apply|create|replace|delete -f <file>|<dir>|-and-know escalate any rule for the same action written on kinds or objects (for examplesecret/*), because the manifest may hold exactly what that rule protects.*already matched the manifest intent and still do, sodelete -f ./manifests/ -n prodstays a BLOCK.manifest/<glob>are still matched against the file name.The reader's table, against the example policy, now tested
kubectl delete deploy web -nprod/--namespace=prod/sh -c '…'kubectl delete deploy webnamespace-unresolvedkubectl rollout restart deploy/webwith anamespace: prodrestart rulenamespace-unresolvedkubectl apply -f -(±-n prod) with asecret/*apply rulemanifest-not-inspectedkubectl delete -f ./manifests/manifest-not-inspectedforno-delete-nodesand otherskubectl delete -f ./manifests/ -n prodImpact
-nin a project with namespace-scoped rules now gets an "ask" in Claude Code, Copilot, VS Code and Cursor, and a deny in Codex, Gemini CLI and OpenCode, where Aegis cannot ask. The way through is to pass-n, or to runaegis checkwith--resolve-current-context.results/benchmark.json. Every accuracy metric is identical; only latency differs, by a few hundredths of a millisecond. No committed results change.test_evade_namespace_omitted…intest_adversarial.pydocumented the old silent non-match as correct. It now asserts the escalation.tests/test_kubectl_namespace_and_manifests.py, plus tests for the context namespace fill.docs/constraints.mdanddocs/cli.mddescribe both behaviours. The CHANGELOG credits the reader.