diff --git a/.github/workflows/helm_ci.yml b/.github/workflows/helm_ci.yml index 7f9679a92..262d0928a 100644 --- a/.github/workflows/helm_ci.yml +++ b/.github/workflows/helm_ci.yml @@ -822,6 +822,184 @@ jobs: PYEOF echo "Hook Job label tests passed" + echo "" + echo "=== Testing NetworkPolicy rendering ===" + # The policies are only exercised for real by the networkpolicy-install + # job below. These assertions cover what template rendering can see: + # that the flag stays off by default, that every deployed component has + # exactly one policy, that each policy allows every port its workload + # declares, and that egress stays a separate opt-in. + python3 - "$CHART_DIR" <<'PYEOF' + import subprocess, sys, yaml + + chart = sys.argv[1] + + def render(values): + args = ["helm", "template", "test", chart] + for k, v in values.items(): + args += ["--set", f"{k}={v}"] + return subprocess.check_output(args, text=True, stderr=subprocess.STDOUT) + + def docs(manifest): + return [d for d in yaml.safe_load_all(manifest) if d] + + def component(labels): + return labels.get("app.kubernetes.io/component") + + def policies(manifest): + out = {} + for d in docs(manifest): + if d.get("kind") != "NetworkPolicy": + continue + out[component(d["spec"]["podSelector"]["matchLabels"])] = d + return out + + def workloads(manifest, kinds=("Deployment", "StatefulSet")): + out = {} + for d in docs(manifest): + if d.get("kind") in kinds: + out[component(d["spec"]["template"]["metadata"]["labels"])] = d + return out + + def allowed_ports(policy): + ports = set() + for rule in policy["spec"].get("ingress") or []: + for p in rule.get("ports") or []: + ports.add(p["port"]) + return ports + + def container_ports(workload): + ports = set() + for c in workload["spec"]["template"]["spec"].get("containers", []): + for p in c.get("ports") or []: + ports.add(p["containerPort"]) + return ports + + EVERYTHING = { + "s3.enabled": "true", + "sftp.enabled": "true", + "admin.enabled": "true", + "worker.enabled": "true", + "cosi.enabled": "true", + "s3.createBuckets[0].name": "b", + "volumes.ssd.port": "8081", + "global.seaweedfs.monitoring.enabled": "true", + } + + failed = [] + + # Off by default: the chart has never shipped a NetworkPolicy and must not + # start now, or every existing release in a default-deny namespace changes + # behaviour on the next upgrade. + if policies(render(EVERYTHING)): + failed.append("networkPolicy.enabled unset: policies rendered anyway") + else: + print("networkPolicy off by default: no policies rendered") + + on = dict(EVERYTHING, **{"networkPolicy.enabled": "true"}) + out = render(on) + pols = policies(out) + wls = workloads(out) + + # Every workload gets exactly one policy, and every policy has a workload. + # A component with no policy is wide open under default-deny; a policy with + # no component is dead weight that hides a renamed label. + for comp in wls: + if comp not in pols: + failed.append(f"{comp}: workload has no NetworkPolicy") + for comp in pols: + # The hook Jobs are covered too, and the resize hook Job only renders + # when a cluster lookup says a PVC needs growing, so it is never in the + # rendered set here. + if comp not in wls and comp not in ("bucket-hook", "volume-resize-hook"): + failed.append(f"{comp}: NetworkPolicy selects a component that is not deployed") + + # The ports a component listens on come from the same values as its + # containerPorts, so the two must agree. This is what catches a port added to + # a workload and forgotten in the policy - the failure mode that only shows up + # once someone turns the flag on. + for comp, wl in wls.items(): + if comp not in pols: + continue + declared = container_ports(wl) + allowed = allowed_ports(pols[comp]) + missing = sorted(declared - allowed) + if missing: + failed.append(f"{comp}: listens on {missing} but its policy does not allow it") + if not failed: + print("every workload has a policy covering all of its containerPorts") + + # Egress is its own opt-in: with it off the policies must not constrain + # outbound traffic at all, or enabling networkPolicy alone would cut the filer + # off from its store. + for comp, p in pols.items(): + if "Egress" in p["spec"]["policyTypes"]: + failed.append(f"{comp}: Egress in policyTypes while networkPolicy.egress.enabled is false") + if not any("Egress" in p["spec"]["policyTypes"] for p in pols.values()): + print("egress off by default: policies are ingress-only") + + # Components that need the API server must not silently lose it: rendering + # fails with a pointer to the value instead. + try: + render(dict(on, **{"networkPolicy.egress.enabled": "true"})) + failed.append("egress on with empty kubeApiServer.cidrs: render should have failed") + except subprocess.CalledProcessError as e: + if "kubeApiServer.cidrs is empty" not in (e.output or ""): + failed.append(f"egress on with empty cidrs: unexpected error: {(e.output or '')[:200]}") + else: + print("empty kubeApiServer.cidrs fails the render with a pointer to the value") + + egress_on = dict(on, **{ + "networkPolicy.egress.enabled": "true", + "networkPolicy.egress.kubeApiServer.cidrs[0]": "10.96.0.1/32", + }) + out = render(egress_on) + pols = policies(out) + + # DNS for everyone: every component addresses its peers by service name. + for comp, p in pols.items(): + dns = [r for r in p["spec"]["egress"] + if {x["port"] for x in r.get("ports") or []} == {53}] + if not dns: + failed.append(f"{comp}: egress on but no DNS rule") + if not failed: + print("every policy allows DNS when egress is on") + + # The API server rule goes only to the components that talk to it. + apiserver = {c for c, p in pols.items() + if any("ipBlock" in t for r in p["spec"]["egress"] for t in r.get("to") or [])} + expected = {"admin", "objectstorage-provisioner", "volume-resize-hook"} + if apiserver != expected: + failed.append(f"API server egress granted to {sorted(apiserver)}, expected {sorted(expected)}") + else: + print(f"API server egress limited to {sorted(expected)}") + + # The resize hook runs as a pre-install hook at weight 0, before the release + # manifest is applied, so its policy has to be a hook itself and has to sort + # ahead of the Job. + rh = pols["volume-resize-hook"]["metadata"].get("annotations", {}) + if rh.get("helm.sh/hook") != "pre-install,pre-upgrade": + failed.append(f"volume-resize-hook policy is not a pre-install hook: {rh}") + elif int(rh.get("helm.sh/hook-weight", 0)) >= 0: + failed.append(f"volume-resize-hook policy weight {rh.get('helm.sh/hook-weight')} does not sort before the Job at 0") + else: + print("volume-resize-hook policy is a pre-install hook ahead of the Job") + + # The bucket hook is post-install, so the release manifest is already applied; + # its policy must be a plain resource that uninstall cleans up. + if "helm.sh/hook" in (pols["bucket-hook"]["metadata"].get("annotations") or {}): + failed.append("bucket-hook policy is a hook resource; post-install runs after the manifest is applied") + else: + print("bucket-hook policy is a plain release resource") + + if failed: + print("\nFAIL:", file=sys.stderr) + for f in failed: + print(f" - {f}", file=sys.stderr) + sys.exit(1) + PYEOF + echo "NetworkPolicy rendering tests passed" + echo "All template rendering tests passed!" - name: Create kind cluster @@ -874,3 +1052,104 @@ jobs: kubectl delete namespace "$NS" echo "SFTP host key lifecycle tests passed" + + - name: Verify install into a default-deny namespace + run: | + set -e + CHART_DIR="k8s/charts/seaweedfs" + NS="netpol" + + # kind enforces NetworkPolicy out of the box since v0.24 (kindnetd + # runs sigs.k8s.io/kube-network-policies), so the cluster created for + # chart-testing above is enough and no extra CNI is needed. The two + # probes at the end fail loudly if that ever stops being true, rather + # than letting this pass vacuously. + kubectl create namespace "$NS" + kubectl apply -n "$NS" -f - <<'EOF' + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: default-deny + spec: + podSelector: {} + policyTypes: [Ingress, Egress] + EOF + + # The backend address, not the kubernetes service ClusterIP: kube-proxy + # rewrites the destination, so an ipBlock has to name the real endpoint. + APISERVER=$(kubectl get endpointslice kubernetes \ + -o jsonpath='{.endpoints[0].addresses[0]}' 2>/dev/null || true) + if [ -z "$APISERVER" ]; then + APISERVER=$(kubectl get endpoints kubernetes -o jsonpath='{.subsets[0].addresses[0].ip}') + fi + echo "kube-apiserver at $APISERVER" + + echo "=== install with the policies on ===" + # Without them this hangs: the components cannot resolve or reach each + # other, and the post-install bucket hook waits on master and filer + # until it gives up. --wait covers the components, and helm fails the + # release if the hook Job does not finish, so a clean install is the + # assertion. + helm install np $CHART_DIR -n "$NS" --wait --timeout 8m \ + --set s3.enabled=true \ + --set s3.createBuckets[0].name=testbucket \ + --set networkPolicy.enabled=true \ + --set networkPolicy.egress.enabled=true \ + --set networkPolicy.egress.kubeApiServer.cidrs[0]="$APISERVER/32" + echo "release came up and the bucket hook finished under default-deny" + + FILER_IP=$(kubectl get pod -n "$NS" -l app.kubernetes.io/component=filer \ + -o jsonpath='{.items[0].status.podIP}') + echo "filer pod at $FILER_IP" + + # Both probes get their own all-egress policy, so the namespace-wide + # default-deny is not what decides the outcome: the only thing left in + # the way is the filer's own policy, which admits release pods only. + # Probing the pod IP keeps DNS out of it. + kubectl apply -n "$NS" -f - <<'EOF' + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: probe-egress + spec: + podSelector: + matchExpressions: + - key: probe + operator: Exists + policyTypes: [Egress] + egress: + - {} + EOF + + probe() { + local name=$1 + local labels=$2 + kubectl run "$name" -n "$NS" --image=busybox:1.36 --restart=Never \ + --labels="probe=$name,$labels" --command -- \ + sh -c "wget -T 5 -q -O /dev/null http://$FILER_IP:8888/; echo exit=\$?" + kubectl wait -n "$NS" --for=jsonpath='{.status.phase}'=Succeeded \ + "pod/$name" --timeout=120s >/dev/null + kubectl logs -n "$NS" "$name" + } + + # A pod carrying the release labels is what the filer's policy allows. + # This has to succeed, otherwise the policies are blocking traffic they + # are supposed to permit - or nothing is enforced and the next probe + # would be meaningless. + ALLOWED=$(probe probe-allowed "app.kubernetes.io/name=seaweedfs,app.kubernetes.io/instance=np") + echo "labelled probe: $ALLOWED" + case "$ALLOWED" in + *exit=0*) echo "a pod with the release labels reaches the filer";; + *) echo "FAIL: the filer policy rejects a pod carrying the release labels"; exit 1;; + esac + + # The same probe without those labels must not get through. + DENIED=$(probe probe-denied "role=outsider") + echo "unlabelled probe: $DENIED" + case "$DENIED" in + *exit=0*) echo "FAIL: a pod outside the release reached the filer; the policy over-allows"; exit 1;; + *) echo "the filer policy refuses a pod outside the release";; + esac + + kubectl delete namespace "$NS" --wait=false + echo "default-deny namespace tests passed" diff --git a/k8s/charts/seaweedfs/README.md b/k8s/charts/seaweedfs/README.md index a7d666d55..1785e714e 100644 --- a/k8s/charts/seaweedfs/README.md +++ b/k8s/charts/seaweedfs/README.md @@ -365,6 +365,36 @@ helm install seaweedfs-worker-vacuum seaweedfs/seaweedfs -f values-worker-vacuum helm install seaweedfs-worker-balance seaweedfs/seaweedfs -f values-worker-balance.yaml ``` +## Network Policies + +In a namespace with a default-deny policy the install hangs: the components cannot resolve each other, and the post-install bucket hook waits on the master and filer until it gives up. `networkPolicy.enabled` renders one `NetworkPolicy` per component, selecting its pods by the standard `app.kubernetes.io/{name,instance,component}` labels and admitting traffic from the other pods of the release on the ports that component listens on. + +```bash +helm install seaweedfs seaweedfs/seaweedfs --set networkPolicy.enabled=true +``` + +That alone leaves outbound traffic untouched. Restricting egress is a second opt-in, because the chart knows where its own components live but not where your filer store, notification sink or remote tier does: + +```yaml +networkPolicy: + enabled: true + egress: + enabled: true + kubeApiServer: + # the endpoint behind the kubernetes service, not its ClusterIP + cidrs: ["172.18.0.2/32"] + extraEgress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: postgresql + ports: + - protocol: TCP + port: 5432 +``` + +Anything reaching the release from outside - an ingress controller, a Prometheus in another namespace - goes into `networkPolicy.extraIngress`, or into `networkPolicy.components..extraIngress` for a single component. See the `networkPolicy` block in `values.yaml` for the full set. + ## OpenShift Support SeaweedFS can be deployed on OpenShift or any cluster enforcing the Kubernetes "restricted" Pod Security Standard. By default, OpenShift blocks containers that run as root or use `hostPath` volumes. diff --git a/k8s/charts/seaweedfs/templates/shared/networkpolicy.yaml b/k8s/charts/seaweedfs/templates/shared/networkpolicy.yaml new file mode 100644 index 000000000..b6a18e75d --- /dev/null +++ b/k8s/charts/seaweedfs/templates/shared/networkpolicy.yaml @@ -0,0 +1,231 @@ +{{- include "seaweedfs.compat" . -}} +{{- if .Values.networkPolicy.enabled }} +{{- $np := .Values.networkPolicy }} +{{- $egressCfg := $np.egress | default dict }} +{{- $overrides := $np.components | default dict }} + +{{- /* One entry per rendered workload, keyed by its app.kubernetes.io/component + label. "ports" is everything the workload listens on, taken from the same + values the containerPorts are taken from, so a retuned port cannot drift + out of the policy. "apiserver" marks the workloads that talk to the + Kubernetes API. */}} +{{- $targets := list }} + +{{- /* allInOne.enabled does not turn the individual components off - their + templates only look at their own enabled flag - so the all-in-one pod is + an addition here, not an alternative. */}} +{{- if .Values.allInOne.enabled }} +{{- $ports := list .Values.master.port .Values.master.grpcPort .Values.volume.port .Values.volume.grpcPort .Values.filer.port .Values.filer.grpcPort }} +{{- if .Values.allInOne.metricsPort }} +{{- $ports = append $ports .Values.allInOne.metricsPort }} +{{- end }} +{{- if .Values.allInOne.s3.enabled }} +{{- $ports = append $ports (.Values.allInOne.s3.port | default .Values.s3.port) }} +{{- $https := .Values.allInOne.s3.httpsPort | default .Values.s3.httpsPort }} +{{- if and $https (gt (int $https) 0) }} +{{- $ports = append $ports $https }} +{{- end }} +{{- end }} +{{- if .Values.allInOne.sftp.enabled }} +{{- $ports = append $ports (.Values.allInOne.sftp.port | default .Values.sftp.port) }} +{{- end }} +{{- $targets = append $targets (dict "component" "seaweedfs-all-in-one" "ports" $ports) }} +{{- end }} +{{- if .Values.master.enabled }} +{{- $ports := list .Values.master.port .Values.master.grpcPort }} +{{- if .Values.master.metricsPort }} +{{- $ports = append $ports .Values.master.metricsPort }} +{{- end }} +{{- $targets = append $targets (dict "component" "master" "ports" $ports) }} +{{- end }} +{{- /* Every volume group carries its own component label and its own ports. */}} +{{- $anyVolume := false }} +{{- $volumes := deepCopy .Values.volumes | mergeOverwrite (dict "" .Values.volume) }} +{{- range $vname, $volume := $volumes }} +{{- $volumeName := trimSuffix "-" (printf "volume-%s" $vname) }} +{{- $volume := mergeOverwrite (deepCopy $.Values.volume) (dict "enabled" true) $volume }} +{{- if $volume.enabled }} +{{- $ports := list $volume.port $volume.grpcPort }} +{{- if $volume.metricsPort }} +{{- $ports = append $ports $volume.metricsPort }} +{{- end }} +{{- $targets = append $targets (dict "component" $volumeName "ports" $ports) }} +{{- $anyVolume = true }} +{{- end }} +{{- end }} +{{- if .Values.filer.enabled }} +{{- $ports := list .Values.filer.port .Values.filer.grpcPort }} +{{- if .Values.filer.metricsPort }} +{{- $ports = append $ports .Values.filer.metricsPort }} +{{- end }} +{{- if .Values.filer.s3.enabled }} +{{- $ports = append $ports .Values.filer.s3.port }} +{{- if and .Values.filer.s3.httpsPort (gt (int .Values.filer.s3.httpsPort) 0) }} +{{- $ports = append $ports .Values.filer.s3.httpsPort }} +{{- end }} +{{- end }} +{{- $targets = append $targets (dict "component" "filer" "ports" $ports) }} +{{- end }} +{{- if .Values.s3.enabled }} +{{- $ports := list .Values.s3.port }} +{{- if and .Values.s3.httpsPort (gt (int .Values.s3.httpsPort) 0) }} +{{- $ports = append $ports .Values.s3.httpsPort }} +{{- end }} +{{- if .Values.s3.icebergPort }} +{{- $ports = append $ports .Values.s3.icebergPort }} +{{- end }} +{{- if .Values.s3.metricsPort }} +{{- $ports = append $ports .Values.s3.metricsPort }} +{{- end }} +{{- $targets = append $targets (dict "component" "s3" "ports" $ports) }} +{{- end }} +{{- if .Values.sftp.enabled }} +{{- $ports := list .Values.sftp.port }} +{{- if .Values.sftp.metricsPort }} +{{- $ports = append $ports .Values.sftp.metricsPort }} +{{- end }} +{{- $targets = append $targets (dict "component" "sftp" "ports" $ports) }} +{{- end }} +{{- if .Values.admin.enabled }} +{{- $targets = append $targets (dict "component" "admin" "ports" (list .Values.admin.port .Values.admin.grpcPort) "apiserver" true) }} +{{- end }} +{{- if .Values.worker.enabled }} +{{- $ports := list }} +{{- if .Values.worker.metricsPort }} +{{- $ports = append $ports .Values.worker.metricsPort }} +{{- end }} +{{- $targets = append $targets (dict "component" "worker" "ports" $ports) }} +{{- end }} + +{{- if .Values.cosi.enabled }} +{{- /* The provisioner sidecar watches the COSI CRs, so it needs the API + server; nothing needs to reach the driver itself. */}} +{{- $targets = append $targets (dict "component" "objectstorage-provisioner" "ports" (list) "apiserver" true) }} +{{- end }} + +{{- /* The hook Jobs. Nothing connects to either, but under a default-deny + namespace they need egress or they hang: the bucket hook waits on the + master and filer, the resize hook shells out to kubectl. + + The bucket hook is post-install, and Helm applies the release manifest + before post-install hooks run, so a plain policy is already in place by + then. The resize hook is pre-install weight 0, which runs before the + manifest is applied at all, so its policy has to be a pre-install hook + itself, at a weight below the Job's. */}} +{{- /* Same condition as the Job in post-install-bucket-hook.yaml: an S3 + endpoint plus buckets to create. Keep the two in step. */}} +{{- $bucketHook := false }} +{{- if .Values.allInOne.enabled }} +{{- $bucketHook = and .Values.allInOne.s3.enabled .Values.allInOne.s3.createBuckets }} +{{- else if .Values.master.enabled }} +{{- if or .Values.filer.s3.enabled .Values.s3.enabled }} +{{- $bucketHook = or .Values.s3.createBuckets .Values.filer.s3.createBuckets }} +{{- end }} +{{- end }} +{{- if $bucketHook }} +{{- $targets = append $targets (dict "component" "bucket-hook" "ports" (list)) }} +{{- end }} +{{- /* Whether the resize hook Job materialises depends on a cluster lookup, so + there is nothing to key on at render time. Its policy is created whenever + the hook is enabled and selects no pods on the upgrades that need no + resize. */}} +{{- if and .Values.volume.resizeHook.enabled $anyVolume }} +{{- $targets = append $targets (dict "component" "volume-resize-hook" "ports" (list) "apiserver" true "hook" "pre-install,pre-upgrade" "hookWeight" "-10") }} +{{- end }} + +{{- range $target := $targets }} +{{- $component := $target.component }} +{{- $override := get $overrides $component | default dict }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "seaweedfs.componentName" (list $ $component) }} + namespace: {{ $.Release.Namespace }} + labels: + app.kubernetes.io/name: {{ template "seaweedfs.name" $ }} + helm.sh/chart: {{ $.Chart.Name }}-{{ $.Chart.Version | replace "+" "_" }} + app.kubernetes.io/managed-by: {{ $.Release.Service }} + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/component: {{ $component }} +{{- if $target.hook }} + annotations: + {{- /* Only before-hook-creation: the policy has to outlive the Job it + covers, so it must not be deleted when the Job succeeds. That does + leave it behind on uninstall, the usual trade-off for Helm hook + resources. */}} + "helm.sh/hook": {{ $target.hook }} + "helm.sh/hook-weight": {{ $target.hookWeight | quote }} + "helm.sh/hook-delete-policy": before-hook-creation +{{- end }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ template "seaweedfs.name" $ }} + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/component: {{ $component }} + policyTypes: + - Ingress +{{- if $egressCfg.enabled }} + - Egress +{{- end }} + ingress: +{{- $extraIngress := concat ($np.extraIngress | default list) ($override.extraIngress | default list) }} +{{- if and (not $target.ports) (not $extraIngress) }} + [] +{{- end }} +{{- if $target.ports }} + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: {{ template "seaweedfs.name" $ }} + app.kubernetes.io/instance: {{ $.Release.Name }} + ports: +{{- range $port := $target.ports }} + - protocol: TCP + port: {{ $port }} +{{- end }} +{{- end }} +{{- with $extraIngress }} +{{- toYaml . | nindent 4 }} +{{- end }} +{{- if $egressCfg.enabled }} + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: {{ template "seaweedfs.name" $ }} + app.kubernetes.io/instance: {{ $.Release.Name }} +{{- if $egressCfg.allowDNS }} + - to: + - namespaceSelector: + {{- toYaml $egressCfg.dnsNamespaceSelector | nindent 12 }} + podSelector: + {{- toYaml $egressCfg.dnsPodSelector | nindent 12 }} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 +{{- end }} +{{- if and $target.apiserver ($egressCfg.kubeApiServer).enabled }} +{{- if not $egressCfg.kubeApiServer.cidrs }} +{{- fail (printf "networkPolicy: %s needs the Kubernetes API server, but networkPolicy.egress.kubeApiServer.cidrs is empty. Set it to your API server address(es), or disable networkPolicy.egress.kubeApiServer and allow it through networkPolicy.egress.extraEgress." $component) }} +{{- end }} +{{- range $cidr := $egressCfg.kubeApiServer.cidrs }} + - to: + - ipBlock: + cidr: {{ $cidr }} + ports: +{{- range $port := $egressCfg.kubeApiServer.ports }} + - protocol: TCP + port: {{ $port }} +{{- end }} +{{- end }} +{{- end }} +{{- with concat ($egressCfg.extraEgress | default list) ($override.extraEgress | default list) }} +{{- toYaml . | nindent 4 }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/k8s/charts/seaweedfs/templates/shared/post-install-bucket-hook.yaml b/k8s/charts/seaweedfs/templates/shared/post-install-bucket-hook.yaml index 8f6cd287f..83fab94b3 100644 --- a/k8s/charts/seaweedfs/templates/shared/post-install-bucket-hook.yaml +++ b/k8s/charts/seaweedfs/templates/shared/post-install-bucket-hook.yaml @@ -40,6 +40,8 @@ {{- end }} {{- end }} +{{- /* networkpolicy.yaml decides whether to render this Job's policy off the + same condition. Keep the two in step. */}} {{- if and $s3Enabled $createBuckets }} --- apiVersion: batch/v1 diff --git a/k8s/charts/seaweedfs/values.yaml b/k8s/charts/seaweedfs/values.yaml index f20726fea..ef97bca9c 100644 --- a/k8s/charts/seaweedfs/values.yaml +++ b/k8s/charts/seaweedfs/values.yaml @@ -1810,3 +1810,111 @@ certificates: podLabels: {} # Annotations to be added to all the created pods podAnnotations: {} + +networkPolicy: + # Opt-in. While false the chart renders no NetworkPolicy objects at all and + # the cluster default (usually allow-all) applies. Turn this on for a + # namespace with a default-deny policy, where the components otherwise + # cannot reach each other and the install hangs. + enabled: false + + # One policy is rendered per component, selecting its pods by the standard + # app.kubernetes.io/{name,instance,component} labels. Each policy admits + # traffic from any pod of this release, on the ports that component listens + # on - the same values the containerPorts are taken from, so retuning a port + # does not leave the policy behind. Everything else is denied. + # + # Traffic from outside the release has to be added here. Rules are plain + # NetworkPolicyIngressRule entries, appended to every component's policy: + # + # extraIngress: + # # an ingress controller reaching filer/s3/admin + # - from: + # - namespaceSelector: + # matchLabels: + # kubernetes.io/metadata.name: ingress-nginx + # ports: + # - protocol: TCP + # port: 8888 + # - protocol: TCP + # port: 8333 + # # Prometheus scraping the metrics ports + # - from: + # - namespaceSelector: + # matchLabels: + # kubernetes.io/metadata.name: monitoring + # ports: + # - protocol: TCP + # port: 9327 + # + # An ingress controller running on the host network is not a pod as far as + # the policy is concerned; select it with an ipBlock of the node addresses + # instead of a namespaceSelector. + # + # Kubelet probes are not covered by these rules. Node-to-pod traffic is + # outside NetworkPolicy in the CNIs that come up without host endpoints + # (Calico, Cilium); if yours polices it, allow the node addresses here. + extraIngress: [] + + egress: + # Egress is a separate opt-in: the chart knows the addresses of its own + # components, but not where your filer store (MySQL, Postgres, Redis, + # ...), notification sink or remote tier lives. Enabling this without + # listing those under extraEgress will cut the filer off from its store. + enabled: false + + # Every component resolves its peers by DNS name, so this is required + # for the release to function at all. + allowDNS: true + dnsNamespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + dnsPodSelector: + matchLabels: + k8s-app: kube-dns + + # The admin component manages pods through the Kubernetes API, the COSI + # driver watches the COSI custom resources, and the volume resize hook + # shells out to kubectl. Only those three get this rule. + kubeApiServer: + enabled: true + # The API server cannot be selected by label, so its address has to be + # given here - there is no sane default, and 0.0.0.0/0 would defeat the + # point. Rendering fails with a pointer to this value if a component + # needs the API server and this is empty. + # + # Use the endpoint behind the kubernetes service, not its ClusterIP: + # kube-proxy rewrites the destination before the packet is matched. + # kubectl get endpointslice kubernetes -o jsonpath='{.endpoints[*].addresses[*]}' + cidrs: [] + ports: [443, 6443] + + # Plain NetworkPolicyEgressRule entries, appended to every component's + # policy. This is where the filer store and the notification sinks go: + # + # extraEgress: + # - to: + # - podSelector: + # matchLabels: + # app.kubernetes.io/name: mysql + # ports: + # - protocol: TCP + # port: 3306 + extraEgress: [] + + # Per-component additions, keyed by the component's app.kubernetes.io/component + # label value, for rules that should not apply to the whole release. The keys + # are master, filer, s3, sftp, admin, worker, bucket-hook, + # volume-resize-hook, objectstorage-provisioner, seaweedfs-all-in-one, and + # volume (plus volume- for every entry under `volumes`): + # + # components: + # filer: + # extraEgress: + # - to: + # - ipBlock: + # cidr: 10.0.0.5/32 + # ports: + # - protocol: TCP + # port: 5432 + components: {}