diff --git a/Makefile b/Makefile index f512bbf..de253ae 100644 --- a/Makefile +++ b/Makefile @@ -226,11 +226,14 @@ e2e-cluster: ## Create the kind cluster used by the e2e tests. .PHONY: e2e-deploy e2e-deploy: docker-build ## Build the image, load it into kind and install the chart. kind load docker-image $(IMG) --name $(KIND_CLUSTER) + API_SERVER_IP="$$(kubectl get service kubernetes --namespace default -o jsonpath='{.spec.clusterIP}')"; \ helm upgrade --install crashloop-operator charts/crashloop-operator \ --namespace $(NAMESPACE) --create-namespace \ --set image.repository=$(firstword $(subst :, ,$(IMG))) \ --set image.tag=$(lastword $(subst :, ,$(IMG))) \ --set image.pullPolicy=Never \ + --set networkPolicy.enabled=true \ + --set networkPolicy.apiServer.ipBlock.cidr="$$API_SERVER_IP/32" \ --wait .PHONY: e2e-run diff --git a/README.md b/README.md index 165a3d9..5968052 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,25 @@ workload names are not, and a series for a workload that is later deleted would never go away. Use `status.activeScaledDown` or the `scaled-down-by` annotation to identify individual workloads. +## NetworkPolicy + +The chart can isolate the operator pod with an ingress and egress NetworkPolicy. +Supply the CIDR for your cluster's Kubernetes API endpoint when enabling it: + +```bash +helm upgrade --install crashloop-operator \ + oci://ghcr.io/slauger/charts/crashloop-operator \ + --namespace crashloop-system --create-namespace \ + --set networkPolicy.enabled=true \ + --set networkPolicy.apiServer.ipBlock.cidr=10.96.0.1/32 +``` + +The policy allows egress only to that API endpoint and to the selected cluster +DNS pods. It denies ingress unless `metrics.service.enabled` is true, in which +case it allows TCP traffic to the metrics container port. Adjust the DNS +namespace and pod selectors for clusters that do not label DNS as `kube-dns` in +`kube-system`. The cluster's network plugin must enforce NetworkPolicy objects. + Two alerts worth having: ```yaml diff --git a/charts/crashloop-operator/README.md b/charts/crashloop-operator/README.md index 14750bd..8605f6a 100644 --- a/charts/crashloop-operator/README.md +++ b/charts/crashloop-operator/README.md @@ -93,6 +93,12 @@ Kubernetes: `>=1.29.0-0` | metrics.serviceMonitor.interval | string | `"30s"` | Scrape interval. | | metrics.serviceMonitor.labels | object | `{}` | Extra labels for the ServiceMonitor, for Prometheus selector matching. | | nameOverride | string | `""` | Override the chart name used in resource names. | +| networkPolicy.apiServer.ipBlock.cidr | string | `""` | CIDR containing the Kubernetes API server endpoint. Required when the policy is enabled. | +| networkPolicy.apiServer.ipBlock.except | list | `[]` | CIDRs to exclude from the API server CIDR. | +| networkPolicy.apiServer.port | int | `443` | Kubernetes API server port. | +| networkPolicy.dns.namespaceSelector | object | `{"matchLabels":{"kubernetes.io/metadata.name":"kube-system"}}` | Namespace selector for the cluster DNS pods. | +| networkPolicy.dns.podSelector | object | `{"matchLabels":{"k8s-app":"kube-dns"}}` | Pod selector for the cluster DNS pods. | +| networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the operator pod. Requires an API server CIDR. | | nodeSelector | object | `{}` | Node selector for the operator pod. | | podAnnotations | object | `{}` | Annotations to add to the operator pod. | | podDisruptionBudget.enabled | bool | `false` | Create a PodDisruptionBudget. Only meaningful with replicaCount above 1, since leader election already keeps a single replica from being active twice. | diff --git a/charts/crashloop-operator/templates/networkpolicy.yaml b/charts/crashloop-operator/templates/networkpolicy.yaml new file mode 100644 index 0000000..4ec34da --- /dev/null +++ b/charts/crashloop-operator/templates/networkpolicy.yaml @@ -0,0 +1,46 @@ +{{- if .Values.networkPolicy.enabled }} +{{- $apiServerCIDR := required "networkPolicy.apiServer.ipBlock.cidr is required when networkPolicy.enabled is true" .Values.networkPolicy.apiServer.ipBlock.cidr }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "crashloop-operator.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "crashloop-operator.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "crashloop-operator.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + {{- if .Values.metrics.service.enabled }} + ingress: + - ports: + - protocol: TCP + port: 8080 + {{- else }} + ingress: [] + {{- end }} + egress: + - to: + - ipBlock: + cidr: {{ $apiServerCIDR }} + {{- with .Values.networkPolicy.apiServer.ipBlock.except }} + except: + {{- toYaml . | nindent 14 }} + {{- end }} + ports: + - protocol: TCP + port: {{ .Values.networkPolicy.apiServer.port }} + - to: + - namespaceSelector: + {{- toYaml .Values.networkPolicy.dns.namespaceSelector | nindent 12 }} + podSelector: + {{- toYaml .Values.networkPolicy.dns.podSelector | nindent 12 }} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 +{{- end }} diff --git a/charts/crashloop-operator/tests/networkpolicy_test.yaml b/charts/crashloop-operator/tests/networkpolicy_test.yaml new file mode 100644 index 0000000..d3656ec --- /dev/null +++ b/charts/crashloop-operator/tests/networkpolicy_test.yaml @@ -0,0 +1,85 @@ +suite: NetworkPolicy tests +templates: + - templates/networkpolicy.yaml +tests: + - it: should not create a NetworkPolicy by default + asserts: + - hasDocuments: + count: 0 + + - it: should require an API server CIDR when enabled + set: + networkPolicy: + enabled: true + asserts: + - failedTemplate: + errorMessage: "networkPolicy.apiServer.ipBlock.cidr is required when networkPolicy.enabled is true" + + - it: should allow only API server and DNS egress with ingress denied + set: + networkPolicy: + enabled: true + apiServer: + ipBlock: + cidr: 10.96.0.0/24 + except: + - 10.96.0.2/32 + port: 6443 + asserts: + - isKind: + of: NetworkPolicy + - equal: + path: spec.podSelector.matchLabels + value: + app.kubernetes.io/name: crashloop-operator + app.kubernetes.io/instance: RELEASE-NAME + - equal: + path: spec.policyTypes + value: + - Ingress + - Egress + - equal: + path: spec.ingress + value: [] + - equal: + path: spec.egress + value: + - to: + - ipBlock: + cidr: 10.96.0.0/24 + except: + - 10.96.0.2/32 + ports: + - protocol: TCP + port: 6443 + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + + - it: should allow metrics ingress only when the metrics Service is enabled + set: + networkPolicy: + enabled: true + apiServer: + ipBlock: + cidr: 10.96.0.1/32 + metrics: + service: + enabled: true + port: 9090 + asserts: + - equal: + path: spec.ingress + value: + - ports: + - protocol: TCP + port: 8080 diff --git a/charts/crashloop-operator/values.schema.json b/charts/crashloop-operator/values.schema.json index f1c3b4a..5ea41c2 100644 --- a/charts/crashloop-operator/values.schema.json +++ b/charts/crashloop-operator/values.schema.json @@ -94,6 +94,70 @@ "description": "Override the chart name used in resource names.", "type": "string" }, + "networkPolicy": { + "type": "object", + "properties": { + "apiServer": { + "type": "object", + "properties": { + "ipBlock": { + "type": "object", + "properties": { + "cidr": { + "description": "CIDR containing the Kubernetes API server endpoint. Required when the policy is enabled.", + "type": "string" + }, + "except": { + "description": "CIDRs to exclude from the API server CIDR.", + "type": "array" + } + } + }, + "port": { + "description": "Kubernetes API server port.", + "type": "integer" + } + } + }, + "dns": { + "type": "object", + "properties": { + "namespaceSelector": { + "description": "Namespace selector for the cluster DNS pods.", + "type": "object", + "properties": { + "matchLabels": { + "type": "object", + "properties": { + "kubernetes.io/metadata.name": { + "type": "string" + } + } + } + } + }, + "podSelector": { + "description": "Pod selector for the cluster DNS pods.", + "type": "object", + "properties": { + "matchLabels": { + "type": "object", + "properties": { + "k8s-app": { + "type": "string" + } + } + } + } + } + } + }, + "enabled": { + "description": "Create a NetworkPolicy for the operator pod. Requires an API server CIDR.", + "type": "boolean" + } + } + }, "nodeSelector": { "description": "Node selector for the operator pod.", "type": "object" diff --git a/charts/crashloop-operator/values.yaml b/charts/crashloop-operator/values.yaml index d7a259c..6f62040 100644 --- a/charts/crashloop-operator/values.yaml +++ b/charts/crashloop-operator/values.yaml @@ -96,6 +96,27 @@ metrics: # -- Extra labels for the ServiceMonitor, for Prometheus selector matching. labels: {} +networkPolicy: + # -- Create a NetworkPolicy for the operator pod. Requires an API server CIDR. + enabled: false + apiServer: + ipBlock: + # -- CIDR containing the Kubernetes API server endpoint. Required when the policy is enabled. + cidr: "" + # -- CIDRs to exclude from the API server CIDR. + except: [] + # -- Kubernetes API server port. + port: 443 + dns: + # -- Namespace selector for the cluster DNS pods. + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + # -- Pod selector for the cluster DNS pods. + podSelector: + matchLabels: + k8s-app: kube-dns + # -- Resource requests and limits for the operator container. resources: limits: