# 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: ralllySecretRef: null oidcSecretRef: null databaseSecretRef: null smtpSecretRef: null rallly: # Replica count for this workload; 0 is allowed and scales it down. # The chart-global scaleDown flag takes precedence. replicas: 1 smtp: enabled: false # Verify the mail server's TLS certificate (passed to the app as # SMTP_REJECT_UNAUTHORIZED). Leave this at true. # # Set it to false ONLY for a mail server with a self-signed certificate. # Without that hint the symptom is misleading: the send fails with a # certificate error naming the mail server, so the search starts at the # mail server or its CA chain rather than at this value. # # false disables the certificate check entirely and exposes the SMTP # session to a man-in-the-middle; prefer trusting the CA over setting it. rejectUnauthorized: true allowedEmails: '*' supportEmail: null oidc: name: 'OIDC' pictureClaimPath: 'picture' emailClaimPath: 'email' nameClaimPath: 'name' # Next.js writes its cache below the app directory (prerendered/ISR pages and # the optimized images of /_next/image). The container runs as uid 20000 # while /app belongs to the image's own "nextjs" user, so that path is not # writable for the app even without readOnlyRootFilesystem — the chart mounts # a writable volume over it. The pod's fsGroup (1000) is what makes the # mounted volume group-writable for the container. # # Deliberately an emptyDir, not a PVC: the cache is volatile. After every pod # restart it starts out empty and Next.js rebuilds it on demand, which costs # a little time on the first request after a restart. That is correct for a # cache — its entries are derived data tied to the build id of the image, so # a persistent volume would only carry stale entries across app updates. cache: # Mount path of the cache volume. Matches the Next.js cache directory of # the upstream image; only needs changing if the image layout changes. path: /app/apps/web/.next/cache # Optional upper bound for the emptyDir, e.g. 512Mi. Unset means the volume # is only bounded by the container's ephemeral-storage limit (computed as # limitFactor x request, see resources below) — note that emptyDir usage # counts against that limit, and exceeding either bound evicts the pod. sizeLimit: null 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 # No capabilities are added: the app is a plain Node process listening on # the unprivileged port 3000, verified to run with all capabilities # dropped. Do not reintroduce an "add:" key here — an empty list item # under it renders as a null entry, which is what Trivy reported as # AVD-KSV-0022 for the older chart version deployed in the cluster (#84). capabilities: drop: - ALL # Extra environment variables, appended to the ones the chart always sets. # Values are rendered through tpl; value, secretKeyRef and configMapKeyRef # forms are supported. env: [] 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: 100Mi 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 ingress: # Set to false to omit the Ingress entirely. Note that ingress.domain stays # required even then: it is also the app's public base URL # (NEXT_PUBLIC_BASE_URL), not just the Ingress host. Only clusterIssuer # becomes unnecessary. # # Mutually exclusive with httpRoute.enabled below (Gitea #114): with both # true the render aborts, naming both values. enabled: true clusterIssuer: null # The public hostname of this release. Read by the Ingress, by # NEXT_PUBLIC_BASE_URL, and - when httpRoute.hostnames is empty - by the # HTTPRoute. Required in every mode (see above). 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. 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. 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. # # For THIS chart, mind that a second hostname does not move # NEXT_PUBLIC_BASE_URL with it, and that the OIDC redirect URI registered at # the provider is built from ingress.domain - a new name needs both sides # updated or logins fail on a redirect-uri mismatch while the pod stays green. 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, and outbound DNS plus TCP 5432 # (Postgres), 443 (the OIDC provider) and - only while rallly.smtp.enabled is # true - 587 and 465 for mail submission. # # WHAT IT DOES NOT DO: all of those are STANDARD ports, not configured ones. # The database, the issuer and the mail server all arrive as connection # details inside 1Password items this chart never parses, so a dependency # anywhere else is BLOCKED - a mail server on 25 or 2525 above all. Put it 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 all three 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 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. # # The usual entries here are a mail server on 25 or 2525, or an OIDC issuer # that is not on 443. extraPorts: []