Skip to content

fix(kubectl): unknown namespace and unread manifests escalate instead of silently not matching - #38

Merged
moneytool merged 3 commits into
mainfrom
kubectl-context-stdin
Oct 9, 2026
Merged

moneytool merged 3 commits into
mainfrom
kubectl-context-stdin

Conversation

@moneytool

Copy link
Copy Markdown
Owner

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

  • New namespace-unresolved: <id>: a rule scoped on namespace that would otherwise match now escalates when the kubectl or helm command names no namespace, or uses -A (which covers prod too). Before, such a rule silently did not apply.
    • This mirrors env-unresolved, using the same mechanism (ConstraintStore.get_scope_unresolved).
    • A tampered or unauthorised rule still cannot force the escalation.
    • Cluster-scoped kinds (node, namespace, …, from the compiler's kind registry) are never affected.
  • --resolve-current-context now also fills a missing namespace from the context's namespace field, or default when the context sets none, as kubectl does. That works for the current context and for an explicit --context. It is listed in resolved_from_environment, and never applied to cluster-scoped kinds or with -A.

Gap 2: objects that live in a manifest

  • New manifest-not-inspected: <id>: kubectl apply|create|replace|delete -f <file>|<dir>|- and -k now escalate any rule for the same action written on kinds or objects (for example secret/*), because the manifest may hold exactly what that rule protects.
    • Rules on * already matched the manifest intent and still do, so delete -f ./manifests/ -n prod stays a BLOCK.
    • Rules on manifest/<glob> are still matched against the file name.
  • Manifests are not read. Reading the file at decision time would let the agent change it between the check and the run, and the spec makes reading optional. The docs say so.

The reader's table, against the example policy, now tested

Command Before Now
kubectl delete deploy web -nprod / --namespace=prod / sh -c '…' BLOCK BLOCK
kubectl delete deploy web ESCALATE (env only) ESCALATE: env and namespace-unresolved
kubectl rollout restart deploy/web with a namespace: prod restart rule ALLOW ESCALATE namespace-unresolved
kubectl apply -f - (± -n prod) with a secret/* apply rule ALLOW ESCALATE manifest-not-inspected
kubectl delete -f ./manifests/ ESCALATE (env) ESCALATE, also manifest-not-inspected for no-delete-nodes and others
kubectl delete -f ./manifests/ -n prod BLOCK BLOCK

Impact

  • Hooks: a kubectl command without -n in 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 run aegis check with --resolve-current-context.
  • Benchmark: I reran the Aegis row and compared it with results/benchmark.json. Every accuracy metric is identical; only latency differs, by a few hundredths of a millisecond. No committed results change.
  • Tests:
    • Changed: test_evade_namespace_omitted… in test_adversarial.py documented the old silent non-match as correct. It now asserts the escalation.
    • New: tests/test_kubectl_namespace_and_manifests.py, plus tests for the context namespace fill.
    • The full suite passes locally (1,923) and ruff is clean.
  • Docs and changelog: docs/constraints.md and docs/cli.md describe both behaviours. The CHANGELOG credits the reader.

… 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 moneytool left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/aegis_core/environments.py Outdated
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"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b2512c6.

  • Root cause: the kubectl parser discarded --kubeconfig, so resolution always read $KUBECONFIG or ~/.kube/config. That affected the context too, not only the namespace.
  • Parser: an explicit --kubeconfig is 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 $KUBECONFIG are now merged as kubectl merges them (the first file to set current-context or define a context wins), not just the first file read.
  • Regression tests: context ctx is in both ~/.kube/config (namespace dev) and production.yaml (namespace prod). --kubeconfig production.yaml, with and without --context ctx, now resolves prod, and the prod rule BLOCKs. Also tested: an unreadable explicit kubeconfig gives ESCALATE namespace-unresolved, and the $KUBECONFIG merge order. These tests fail on the previous commit.

Comment thread src/aegis_core/environments.py Outdated
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))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-scoped secret/* 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.
@moneytool
moneytool merged commit f798a3a into main Oct 9, 2026
9 checks passed
@moneytool
moneytool deleted the kubectl-context-stdin branch October 9, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant