{{- /* One NetworkPolicy per node group. This object is the CEILING ON REACHABILITY, and that is its real job here: it holds no matter which ports the process actually bound. A node whose ClusterRole is missing from the database fail-opens into a full node and binds every listener - including IMAP and the admin interface on a group meant to take SMTP only - while staying Running with a green probe. The per-port Services publish only what was asked for, and this policy is the second, independent limit. See values.yaml for the source-level detail. !!! THE PORT NUMBERS HERE ARE POD PORTS (25/465/993/8080), NOT NODEPORTS !!! This is the easiest mistake to make in this chart, so it is spelled out. A NetworkPolicy is evaluated at the POD's network interface, after kube-proxy has already translated the NodePort to the targetPort. Writing 30025 here would match nothing: the packet arrives at the pod on 25. The policy would then look strict, be accepted by the API server, and silently block every inbound connection - or, if it is the only rule, block the lot. Egress is restricted BY PORT and not by destination, because a NetworkPolicy matches CIDRs and selectors and never hostnames - so dataStore.postgres.host and externalServices.*.host cannot appear in it. What it still buys: a compromised pod cannot open arbitrary outbound ports. And a NetworkPolicy is only a boundary where the cluster's CNI enforces one. An unenforced policy is accepted and filters nothing. */ -}} {{- if .Values.networkPolicy.enabled }} {{- $root := . -}} {{- $ports := fromYaml (include "stalwart.ports" .) -}} {{- $egress := .Values.networkPolicy.egress -}} {{- $probeSources := .Values.networkPolicy.probeSources | default list -}} {{- $allowedNs := .Values.networkPolicy.allowedNamespaces | default list -}} {{- range $group, $spec := fromYaml (include "stalwart.nodes" .) }} {{- $name := printf "%s-%s" (include "stalwart.fullname" $root) $group }} --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: {{ $name }}-networkpolicy labels: app: {{ $name }} spec: podSelector: matchLabels: app: {{ $name }}-sfs policyTypes: - Ingress - Egress ingress: {{- /* The public mail ports: open to any source. They have to be - the senders are the whole internet, and after NodePort masquerading the source address is a control-plane node anyway. */}} {{- $publicPorts := list }} {{- range $portName := $spec.exposedPorts }} {{- $port := index $ports $portName }} {{- if $port.public }}{{- $publicPorts = append $publicPorts $portName }}{{- end }} {{- end }} {{- if $publicPorts }} - ports: {{- range $portName := $publicPorts }} {{- $port := index $ports $portName }} # {{ $portName }}: published on nodePort {{ int $port.nodePort }}, reaching the pod on {{ int $port.containerPort }} - protocol: {{ $port.protocol | default "TCP" }} port: {{ int $port.containerPort }} {{- end }} {{- end }} {{- /* The http port: probes plus the ingress controller. */}} {{- $http := index $ports "http" }} - ports: - protocol: {{ $http.protocol | default "TCP" }} port: {{ int $http.containerPort }} {{- if or $probeSources $allowedNs }} # Restricted because networkPolicy.probeSources or .allowedNamespaces is # set. MIND THE KUBELET: probes come from the IP of the worker node # running the pod, so that address has to be in probeSources or the pod # never becomes Ready - and a pod that is never Ready reads as a broken # application rather than as a policy. from: - namespaceSelector: matchLabels: # Set by Kubernetes itself on every namespace (>= 1.21), so this # needs no labelling work. kubernetes.io/metadata.name: {{ $root.Release.Namespace }} {{- range $allowedNs }} - namespaceSelector: matchLabels: kubernetes.io/metadata.name: {{ . | quote }} {{- end }} {{- range $probeSources }} - ipBlock: cidr: {{ . | quote }} {{- end }} {{- else }} # NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside # the cluster: http carries no nodePort, so nothing outside reaches it # through this chart. The alternative was to guess the worker node CIDRs, # and a wrong guess blocks every kubelet probe. Narrow it by setting # networkPolicy.probeSources (and then .allowedNamespaces for the # ingress controller, or the Ingress goes dark). {{- end }} egress: {{- if $egress.dns }} - ports: - protocol: UDP port: 53 - protocol: TCP port: 53 {{- end }} {{- if $egress.smtp }} # Outbound delivery: port 25 on any host on the internet. Uniform for every # group on purpose - the chart does not know which groups run the # outboundMta task, the value would drift away from the database, and the # failure mode of getting it wrong is mail silently not leaving. - ports: - protocol: TCP port: 25 {{- end }} {{- $outPorts := list (int $root.Values.dataStore.postgres.port) }} {{- with $root.Values.externalServices }} {{- if .s3 }}{{- $outPorts = append $outPorts (int .s3.port) }}{{- end }} {{- if .valkey }}{{- $outPorts = append $outPorts (int .valkey.port) }}{{- end }} {{- end }} {{- range $root.Values.networkPolicy.egress.extraPorts }} {{- $outPorts = append $outPorts (int .) }} {{- end }} # The data store, the blob store, the in-memory store and anything added # through networkPolicy.egress.extraPorts. Ports only - see the header. - ports: {{- range $outPorts | uniq }} - protocol: TCP port: {{ . }} {{- end }} {{- end }} {{- end }}