diff --git a/Makefile b/Makefile index 4c5651e..9b15b70 100644 --- a/Makefile +++ b/Makefile @@ -84,13 +84,35 @@ get-credentials: check-terraform ## Configure kubectl credentials from Terraform # ==== Kind Targets ==== +KIND_CONFIG ?= scripts/kind-config.yaml + +# kind's default CNI (kindnet) has no NetworkPolicy enforcement, so it's +# disabled (see scripts/kind-config.yaml) and install-kind-cilium installs +# Cilium as the sole CNI, providing both pod networking and policy enforcement. +# GKE gets equivalent enforcement via Dataplane V2 (see terraform/modules/cluster/gke), +# though its managed Cilium build may differ in version/config from this pinned chart. +CILIUM_VERSION ?= 1.20.1 +CILIUM_NAMESPACE ?= kube-system + .PHONY: create-kind-cluster create-kind-cluster: check-kind ## Create a new kind cluster or export kubeconfig if exists + @test -n "$(KIND_CLUSTER_NAME)" || { echo "ERROR: KIND_CLUSTER_NAME is empty. HELMFILE_ENV=$(HELMFILE_ENV) does not include env.kind (only HELMFILE_ENV values without 'gcp' do) - run with HELMFILE_ENV=kind or e2e-kind."; exit 1; } @if kind get clusters 2>/dev/null | grep -q "^$(KIND_CLUSTER_NAME)$$"; then \ echo "kind cluster '$(KIND_CLUSTER_NAME)' already exists ..."; \ + _kindnet_check_kubeconfig=$$(mktemp); \ + kind get kubeconfig --name $(KIND_CLUSTER_NAME) > $$_kindnet_check_kubeconfig; \ + _has_kindnet=0; \ + kubectl --kubeconfig $$_kindnet_check_kubeconfig get daemonset -n kube-system kindnet >/dev/null 2>&1 && _has_kindnet=1; \ + rm -f $$_kindnet_check_kubeconfig; \ + if [ "$$_has_kindnet" = "1" ]; then \ + echo "ERROR: existing kind cluster '$(KIND_CLUSTER_NAME)' still runs the default CNI (kindnet)."; \ + echo "It predates disableDefaultCNI, so Cilium NetworkPolicy enforcement will not be effective."; \ + echo "Delete and recreate it: make delete-kind-cluster KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) && make create-kind-cluster KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME)"; \ + exit 1; \ + fi; \ else \ echo "Creating new kind cluster '$(KIND_CLUSTER_NAME)'..."; \ - kind create cluster --name $(KIND_CLUSTER_NAME); \ + kind create cluster --name $(KIND_CLUSTER_NAME) --config $(KIND_CONFIG); \ fi @kind export kubeconfig --name $(KIND_CLUSTER_NAME) --kubeconfig $(KUBECONFIG) @kubectl config use-context kind-$(KIND_CLUSTER_NAME) --kubeconfig $(KUBECONFIG) @@ -111,6 +133,28 @@ else @echo "To enable kind image builds set BUILD_IMAGES=true in env.kind" endif +# macOS + podman: the podman machine must run rootful (podman machine set +# --rootful ) or Cilium's bpf-mount init container crash-loops - +# rootless podman can't mount bpffs. +.PHONY: install-kind-cilium +install-kind-cilium: check-helm check-kubectl-context ## Install Cilium CNI on the kind cluster (GKE uses Dataplane V2 instead) + @echo "Installing Cilium $(CILIUM_VERSION)..." + @helm repo add cilium https://helm.cilium.io/ >/dev/null + @helm repo update cilium >/dev/null + helm upgrade --install $(DRY_RUN_FLAG) cilium cilium/cilium \ + --version $(CILIUM_VERSION) \ + --namespace $(CILIUM_NAMESPACE) \ + --set ipam.mode=kubernetes \ + --set operator.replicas=1 \ + --wait --timeout 5m + @kubectl wait --for=condition=Ready pod -l k8s-app=cilium --namespace $(CILIUM_NAMESPACE) --timeout=180s + @echo "OK: Cilium is ready" + +.PHONY: uninstall-kind-cilium +uninstall-kind-cilium: check-helm check-kubectl-context ## Uninstall Cilium CNI from the kind cluster + @helm uninstall cilium --namespace $(CILIUM_NAMESPACE) || true + @echo "OK: Cilium uninstalled (cluster networking is broken until Cilium is reinstalled or the cluster is deleted)" + # ==== Helmfile Targets ==== .PHONY: template-helmfile template-helmfile: check-helmfile ## Template the helmfile for the current environment @@ -682,13 +726,25 @@ validate-authorino: check-helm ## Validate gateway auth templates || { echo "ERROR: configured authorino.hosts entry not rendered"; exit 1; } @echo "OK: Authorino gateway templates valid (ext_authz before router, fail-closed, when-gated optional header, model guard, additive AUTHORINO_HOSTS)" +.PHONY: validate-network-policies +validate-network-policies: check-helm ## Validate network-policies Helm chart rendering + @echo "Validating network-policies chart..." + @out=$$(helm template netpol $(HELM_DIR)/network-policies --set namespace=hyperfleet-local) \ + || { echo "ERROR: network-policies chart failed to render"; exit 1; }; \ + echo "$$out" | grep -q "name: hyperfleet-api-ingress" \ + || { echo "ERROR: hyperfleet-api-ingress NetworkPolicy not rendered"; exit 1; }; \ + echo "$$out" | grep -q "name: hyperfleet-api-postgres-ingress" \ + || { echo "ERROR: hyperfleet-api-postgres-ingress NetworkPolicy not rendered"; exit 1; } + @echo "OK: network-policies chart rendered successfully" + .PHONY: ci-validate ci-validate: validate-terraform lint-helm lint-shellcheck ## Ci validate: validate terraform + lint helm + lint shellcheck .PHONY: ci-dry-run -ci-dry-run: ci-validate ## Ci dry-run: ci-validate + validate maestro + validate authorino +ci-dry-run: ci-validate ## Ci dry-run: ci-validate + validate maestro + validate authorino + validate network policies $(MAKE) validate-maestro $(MAKE) validate-authorino + $(MAKE) validate-network-policies .PHONY: health-check-maestro health-check-maestro: check-kubectl ## Verify Maestro Components @@ -712,7 +768,7 @@ ci-cleanup: uninstall-maestro destroy-terraform ## Ci cleanup: uninstall maestro # Kind targets .PHONY: local-up-kind -local-up-kind: create-kind-cluster kind-build-images install-priority-classes install-maestro-all generate-rabbitmq-values maybe-install-grafana maybe-install-tracing install-hyperfleet ## Full local kind setup +local-up-kind: create-kind-cluster install-kind-cilium kind-build-images install-priority-classes install-maestro-all generate-rabbitmq-values maybe-install-grafana maybe-install-tracing install-hyperfleet ## Full local kind setup .PHONY: local-down-kind local-down-kind: uninstall-hyperfleet uninstall-tracing uninstall-grafana uninstall-maestro delete-kind-cluster ## Tear down kind stack and delete cluster diff --git a/helm/network-policies/Chart.yaml b/helm/network-policies/Chart.yaml new file mode 100644 index 0000000..9bc4336 --- /dev/null +++ b/helm/network-policies/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: network-policies +description: NetworkPolicy objects enforcing the HyperFleet app-namespace trust boundary +type: application +version: 0.1.0 +appVersion: "1.0" diff --git a/helm/network-policies/templates/_helpers.tpl b/helm/network-policies/templates/_helpers.tpl new file mode 100644 index 0000000..0e401c5 --- /dev/null +++ b/helm/network-policies/templates/_helpers.tpl @@ -0,0 +1,49 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "network-policies.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "network-policies.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "network-policies.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "network-policies.labels" -}} +helm.sh/chart: {{ include "network-policies.chart" . }} +{{ include "network-policies.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "network-policies.selectorLabels" -}} +app.kubernetes.io/name: {{ include "network-policies.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/helm/network-policies/templates/networkpolicy.yaml b/helm/network-policies/templates/networkpolicy.yaml new file mode 100644 index 0000000..eab91be --- /dev/null +++ b/helm/network-policies/templates/networkpolicy.yaml @@ -0,0 +1,18 @@ +{{- range .Values.networkPolicies }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ .name }} + namespace: {{ $.Values.namespace }} + labels: + {{- include "network-policies.labels" $ | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- tpl (toYaml .podSelector) $ | nindent 6 }} + policyTypes: + - Ingress + ingress: + {{- tpl (toYaml .ingress) $ | nindent 4 }} +{{- end }} diff --git a/helm/network-policies/values.yaml b/helm/network-policies/values.yaml new file mode 100644 index 0000000..1cb4c4e --- /dev/null +++ b/helm/network-policies/values.yaml @@ -0,0 +1,66 @@ +# Default values for network-policies. +nameOverride: "" +fullnameOverride: "" + +monitoringNamespace: monitoring + +# Name/instance labels of the workloads this chart guards. Must match the +# `app.kubernetes.io/name` and `app.kubernetes.io/instance` labels rendered by +# the hyperfleet-api and hyperfleet-gateway releases (helmfile release names by +# default) — override these if either release is installed under a different +# name or release instance. +apiNameLabel: hyperfleet-api +apiInstanceLabel: hyperfleet-api +gatewayNameLabel: hyperfleet-gateway +gatewayInstanceLabel: hyperfleet-gateway + +# Add or edit policies here — this is the only file that needs to change. +# `podSelector` and `ingress` are passed through `tpl`, so they can reference +# `.Values` (e.g. the labels above). +networkPolicies: + - name: hyperfleet-api-ingress + podSelector: + app.kubernetes.io/name: "{{ .Values.apiNameLabel }}" + app.kubernetes.io/instance: "{{ .Values.apiInstanceLabel }}" + app.kubernetes.io/component: api + ingress: + # Sole ingress source for the API's app port: the gateway. No + # sentinel/adapter exception — they reach the API through the gateway + # (baseUrl defaults to http://hyperfleet-gateway:8000). + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: "{{ .Values.gatewayNameLabel }}" + app.kubernetes.io/instance: "{{ .Values.gatewayInstanceLabel }}" + ports: + - protocol: TCP + port: 8000 + # Prometheus metrics-only exception: the actual Prometheus pods + # (not the whole monitoring namespace), scrape :9090 only. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: "{{ .Values.monitoringNamespace }}" + podSelector: + matchLabels: + app.kubernetes.io/name: prometheus + operator.prometheus.io/name: kube-prometheus-stack-prometheus + ports: + - protocol: TCP + port: 9090 + + - name: hyperfleet-api-postgres-ingress + podSelector: + app.kubernetes.io/name: "{{ .Values.apiNameLabel }}" + app.kubernetes.io/instance: "{{ .Values.apiInstanceLabel }}" + app.kubernetes.io/component: postgresql + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: "{{ .Values.apiNameLabel }}" + app.kubernetes.io/instance: "{{ .Values.apiInstanceLabel }}" + app.kubernetes.io/component: api + ports: + - protocol: TCP + port: 5432 diff --git a/helmfile/helmfile.yaml.gotmpl b/helmfile/helmfile.yaml.gotmpl index 3427b89..9abff9d 100644 --- a/helmfile/helmfile.yaml.gotmpl +++ b/helmfile/helmfile.yaml.gotmpl @@ -92,6 +92,17 @@ releases: values: - values/base-gateway.yaml.gotmpl + - name: network-policies + namespace: {{ .Values.namespace }} + chart: ../helm/network-policies + needs: + - hyperfleet-api + - hyperfleet-gateway + labels: + component: network-policies + values: + - values/base-network-policies.yaml.gotmpl + {{ range .Values.sentinels }} - name: {{ .name }} namespace: {{ $.Values.namespace }} diff --git a/helmfile/values/base-network-policies.yaml.gotmpl b/helmfile/values/base-network-policies.yaml.gotmpl new file mode 100644 index 0000000..038dc67 --- /dev/null +++ b/helmfile/values/base-network-policies.yaml.gotmpl @@ -0,0 +1,2 @@ +namespace: {{ .Values.namespace }} +monitoringNamespace: {{ env "MONITORING_NAMESPACE" | default "monitoring" }} diff --git a/scripts/kind-config.yaml b/scripts/kind-config.yaml new file mode 100644 index 0000000..9c7419d --- /dev/null +++ b/scripts/kind-config.yaml @@ -0,0 +1,6 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane +networking: + disableDefaultCNI: true diff --git a/terraform/modules/cluster/gke/main.tf b/terraform/modules/cluster/gke/main.tf index 97ece5c..f1d47dc 100644 --- a/terraform/modules/cluster/gke/main.tf +++ b/terraform/modules/cluster/gke/main.tf @@ -13,6 +13,12 @@ resource "google_container_cluster" "primary" { services_secondary_range_name = var.services_range_name } + # GKE Dataplane V2 (Cilium-based) — required for NetworkPolicy enforcement. + # Without this, helm/network-policies' NetworkPolicy objects are inert. + # NOTE: immutable after cluster creation — changing this on an existing + # cluster requires recreating it, not an in-place update. + datapath_provider = "ADVANCED_DATAPATH" + # We manage the node pool separately remove_default_node_pool = true initial_node_count = 1