Gitea #143. Additive, default false, so the render is byte-identical to the previous release - proven by diffing both renders against origin/main rather than asserted. Switched on it adds 15008/TCP, ztunnel's HBONE tunnel, to the rule carrying the probe port, and - inside that rule's source-restricted branch - 169.254.7.127/32, the address the kubelet's probes arrive from once the pod is in the mesh. Ambient does not bypass NetworkPolicy enforcement, so without the port a pod whose namespace has joined the mesh is unreachable. The two halves are one value because the second is the one nobody thinks of: 15008 added by hand leaves a policy that passes review and drops every probe the first time probeSources is narrowed, and a pod that never becomes Ready reads as a broken application rather than as a policy. Minor, not major: additive value, unchanged default, unchanged render.
511 lines
26 KiB
YAML
511 lines
26 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
|
|
|
|
# 1Password item paths (op://<vault>/<item>). The 1Password operator turns each
|
|
# OnePasswordItem into a Kubernetes Secret whose keys are the FIELD NAMES of the
|
|
# item; the chart references those field names, so a renamed field is a pod in
|
|
# CreateContainerConfigError, not a render error. NOTES.txt lists every field.
|
|
#
|
|
# Helm never sees the CONTENT of these secrets, so no checksum/config-style
|
|
# annotation can cover them (README, guideline 9). The pod template does carry a
|
|
# checksum/secret-ref over the rendered OnePasswordItems, so REPOINTING one of
|
|
# the *SecretRef values at a different item restarts the pod; editing a value
|
|
# INSIDE the same item does not, and needs a manual
|
|
# kubectl rollout restart statefulset/<release>-sure-sfs
|
|
# The operator's operator.1password.io/auto-restart annotation is deliberately
|
|
# NOT set: it only ever restarts Deployments, so on this StatefulSet it would
|
|
# advertise a guarantee that does not exist (README, guideline 9).
|
|
secrets:
|
|
# SECRET_KEY_BASE (field: secret-key-base)
|
|
appSecretRef: null
|
|
# PostgreSQL connection (fields: host, port, database, username, password)
|
|
dbSecretRef: null
|
|
# Redis connection URL (field: connection-url)
|
|
redisSecretRef: null
|
|
|
|
sure:
|
|
# Replica count for the StatefulSet; 0 is allowed and scales it down. The
|
|
# chart-global scaleDown flag takes precedence.
|
|
#
|
|
# Both containers live in the SAME pod, so this value scales web and worker
|
|
# together. That is a consequence of the single ReadWriteOnce PVC they share
|
|
# at /rails/storage (Active Storage): splitting them into two workloads would
|
|
# need a ReadWriteMany volume. Above 1 the extra pods normally stay Pending on
|
|
# "Multi-Attach error for volume" - see NOTES.txt.
|
|
replicas: 1
|
|
# Pod-level hardening, shared by both containers and by the init container.
|
|
# The upstream image declares USER 1000:1000 (group and user "rails", created
|
|
# in the Dockerfile), so the chart needs no deviation from the repo hardening
|
|
# standard; fsGroup 1000 makes the storage PVC writable for that uid.
|
|
#
|
|
# readOnlyRootFilesystem: true holds, and the writable paths are emptyDirs:
|
|
# /rails/tmp, /rails/log and /tmp, one set per container. /tmp needs one extra
|
|
# step that is easy to mistake for decoration - a tiny init container creates
|
|
# two private subdirectories in it, and the containers run with TMPDIR and
|
|
# HOME pointing there. Ruby refuses a world-writable directory without a
|
|
# sticky bit as its temporary directory and an emptyDir is exactly that, so
|
|
# without this the app aborts during boot; HOME follows because the image's
|
|
# /home/rails is read-only and Bundler otherwise builds a throwaway home per
|
|
# invocation. The StatefulSet carries the measured evidence for both.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
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
|
|
# - web and worker alike - 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
|
|
# <container>.securityContext.container, which wins over this value.
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# web - Puma, the container that also runs the database migrations
|
|
# ---------------------------------------------------------------------------
|
|
web:
|
|
securityContext:
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
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".
|
|
#
|
|
# The request follows upstream's own sizing note (docs/hosting/docker.md
|
|
# for 0.7.4): steady-state memory "around 352 MB", comfortable under a
|
|
# 512 MB limit, while "if you enable AI, run at least 1 GB". 512Mi request
|
|
# x limitFactor 3 = 1536Mi limit, which holds both the everyday figure and
|
|
# the AI recommendation without reserving the larger number on the node.
|
|
# Read from upstream's documentation, not measured here.
|
|
#
|
|
# ephemeral-storage is NOT the template default of 10Mi, and the reason is
|
|
# measured: with 10Mi (limit 30Mi) per container the kubelet evicted the
|
|
# pod within two minutes of every start - "Pod ephemeral local storage
|
|
# usage exceeds the total limit of containers 60Mi". Both emptyDirs and
|
|
# the container's stdout log count against that limit, and this image
|
|
# rebuilds its Bootsnap cache under /rails/tmp on every pod start because
|
|
# the emptyDir hides the one baked into the image. 256Mi request (768Mi
|
|
# limit) is headroom for that, not a measurement of it - the steady-state
|
|
# figure is still unknown.
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 0.1
|
|
memory: 512Mi
|
|
ephemeral-storage: 256Mi
|
|
# PROBES - what each one actually proves (README, guideline 12.3)
|
|
#
|
|
# /up is Rails' own health route (config/routes.rb: get "up" =>
|
|
# "rails/health#show"). READ FROM THE RAILS SOURCE, NOT MEASURED HERE: that
|
|
# controller renders 200 as soon as the app has booted without raising and
|
|
# touches NO database and NO Redis. It is a boot check, and the widespread
|
|
# belief that it tests the database is exactly what guideline 12.3 exists to
|
|
# catch - upstream's own Helm chart (charts/sure in the app repository) uses
|
|
# /up for readiness, which is the mistake this chart does not copy.
|
|
#
|
|
# Liveness therefore uses /up: a Postgres outage must not restart the pod.
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /up
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
# Readiness asks for "/" instead, because that path provably reaches the
|
|
# database - and it does so through curl rather than an httpGet probe, for
|
|
# a reason that was measured rather than guessed (see below).
|
|
#
|
|
# Why "/" reaches the database: every request runs ApplicationController's
|
|
# authenticate_user! before_action (app/controllers/concerns/
|
|
# authentication.rb); without a session cookie - which a probe never has -
|
|
# it falls into self_hosted_first_login?, i.e.
|
|
# "config.app_mode.self_hosted? && User.count.zero?". User.count is an SQL
|
|
# query. Measured in the install-test: against an empty database the app
|
|
# answered 302 to /registration/new, which is the branch that query decides,
|
|
# so the path demonstrably touches Postgres. With the database down the
|
|
# query raises and Rails answers 500.
|
|
#
|
|
# Why exec + curl and not httpGet: the kubelet's HTTP prober FOLLOWS
|
|
# redirects to the same host, and with RAILS_ASSUME_SSL=true (which the
|
|
# chart sets whenever the Ingress is on) Rails builds that redirect as
|
|
# https://<pod-ip>:3000/... - a scheme Puma does not speak. The install-test
|
|
# failed with "http: server gave HTTP response to HTTPS client" on a pod
|
|
# that was answering correctly. curl without -L asks once and reports the
|
|
# status code; the test accepts 2xx and 3xx exactly as the kubelet would,
|
|
# and a 500 from a missing database still fails. curl is part of the image
|
|
# (the Dockerfile installs it), so nothing extra is pulled in for this.
|
|
#
|
|
# One condition this depends on: SELF_HOSTED=true, which the chart always
|
|
# sets. In managed mode the same code short-circuits before User.count and
|
|
# "/" would redirect without touching Postgres at all.
|
|
readinessProbe:
|
|
exec:
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- "code=\"$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/)\"; test \"$code\" -ge 200 -a \"$code\" -lt 400"
|
|
periodSeconds: 10
|
|
timeoutSeconds: 10
|
|
failureThreshold: 3
|
|
# The startup probe gates liveness and readiness, so neither needs an
|
|
# initialDelaySeconds. Its budget has to cover the first start, which is the
|
|
# expensive one: the image entrypoint (/rails/bin/docker-entrypoint) runs
|
|
# "./bin/rails db:prepare" whenever the command is "./bin/rails server", so
|
|
# the web container creates and migrates the schema before Puma binds the
|
|
# port. Budget 10s x 60 = 600s. Raise it before a large app-version jump;
|
|
# a restart in the middle of a migration is the failure mode it prevents.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /up
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 60
|
|
# Extra environment variables, appended to the ones the chart always sets.
|
|
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
|
|
# forms are supported.
|
|
#
|
|
# This is also where the AI features go - the chart models none of them as
|
|
# values on purpose. OPENAI_ACCESS_TOKEN (secretKeyRef form), OPENAI_MODEL,
|
|
# OPENAI_URI_BASE, LLM_CONTEXT_WINDOW, OPENAI_REQUEST_TIMEOUT,
|
|
# AI_RESPONSE_TIMEOUT, ASSISTANT_MAX_TOOL_CALL_ITERATIONS and the SMTP_*
|
|
# block are plain environment variables to this image. Set them on BOTH
|
|
# containers where a background job needs them as well (the assistant runs
|
|
# in the worker).
|
|
#
|
|
# Additive means additive: an entry repeating a name the chart already sets
|
|
# reaches the pod twice at install time (the later one, yours, wins) but is
|
|
# LOST on a later helm upgrade, because Kubernetes merges env by name and
|
|
# the pair collapses to the chart's own value - with a green upgrade either
|
|
# way (README, guideline 10).
|
|
env: []
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# worker - Sidekiq: the same image, started with "bundle exec sidekiq" as
|
|
# its args (the image entrypoint stays in place; see the StatefulSet)
|
|
# ---------------------------------------------------------------------------
|
|
worker:
|
|
securityContext:
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
resources:
|
|
# Same sizing as the web container, for a documented reason: upstream
|
|
# notes that the sample/demo-data generator runs in the WORKER and is the
|
|
# one operation that deterministically exceeds 512 MB (observed flat at
|
|
# ~680 MB on a 2 GB container). The computed limit of 3 x 512Mi = 1536Mi
|
|
# covers that; the request stays at the everyday figure.
|
|
#
|
|
# ephemeral-storage: same 256Mi as the web container and for the same
|
|
# measured reason - see the comment there.
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 0.1
|
|
memory: 512Mi
|
|
ephemeral-storage: 256Mi
|
|
# PROBES - the worker speaks no HTTP at all, so all three are exec probes.
|
|
#
|
|
# The command asks Sidekiq's own monitor whether THIS process is in the
|
|
# process set: sidekiqmon prints one line per registered process, each
|
|
# starting with the identity "<hostname>:<pid>:<nonce>" (sidekiq 8.0.5,
|
|
# lib/sidekiq/component.rb), and in a pod the hostname is the pod name that
|
|
# bash exposes as $HOSTNAME. Sidekiq keeps that registration alive with a
|
|
# heartbeat whose Redis key expires after about 60s, so a process that hangs
|
|
# disappears from the set while its PID still exists - which is precisely
|
|
# what a process check like "pgrep sidekiq" could not tell you (and would
|
|
# not tell you anything at all: Sidekiq is PID 1 here, so if it dies the
|
|
# container dies with it and the check can never fail - a check a broken
|
|
# system passes, README).
|
|
#
|
|
# READ FROM THE SIDEKIQ 8.0.5 SOURCE, NOT MEASURED: sidekiqmon is a plain
|
|
# Ruby entry point that requires sidekiq/api only - it does NOT boot Rails -
|
|
# and it reads REDIS_URL from the environment, which the chart sets on this
|
|
# container. Cost per check is therefore one Ruby start, not an application
|
|
# boot; the periods below are chosen on the assumption that this takes a few
|
|
# seconds, and the PR install-test is the first place it actually runs.
|
|
#
|
|
# Liveness hangs on Redis, which guideline 12.3 allows only where no
|
|
# dependency-free path proves anything - that is the case here (see above) -
|
|
# and it is given a deliberately generous budget of 60s x 5 = 300s of
|
|
# missing heartbeat, so a short Redis blip does not restart the pod.
|
|
livenessProbe:
|
|
exec:
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
|
|
periodSeconds: 60
|
|
timeoutSeconds: 15
|
|
failureThreshold: 5
|
|
# Readiness uses the same check with a tighter budget: a worker that is not
|
|
# registered in Redis is not doing any work, whatever its process state.
|
|
readinessProbe:
|
|
exec:
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
|
|
periodSeconds: 30
|
|
timeoutSeconds: 15
|
|
failureThreshold: 3
|
|
# The startup probe gates the other two. Sidekiq boots the full Rails app
|
|
# before it registers, and on a first install it waits for the web container
|
|
# to finish db:prepare, so the budget matches the web container's: 20s x 30
|
|
# = 600s.
|
|
startupProbe:
|
|
exec:
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
|
|
periodSeconds: 20
|
|
timeoutSeconds: 15
|
|
failureThreshold: 30
|
|
# Extra environment variables for the worker, appended to the ones the chart
|
|
# always sets. Same rules as sure.web.env above.
|
|
env: []
|
|
|
|
# The Active Storage volume, mounted at /rails/storage in BOTH containers.
|
|
# Deliberately not null-safe: an erased block yields an invalid PVC, which fails
|
|
# loudly (README, guideline 12).
|
|
storage:
|
|
amount: 10Gi
|
|
storageClass: null
|
|
|
|
ingress:
|
|
# Gates the whole Ingress template. Note that ingress.domain is read three
|
|
# more times outside it - APP_DOMAIN (the host Action Mailer builds link URLs
|
|
# from), WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS (the relying party a
|
|
# passkey is bound to) - so it keeps its own required() there and stays
|
|
# mandatory with the Ingress switched off; only clusterIssuer becomes
|
|
# unnecessary (README, guideline 12.1). Without that guard the app would come
|
|
# up with WEBAUTHN_RP_ID="" and register passkeys against the wrong relying
|
|
# party - rendered cleanly, pod green.
|
|
#
|
|
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
|
|
# true the render aborts, naming both values.
|
|
#
|
|
# HOW THE APP IS TOLD ABOUT TLS NO LONGER HANGS ON THIS VALUE ALONE (#114).
|
|
# RAILS_ASSUME_SSL and RAILS_FORCE_SSL follow "is this release published
|
|
# through a TLS-terminating proxy at all", i.e. ingress.enabled OR
|
|
# httpRoute.enabled: with either on they render "true" (the proxy terminates
|
|
# TLS, so Rails must trust the forwarded scheme and may set secure cookies
|
|
# and HSTS), with both off they render "false" so a plain-HTTP port-forward
|
|
# can log in at all. Deriving them from ingress.enabled alone was right while
|
|
# an Ingress was the only way to publish; in Gateway mode ingress.enabled is
|
|
# false by construction, and the flags would have gone false with TLS still
|
|
# terminated at the Gateway - insecure cookies and http:// links, with a
|
|
# green pod and a working UI as the only symptom.
|
|
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.
|
|
#
|
|
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
|
|
# annotation channel for controller behaviour, so anything configured here is
|
|
# simply gone when httpRoute.enabled is used instead - it is then the
|
|
# Gateway's business, or that of an implementation-specific policy object
|
|
# this chart deliberately does not render. Nothing this chart needs lives
|
|
# here by default; the TLS flags above are environment variables and are
|
|
# handled in both modes.
|
|
annotations: {}
|
|
|
|
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
|
|
# an existing release is unaffected; 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 this 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.
|
|
#
|
|
# This value also feeds RAILS_ASSUME_SSL / RAILS_FORCE_SSL - see the note under
|
|
# ingress.enabled above.
|
|
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.
|
|
#
|
|
# BE CAREFUL WITH THIS ONE HERE. A second hostname does not move
|
|
# WEBAUTHN_RP_ID, which keeps following ingress.domain - and passkeys are
|
|
# bound to the relying party id, so changing ingress.domain to match
|
|
# invalidates every registered credential (upstream
|
|
# docs/hosting/webauthn.md). APP_DOMAIN and WEBAUTHN_ALLOWED_ORIGINS follow
|
|
# ingress.domain as well.
|
|
hostnames: []
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NetworkPolicy (Gitea #5). On by default. The reference implementation with
|
|
# the full reasoning is template-chart; the convention is in README, "Chart
|
|
# style guidelines", and the operator's view on this chart's wiki page.
|
|
# ---------------------------------------------------------------------------
|
|
networkPolicy:
|
|
# WHAT THIS POLICY ALLOWS: inbound 3000 only (declared by the web container;
|
|
# the worker declares no port), and outbound DNS plus TCP 5432 (Postgres),
|
|
# 6379 (Redis) and 443 (the external data the worker fetches).
|
|
#
|
|
# IT COVERS BOTH CONTAINERS. A NetworkPolicy selects pods, not containers, so
|
|
# the worker is subject to the egress rules too - which is where it does all
|
|
# of its work.
|
|
#
|
|
# WHAT IT DOES NOT DO: 5432 and 6379 are the STANDARD ports. The Postgres
|
|
# host and port are FIELDS of the 1Password item, which the chart passes
|
|
# through without reading, so it cannot know them. A dependency elsewhere is
|
|
# BLOCKED and belongs in egress.extraPorts.
|
|
#
|
|
# Inbound 3000 is not restricted by source in the default (probe port), and a
|
|
# policy is only a boundary where the CNI enforces one.
|
|
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 on the "kubernetes.io/metadata.name" label Kubernetes sets on every
|
|
# namespace itself, so no labelling work is needed on a namespace this chart
|
|
# does not own - and no typo protection either: a name that does not exist
|
|
# matches nothing and is silently ineffective. Note the granularity: an entry
|
|
# opens the port to EVERY pod in that namespace.
|
|
allowedNamespaces: []
|
|
# Source CIDRs allowed to reach port 3000, which is the port the web container's probes address.
|
|
#
|
|
# EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a
|
|
# "from" selector, so the port is reachable from anywhere the CNI routes to -
|
|
# inside the cluster, since it carries no NodePort.
|
|
#
|
|
# That is the deliberate choice between two bad defaults. The kubelet's probes
|
|
# originate from the IP of the WORKER NODE running the pod, an address this
|
|
# chart cannot know and must not guess: a CIDR that excludes the real node
|
|
# 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.
|
|
#
|
|
# Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind
|
|
# the companion trap: once this list is non-empty the rule IS restricted, so
|
|
# everything else that needs the port has to be in it. The chart adds the own
|
|
# namespace, the ingressController below and allowedNamespaces to the same rule for exactly that reason.
|
|
probeSources: []
|
|
# The ingress controller allowed to reach the routed port once probeSources
|
|
# restricts it. Rendered only while ingress.enabled is true, and only in
|
|
# the restricted branch - with the default (open) rule the controller reaches
|
|
# the port anyway. The two keys become ONE "from" entry and are therefore an
|
|
# AND: pods carrying podLabels IN namespace. That is the point of having them
|
|
# at all - the controller's namespace in allowedNamespaces would open the port
|
|
# to every pod in kube-system.
|
|
#
|
|
# MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is gone,
|
|
# the only IngressClass is "traefik" and it is the default, and the controller
|
|
# is the DaemonSet rke2-traefik in kube-system. The same Traefik serves the
|
|
# Gateway API here, so this one source covers the Ingress and the HTTPRoute
|
|
# path alike. A policy written against the wrong controller is accepted, looks
|
|
# strict, and silently blocks all web traffic - and ingress.className says
|
|
# nothing about it, because a class names an implementation, not the pods the
|
|
# 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 different
|
|
# things:
|
|
#
|
|
# 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 ingress
|
|
# 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 (Gitea #143). OFF by default and
|
|
# additive: while it is false this chart renders byte for byte as it did
|
|
# before the value existed.
|
|
#
|
|
# IT RENDERS TWO THINGS, and that they come together is the point:
|
|
#
|
|
# 1. 15008/TCP inbound - ztunnel's HBONE tunnel. Every inbound mesh
|
|
# connection arrives on it, and ambient "does not and will never bypass
|
|
# Kubernetes NetworkPolicy enforcement", so without this port a pod
|
|
# whose namespace has joined the mesh is unreachable.
|
|
# 2. 169.254.7.127/32 as an additional peer - but ONLY where the rule
|
|
# restricts sources at all, which here means probeSources is non-empty.
|
|
# 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. Whoever
|
|
# 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 this is a named intention and not a port list.
|
|
#
|
|
# TWO THINGS IT DOES NOT PROVE:
|
|
# - That 15008 alone is enough. That comes from Istio's documentation;
|
|
# measured on this cluster is only which listeners ztunnel opens.
|
|
# Provable at the first namespace to join the mesh - none carries
|
|
# istio.io/dataplane-mode yet.
|
|
# - That the probe-source half works. probeSources is empty in every chart
|
|
# here, so no release reaches that branch. CI proves its render, not its
|
|
# effect.
|
|
#
|
|
# Note where the port lands: it joins the rule that carries the probe port,
|
|
# so once probeSources restricts that rule, 15008 is restricted with it.
|
|
# Whether ztunnel needs a peer of its own is open - it runs with its own pod
|
|
# IPs in istio-system (measured: no hostNetwork on the DaemonSet, Istio
|
|
# 1.30.6), so if one proves necessary it is expressible as a
|
|
# namespaceSelector plus podSelector and needs no unknowable node CIDR.
|
|
ambientMesh: false
|
|
|
|
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 CIDRs and selectors and never hostnames,
|
|
# so "only to the database host" cannot be written at all. Each entry
|
|
# permits its port to ANY destination. What the cap still buys is real: a
|
|
# compromised pod cannot open arbitrary outbound connections.
|
|
#
|
|
# Add the real port whenever Postgres or Redis is not on 5432 / 6379, or
|
|
# for an SMTP server (587 or 465) if mail is configured.
|
|
extraPorts: []
|