From a9b991ba55815b84038fe2f5506560383990c03b Mon Sep 17 00:00:00 2001 From: Krstan Vjestica Date: Thu, 23 Jul 2026 14:32:22 +0200 Subject: [PATCH 1/3] fix: allow the forail-web Service DNS names as Host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clients inside the cluster reach the API through the forail-web Service, so their Host header is the Service name, not the ingress host. The hardened allowedHosts list did not cover it, so Django answered 400 and the documented forail-operator install (--set forail.url=http://forail-web..svc.cluster.local:8013) could not resolve a single object. Append the four Service DNS forms to whatever allowedHosts is set to. They are cluster-internal and derived from the release, so this keeps the list tight — an unknown Host is still rejected with 400 — while making in-cluster access work without every caller having to override its Host header. Verified in the dev cluster: in-cluster calls return 200 with no hostHeader override, a bogus Host still gets 400 at the Django layer, forail-web stays at 0 restarts, and the full Cypress suite passes 101/101. --- CHANGELOG.md | 8 ++++++++ templates/_helpers.tpl | 12 +++++++++++- values.yaml | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfecfa9..017e437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,14 @@ and the chart uses SemVer (`version`) plus the upstream Forail CalVer than replacing the loopback entries. - **Chart render checks in CI** pass a throwaway `secrets.forailAdminPassword`, so the now-required password does not fail `helm lint` / `helm template`. +- **In-cluster callers are no longer rejected by `forail.allowedHosts`.** Clients + that reach the API through the `forail-web` Service send the Service DNS name + as their `Host`, which the hardened list did not cover, so Django answered + `400` — the documented forail-operator install + (`--set forail.url=http://forail-web..svc.cluster.local:8013`) could not + resolve a single object. The chart now appends `forail-web`, + `forail-web.`, `forail-web..svc` and `forail-web..svc.cluster.local` + to whatever `forail.allowedHosts` is set to. Unknown hosts are still rejected. ### Security - **No working secret defaults**: `postgresPassword`, `forailSecretKey` and diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 3a7a798..51c24c8 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -76,8 +76,18 @@ Wraps DB, Redis, secrets, OTel, admin into one block to avoid drift. secretKeyRef: name: forail-secrets key: forailAdminPassword +{{- $ns := include "forail.namespace" . }} - name: FORAIL_ALLOWED_HOSTS - value: {{ .Values.forail.allowedHosts | quote }} + {{- /* + In-cluster consumers reach the API through the forail-web Service, so their + Host header is the Service DNS name, never the ingress host. Django answers + 400 ("The request could not be understood by the server.") for any Host not + listed, which broke the documented forail-operator install + (--set forail.url=http://forail-web..svc.cluster.local:8013). These four + names are cluster-internal and derived from the release, so appending them + keeps needtofix M9 tight while making in-cluster access work by default. + */}} + value: {{ printf "%s,forail-web,forail-web.%s,forail-web.%s.svc,forail-web.%s.svc.cluster.local" .Values.forail.allowedHosts $ns $ns $ns | quote }} - name: FORAIL_CSRF_TRUSTED_ORIGINS value: {{ .Values.forail.csrfTrustedOrigins | quote }} - name: FORAIL_COOKIE_SECURE diff --git a/values.yaml b/values.yaml index 1e881e5..33810fb 100644 --- a/values.yaml +++ b/values.yaml @@ -73,6 +73,12 @@ forail: # here; 127.0.0.1 and localhost MUST stay in the list because the in-cluster # liveness/readiness probes reach the app over the loopback address (Django # rejects any Host not listed with HTTP 400, which would crash-loop the pod). + # The chart also appends the forail-web Service DNS names (forail-web, + # forail-web., forail-web..svc, forail-web..svc.cluster.local) to + # whatever you set here, so in-cluster clients that call the Service directly + # — forail-operator with + # --set forail.url=http://forail-web..svc.cluster.local:8013 — are not + # rejected. Do not list them here yourself. allowedHosts: "forail.lan,127.0.0.1,localhost" # NOTE: avoid the .local TLD — Avahi/mDNS (`mdns_minimal [NOTFOUND=return]` # in nsswitch.conf on most desktop Linux distros) intercepts every @@ -242,3 +248,6 @@ ingress: # 1) kubectl -n forail exec deploy/forail-web -- forail-manage create_oauth2_token --user admin # 2) helm install forail-operator -n forail \ # --set forail.token= --set forail.url=http://forail-web.forail.svc.cluster.local:8013 +# That Service DNS name is covered by forail.allowedHosts automatically (see +# above). If you point the operator at the ingress host instead, or front it +# with another name, pass --set forail.hostHeader=. From e6a6e73f6430bca959381b4420897d62287f03af Mon Sep 17 00:00:00 2001 From: Krstan Vjestica Date: Sat, 25 Jul 2026 22:57:09 +0200 Subject: [PATCH 2/3] helm: say so when an install cannot run jobs task.privileged=false is the right default to publish -- a privileged pod with a host cgroup mount is a trivial container escape to node-root. But Forail runs project updates and automation jobs through podman inside the task pod, and podman cannot mount its overlay storage without those privileges, so with the shipped defaults every job dies moments after launch with [graphdriver] prior storage driver overlay failed: mount /var/lib/containers/storage/overlay: permission denied Nothing surfaces that. The UI shows a project or job sitting in "Pending", the pods are all healthy, and the reason is buried in the task container's log. The install looks successful and simply cannot do the one thing it exists for. Add NOTES.txt so helm prints the warning, the exact error to expect and the two flags that enable execution, right after install -- and stays quiet once they are set. Verified both ways with helm install --dry-run. --- templates/NOTES.txt | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 templates/NOTES.txt diff --git a/templates/NOTES.txt b/templates/NOTES.txt new file mode 100644 index 0000000..d27aaa6 --- /dev/null +++ b/templates/NOTES.txt @@ -0,0 +1,39 @@ +Forail {{ .Chart.AppVersion }} installed into namespace {{ .Release.Namespace }}. + +{{- if .Values.ingress.enabled }} + + URL: http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.ingress.host }} + Map {{ .Values.ingress.host }} to any node running the ingress controller. +{{- end }} + Admin: admin + +Check the rollout with: + + kubectl -n {{ .Release.Namespace }} get pods + +{{- if not .Values.task.privileged }} + +──────────────────────────────────────────────────────────────────────── + WARNING — job execution is NOT enabled in this install. +──────────────────────────────────────────────────────────────────────── + +forail-task runs project updates and automation jobs through podman inside +the task pod, and podman cannot mount its overlay storage in a +non-privileged container. With task.privileged=false every job fails a +moment after launch with + + [graphdriver] prior storage driver overlay failed: + mount /var/lib/containers/storage/overlay: permission denied + +and the only thing visible in the UI is a project or job stuck in "Pending". + +This is the safe default on purpose: a privileged pod with a host cgroup +mount is a trivial container escape to node-root. Enable it only when you +have somewhere safe to run it -- ideally dedicated, tainted execution +nodes: + + helm upgrade {{ .Release.Name }} ... \ + --set task.privileged=true \ + --set task.hostCgroup=true + +{{- end }} From 63aa85f13ea98cdeea4685d9be2e29892a0d11fc Mon Sep 17 00:00:00 2001 From: Krstan Vjestica Date: Sat, 25 Jul 2026 23:05:29 +0200 Subject: [PATCH 3/3] helm: pin the assistant image to the last build that exists forail-assistant is not part of the 2026.07.0 release. It ships disabled and was not built or tested for it, but values.yaml still pointed at forail-assistant:2026.07.0 -- a tag nobody is going to push. Nothing pulls it while assistant.enabled=false, so the trap only springs on whoever turns the assistant on, and it springs as ImagePullBackOff with no hint that the tag was never published. Point it at 2026.06.0, which exists, and say in the values file that it moves in lockstep with the next assistant release. --- values.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/values.yaml b/values.yaml index 33810fb..b6cb167 100644 --- a/values.yaml +++ b/values.yaml @@ -21,7 +21,12 @@ images: pullPolicy: IfNotPresent assistant: repository: ghcr.io/forail-platform/forail-assistant - tag: "2026.07.0" + # forail-assistant is NOT part of the 2026.07.0 release -- it ships + # disabled (assistant.enabled=false below) and was not built or tested for + # it, so this stays on the last published build. Pointing it at a tag that + # was never pushed would give ImagePullBackOff the moment anyone enabled + # it. Bump this in lockstep the next time the assistant is released. + tag: "2026.06.0" pullPolicy: IfNotPresent postgres: repository: postgres