Skip to content

Keystone Operator NetworkPolicy ​

Reference documentation for the chart-level NetworkPolicy that restricts the egress and ingress of the keystone-operator pod itself. This is distinct from the per-CR NetworkPolicy emitted by reconcileNetworkPolicy, which protects Keystone API pods rendered from a Keystone CR.

  • Scope: the operator Deployment pod selected by operator-library.selectorLabels.
  • Chart: operators/keystone/helm/keystone-operator.
  • Template: templates/networkpolicy.yaml.
  • Values schema: the authoritative contract for all tunables is values.schema.json.

Overview ​

When networkPolicy.enabled=true, the chart renders one networking.k8s.io/v1NetworkPolicy that:

  1. Default-denies both directions for the operator pod by listing Ingress and Egress in policyTypes without a catch-all rule.
  2. Opens explicit egress to the kube-apiserver (required for all controller-runtime clients and leader election).
  3. Opens explicit egress to cluster DNS (required to resolve Service DNS names used by ESO, MariaDB, and any hostname-based client config).
  4. Opens explicit egress to the operator's own API pods for its API health check: TCP 5000 and 5050 to the pods labelled app.kubernetes.io/name: keystone and app.kubernetes.io/managed-by: keystone-operator in any namespace (see Egress for the other charts).
  5. When webhook.enabled=true, opens explicit ingress on TCP 9443 from the API-server CIDRs so admission webhooks are reachable.
  6. When networkPolicy.allowMetricsFrom is non-empty, opens explicit ingress on the metrics port from the listed peers (opt-in).

Two failure modes are explicitly refused by the template at render time (fail-closed): an empty kubeApiServer.cidrs or an empty kubeApiServer.ports list while enabled=true. Either condition triggers rather than rendering a NetworkPolicy that would block all controller traffic or open every port.

Default-off posture ​

networkPolicy.enabled defaults to false. Rationale:

  • Many production clusters run CNIs that do not enforce NetworkPolicy (kindnet without extension, Flannel without kube-router, etc.). Opting in by default would silently provide no protection on those clusters while adding surface area on clusters that do enforce it.
  • Both kubeApiServer.cidrs and kubeApiServer.ports are cluster-specific. There is no safe default that works across kind, GKE, EKS, AKS, and on-prem kubeadm installations.
  • Operators upgrading from earlier chart versions keep working with no change in values.

Operators must explicitly opt in by setting networkPolicy.enabled=true and populating kubeApiServer.cidrs / kubeApiServer.ports. The how-to guide walks through the enablement steps.

Rules rendered ​

All rules below are emitted on a single NetworkPolicy object named after include "operator-library.fullname" . in the release namespace, with spec.podSelector matching the operator Deployment's selector labels.

Egress ​

DirectionProtocol / PortPeerValues keyDefaultGated by
EgressUDP 53 + TCP 53namespaceSelector + podSelectornetworkPolicy.dns.namespaceSelector, networkPolicy.dns.podSelectorkubernetes.io/metadata.name: kube-system + k8s-app: kube-dnsnetworkPolicy.dns.enabled (default true)
EgressTCP <ports[*]>ipBlock[*]networkPolicy.kubeApiServer.cidrs, networkPolicy.kubeApiServer.ports[] (must be set when enabled=true)Always when networkPolicy.enabled=true
EgressTCP 5000, 5050namespaceSelector: {} + podSelector (app.kubernetes.io/name: keystone, app.kubernetes.io/managed-by: keystone-operator)nonenoneAlways when networkPolicy.enabled=true

One rule, N×M tuples. The kube-apiserver rule emits a singleegress entry with all CIDRs under to: and all ports under ports:. By NetworkPolicy semantics this permits every (cidr, port) combination — one rule with three CIDRs and two ports covers six tuples. Do not expand the list into one rule per tuple.

Every service operator chart renders the same kind of rule for its own API through the operator-library.chart.networkPolicyEgress hook, so the health check reaches the API pods on the port the CNI sees after the Service DNAT: horizon 8080, glance 9292, placement 8778, neutron 9696, nova 8774, cinder 8776 and barbican 9311, each to the pods labelled with its service name and <service>-operator. The keystone rule names 5050 too, because federation switches the Keystone Service's target port to the OIDC sidecar. The nova-operator chart adds TCP 5000 and 5050 to the keystone pods and TCP 8778 to the placement pods for its NovaCompute controller, and the barbican-operator chart adds a ports-only TCP 8200 rule for OpenBao (networkPolicy.openBao). The ovn-operator chart probes no API and adds no rule.

Ingress ​

DirectionProtocol / PortPeerValues keyDefaultGated by
IngressTCP 9443ipBlock[*]networkPolicy.webhookClients.cidrs (falls back to networkPolicy.kubeApiServer.cidrs when empty)fallback to kubeApiServer.cidrswebhook.enabled (default true)
IngressTCP <metrics.port>each entry from allowMetricsFrom rendered verbatim as a NetworkPolicyPeernetworkPolicy.allowMetricsFrom[]Non-empty allowMetricsFrom

Not covered: health probes (port 8081) ​

The operator exposes liveness and readiness probes on TCP 8081. These probes are called by the kubelet from the node's host network namespace, which is not subject to NetworkPolicy in the standard CNIs (Calico, Cilium, Antrea) — see the upstream Kubernetes NetworkPolicy "what you can't do" list. Therefore the template renders no ingress rule for 8081, and adding one is unnecessary. Probes continue to work with the default-deny ingress posture.

Values snippet: kind cluster ​

The following snippet enables the policy on a local kind cluster. It names the API server's endpoint, the control-plane node on the kind bridge, and not the 10.96.0.1 VIP of the kubernetes Service: kind's kindnet CNI judges egress after the Service DNAT, so a rule naming the VIP never matches. Read the address and the port with:

bash
kubectl get endpointslice kubernetes -n default \
  -o jsonpath='{.endpoints[*].addresses[*]} {.ports[0].port}{"\n"}'

A single-node kind cluster prints one address, for example 172.18.0.2 6443:

yaml
networkPolicy:
  enabled: true
  kubeApiServer:
    cidrs:
      - 172.18.0.2/32
    ports:
      - 6443
  # dns, allowMetricsFrom, webhookClients left at defaults

The chart-level E2E test tests/e2e/keystone-operator/network-policy-egress/chainsaw-test.yaml reads the same EndpointSlice and passes the result with --set, because the node address changes whenever the kind cluster is re-created.

Production example ​

Production clusters typically have the API server behind a VIP or NLB outside the cluster CIDR. Discover the endpoint with:

bash
kubectl get endpoints kubernetes -o json \
  | jq -r '.subsets[] | .addresses[].ip as $ip | .ports[].port as $p | "\($ip) \($p)"'

and set cidrs/ports accordingly. Metrics scraping peers are added explicitly:

yaml
networkPolicy:
  enabled: true
  kubeApiServer:
    cidrs: ["10.0.0.10/32", "10.0.0.11/32", "10.0.0.12/32"]
    ports: [6443]
  allowMetricsFrom:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
      podSelector:
        matchLabels:
          app.kubernetes.io/name: prometheus

Fail-closed guards ​

The template mirrors the defensive guard convention from the per-CR NetworkPolicy sub-reconciler (operators/keystone/internal/controller/reconcile_networkpolicy.go, lines ~61-63). If networkPolicy.enabled=true but either kubeApiServer.cidrs or kubeApiServer.ports is empty, Helm render aborts with one of:

Error: execution error at (keystone-operator/templates/networkpolicy.yaml):
networkPolicy.kubeApiServer.cidrs must not be empty when networkPolicy.enabled=true:
refusing to render a NetworkPolicy that would block all kube-apiserver egress
Error: execution error at (keystone-operator/templates/networkpolicy.yaml):
networkPolicy.kubeApiServer.ports must not be empty when networkPolicy.enabled=true:
refusing to render a NetworkPolicy that would open all ports to kube-apiserver

The JSON schema (values.schema.json) also enforces minItems: 1 on both lists when enabled=true, but schema validation can be bypassed with helm --skip-schema-validation. The template guard is the defense-in-depth backstop that catches that case.

Testing ​

FileScope
operators/keystone/helm/keystone-operator/tests/networkpolicy_test.yamlhelm-unittest suite: default-off, the Keystone API health-check egress rule (TCP 5000 and 5050) the chart adds through the hook, its position with DNS egress off, the cidrs fail-closed guard; the shared rules are pinned by operators/shared/helm/operator-library-testbed/tests/networkpolicy_test.yaml
operators/keystone/helm/keystone-operator/tests/schema_validation_test.yamlSchema tests for networkPolicy (invalid CIDR, out-of-range port, non-boolean enabled)
tests/e2e/keystone-operator/network-policy-egress/chainsaw-test.yamlChainsaw E2E: operator installed with networkPolicy.enabled=true on kind still reconciles a minimal Keystone CR to Ready=True, and a probe pod the policy selects is dropped on the MariaDB port and reaches the Keystone API through its Service

Run the helm-unittest suite locally with:

bash
helm unittest operators/keystone/helm/keystone-operator \
  -f 'tests/networkpolicy_test.yaml'