Files
helm-charts/template-chart/values.yaml
T
jklein 22977c183f feat(template-chart)!: ambientMesh becomes an object, and the CI exception gets a pinned expectation
Gitea #143, maintainer's instruction. Both numbers this switch renders are
ISTIO CONSTANTS rather than standards, and a value with a documented default
is the better home for a vendor constant than a template literal: an operator
can correct it when Istio changes the number, without waiting for a chart
release.

    networkPolicy:
      ingress:
        ambientMesh:
          enabled: false
          port: 15008
          redirectedProbeSources:
            - 169.254.7.127/32

One object under one switch, so the coupling argument survives the change
instead of being undermined by it - two separate values could drift apart,
which is the thing this design exists to prevent. redirectedProbeSources is a
LIST because a dual-stack cluster needs BOTH of Istio's addresses at once, not
one of two; that is a nameable case rather than speculation, so it does not
contradict reason 4 of the issue. Istio's IPv6 counterpart is documented as the
dual-stack addition, with the reason it is absent today unchanged: SingleStack
IPv4, and an ipBlock of the wrong family is accepted and matches nothing.

Named redirectedProbeSources, not probeSources, because the confusion risk with
networkPolicy.probeSources one level up is larger than the ingress/
ingressController one: that value holds YOUR worker node CIDRs, is empty by
default and means OPEN when empty; this one holds ISTIO's rewrite address and
ships filled, because unlike the node IPs it is a constant the chart can know.
The distinction is written where both appear.

PROVENANCE, which the previous round explained the coupling of but never
attributed. 15008 is Istio's HBONE port - not IANA-registered for this, not
prescribed by any protocol, unknown to Kubernetes. 169.254.7.127 is link-local
(169.254.0.0/16, RFC 3927, never routed - the range one picks vendor magic out
of); Istio's IPv6 counterpart fd16:9254:7127:1337:ffff:ffff:ffff:ffff carries
the same digits plus 1337, which is what vendor constants look like; and
Kubernetes knows the address in neither the NetworkPolicy API nor the kubelet -
only Istio's data plane rewrites the probe source onto it. Both read for
ztunnel 1.30.6. If either changes, the failure is SILENT: a pod unreachable in
the mesh, or probes dropped where probeSources is non-empty, which is nowhere
today.

NULL-SAFE, per #99 and because #136 shipped this exact defect one value away
(its NOTES.txt branch dereferenced .networkPolicy.egress directly): every read
goes through a fallback reproducing the chart's own declared defaults, so a
deleted object or one carrying only "enabled: true" renders the defaults rather
than failing. Clearing the probe-source list restores the constant instead of
removing the ipBlock, deliberately - the halves are not separable, and
enabled: false is how you open nothing.

THE CONSEQUENCE, and it is the point of the CI half. With the port
configurable, the exception can no longer be checked against the rendered
value: "whatever the value says" is design A's hole wearing design C's name,
and a mistyped port would pass by sitting in the configurable field. So the
expectation is PINNED IN THE TEST - AMBIENT_PORT and AMBIENT_CIDR - and
assertion 7 now checks two things instead of one: that the render at chart
defaults produces exactly that port and that ipBlock, and that every chart's
values.yaml DECLARES the same defaults, read back through ci/yaml2json rather
than by regex. A chart that drifts is red. Changing an Istio constant means
touching 30 values files AND the pinned lines, and the pinned lines are what
force someone to look.

That block is therefore also the single place saying what we hold Istio's
numbers to be, so it carries the provenance too. The renders set only
enabled=true and never a port override, on purpose: the pin is a statement
about the defaults we ship, not about what a release may choose - an operator
overriding a value is outside this test's reach, as for every value here.

The declared-defaults check is conditional on the chart offering probeSources,
like the inseparability check and assertion 4: the five charts whose probe rule
is never source-restricted render no ipBlock, so for them the key must be
ABSENT - a value reaching no rule reads as a control that does nothing - and
that absence is asserted rather than tolerated. Their schemas drop it too.

No version change: nothing of this is published, the switch stays additive with
its default off, and the render at defaults is byte-identical - re-diffed per
chart against origin/main, not assumed.
2026-10-19 17:56:58 +00:00

411 lines
20 KiB
YAML

# 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: []