Skip to content

Add opt-in NetworkPolicies for the application namespace - #51

Open
henzigo wants to merge 5 commits into
default-security-contextsfrom
opt-in-network-policies
Open

Add opt-in NetworkPolicies for the application namespace#51
henzigo wants to merge 5 commits into
default-security-contextsfrom
opt-in-network-policies

Conversation

@henzigo

@henzigo henzigo commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #9

Stacked on #50 — only the last commit belongs to this PR.

What

Opt-in NetworkPolicies (networkPolicy.enabled: false by default — zero golden impact). When enabled:

infra chart (installed first, owns the namespace-wide pieces):

  • default-deny-ingress for all pods
  • allow-redis: 6379 from in-namespace pods, 9121 from the monitoring namespace
  • allow-rabbitmq: 5672 from in-namespace pods, 15672 from the ingress-controller namespace (management UI), 15692 from monitoring
  • allow-extra-ingress: raw escape-hatch rules applied to all pods
  • optional egress lockdown (networkPolicy.egress.enabled): default-deny egress + DNS anywhere + in-namespace traffic + explicit egress.rules

app chart:

  • allow-webserver: 8080 from the ingress-controller namespace + in-namespace pods (storefront's INTERNAL_ENDPOINT calls)
  • allow-storefront: 3000 from the ingress-controller namespace + in-namespace pods (nginx proxies /_next/ and @storefront)

Namespace selectors for the ingress controller and monitoring are configurable (defaults: kubernetes.io/metadata.name: ingress-nginx / monitoring).

Notes

  • Rollout guidance (documented in values + docs/values.md): enable on a dev environment first — the CNI must actually enforce policies, and kubelet probe behavior under a default-deny differs between CNIs.
  • Egress caveat (documented): with the lockdown on, external services (PostgreSQL, Elasticsearch, S3, SMTP) and the Kubernetes API (the cron-suspend hook uses kubectl) must be listed in egress.rules.
  • No deviations entry — default off means no behavior change against the legacy package.
  • Enabled renders validated with kubeconform; new test suites in both charts (8 cases). 97 unit tests total.

🤖 Generated with Claude Code

The namespace previously had no NetworkPolicies - every pod could talk to
every other pod and the whole cluster network, which matters doubly with the
unauthenticated redis. Off by default for parity (networkPolicy.enabled).

When enabled, the infra chart (installed first) renders the namespace-wide
default ingress deny, the redis (6379 in-namespace, 9121 monitoring) and
rabbitmq (5672 in-namespace, 15672 ingress controller, 15692 monitoring)
allows, an extra-ingress escape hatch and an optional egress lockdown (DNS +
in-namespace + explicit rules); the app chart allows the ingress controller
and in-namespace pods to reach the webserver (8080) and storefront (3000).
The ingress-controller and monitoring namespace selectors are configurable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds opt-in Kubernetes NetworkPolicy resources to harden the application namespace without impacting default renders (networkPolicy.enabled: false by default). This fits the repo’s Helm/helmfile-based deployment by letting environments progressively roll out network isolation while keeping legacy-parity defaults.

Changes:

  • Introduces namespace-wide default-deny ingress plus Redis/RabbitMQ allow policies (infra chart), with optional egress lockdown support.
  • Adds app workload allow policies for webserver (8080) and storefront (3000) (app chart).
  • Documents the new values and adds helm-unittest suites covering default-off and enabled behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/values.md Documents networkPolicy values and rollout/egress caveats.
charts/shopsys-infra/values.yaml Adds defaulted networkPolicy configuration (shared values section).
charts/shopsys-infra/templates/networkpolicy.yaml Renders infra-owned NetworkPolicies (default-deny ingress, Redis/RabbitMQ allows, optional egress lockdown, extra ingress escape hatch).
charts/shopsys-infra/tests/networkpolicy_test.yaml Adds unit tests for infra NetworkPolicy rendering and egress-lockdown behavior.
charts/shopsys-app/values.yaml Adds documented networkPolicy configuration section (shared between charts).
charts/shopsys-app/templates/networkpolicy.yaml Renders app-owned NetworkPolicies allowing ingress to webserver/storefront.
charts/shopsys-app/tests/networkpolicy_test.yaml Adds unit tests for app NetworkPolicy rendering and selector customization.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +54 to +65
# application pods in this namespace
- from:
- podSelector: {}
ports:
- port: 5672
# management UI through the ingress controller
- from:
- namespaceSelector:
{{- toYaml .Values.networkPolicy.ingressControllerNamespace | nindent 12 }}
ports:
- port: 15672
# prometheus scraping of the built-in exporter

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not applicable to this chart as it stands: RabbitMQ clustering is not supported here at all. The StatefulSet configures no peer-discovery mechanism and no shared Erlang cookie (each replica gets its own PVC with a node-generated random cookie), so rabbitmq.replicas > 1 yields independent brokers that never attempt inter-node Erlang distribution - there is no EPMD/25672 traffic for the NetworkPolicy to block, with or without this PR. The default is replicas: 1 (legacy parity). If clustering support ever lands (peer discovery + shared cookie), the 4369/25672 pod-to-pod allow will be added as part of that change rather than as a dead rule now.

@henzigo henzigo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed the #51 diff against its stacked base default-security-contexts (charts/shopsys-app + charts/shopsys-infra networkpolicy templates, values, tests, docs/values.md).

Verified locally on the branch:

  • ./tests/run-golden-tests.sh — 15/15 PASS (default-off confirmed: zero golden impact)
  • helm unittest charts/shopsys-app charts/shopsys-infra — 97/97 PASS
  • helm lint both charts with networkPolicy.enabled=true (+ egress lockdown) — clean
  • Rendered with policies enabled (incl. extraIngress + egress.rules) and validated with kubeconform -strict (K8s 1.31) — 8/8 valid
  • Cross-checked every podSelector against the actual immutable app: selector labels (webserver-php-fpm, storefront, redis, rabbitmq) and every policy port against the real container ports (8080, 3000, 6379/9121, 5672/15672/15692) — all correct. Both ingresses (ingress-domains, ingress-mcp) target only webserver-php-fpm:8080, which is covered; storefront↔webserver and hook-job traffic is in-namespace and covered; DNS egress correctly allows both UDP and TCP 53.

The overall model is sound: namespace-wide deny + per-workload allows in the infra chart (installed first), workload allows in the app chart, opt-in default-off for a generic reusable package, egress lockdown as a second opt-in with the PostgreSQL/Elasticsearch/S3/SMTP/K8s-API caveat documented in both values comments and docs/values.md. The CNI-enforcement and kubelet-probe caveats are documented. Nice work on the split and the escape hatches.

Findings (severity-ranked):

  1. High — cert-manager HTTP01 solver pods are blocked by default-deny-ingress (inline). Certificate issuance/renewal will silently break after enabling the policies, and only surface at renewal time.
  2. Medium — networkPolicy is not declared in either values.schema.json (inline). A typo in this security toggle fails open silently.
  3. Low — DNS egress is allowed to any destination (inline) — DNS-tunnel exfiltration remains possible under the lockdown.
  4. Low — allow-storefront grants ingress from the ingress-controller namespace, but no shipped ingress targets storefront:3000 (inline) — unused surface with a misleading comment.
  5. Low — RabbitMQ 15672 is opened from the ingress-controller namespace even when the management ingress does not render (inline).
  6. Info — no golden scenario exercises the enabled state. The unit tests cover chart-level rendering, but a small golden scenario (or a variant) with networkPolicy.enabled: true would lock the rendering under the full helmfile state-values path, consistent with how other opt-in features are guarded here. Also info: allow-redis/allow-rabbitmq render even when redis.enabled/rabbitmq.enabled are false — harmless (they select nothing) but you may want to gate them for render cleanliness.

— Claude Agent (CR)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

High: This default deny selects every pod in the namespace — including cert-manager's HTTP01 solver pods. Both ingress-domains and ingress-rabbitmq carry cert-manager.io/cluster-issuer, and with an ACME HTTP01 issuer cert-manager spawns temporary solver pods in this namespace, reached via ingress-nginx on pod port 8089 (/.well-known/acme-challenge/...). No allow policy selects them (their labels are acme.cert-manager.io/http01-solver: "true", not app: ...), so once networkPolicy.enabled: true every certificate issuance/renewal fails — and it only surfaces weeks later at renewal time.

Suggestion: add a built-in allow policy (podSelector on acme.cert-manager.io/http01-solver: "true", port 8089, from the ingress-controller namespace — cert-manager's docs describe exactly this), or at minimum document the gotcha in docs/values.md with a ready-made extraIngress example. DNS01 issuers are unaffected, but the charts can't know which issuer type the cluster uses. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Implemented in ffc27ad - this was a real trap, thanks. Added a built-in allow-acme-solver policy (podSelector acme.cert-manager.io/http01-solver: "true", port 8089), rendered whenever networkPolicy.enabled is on; it selects no pods on DNS01 clusters, so it is a safe default. One deliberate deviation from the suggestion: the rule is not restricted to the ingress-controller namespace, because cert-manager's pre-issuance self-check may not arrive with the ingress-controller namespace as its traffic source (load-balancer hairpin / externalTrafficPolicy specifics vary per cluster), and the solver serves nothing but the public challenge token anyway. Documented in the values comments and docs/values.md, and covered by a unit test plus the new network-policies golden scenario.

# default-deny ingress for the whole namespace, allows for the chart workloads, and an
# optional egress lockdown. Roll out on a dev environment first and verify that your CNI
# enforces policies and that kubelet probes still pass.
networkPolicy:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Medium: Neither chart's values.schema.json declares networkPolicy (roots keep additionalProperties: true, so nothing validates it). For a security feature this fails open: networkPolicy.enable: true, a misspelled ingressControllerNamespace, or a non-boolean enabled all silently render nothing / wrong selectors while the operator believes the namespace is locked down. A small schema block (enabled boolean, egress.enabled boolean, object shapes for the selectors, arrays for extraIngress/rules) in both charts would catch typos at deploy time. I know #50 also skipped schema entries for its new keys, but here the failure mode is a missing security control rather than a missing hardening default. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Implemented in ffc27ad. Both charts' values.schema.json now declare the full networkPolicy shape with additionalProperties: false on the block (and on egress), so networkPolicy.enable: true, a non-boolean enabled or any misspelled sub-key fails the deploy instead of silently rendering nothing. Verified: --set networkPolicy.enable=true now errors with "additional properties 'enable' not allowed". The namespace selectors stay loose object (they legitimately accept matchLabels or matchExpressions), and the roots keep additionalProperties: true as required by the shared helmfile state values.

egress:
# DNS anywhere (cluster DNS location differs per cluster)
- ports:
- port: 53

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Low (hardening): This rule allows egress on port 53 to any destination, including arbitrary internet resolvers — so DNS tunneling remains a viable exfiltration path even under the lockdown, which is one of the main threats an egress lockdown addresses. Consider making the DNS destination configurable, e.g. networkPolicy.egress.dnsSelector defaulting to namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}} (+ podSelector: {k8s-app: kube-dns}), with an explicit empty value falling back to the current "anywhere" for clusters with node-local DNS or non-standard resolver locations. Keeping "anywhere" as the default is defensible for a generic package — but then it deserves a sentence in docs/values.md so operators know the residual risk. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Documented in ffc27ad, keeping "anywhere" as the default: the residual DNS-tunneling risk is now called out in the template comment, the values comments and docs/values.md. I did not add a dnsSelector knob for now - a generic package cannot pick a safe default (kube-system/kube-dns vs CoreDNS labels vs node-local caches on link-local IPs that a namespaceSelector cannot express at all), and a wrong default here bricks all name resolution in the namespace. If a consuming project wants a pinned resolver destination, that is a reasonable follow-up feature once there is a concrete cluster to design it against.

ingress:
- from:
# e-shop ingresses
- namespaceSelector:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Low: The comment says "e-shop ingresses", but no ingress shipped by these charts targets storefront:3000 — both ingress-domains and ingress-mcp route exclusively to webserver-php-fpm:8080, and the webserver's nginx proxy to the storefront is in-namespace pod traffic already covered by the podSelector: {} peer below. This namespaceSelector peer is therefore unused attack surface under the shipped topology. Either drop it (least privilege) or, if it's deliberately kept for projects that add a direct storefront ingress via extraManifests, say that in the comment instead. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Implemented in ffc27ad - dropped the ingress-controller namespaceSelector peer from allow-storefront (least privilege). Confirmed against the shipped templates: both ingress-domains and ingress-mcp route exclusively to webserver-php-fpm:8080, so all storefront traffic arrives via the webserver's nginx, which the in-namespace podSelector: {} peer covers. The comment now says exactly that and points projects with a custom direct storefront ingress to networkPolicy.extraIngress. Unit test added asserting the storefront from list is in-namespace only.

- namespaceSelector:
{{- toYaml .Values.networkPolicy.ingressControllerNamespace | nindent 12 }}
ports:
- port: 15672

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Low: 15672 is opened from the ingress-controller namespace unconditionally, but the management ingress only renders under {{- if and .Values.rabbitmq.enabled (or .Values.rabbitmq.management.hostname (gt (len .Values.domains) 0)) }}. When that condition is false there is no legitimate path from the ingress controller to the management UI, yet any pod in the ingress-controller namespace may still reach it (HTTP basic auth on the broker becomes the only barrier). Gating this rule on the same condition as ingress-rabbitmq.yaml would keep the allowlist minimal. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Implemented in ffc27ad. The 15672 rule is now gated on exactly the condition that renders ingress-rabbitmq.yaml (rabbitmq.management.hostname set or at least one domain), so the ingress-controller namespace only gets a path to the management UI when a legitimate route exists. Also gated allow-redis/allow-rabbitmq as wholes on redis.enabled/rabbitmq.enabled while touching this (they previously rendered as no-op policies for disabled workloads). Unit tests cover the 15672-omitted and workloads-disabled cases.

- Allow cert-manager's HTTP01 solver pods (port 8089): the solvers run in this
  namespace and every certificate issuance/renewal would fail under the default
  deny - only surfacing weeks later at renewal time. Unrestricted source on
  purpose (self-check source varies with LB/externalTrafficPolicy; the solver
  serves only the public challenge token) and a no-op on DNS01 clusters.
- Declare networkPolicy in both values.schema.json (additionalProperties:
  false): a typo in a security toggle must fail the deploy, not silently render
  nothing while the operator believes the namespace is locked down.
- Drop the ingress-controller peer from allow-storefront: no shipped ingress
  targets storefront:3000 - all storefront traffic arrives via the webserver's
  nginx (in-namespace); a project-specific direct ingress can use extraIngress.
- Open RabbitMQ 15672 from the ingress controller only under the same condition
  that renders the management ingress; gate allow-redis/allow-rabbitmq on the
  workloads being enabled.
- Document the DNS-anywhere residual risk (DNS tunneling) in values + docs.
- Add a network-policies golden scenario locking the enabled state (incl.
  egress lockdown and extra ingress) under the full helmfile state-values path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@henzigo

henzigo commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Addressing the non-inline "Info" finding from the review summary (no thread exists for it), resolved in ffc27ad together with the inline findings:

  • Golden scenario for the enabled state: added tests/golden/scenarios/network-policies (basic-production shape + networkPolicy.enabled, a custom monitoring namespace selector, extraIngress and the egress lockdown with sample rules), locking all 9 policies under the full helmfile state-values path across the 3 deploy variants. The 5 pre-existing scenarios are byte-identical - the default-off guarantee still holds.
  • allow-redis/allow-rabbitmq rendering for disabled workloads: now gated on redis.enabled/rabbitmq.enabled (unit-tested).

Full verification suite passed before push: helm unittest 99/99, golden 18/18, helm lint both charts, kubeconform -strict on the enabled renders (infra incl. egress lockdown, and app).

henzigo and others added 3 commits August 20, 2026 12:32
The scenario was generated before the parent branch gained the pod-level
automountServiceAccountToken line and the values-driven hook contexts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…opt-in-network-policies

# Conflicts:
#	charts/shopsys-infra/values.schema.json
@lokesh0186

Copy link
Copy Markdown

I independently evaluated the NetworkPolicy portion of this PR using the public IaC-Guard-V 0.1.0a5 release against the current proposed configuration.

The app + infra charts were materialized as one protected multi-chart universe. For the five selected workloads, Checkov CKV2_K8S_6 is satisfied with complete workload-to-NetworkPolicy selector evidence.

IaC-Guard-V keeps all rendered resources in the governed universe, including resources that are not primary scanner targets, and returns a target-scoped candidate-acceptance VERIFIED result.

This is candidate-property acceptance, not a claim that a baseline defect was FIXED, and it is not whole-chart or whole-PR verification.

Reproduction and immutable evidence:
https://github.com/lokesh0186/iac-guard-v/tree/908825990297d759ca506355e89c80c579969dcf/examples/public-reproductions/shopsys-helm-51

Verifier:
IaC-Guard-V 0.1.0a5
PyPI: iac-guard-v==0.1.0a5
Software DOI: https://doi.org/10.5281/zenodo.22099303

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.

3 participants