Major, as every chart in this rollout is. Inbound 3000 only; outbound DNS plus 5432, 6379 and 443. Those three egress ports are the STANDARD ones and not the configured ones, which is this chart's real limitation and is written into values.yaml, the template header and NOTES.txt rather than left to be discovered: Outline's database, Redis and OIDC provider all arrive as connection URLs inside 1Password items that the chart never parses, so it cannot know where they listen and cannot validate a guess. A dependency on another port is blocked, and because all three probes hit /_health - which checks Postgres and Redis - the symptom is a pod that never becomes Ready rather than an error anyone reads. networkPolicy.egress.extraPorts is the escape hatch. That same property is what makes this chart the right place for the enforcement experiment in the next commit. Also reverts the cyberchef fixture from the first experiment: with the probe port restricted to an address that is not the node's, the install-test went GREEN, so the claim that it catches a blocked probe path does not hold as written. The next commit establishes whether policies are enforced in CI at all. Part of #5
261 lines
13 KiB
YAML
261 lines
13 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:
|
|
configSecretRef: null
|
|
redisSecretRef: null
|
|
dbSecretRef: null
|
|
outline:
|
|
# Replica count for this workload; 0 is allowed and scales it down.
|
|
# The chart-global scaleDown flag takes precedence.
|
|
replicas: 1
|
|
maxUploadSize: '262144000'
|
|
forceHttps: 'false'
|
|
oidc:
|
|
displayName: OIDC
|
|
storage:
|
|
size: 10Gi
|
|
storageClass: null
|
|
# The upstream image (outlinewiki/outline) runs as its nodejs user
|
|
# (uid/gid 1001). Outline only writes to the upload storage (PVC) and to
|
|
# /tmp (import/export scratch files), which the chart mounts as an
|
|
# emptyDir, so the root filesystem stays read-only.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 1001
|
|
runAsGroup: 1001
|
|
fsGroup: 1001
|
|
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
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
# Outline exposes a dedicated health endpoint at /_health (also used
|
|
# by the upstream Docker HEALTHCHECK). It checks Postgres and Redis;
|
|
# there is no dependency-free HTTP endpoint ("/" returns 500 too when
|
|
# the database is down), so all three probes use /_health.
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /_health
|
|
port: 3000
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /_health
|
|
port: 3000
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
# The startup probe gates liveness/readiness. Outline runs database
|
|
# migrations on startup: budget 10s * 30 = 300s.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /_health
|
|
port: 3000
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 30
|
|
# 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
|
|
ephemeralStorage: 10Mi
|
|
|
|
ingress:
|
|
# Set to false to omit the Ingress entirely. clusterIssuer is then not needed
|
|
# either, because it is only read inside the Ingress template. ingress.domain
|
|
# is a different case: it is also Outline's URL variable (see below) and
|
|
# stays mandatory in every mode.
|
|
#
|
|
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
|
|
# true the render aborts, naming both values.
|
|
enabled: true
|
|
# The request-body cap, and it lives entirely in the INGRESS: the chart
|
|
# renders it into nginx.ingress.kubernetes.io/proxy-body-size and
|
|
# nginx.org/client-max-body-size, so it is enforced by the nginx-family
|
|
# controllers and by nothing inside Outline.
|
|
#
|
|
# TWO CEILINGS APPLY TO AN UPLOAD AND THE SMALLER ONE WINS: this value and
|
|
# outline.maxUploadSize above, which default to 25m and ~250 MB - a factor of
|
|
# ten apart, with the ingress limit the tighter one (Gitea #12, open). In
|
|
# GATEWAY MODE this half disappears: the Gateway API has no body-size field,
|
|
# the value is NOT ENFORCED, and the ceiling becomes whatever the Gateway
|
|
# enforces - which is neither of the two numbers this chart documents. That
|
|
# moves #12 rather than resolving it; NOTES.txt says so in that mode.
|
|
maxUploadFileSize: 25m
|
|
clusterIssuer: null
|
|
# The public hostname of this release. Read by the Ingress, by Outline's URL
|
|
# variable 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 proxy-body-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 above.
|
|
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.
|
|
#
|
|
# WHAT THIS MODE COSTS HERE: ingress.maxUploadFileSize is not enforced, so the
|
|
# upload ceiling is the Gateway's. Check it before switching - this chart is
|
|
# the one with the open upload defect (#12), and in this mode neither of the
|
|
# two numbers the chart knows is necessarily the one that bites.
|
|
#
|
|
# 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 Outline's URL
|
|
# variable with it: it keeps following ingress.domain, so every generated
|
|
# link, every share URL and the OIDC redirect URI still name the old host
|
|
# until ingress.domain is changed too.
|
|
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 - the single port this pod
|
|
# declares - and outbound DNS plus TCP 5432 (Postgres), 6379 (Redis) and 443
|
|
# (the OIDC provider's discovery document, fetched at boot).
|
|
#
|
|
# WHAT IT DOES NOT DO, and this is the one to read before upgrading: THOSE
|
|
# THREE EGRESS PORTS ARE THE STANDARD ONES, NOT THE CONFIGURED ONES. Outline's
|
|
# database, Redis and OIDC provider all arrive as connection URLs inside
|
|
# 1Password items, which this chart never parses - so it cannot know your
|
|
# ports and cannot validate them. A dependency on a non-standard port is
|
|
# BLOCKED, and because all three probes hit /_health (which checks Postgres
|
|
# and Redis), that shows up as a pod which never becomes Ready. Put the real
|
|
# port in egress.extraPorts below.
|
|
#
|
|
# The 3000 rule is NOT restricted by source in the default - 3000 is the
|
|
# probe port, see probeSources. And a policy is only a boundary where the CNI
|
|
# enforces one; unenforced, it is accepted and filters nothing.
|
|
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 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:
|
|
# 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.
|
|
#
|
|
# Outline needs one entry here more often than most charts: a Postgres or
|
|
# Redis on a non-standard port, an OIDC provider that is not on 443 (a CI
|
|
# dex on 5556), an S3 endpoint on a custom port, or SMTP - the chart sets
|
|
# no SMTP_* variable itself, but a deployment that adds them through
|
|
# outline.env needs 587 or 465 here too.
|
|
extraPorts: []
|