The rule allowed outbound 80, the port the apache-tika and gotenberg Services listen on, and justified it in a comment: an egress rule names the port on the destination as the client dials it, and the client dials the Service. That is wrong. kube-proxy does a ClusterIP Service's destination NAT BEFORE the egress policy is evaluated at the pod's network interface, so the policy sees podIP:targetPort and never clusterIP:port - and those two charts forward 80 to 9998 and 3000. Deployed as 14.0.0+up3.3.1 it cut Paperless off from both services on 2026-10-15: mail import, EML, HTML and Office documents stopped, plain PDFs and images kept working, and the error was httpx.ConnectTimeout with no mention of a NetworkPolicy. The rule now names 9998, 3000 and 80. The 80 stays because it costs nothing and covers a foreign Tika that really does listen on 80. The refuted sentence is quoted rather than deleted: it reads perfectly, which is how it survived review, and the timeout-not-refused symptom is written down with it. No policy switch of its own: the rule stays gated on paperless.files.tikaGotenberg.enabled, because a second value that can drift from the feature re-arms this very outage. New networkPolicy.egress.mailImport opens IMAPS 993, off by default - paperless' mail accounts are rows in its database, so the chart cannot derive the rule. Plain IMAP (143) and POP3 stay in extraPorts, and 993 is noted as a real destination port, because a mail server outside the cluster has no DNAT in the path. Minor: one additive value, and a fix that widens a rule. Nothing breaks, no value becomes required, appVersion unchanged. Part of #136
674 lines
35 KiB
YAML
674 lines
35 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:
|
|
# op:// path of the item holding the Redis/Valkey connection. Fields:
|
|
# "redis-connection-url" and "key-prefix" always, plus "auth-conf" when the
|
|
# bundled Valkey subchart is used - see the valkeySubchart block at the end
|
|
# of this file. NOTES.txt prints all three.
|
|
redisSecretRef: null
|
|
databaseSecretRef: null
|
|
paperlessSecretRef: null
|
|
smbConnectionSecretRef: null
|
|
# REMOVED IN 13.0.0: secrets.oidcSecretRef (Gitea #11). It is gone from the
|
|
# schema too, so a release that still sets it now fails to render with
|
|
# additional properties 'oidcSecretRef' not allowed
|
|
# instead of being ignored - DELETE THE KEY from your values, nothing else is
|
|
# needed. It never did anything: no template read it, no OnePasswordItem was
|
|
# created for it, and whatever the item held never reached the pod.
|
|
#
|
|
# It is not coming back as a feature, and the reason is in paperless itself:
|
|
# the whole OIDC configuration travels in ONE json environment variable
|
|
# (SOCIALACCOUNT_PROVIDERS = json.loads(os.getenv(
|
|
# "PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"))), and there is no _FILE variant
|
|
# of it. A secretKeyRef cannot substitute a fragment INTO a json variable, so
|
|
# an honest implementation would have to either inline the secret in the pod
|
|
# spec or push the complete json into the vault by hand - and the second one
|
|
# makes every required() in the oidc block below dead wood.
|
|
#
|
|
# The operational question behind it is answered rather than dodged: this
|
|
# chart registers a PUBLIC client with PKCE, Keycloak (Gitea #83) can mark a
|
|
# client public per client, and so no provider constraint forces a
|
|
# confidential client here. See the oidc block below.
|
|
|
|
ingress:
|
|
# Set to false to omit the Ingress entirely. NEW in the chart version that
|
|
# added the httpRoute block below (Gitea #114): this chart used to render its
|
|
# Ingress unconditionally, which is why it has no issue-#93 switch like the
|
|
# rest. The default is true, so an existing release is unaffected.
|
|
#
|
|
# It was not added for uniformity. Without it the Ingress would keep
|
|
# rendering in Gateway mode, and with it the required() on clusterIssuer -
|
|
# a value demanded for something the chart then does not render. With the
|
|
# switch that required() is conditional, as everywhere else here.
|
|
#
|
|
# clusterIssuer becomes unnecessary with the Ingress off. ingress.domain does
|
|
# NOT: it is also PAPERLESS_URL, the only origin this release declares to
|
|
# Django, and it carries its own required() in the StatefulSet.
|
|
#
|
|
# Mutually exclusive with httpRoute.enabled below: with both true the render
|
|
# aborts, naming both values. Mind that enabled defaults to TRUE here, so
|
|
# turning the Gateway mode on means switching this off in the same values
|
|
# file.
|
|
enabled: true
|
|
clusterIssuer: null
|
|
# The public hostname of this release. Read by the Ingress, by PAPERLESS_URL
|
|
# in the StatefulSet (with its own required()), and - when
|
|
# httpRoute.hostnames is empty - by the HTTPRoute. Required in every mode.
|
|
domain: null
|
|
className: nginx
|
|
# Extra annotations for the Ingress; merged with the cert-manager
|
|
# cluster-issuer and upload-size annotations 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. For THIS chart that includes the
|
|
# upload cap: see paperless.files.maxUploadFileSize.
|
|
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 - and ingress.enabled defaults to true, so it
|
|
# has to be switched off explicitly.
|
|
#
|
|
# 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.
|
|
#
|
|
# WHAT THIS MODE COSTS HERE: paperless.files.maxUploadFileSize is not enforced,
|
|
# so the web UI's upload ceiling is the Gateway's. The consume directory is
|
|
# unaffected - it passes no proxy and has no limit at all.
|
|
#
|
|
# 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 PAPERLESS_URL
|
|
# with it: Django keeps trusting the origin built from ingress.domain, and
|
|
# every login POST from the new name is refused as a CSRF failure until that
|
|
# value is changed too (and the pods restarted).
|
|
hostnames: []
|
|
|
|
paperless:
|
|
# Replica count for this workload; 0 is allowed and scales it down.
|
|
# The chart-global scaleDown flag takes precedence.
|
|
replicas: 1
|
|
# Auxiliary busybox image, used by the /run-ownership init container and the
|
|
# SMB consume-mover CronJob. Pinned independently of appVersion (which
|
|
# tracks paperless-ngx); reviewed alongside every app-version update round.
|
|
busybox:
|
|
image:
|
|
repository: busybox
|
|
tag: "1.38.0"
|
|
ocr:
|
|
languages: "eng"
|
|
# Paperless-ngx 3.x modes: auto (default, skips OCR when embedded text
|
|
# exists), redo, force, off. The 2.x values "skip"/"skip_noarchive" were
|
|
# removed upstream; "skip" maps to "auto".
|
|
mode: "auto"
|
|
# Archive (PDF/A) generation, decoupled from OCR in 3.0: auto (default,
|
|
# skip archive for born-digital PDFs), always (2.x behavior), never.
|
|
archiveFileGeneration: "auto"
|
|
rotate_pages: "true"
|
|
output_type: "pdfa"
|
|
files:
|
|
filenameFormat: "{{ created_year }}/{{ correspondent }}/{{ title }}"
|
|
storage: 20Gi
|
|
maxUploadFileSize: 10m
|
|
# The barcode scanner backend is no longer configurable: paperless-ngx 3.0
|
|
# removed pyzbar and PAPERLESS_CONSUMER_BARCODE_SCANNER; zxing-cpp is the
|
|
# only backend now.
|
|
#
|
|
# An empty or null "barcodes:" block in an override means "the defaults
|
|
# below apply" — the chart merges the block onto these defaults instead of
|
|
# letting a childless key erase them (issue #61; the fallback copy lives in
|
|
# the paperless-ngx.barcodes helper and is kept in sync with this block).
|
|
# To switch barcode handling off, set barcodes.enabled: false explicitly;
|
|
# deleting the sub-keys does not disable it.
|
|
barcodes:
|
|
enabled: true
|
|
barcodeString: "PATCHT"
|
|
retainBarcodePage: false
|
|
asn:
|
|
enabled: true
|
|
# Paperless-ngx 3.0 no longer rejects duplicate documents by default; set
|
|
# to true to restore the 2.x behavior of deleting duplicates on consume.
|
|
deleteDuplicates: false
|
|
tikaGotenberg:
|
|
enabled: false
|
|
tikaEndpoint: null
|
|
gotenbergEndpoint: null
|
|
scripts:
|
|
postConsumptionScripts: []
|
|
preConsumptionScripts: []
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 8000
|
|
periodSeconds: 30
|
|
failureThreshold: 3
|
|
successThreshold: 1
|
|
timeoutSeconds: 5
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 8000
|
|
periodSeconds: 10
|
|
failureThreshold: 3
|
|
successThreshold: 1
|
|
timeoutSeconds: 5
|
|
# The startup probe gates liveness/readiness. Paperless runs database
|
|
# migrations on startup, and since 3.0 the first start after an upgrade
|
|
# also rebuilds the search index from scratch (Whoosh -> Tantivy):
|
|
# budget 10s * 60 = 600s.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 8000
|
|
periodSeconds: 10
|
|
failureThreshold: 60
|
|
timeoutSeconds: 5
|
|
# Extra environment variables, appended to the ones the chart always sets.
|
|
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
|
|
# forms are supported. paperless-ngx reads around 80 PAPERLESS_* variables and
|
|
# this chart maps a handful of them explicitly - everything else goes here.
|
|
#
|
|
# env:
|
|
# - name: PAPERLESS_LOGGING_DIR
|
|
# value: /tmp/paperless-log
|
|
#
|
|
# ADDITIVE MEANS ADDITIVE, and the timing matters: an entry repeating a name
|
|
# the chart already sets reaches the pod twice, and at INSTALL the later one -
|
|
# yours - wins. On an EXISTING release the same entry is lost, because
|
|
# Kubernetes merges env by name and the pair collapses to the chart's own
|
|
# value, while helm upgrade still reports success. Read an override back off
|
|
# the StatefulSet, never off the upgrade's exit code.
|
|
#
|
|
# VALUES MUST BE STRINGS (the schema enforces it) and they are quoted on the
|
|
# way out since 13.0.0. Before that a value beginning with "*" aborted the
|
|
# render and "true" or "0755" arrived as a bool or an integer, which the API
|
|
# server rejects - see the comment at the render site in
|
|
# templates/paperless-server.sfs.yaml.
|
|
#
|
|
# THE WORKED EXAMPLE, PAPERLESS_LOGGING_DIR, AND WHAT IT DOES NOT FIX
|
|
# (jk-cluster-config #360): with the data directory on an RWX volume that
|
|
# Longhorn exports over NFS, paperless' concurrent_log_handler cannot hold its
|
|
# lock file there and fails with "RuntimeError: Cannot acquire lock after 20
|
|
# attempts" - measured at 45 % of the application log across all three
|
|
# installations. Pointing PAPERLESS_LOGGING_DIR at a path under the /tmp
|
|
# emptyDir this pod already mounts takes the log off the NFS share and the
|
|
# noise stops. It does NOT fix the RWX data volume, which is the actual root
|
|
# cause and a cluster-config decision (the PVC asks for ReadWriteOnce and was
|
|
# provisioned ReadWriteMany; taking RWX back means recreating the volume).
|
|
# A log under /tmp does not survive a pod restart - no loss here, paperless
|
|
# also writes to stdout, which is what kubectl logs reads.
|
|
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.2
|
|
memory: 1024Mi
|
|
ephemeralStorage: 200Mi
|
|
# Paperless-ngx supports rootless operation: the pod runs as uid/gid 1000
|
|
# (the image's default paperless user), fsGroup makes the PVCs writable and
|
|
# /tmp + /run are emptyDirs so the root filesystem stays read-only.
|
|
# Documented deviation: s6-overlay requires /run to be OWNED by the runtime
|
|
# uid (group-write is not enough), so a minimal root init container chowns
|
|
# the /run emptyDir to runAsUser/runAsGroup before the hardened main
|
|
# container starts.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: Always
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
smbConnection:
|
|
enabled: false
|
|
server: null
|
|
# Cron schedule of the consume mover. Quote it or not as you like - the chart
|
|
# quotes it on the way into the manifest (since 13.0.0, Gitea #11), which is
|
|
# what a schedule beginning with "*" or "@" needs. Before that such a
|
|
# schedule aborted the render with a YAML error pointing at the template.
|
|
cronJob: "0 0,12 * * *"
|
|
cron:
|
|
# Run the mover on the node that carries the StatefulSet pod (Gitea #101).
|
|
#
|
|
# WHY IT DEFAULTS TO TRUE: this pod mounts the consume volume, which is RWX
|
|
# and comes from Longhorn, and Longhorn's CSI driver refuses to mount an RWX
|
|
# volume unless it finds a workload pod in Pending - a mover pod that ran on
|
|
# a different node leaves the kubelet of the StatefulSet's node retrying
|
|
# every two minutes with "no Pending workload pods for volume ... to be
|
|
# mounted" (291 events in 10 hours, in both releases with SMB on; the
|
|
# release without the CronJob produced none).
|
|
#
|
|
# It is a switch and not a free-form affinity block on purpose: the default
|
|
# has to name the StatefulSet pod's label, that label contains the RELEASE
|
|
# NAME, and a values.yaml is never templated - so the useful default cannot
|
|
# be expressed here at all (the same property that forces
|
|
# valkey.secrets.authSecretRef to be typed out twice, further down).
|
|
#
|
|
# WHAT IT COSTS, AND IT IS A REAL COST: the affinity is required, so with
|
|
# the StatefulSet pod absent the mover cannot be scheduled. The two ways
|
|
# that happens are handled rather than left open - scaleDown: true suspends
|
|
# this CronJob outright, and activeDeadlineSeconds below fails a job that
|
|
# cannot be placed instead of letting Pending pods pile up. What stays: with
|
|
# paperless.replicas: 0 and SMB on, the mover does not run and reports one
|
|
# failed job per schedule. Set this to false to get the old, unpinned
|
|
# behaviour back - including the mount errors.
|
|
pinToServerNode: true
|
|
# How late a missed run may still start. Also keeps the CronJob controller
|
|
# from reaching "too many missed start times (> 100)", after which it stops
|
|
# scheduling this job entirely and says so only in its own log - reachable
|
|
# by leaving scaleDown: true in place for months. The cost of a window: a
|
|
# run that could not start inside it is skipped, not queued. Must stay well
|
|
# above 10s, below which the controller may never schedule at all.
|
|
startingDeadlineSeconds: 600
|
|
# Hard stop for one mover run, counted from the job's start - so it also
|
|
# bounds a pod that never gets scheduled (see pinToServerNode). Exceeding it
|
|
# shows up as a job with reason DeadlineExceeded, which is the point: a
|
|
# loud, self-clearing failure instead of a Pending pod that nobody sees.
|
|
# Raise it if a run legitimately moves more than an hour's worth of files;
|
|
# the mover copies across filesystems, so large scans are not instant.
|
|
activeDeadlineSeconds: 3600
|
|
# Extra environment variables for the mover container, same form as
|
|
# paperless.env above. Deliberately empty: the container is a busybox
|
|
# running one mv, so this exists for the odd proxy or locale variable and
|
|
# for the convention that every workload container offers the block - not
|
|
# because the mover needs configuration. Note that the cron SCHEDULE's time
|
|
# zone is the controller's and cannot be changed with a TZ entry here.
|
|
env: []
|
|
# The consume mover only shuffles files between the SMB share (mounted
|
|
# with 0777 modes) and the consume PVC, so it runs as the same non-root
|
|
# uid/gid as paperless itself with a fully hardened context.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: Always
|
|
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".
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 10m
|
|
memory: 16Mi
|
|
ephemeralStorage: 10Mi
|
|
oidc:
|
|
enabled: false
|
|
autoLoginWithProvider: false
|
|
disableRegularLogin: false
|
|
provider:
|
|
id: null
|
|
name: null
|
|
clientId: null
|
|
discoveryUrl: null
|
|
# Optional: explicit token endpoint auth method. "none" is the only accepted
|
|
# value, and the schema enforces that (Gitea #11).
|
|
#
|
|
# WHY IT IS RESTRICTED, since the previous comment here named
|
|
# client_secret_basic as the example and that was actively misleading: this
|
|
# chart registers a PUBLIC client and configures NO client secret - it
|
|
# renders OAUTH_PKCE_ENABLED and nothing else, and secrets.oidcSecretRef was
|
|
# removed in 13.0.0 because no secret can reach paperless through a json
|
|
# environment variable anyway. A method that authenticates WITH a client
|
|
# secret therefore cannot work here; asking for one produced a login that
|
|
# died at the callback with invalid_client, while the two values read
|
|
# together looked like a working secret setup.
|
|
#
|
|
# So: null omits the key and lets allauth decide, "none" states the public
|
|
# client explicitly - which is what some providers need spelled out since
|
|
# the allauth update in paperless-ngx 3.0, and the symptom when they do is
|
|
# that same invalid_client at the callback. If a future paperless release
|
|
# supports an auth method that needs no secret, widening the enum is a minor
|
|
# bump; allowing one that needs a secret is not on the table.
|
|
tokenAuthMethod: null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The bundled Valkey - this repo's own valkey chart as an optional subchart
|
|
# ---------------------------------------------------------------------------
|
|
# The switch for the dependency declared in Chart.yaml. Off by default: an
|
|
# installation that already has a Redis points redis-connection-url at it and
|
|
# this renders nothing.
|
|
#
|
|
# WHY THE KEY IS HERE AND NOT "valkey.enabled": everything under "valkey:" is
|
|
# handed to the subchart as its values and validated against ITS strict schema,
|
|
# which has no "enabled" property - so "valkey.enabled" is rejected BY THE
|
|
# SUBCHART. And because Helm skips a disabled subchart's schema validation, the
|
|
# error would only ever have appeared for whoever first switched it on.
|
|
# Chart.yaml carries the exact error text.
|
|
#
|
|
# DO NOT ERASE THIS BLOCK. Unlike every other block in this file (issue #99),
|
|
# "valkeySubchart: null" is not "the default applies" - a null override DELETES
|
|
# the key, the dependency's condition path then does not resolve, and Helm
|
|
# treats an unresolvable condition as ENABLED. So erasing the switch turns the
|
|
# subchart ON, with no password set and only the subchart's own error to show
|
|
# for it. Measured for Gitea #78; values.schema.json rejects it before anything
|
|
# renders. An empty block ("valkeySubchart: {}") is fine, because Helm merges
|
|
# that with the default below.
|
|
valkeySubchart:
|
|
enabled: false
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Values passed THROUGH to the valkey subchart
|
|
# ---------------------------------------------------------------------------
|
|
# Read as the SUBCHART's own values, so the key names are its schema, not this
|
|
# chart's. `valkey/values.yaml` in this repo is the reference. Nothing here has
|
|
# any effect while valkeySubchart.enabled is false.
|
|
#
|
|
# What still applies in full: the subchart is invisible to everything
|
|
# downstream that watches Helm release versions (Chart.yaml has the detail and
|
|
# the PR-ordering warning - never change valkey/ and this chart in the same
|
|
# PR), and this chart's scaleDown flag does NOT reach it. scaleDown: true
|
|
# scales paperless to zero and leaves the Valkey running; that is the right way
|
|
# round for a queue, because the backlog stays where it is.
|
|
valkey:
|
|
secrets:
|
|
# REQUIRED once the subchart is on, and it has to be set BY HAND to the
|
|
# SAME op:// path as secrets.redisSecretRef above - because the
|
|
# "requirepass" line lives in that same item (see the three traps below).
|
|
#
|
|
# IT CANNOT DEFAULT TO IT, and that is a property of Helm, not an omission:
|
|
# a values.yaml is never templated and a parent chart cannot compute a
|
|
# subchart's value - values are merged statically before any template runs.
|
|
# Writing "{{ .Values.secrets.redisSecretRef }}" here does not resolve, it
|
|
# lands verbatim in the subchart's itemPath and fails as a YAML parse error
|
|
# pointing at the subchart's template, which names neither this value nor
|
|
# the real cause. Measured; Gitea #78 has the output. So the path is typed
|
|
# twice, and forgetting it is caught by a conditional rule in
|
|
# values.schema.json - NOT by a fail() in a template, which cannot work
|
|
# here: Helm renders a subchart's templates BEFORE the parent's, so the
|
|
# subchart's own required() always wins that race and reports
|
|
# "secrets.authSecretRef" instead of this value's real path. The schema runs
|
|
# earlier than any rendering, which is why the rule lives there.
|
|
#
|
|
# MIND THE FIELD, it is not a password. The item must carry a field
|
|
# "auth-conf" whose value is the COMPLETE configuration line
|
|
# requirepass <password>
|
|
# The valkey chart mounts that field as a file and `include`s it, so the
|
|
# password never reaches the process list, an environment variable, or
|
|
# Helm's field of view. An init container refuses to start the pod unless
|
|
# the line is really there, because the alternative failure is an open
|
|
# server that looks perfectly healthy. A bare password does not work.
|
|
#
|
|
# OVERRIDING IT WITH A DIFFERENT ITEM IS ALLOWED and is not a mistake: it
|
|
# is the way to feed the bundled Valkey from its own item. The cost is that
|
|
# two items then have to be kept in step instead of one.
|
|
authSecretRef: null
|
|
|
|
# QUEUE, not cache - a decision, with the conflict it does not resolve.
|
|
#
|
|
# The valkey chart treats persistence and eviction as a PAIR and warns
|
|
# against mixing them: QUEUE is persistence.enabled: true + "noeviction"
|
|
# (its default), CACHE is persistence.enabled: false + "allkeys-lru".
|
|
# stalwart picked CACHE. paperless-ngx is the other case - which happens to
|
|
# be the subchart's own default, so the block below changes nothing. It is
|
|
# written out anyway, for two reasons: the subchart's persistence block is
|
|
# deliberately NOT null-safe (an erased one renders an invalid PVC), and the
|
|
# decision belongs where it is argued rather than one chart away. The
|
|
# eviction policy is NOT restated - valkey.maxmemoryPolicy stays at the
|
|
# subchart's "noeviction", which is the queue half of the pair. storageClass
|
|
# stays at the subchart default too (longhorn-encrypted-ns): queue payloads
|
|
# are the same documents that are in the database, so encrypted at rest.
|
|
#
|
|
# Read off the paperless-ngx 3.2.1 source (src/paperless/settings/__init__.py
|
|
# and settings/custom.py), not assumed: PAPERLESS_REDIS is ONE url, and
|
|
# parse_redis_url returns it twice for a TCP url, so one instance carries
|
|
# five consumers:
|
|
#
|
|
# 1. CELERY_BROKER_URL - the queue: consume, OCR, index
|
|
# updates, classifier training, the
|
|
# scheduled tasks
|
|
# 2. CELERY_RESULT_BACKEND - also Redis in 3.x (it was the
|
|
# database before), CELERY_RESULT_EXPIRES
|
|
# 3600
|
|
# 3. CHANNEL_LAYERS - channels_redis.pubsub, expiry 15s;
|
|
# pub/sub, not stored keys
|
|
# 4. CACHES["default"] - Django cache, recomputable
|
|
# 5. CACHES["read-cache"] - Cachalot DB read cache, TTL 3600,
|
|
# off unless
|
|
# PAPERLESS_DB_READ_CACHE_ENABLED
|
|
#
|
|
# WHY QUEUE: "allkeys-lru" would not be a trade here, it would be actively
|
|
# wrong. LRU evicts across the WHOLE keyspace with no regard for key type,
|
|
# and a backlog of waiting consume tasks is exactly what LRU reads as cold -
|
|
# written once, read later. So a cache configuration would preferentially
|
|
# throw away the work and keep the cache entries. A dropped consume or index
|
|
# task means a document that is silently never processed: the file stays in
|
|
# the consume directory although the directory watcher has already fired.
|
|
# And an evicted Celery result means a task that shows as "started" in the UI
|
|
# forever.
|
|
#
|
|
# THE CONFLICT THAT STAYS, stated rather than smoothed over: two of the five
|
|
# consumers are caches and they get the policy that does not suit them. When
|
|
# the instance fills, "noeviction" makes Valkey REFUSE WRITES - which hits
|
|
# cache writes and NEW TASKS alike, so paperless starts erroring instead of
|
|
# quietly degrading. That is the loud direction and therefore the right one,
|
|
# but it is a price and not a side effect, and the answer to it is to SIZE
|
|
# MEMORY for the working set, never to switch the policy. What makes the
|
|
# price small: all three cache-like consumers are TTL-bounded (15s, 3600s,
|
|
# 3600s), so they bound themselves and do not need eviction to stay in check,
|
|
# and the read cache is off by default.
|
|
persistence:
|
|
enabled: true
|
|
appendonly: true
|
|
# Sized for a task backlog and its results, not for documents - the
|
|
# documents are on paperless' own PVC. Grow it if a release regularly
|
|
# queues thousands of tasks (a PVC can grow, it can never shrink).
|
|
size: 1Gi
|
|
|
|
# networkPolicy and the Valkey resources are left at the subchart's defaults
|
|
# on purpose: its NetworkPolicy is on and allows the release's own namespace,
|
|
# which with a subchart is where paperless runs. allowedNamespaces only needs
|
|
# an entry when the two are in DIFFERENT namespaces, which a subchart rules
|
|
# out.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# THE THREE TRAPS OF THE BUNDLED VALKEY - all three are operator work
|
|
# ---------------------------------------------------------------------------
|
|
# 1. THE PASSWORD IS IN THE ITEM TWICE: inside redis-connection-url and again
|
|
# in the "requirepass" line of auth-conf. A rotation has to touch BOTH
|
|
# fields; touching one leaves a paperless that cannot authenticate against
|
|
# its own bundled Valkey. The alternative - assembling the url from parts -
|
|
# would break the secret contract of every running release (a major) and
|
|
# route the password through a template. Deliberately not chosen; the trap
|
|
# is named instead.
|
|
#
|
|
# 2. WITH THE SUBCHART ON, redis-connection-url MUST POINT AT THE SUBCHART:
|
|
# redis://:<password>@<release>-valkey-service:6379
|
|
# The name is determined and confirmed against the rendered output, but the
|
|
# url lives in the vault, so this is a MANDATORY STEP of the installation
|
|
# and not a hint. Left pointing at an external Redis, the bundled Valkey
|
|
# runs along unused and nothing says so. NOTES.txt prints the exact name for
|
|
# the release at hand.
|
|
#
|
|
# 3. A OnePasswordItem MATERIALIZES EVERY FIELD OF THE ITEM. With both CRs
|
|
# pointing at the same item, the Valkey Secret therefore also carries
|
|
# redis-connection-url and key-prefix, and this chart's redis Secret also
|
|
# carries auth-conf. Untidy, harmless, and written down here so nobody reads
|
|
# it as a leak or a bug and "tidies it up": the keys are unused, and no
|
|
# container mounts a field it does not reference.
|
|
#
|
|
# And one rule that is about this repository rather than about an installation:
|
|
# changes to valkey/ must not share a pull request with changes to this chart.
|
|
# Chart.yaml says why; README and the wiki's valkey page carry the long form.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 8000 only, and outbound DNS plus TCP 5432
|
|
# (Postgres), 6379 (Redis/Valkey), 443, and - only while
|
|
# paperless.files.tikaGotenberg.enabled is true - 9998, 3000 and 80 for Tika
|
|
# and Gotenberg.
|
|
#
|
|
# WHY THAT RULE NAMES 9998/3000 AND NOT JUST THE SERVICES' 80 (Gitea #136, an
|
|
# outage on 2026-10-15). The note that used to stand here said:
|
|
#
|
|
# "an egress rule names the port on the DESTINATION as the client dials it,
|
|
# and paperless dials those charts' SERVICES, which listen on 80 and
|
|
# forward to the containers' own ports."
|
|
#
|
|
# That is wrong, and it is kept as a quotation because it sounds right - which
|
|
# is why it was never checked. kube-proxy does the ClusterIP Service's
|
|
# destination NAT BEFORE the egress policy is evaluated at the pod's network
|
|
# interface, so the policy sees podIP:targetPort, never clusterIP:port. An
|
|
# egress rule therefore names the POD port: 9998 for apache-tika, 3000 for
|
|
# gotenberg, both read from those charts' containerPorts. The 80 stays because
|
|
# it costs nothing and covers a foreign Tika that really does listen on 80.
|
|
#
|
|
# AND THE FAILURE MODE IS A TIMEOUT, NOT "connection refused": a missing
|
|
# service refuses, a dropped packet runs into the timeout. 5432, 6379 and 443
|
|
# were unaffected only because their Service port and pod port are the same
|
|
# number.
|
|
#
|
|
# THREE THINGS IT DOES NOT COVER:
|
|
# - The VALKEY SUBCHART'S POD. It carries its own labels, so this policy
|
|
# does not select it - and it does not need to: the valkey chart ships a
|
|
# NetworkPolicy of its own, and a tighter one.
|
|
# - The CONSUME CRONJOB'S PODS. Their pod template carries NO LABELS, so no
|
|
# podSelector can match them and nothing here restricts them. Tolerable
|
|
# only because the job moves files between mounted volumes and the SMB
|
|
# share arrives as a CSI volume mounted by the NODE - the pod opens no
|
|
# connection of its own. If that changes, the CronJob needs labels first.
|
|
# - MAIL FETCHING, unless egress.mailImport is switched on. Paperless' mail
|
|
# accounts are rows in its database, configured in the web UI, so this
|
|
# chart cannot see them and cannot derive the rule. Left off on a release
|
|
# that does fetch mail, a mail rule silently stops importing.
|
|
#
|
|
# And the usual two: 5432 and 6379 are the STANDARD ports rather than the
|
|
# configured ones, 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 8000, 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
|
|
egress:
|
|
# Outbound IMAPS (993) for paperless' mail import. OFF, and it has to be a
|
|
# value rather than something the chart derives: the mail accounts are ROWS
|
|
# IN THE DATABASE, entered in the web UI, so nothing in these values says
|
|
# whether a release fetches mail. Switching this on is the operator saying
|
|
# that it does.
|
|
#
|
|
# 993 ONLY, deliberately. Plain IMAP (143) and POP3 (110, 995) are not
|
|
# covered - a value named mailImport that opened four ports would be wider
|
|
# than its name - and belong in extraPorts below.
|
|
#
|
|
# NOTE THE DIFFERENCE FROM THE TIKA RULE: 993 is the real destination port,
|
|
# because a mail server outside the cluster is reached with no Service and
|
|
# so no destination NAT in the path. What decides whether an egress rule
|
|
# needs the Service port or the pod port is that NAT, and nothing else.
|
|
mailImport: false
|
|
# 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.
|
|
#
|
|
# PLAIN IMAP (143) and POP3 (110, 995) go here - IMAPS 993 has its own
|
|
# switch above. Also a Postgres or Redis on a non-standard port, and a Tika
|
|
# or Gotenberg that listens on neither its chart's container port nor 80.
|
|
#
|
|
# These are POD ports for anything inside the cluster, for the reason given
|
|
# at the top of this block: a ClusterIP Service's port is translated away
|
|
# before the policy sees the packet. For a destination outside the cluster
|
|
# there is no translation and the port is simply the one being dialled.
|
|
extraPorts: []
|