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.
429 lines
22 KiB
YAML
429 lines
22 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
|
|
|
|
bentopdf:
|
|
# Replica count for this workload; 0 is allowed and scales it down.
|
|
# The chart-global scaleDown flag takes precedence.
|
|
replicas: 1
|
|
|
|
# The runtime configuration, rendered to JSON and mounted as
|
|
# /usr/share/nginx/html/config.json - a static file in the web root that the
|
|
# application fetches in the browser. It is passed through verbatim: the two
|
|
# keys documented today are not hardcoded, so a third one upstream adds is a
|
|
# values change here and not a chart change.
|
|
#
|
|
# config:
|
|
# disabledTools: []
|
|
# editorDisabledCategories: []
|
|
#
|
|
# disabledTools takes TOOL IDS, and a tool id is its page URL without the
|
|
# ".html" - "merge-pdf", "encrypt-pdf", "word-to-pdf". There are roughly 130
|
|
# of them; the full list is deliberately NOT copied here, because a copy is
|
|
# wrong from the next upstream release on. It lives in docs/self-hosting/
|
|
# docker.md of the upstream repo.
|
|
#
|
|
# editorDisabledCategories switches off features INSIDE the PDF editor
|
|
# without disabling the editor itself. The categories are HIERARCHICAL:
|
|
# disabling a parent disables all its children, so "annotation" also takes
|
|
# out annotation-highlight, annotation-ink, annotation-stamp and the rest.
|
|
# That is the point where more disappears than expected.
|
|
#
|
|
# Two things happen silently and are worth knowing before you debug them:
|
|
#
|
|
# - A MISSPELLED ID DISABLES NOTHING AND REPORTS NOTHING. There is no
|
|
# validation, server-side or otherwise: an unknown id is simply an id
|
|
# that matches no tool. The tool stays in the UI and the file is still
|
|
# served with 200.
|
|
# - THE MOUNT REPLACES THE IMAGE'S OWN config.json (which ships as "{}").
|
|
# Whatever stands here is the complete file, not a patch on top of it.
|
|
#
|
|
# The build-time argument DISABLE_TOOLS does the same job at image build
|
|
# time; both lists are merged by the app, and a tool named in either one is
|
|
# off. Only this runtime path is reachable from a chart (see "WHAT THIS
|
|
# CHART CANNOT DO" below).
|
|
#
|
|
# THE CHART ROLLS THE PODS, THE BROWSER MAY STILL CACHE. A changed config
|
|
# here changes the checksum/config annotation on the pod template, so the
|
|
# pods restart and the new file is served - that part is guaranteed (and it
|
|
# is not optional: this is a subPath mount, which the kubelet never
|
|
# refreshes). What the chart cannot control is the visitor's browser: the
|
|
# image's nginx.conf serves .json with "expires 1w", measured as
|
|
# "Cache-Control: max-age=604800" on /config.json. A browser that already
|
|
# fetched the old file can keep using it for up to seven days. Where a
|
|
# change has to be visible immediately, reload hard in the browser, or
|
|
# override the caching for that one path in the ingress.
|
|
config: {}
|
|
|
|
# The image is quay.io/nginx/nginx-unprivileged based: nginx runs as its
|
|
# nginx user (uid/gid 101) and listens on the unprivileged port 8080, so the
|
|
# workload runs fully non-root without sysctl tweaks. Same situation as
|
|
# cyberchef.
|
|
#
|
|
# THREE IMAGE ENV SWITCHES ARE INCOMPATIBLE WITH readOnlyRootFilesystem:
|
|
# true, AND THEY FAIL LOUDLY. PORT, DISABLE_IPV6 and ROBOTS_NOINDEX are all
|
|
# implemented as "sed -i" against files in the image - the first two against
|
|
# /etc/nginx/nginx.conf, the third against every .html file in the web root.
|
|
# The scripts run under "set -e" inside /docker-entrypoint.d/, and the base
|
|
# image's entrypoint aborts when one of them fails. So the pod does NOT
|
|
# ignore the switch, it never starts at all. Measured against 2.8.7:
|
|
#
|
|
# DISABLE_IPV6=true -> sed: can't create temp file
|
|
# '/etc/nginx/nginx.confXXXXXX': Read-only file
|
|
# system (exit 1)
|
|
# PORT=9090 -> same message, same file (exit 1)
|
|
# ROBOTS_NOINDEX=true-> sed: can't create temp file
|
|
# '/usr/share/nginx/html/50x.htmlXXXXXX':
|
|
# Read-only file system (exit 1)
|
|
#
|
|
# In Kubernetes that is a CrashLoopBackOff without a single line from nginx.
|
|
# That is why none of the three is offered as a first-class value here: a
|
|
# value that looks like it works and instead kills the pod is worse than no
|
|
# value. Whoever really needs one sets it through the env block below AND
|
|
# securityContext.container.readOnlyRootFilesystem: false - both are already
|
|
# configurable, and the combination is then a deliberate, visible decision.
|
|
# The port stays 8080 either way; the Service maps onto it regardless.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 101
|
|
runAsGroup: 101
|
|
# 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
|
|
|
|
# All three probes hit "/", because the image has NO health endpoint: there
|
|
# is no /healthz, and "/" is the only path that answers 200 without a
|
|
# special rule.
|
|
#
|
|
# Be honest about what that proves: it proves nginx is serving index.html.
|
|
# It says nothing about the tools. BentoPDF computes entirely in the
|
|
# visitor's browser - not one of its ~130 tools touches this pod, which only
|
|
# ever hands out static files. A green pod here is a delivery receipt, not a
|
|
# statement about whether PDF processing works.
|
|
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. Startup budget = 5s * 30 = 150s.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 5
|
|
failureThreshold: 30
|
|
|
|
# Extra environment variables. Values are rendered through tpl; value,
|
|
# secretKeyRef and configMapKeyRef forms are supported.
|
|
#
|
|
# Note that the served BentoPDF bundle reads NONE of these - it is static
|
|
# JavaScript, configured through config above. Entries here only reach the
|
|
# nginx process and its entrypoint scripts, which is exactly why the three
|
|
# switches described above are reachable from here, together with their
|
|
# read-only-root-filesystem trap.
|
|
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".
|
|
#
|
|
# Static file delivery, so much smaller than cyberchef's 512Mi. Measured
|
|
# in k3d: 1m CPU and 2Mi working set, both while idle with probes running
|
|
# and right after serving the 48MB LibreOffice WASM asset - nginx sends
|
|
# the pre-compressed file (gzip_static) and streams it, so neither figure
|
|
# moved. The requests below sit deliberately far above that, because the
|
|
# measurement is a single-user one: gzip_comp_level 9 does compress at
|
|
# request time for anything without a pre-compressed twin, and that cost
|
|
# scales with concurrent requests, which a one-pod k3d run cannot show.
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 0.1
|
|
memory: 128Mi
|
|
ephemeralStorage: 16Mi
|
|
|
|
ingress:
|
|
# Set to false to omit the Ingress entirely (the required domain and
|
|
# clusterIssuer values are then not needed either, because they are only
|
|
# read inside the Ingress template - unlike searxng, this chart has no
|
|
# second reader of ingress.domain: the upstream counterpart BASE_URL is a
|
|
# build-time argument and empty in the published image).
|
|
#
|
|
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
|
|
# true the render aborts, naming both values. In Gateway mode ingress.domain
|
|
# is read again - by the HTTPRoute, as its single hostname - so "both modes
|
|
# off" is what frees it, not "ingress off".
|
|
enabled: true
|
|
clusterIssuer: null
|
|
# The public hostname of this release. Read by the Ingress AND - when
|
|
# httpRoute.hostnames is empty - by the HTTPRoute, so it stays the single
|
|
# source of truth for the hostname in both modes.
|
|
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, AND FOR THIS CHART THAT COSTS
|
|
# SOMETHING CONCRETE. The Gateway API has no annotation channel for
|
|
# controller behaviour, so both uses documented below - the X-Robots-Tag
|
|
# header and any repair for a controller that mangles the headers or the
|
|
# .mjs MIME type - are simply gone when httpRoute.enabled is used instead.
|
|
# The chart does not rebuild them: a policy object would bind it to one
|
|
# Gateway implementation. It is then the Gateway's business. NOTES.txt says
|
|
# so in that mode.
|
|
#
|
|
# TWO HEADERS MUST SURVIVE THE PROXY, OR OFFICE CONVERSION BREAKS. The image
|
|
# sends Cross-Origin-Opener-Policy: same-origin and
|
|
# Cross-Origin-Embedder-Policy: credentialless. Together they make the page
|
|
# crossOriginIsolated, which is the precondition for SharedArrayBuffer -
|
|
# and without SharedArrayBuffer the LibreOffice-backed conversions (Word,
|
|
# Excel, PowerPoint) do not work. An ingress controller is a reverse proxy
|
|
# and therefore a place where these headers can get lost or overwritten. The
|
|
# chart cannot enforce them; it only names the failure picture, because it
|
|
# points at the application while the cause sits in the ingress: Office
|
|
# conversion fails, everything else keeps working.
|
|
#
|
|
# The second trap of the same kind: some controllers override the MIME type
|
|
# for .mjs files, which leaves the app stuck on a module loading error. The
|
|
# image itself maps mjs to application/javascript.
|
|
#
|
|
# A PUBLICLY REACHABLE INSTANCE GETS INDEXED, and its pages carry a
|
|
# canonical link to the vendor's site (SITE_URL is a build-time argument and
|
|
# defaults to the vendor domain). There is no usable image switch for this -
|
|
# ROBOTS_NOINDEX exists but cannot run with a read-only root filesystem, see
|
|
# the block above. The ingress is the only lever left, e.g.
|
|
#
|
|
# annotations:
|
|
# nginx.ingress.kubernetes.io/configuration-snippet: |
|
|
# add_header X-Robots-Tag "noindex, nofollow";
|
|
#
|
|
# Documented as an example, not set as a default. Mind that ingress-nginx
|
|
# disables snippet annotations by default since v1.9
|
|
# (allow-snippet-annotations: false); where that stands, the controller-wide
|
|
# add-headers ConfigMap is the alternative.
|
|
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.
|
|
#
|
|
# WHAT THIS MODE COSTS HERE: the whole ingress.annotations escape hatch above,
|
|
# including the X-Robots-Tag header - a public instance in Gateway mode gets
|
|
# indexed unless the Gateway adds the header. The two cross-origin headers are
|
|
# a different case: the image sends them itself, so they still arrive in this
|
|
# mode unless something in the path strips them (NOTES.txt has the curl).
|
|
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: []
|
|
|
|
# WHAT THIS CHART CANNOT DO
|
|
#
|
|
# Almost everything else about BentoPDF is a BUILD-TIME argument and therefore
|
|
# out of reach for any chart: BASE_URL (running under a sub-path),
|
|
# SIMPLE_MODE, VITE_BRAND_NAME / VITE_BRAND_LOGO / VITE_FOOTER_TEXT
|
|
# (branding), VITE_DEFAULT_LANGUAGE, SITE_URL (the canonical link),
|
|
# DISABLE_GITHUB_STARS and all the WASM/OCR source URLs. Disabling tools is
|
|
# the one exception with a runtime path, which is bentopdf.config above.
|
|
# Changing any of the others needs a self-built image - this repository
|
|
# publishes charts, not images, so that is a project of its own with its own
|
|
# build chain (the air-gap route via bentopdf-airgap-bundle included).
|
|
#
|
|
# Two consequences that are visible in operation and deliberately accepted:
|
|
#
|
|
# - "RUNS IN THE BROWSER" IS NOT "NO OUTBOUND CONNECTIONS". Several tools
|
|
# fetch their WASM modules from cdn.jsdelivr.net at runtime (pymupdf-wasm,
|
|
# gs-wasm, coherentpdf), and the navbar fetches a GitHub star count from
|
|
# api.github.com on page load. The image's own CSP allows exactly these
|
|
# sources. The PDFs themselves never leave the device - what is loaded is
|
|
# program code, not documents.
|
|
# - THE SERVED PAGES POINT AT THE VENDOR. Their canonical link is the
|
|
# vendor's site, per the SITE_URL default described above.
|
|
#
|
|
# NO SECRETS, NO VOLUMES. The application has no credentials, no session, no
|
|
# upload and no database - it hands out static files. That is also why this
|
|
# chart has no OnePasswordItem, and why the checksum limitation for
|
|
# operator-filled secrets does not apply here: the only thing mounted is the
|
|
# ConfigMap, and that one is covered by checksum/config.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 8080 only, outbound nothing but DNS.
|
|
# BentoPDF processes documents in the visitor's browser and the chart has no
|
|
# secrets and no volumes, so the pod has no reason to open an outbound
|
|
# connection at all.
|
|
#
|
|
# WHAT IT DOES NOT DO: the 8080 rule is not restricted by source in the
|
|
# default, because 8080 is the probe port - see probeSources. So this is a
|
|
# port and egress ceiling, not pod-level isolation. And it is only a boundary
|
|
# where the CNI enforces policies.
|
|
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 8080, 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 or httpRoute.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.
|
|
#
|
|
# Nothing is needed here for BentoPDF itself.
|
|
extraPorts: []
|