Files
helm-charts/bentopdf/values.yaml
T
jklein d7d6f8a907 feat(bentopdf): networkPolicy.ingress.ambientMesh
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.
2026-10-19 17:56:19 +00:00

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