# Scale the whole chart down without uninstalling it: scaleDown: true renders # every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and # takes precedence over any per-workload replicas value. PVCs, Secrets and # Services stay in place; scaleDown: false brings the workloads back up. scaleDown: false secrets: xxxSecretRef: null xxx: # Replica count for this workload; 0 is allowed and scales it down. # The chart-global scaleDown flag takes precedence. replicas: 1 resources: # Limits are optional: memory and ephemeral-storage limits default to # limitFactor x the request, an explicitly set limit always wins, and a # CPU limit is only set when configured explicitly under "limits". limitFactor: 3 requests: cpu: 0.1 memory: 512Mi ephemeral-storage: 10Mi livenessProbe: httpGet: path: / port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: / port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # The startup probe gates liveness/readiness, so those need no initialDelaySeconds. # Startup budget = periodSeconds * failureThreshold = 150s. startupProbe: httpGet: path: / port: http periodSeconds: 5 timeoutSeconds: 5 failureThreshold: 30 securityContext: pod: runAsNonRoot: true runAsUser: 20000 runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: Always # RuntimeDefault applies the container runtime's default seccomp filter, # which blocks the syscalls a normal workload never makes. It sits in the # pod context on purpose: from here it covers every container in the pod # including init containers, while a container-level seccompProfile would # have to be repeated for each one (and silently missed on the next one # added). A container that genuinely needs a different filter can still # override it under securityContext.container, which wins over this value. seccompProfile: type: RuntimeDefault container: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: - ALL add: [] ports: # An unprivileged port on purpose (AVD-KSV-0117 flags containerPort < 1024). # # Note what the reason is NOT: on current Kubernetes a non-root container # can bind port 80 just fine. The kubelet sets # net.ipv4.ip_unprivileged_port_start=0 in the pod network namespace, so # the classic "ports below 1024 need NET_BIND_SERVICE" rule does not apply # inside a pod. Measured in k3d (k3s v1.35.5): the sysctl reads 0, and a # container running as runAsUser 20000 with capabilities.drop: [ALL] binds # port 80 successfully. Anyone who "fixes" this back to 80 after testing it # once will find that it works - which is exactly why the reason is written # down here. # # The reason to stay above 1024 is that this is a runtime default, not a # guarantee: a cluster that restores ip_unprivileged_port_start=1024 (node # sysctl or a hardened distribution) breaks every workload that copied the # 80, and the failure then appears in the copied chart, not here. Every # real chart in this repo listens above 1024 (3000, 8080, 8123, 9998, ...); # the one exception is samba, which needs 139/445 and adds NET_BIND_SERVICE # with a documented justification. # # containerPort is declarative - it does not force the application onto the # port. Point the application at the same port when copying this template. - containerPort: 8080 protocol: TCP name: http env: # Reference for guideline 1: ingress.domain is read HERE, outside the # Ingress template, so it needs its own required(). Without it, switching # the Ingress off via ingress.enabled would render an empty value and the # workload would start with a broken address - silently. Copy this shape # whenever a chart uses the domain for anything but the Ingress host. - name: NORMAL_ENV value: '{{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}' - name: ENV_FROM_SECRET valueFrom: secretKeyRef: name: "{{ .Release.Name }}-{{ .Chart.Name }}-secret" key: "secretKey" - name: ENV_FROM_CONFIGMAP valueFrom: configMapKeyRef: name: "{{ .Release.Name }}-{{ .Chart.Name }}-config-map" key: "configMapKey" storage: amount: 10Gi storageClass: null ingress: # Gates the whole Ingress template. Note that ingress.domain is ALSO read by # the env block above AND by the HTTPRoute below, so it stays required even # with the Ingress switched off; only clusterIssuer becomes unnecessary. # Check this per chart instead of assuming either way (guideline 1). # # Mutually exclusive with httpRoute.enabled below: with both true the render # aborts, naming both values. enabled: true clusterIssuer: null domain: null className: nginx # Extra annotations for the Ingress; merged with the cert-manager # cluster-issuer annotation the chart always sets. # # NO COUNTERPART IN GATEWAY MODE. The Gateway API has no annotation channel # for controller behaviour, so everything set here is gone when # httpRoute.enabled is used instead. rewrite-target has an equivalent (the # URLRewrite filter in an HTTPRoute rule); an upload limit # (proxy-body-size) has none at all, and a chart that depends on one must say # so in its NOTES.txt for that mode. annotations: {} # Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so # adding it to an existing chart is additive; exactly one of ingress.enabled # and httpRoute.enabled may be true. # # The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no # gatewayClassName, no Certificate and no implementation-specific policy # object. Class and certificate belong to the Gateway and therefore to the # platform, which is what keeps a chart independent of whether the cluster runs # nginx-gateway-fabric, Cilium, Envoy Gateway or Istio. # # The Gateway-API CRDs must be installed in the cluster: the chart does not and # cannot detect them (.Capabilities.APIVersions is unreliable at template # time), so with httpRoute.enabled: true and no CRDs the INSTALL fails. httpRoute: enabled: false # The Gateway(s) this route attaches to - a platform object, usually in # another namespace, which must allow this namespace in its listener's # allowedRoutes. One entry per parent; the fields are the Gateway API's # ParentReference (name, and optionally namespace, sectionName, port, group, # kind), passed through unchanged: # # parentRefs: # - name: public # namespace: gateway # sectionName: https # # REQUIRED while httpRoute.enabled is true - the render aborts on an empty # list, because a route without a parent is created, is valid and routes # nothing, with no event to say so. parentRefs: [] # Empty means [ingress.domain], so the hostname has one source of truth. Set # explicitly only to publish under a different or an additional name - which # is how a migration from the Ingress runs: the HTTPRoute takes a second # hostname while the Ingress keeps ingress.domain. hostnames: [] # --------------------------------------------------------------------------- # NetworkPolicy (Gitea #5) — the reference block. Copy it with the template. # --------------------------------------------------------------------------- networkPolicy: # ON BY DEFAULT, which is the decision this whole rollout rests on: a # NetworkPolicy that ships switched off is reached only when somebody # remembers to switch it on, and then the issue is closed while nothing is # filtered. # # READ THIS BEFORE RELYING ON IT: a NetworkPolicy is only a boundary if the # cluster's CNI enforces policies. Where it does not, the API server still # accepts the object and everything looks configured while nothing is # filtered - an unenforced policy is a false sense of security, not a weak # one. Verify on the cluster (try to connect from an unrelated namespace) # rather than reading it off this value. # # WHAT IS ACTUALLY CAPPED, and what is not, is written out in the template's # header - read it once before assuming this value means "the workload is # isolated". In short: only the declared ports are reachable, egress is # capped by PORT (never by host - the object cannot express a hostname), and # the probed port stays open inside the cluster until probeSources is set. enabled: true # Namespaces whose pods may reach this workload, in addition to the release's # own namespace - which is always allowed. Empty means: own namespace only. # # Matched by the "kubernetes.io/metadata.name" label, which Kubernetes sets on # every namespace itself (>= 1.21), so no labelling work is needed on a # namespace this chart does not own - but also no typo protection: a # namespace name that does not exist matches nothing and is silently # ineffective. # # NOTE THE GRANULARITY: an entry here opens the port to EVERY pod in that # namespace. For the ingress controller use ingressController below instead, # which pins the source down to the controller's pods. allowedNamespaces: [] # Source CIDRs allowed to reach the port the probes address. This is the # single most dangerous value in this block, in both directions, so it is # spelled out in full. # # EMPTY MEANS OPEN, NOT BLOCKED. With no entry the rule for that port is # rendered WITHOUT a "from" selector, so the port is reachable from anywhere # the CNI routes to - which, since this port carries no NodePort, means from # inside the cluster and not from the internet. # # That is the deliberate choice between two bad defaults. The kubelet's # liveness/readiness/startup probes originate from the IP of the WORKER NODE # running the pod, an address this chart cannot know and must not guess: a # guessed CIDR that excludes the real node addresses blocks every probe, the # pod never becomes Ready, the rollout times out, and the symptom reads as a # broken application rather than as a policy. Shipping "open" instead is # honest about what it does. # # Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32", # "10.0.1.12/32"]) — and mind the companion trap: once this list is # non-empty the rule IS restricted, which also cuts off everything else that # reaches this port. The chart adds the release's own namespace, the # ingressController below and allowedNamespaces to the same rule for exactly # that reason. # # A probe that is an "exec" probe is NOT affected by any of this: it runs # inside the container through the kubelet's runtime and puts no packet on # the network, which is why a chart with exec-only probes (valkey) can # restrict its port by source with no probeSources at all. probeSources: [] # The ingress controller allowed to reach the routed port once probeSources # restricts it. Rendered ONLY while ingress.enabled or httpRoute.enabled is # true, and only inside the restricted branch - with the default (open) rule # the controller reaches the port anyway and naming it would be noise. # # The two keys are rendered as ONE "from" entry, which makes them an AND: # pods carrying podLabels IN namespace. That is the point of having them # here at all - putting the controller's namespace into allowedNamespaces # instead would open the port to every pod in kube-system. # # THE DEFAULT IS MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): # ingress-nginx is gone (no DaemonSet, no pods), the only IngressClass is # "traefik" and it is the default, and the controller is the DaemonSet # rke2-traefik in kube-system with the pod label # app.kubernetes.io/name=rke2-traefik. The same Traefik also serves the # Gateway API here (GatewayClass "traefik", controller # traefik.io/gateway-controller, no pods of its own in the Gateway's # namespace), so this one source covers BOTH the Ingress and the HTTPRoute # path. # # A policy written against the WRONG controller is the expensive mistake in # this block: it is accepted, it looks strict, and it silently blocks all # web traffic. Check the cluster before changing these two keys, and note # that ingress.className says nothing about them - the class names the # controller's implementation, not the pods its traffic comes from. ingressController: namespace: kube-system podLabels: app.kubernetes.io/name: rke2-traefik # The policy's INBOUND side. Mind the three names that overlap as substrings, # because a search for "ingress" matches all of them and they are not the # same thing: # # ingress (top level) the Ingress OBJECT this chart # renders - host, TLS, className # networkPolicy.ingress this block: what the policy lets IN # networkPolicy.ingressController the SOURCE SELECTOR for the controller's # pods. A SIBLING of this block and NO PART # OF IT, despite the name. ingress: # Opens the pod for Istio's ambient mesh. OFF by default, and additive: with # it off this chart renders byte for byte as it did before the value existed. # # WHAT IT RENDERS, and the two halves are the point: # # 1. The HBONE port inbound (default 15008/TCP) - ztunnel's tunnel. Every # inbound mesh connection arrives on it, and Istio's documentation is # explicit that ambient "does not and will never bypass Kubernetes # NetworkPolicy enforcement". Without this port a pod whose namespace # has joined the mesh is unreachable. # 2. The redirected probe source (default 169.254.7.127/32) - but ONLY # where networkPolicy.probeSources is non-empty, because only then does # the rule have a "from" list at all. Inside the mesh the kubelet's # probes are redirected and no longer come from the worker node's # address. # # WHY ONE SWITCH AND NOT TWO. Point 2 is the one nobody thinks of. An # operator who adds "15008" to a port list by hand writes a policy that # passes review, and the first time probeSources is narrowed every probe is # dropped: the pod never becomes Ready and the symptom reads as a broken # application rather than as a policy. A switch whose halves can only be had # together cannot be set halfway - which is why the two live in ONE object # under one "enabled", and not as separate values that can drift apart. # # BOTH NUMBERS BELOW ARE ISTIO CONSTANTS, NOT STANDARDS, and they are values # rather than template literals for exactly that reason: a vendor constant # can change, and an operator should be able to correct it without waiting # for a chart release. Their provenance is written on each one. # # DELETING THIS OBJECT IS SAFE. Every read falls back to the defaults # declared here (#99), so "ambientMesh: null", or an object carrying only # "enabled: true", renders the documented defaults instead of failing. ambientMesh: # The switch. Everything else in this object is inert while it is false, # and the chart renders exactly as it did before the object existed. enabled: false # ISTIO'S HBONE PORT. A constant Istio chose, not a standard: it is not # registered with IANA for this, no protocol prescribes it, and Kubernetes # knows nothing about it. Read for ztunnel 1.30.6 (measured on this # cluster: ztunnel opens [::]:15008 as its inbound listener). # # If Istio changes it, a pod in the mesh simply cannot be reached. Change # it here in all 30 charts AND in the pinned expectation inside # ci/run-networkpolicy-test.sh - the test is deliberately the thing that # forces someone to look, because the exception it grants this port is # pinned to the number IT knows and not to whatever this value says. A # value-driven exception would let a mistyped port through, which is the # hole design A of #143 was rejected for. # # NOT MEASURED: that this port ALONE suffices comes from Istio's # documentation. Measured here is only which listeners ztunnel opens. # Provable at the first namespace to join the mesh; none carries # istio.io/dataplane-mode yet. port: 15008 # SOURCES THE KUBELET'S PROBES ARRIVE FROM ONCE ISTIO HAS REDIRECTED THEM. # # NOT THE SAME VALUE AS networkPolicy.probeSources one level up, and the # two are easy to confuse, so: # # networkPolicy.probeSources # YOUR worker node CIDRs. Empty by default, and empty means the # probe rule is OPEN. The chart cannot know these addresses. # networkPolicy.ingress.ambientMesh.redirectedProbeSources # ISTIO's rewrite address. Shipped filled, because unlike the node # IPs it is a constant the chart CAN know. Only rendered where the # rule above is restricted, i.e. where probeSources is non-empty. # # WHERE 169.254.7.127 COMES FROM, so it is recognisable as vendor magic # rather than as protocol: # # - It is LINK-LOCAL, 169.254.0.0/16 (RFC 3927): a range reserved for # node-local use that is never routed - exactly the range one picks a # vendor's magic address out of. # - Istio's IPv6 counterpart gives it away: # fd16:9254:7127:1337:ffff:ffff:ffff:ffff carries the SAME DIGITS # (9254:7127 is 169.254.7.127) plus 1337. Numbers built that way are # vendor constants, not protocols. # - KUBERNETES DOES NOT KNOW THIS ADDRESS. Neither the NetworkPolicy API # nor the kubelet has any notion of it; only Istio's own data plane # rewrites the probe source onto it. # # Read for ztunnel 1.30.6. IF ISTIO CHANGES IT, THE FAILURE IS SILENT: # probes would be dropped, and only where probeSources is non-empty - which # is nowhere in this repository today - so nothing here and nothing in # Kubernetes would report it. What to re-check then: the address Istio's # release notes name for probe redirection, and this list in all 30 charts # plus the pinned expectation in ci/run-networkpolicy-test.sh. # # A LIST, not a string, and this is the one case where that does not # contradict "a constant, not a configuration": on a DUAL-STACK cluster # BOTH entries are needed at the same time, not one of two. This cluster is # SingleStack IPv4, so the IPv6 counterpart is left out - an ipBlock of the # wrong family is accepted by the API server and matches nothing. Add it # ALONGSIDE, never instead, if the cluster gains IPv6. # # Clearing this list restores the default rather than removing the # ipBlock, deliberately: the two halves of this switch are not separable. # To not open the port at all, set enabled: false. # # UNPROVEN IN OPERATION: probeSources is empty in every chart here, so no # release reaches this branch. CI proves the render, not the effect. redirectedProbeSources: - 169.254.7.127/32 egress: # Additional outbound TCP ports. DNS (UDP and TCP 53) is always allowed and # has no switch - a knob whose only other setting breaks every name lookup # in the pod is a trap, not a feature. # # PORTS, NOT DESTINATIONS, and that is a limit of the object rather than # laziness: a NetworkPolicy matches ipBlock CIDRs and pod/namespace # selectors, never hostnames, so "only to the database host" cannot be # written at all. Each entry here permits its port to ANY destination. # What the cap still buys is real: a compromised pod cannot open arbitrary # outbound connections. # # Add what the deployment actually needs: 443 for an outbound HTTPS call # (an OIDC provider's discovery document, an S3 endpoint, an app that # fetches), 5432 for Postgres, 3306 for MariaDB, 6379 for Valkey/Redis, # 587 or 465 for SMTP submission. A chart that KNOWS its outbound port # from its own values should derive it in the template instead of asking # for it twice (stalwart does this with dataStore.postgres.port). extraPorts: []