Forail 2026.07.0 — Release Notes
Release date: 2026-07-25 Based on: Forail 2026.06.0 License: Apache License 2.0
Overview
2026.07.0 is a security-hardening and migration release. It tightens several authentication and audit defaults (some of which are breaking for existing SAML deployments), makes tenant isolation fail closed, replaces the insecure defaults in the Helm chart and Compose stack (breaking — installs now require an explicit admin password), and introduces a one-shot AWX → Forail importer so teams can migrate off AWX/AAP without rebuilding their configuration by hand.
Kubernetes installs also gain the pod RBAC and receptor worktype that in-cluster job execution needs — see Fixed.
There are no data-model changes. Two idempotent migrations (0209, 0210) ship
with the tenancy work; both only drop and re-create PostgreSQL row-level-security
policies, so they apply to an existing database without touching table schemas or
rows.
⚠️ Known issue — upgrading breaks job execution
Fixed in 2026.07.1. Upgrade to it instead of applying the workaround below — see the 2026.07.1 release notes. The rest of this section describes what happens if you stay on 2026.07.0.
If you upgrade an existing 2026.06.0 install, jobs stop running: they are
accepted and then stay in pending indefinitely. The only hint is the job's
job_explanation, "This job is not ready to start because there is not enough
available capacity" — accurate, but it does not point at the cause. Fresh
installs are unaffected. The workaround below is verified on a live cluster.
The default instance group has to satisfy two conditions at once for a job to
run locally, and an upgrade breaks both:
- It must contain an instance. The chart's init Job calls
register_queue --queuename=default, which on an upgrade finds the group already there, printsInstance Group already registered defaultand assigns nothing. - That instance must be able to execute. The task pod re-registers itself
as
node_type=controlon every start, and a control node only orchestrates.
Either one alone is enough to hang every launch — both were measured individually, holding the other fixed.
Project updates keep working, because they run in controlplane, which does
have the instance. The install therefore looks healthy right up until someone
launches a job.
Workaround, after helm upgrade completes:
kubectl -n forail exec deploy/forail-web -- forail-manage shell -c "
from forail.main.models import Instance, InstanceGroup
i = Instance.objects.get(hostname='forail-node')
i.node_type='hybrid'; i.save(update_fields=['node_type'])
InstanceGroup.objects.get(name='default').instances.add(i)"
Substitute your own instance hostname if you did not install with the chart
defaults. Any job already sitting in pending starts on its own within about a
minute; a fresh short job should return a normal PLAY RECAP in a few seconds.
The workaround does not survive a restart of
forail-task. That pod re-runsprovision_instanceevery time it starts — after a node reboot, an eviction, or the nexthelm upgrade— and that call resets bothnode_typeand the group's execution mode. Re-apply it, and re-check job execution, after any task-pod restart until the fix ships.
Also re-apply your role assignments after upgrading. Any role assignment
attempted on 2026.06.0 failed silently (the ScanFinding /
TenantIsolationEvent FieldDoesNotExist bug fixed in this release, see
Fixed). The upgrade fixes the cause but does not recreate the assignments that
were lost, and Fixed saying "no data migration is required" refers to the
schema only. Check the members of every role you rely on and re-grant what is
missing.
What the upgrade does do correctly: it succeeds, migrations 0209 and 0210
apply cleanly, and no data is lost — object counts and names are identical
before and after.
Security advisories
Upgrade from 2026.06.0 or earlier is strongly recommended. Several of the fixes below are exploitable on a default install of an earlier release; the detail is in Security hardening and Breaking changes further down.
Severity is this project's own assessment of impact on a default deployment. It is not CVSS, and no CVE identifiers have been requested. "Affected" means every release up to and including 2026.06.0.
| Issue | Severity | Who is exposed | What to do |
|---|---|---|---|
| Deployment artifacts shipped working default credentials | Critical | Any install that did not override the shipped secrets | Upgrade; rotate forailAdminPassword, postgresPassword, forailSecretKey and the websocket secret |
| SSO account takeover — accounts associated by email address, not provider UID | Critical | Installs using SAML/OIDC/social auth where an IdP can assert an arbitrary email | Upgrade; audit existing SSO-linked accounts for unexpected associations |
| Tenant isolation gate could essentially never fire; RLS failures degraded to global row visibility | High | Multi-tenant installs | Upgrade; review TenantIsolationEvent records and cross-tenant access in the audit trail |
forail-task ran privileged with a host cgroup mount by default — a container escape to node-root |
High | Kubernetes and Compose installs using shipped defaults | Upgrade; job execution now needs an explicit opt-in, ideally on dedicated tainted nodes |
ALLOWED_HOSTS defaulted to * and session cookies were not Secure |
Medium | Internet-reachable installs | Upgrade; set your real ingress host(s) and terminate TLS |
Audit records stored the raw session key; X-Forwarded-For was trusted unconditionally for the audit source IP |
Medium | Any install; higher where audit logs are broadly readable | Upgrade; configure PROXY_IP_ALLOWED_LIST; treat historical audit rows as sensitive |
IaC scanner could be pointed outside the project checkout via a job template's playbook field |
Medium | Installs where non-admins can edit job templates | Upgrade |
Operator held cluster-wide get/list/watch on every Secret |
Medium | Kubernetes installs running the operator | Upgrade the operator to 2026.07.1; its Secret access is now a namespaced Role |
OAuth refresh_token was recorded in activity-stream entries; superuser grant/revoke was not separately audited |
Low | Any install | Upgrade; consider rotating OAuth tokens that appear in historical activity entries |
Upgrading does not retroactively clean data written by an earlier release — rotate the credentials above, and treat pre-upgrade audit and activity rows as potentially containing secrets.
Added
AWX → Forail migration importer
A new backend management command migrates configuration from an existing AWX (or AAP) installation via its REST API:
forail-manage import_from_awx \
--url https://awx.example.com \
--token "$AWX_TOKEN" \
--dry-run # preview; remove to apply
- Imports Organizations, Users, Teams, Credential Types, Credentials, Projects, Inventories (with group hierarchy and host membership), Inventory Sources, Job Templates, Workflow Job Templates (with their node DAG), Schedules, Notification Templates, and RBAC role assignments, in dependency order.
- Idempotent — re-running matches existing objects by natural key (name within organization; username for users) and updates rather than duplicating.
--dry-runpreviews all changes inside a rolled-back transaction.--resource <type>(repeatable) limits the run to specific resource types.- Auth via
--token(preferred) or--username/--password;--insecureskips source TLS verification.
Secrets are not migrated. The AWX API never returns secret credential inputs
(it sends $encrypted$), and user passwords are not exported. The importer
brings over credential structure and non-secret inputs, creates users with an
unusable password, and reports exactly how many secret fields need manual
re-entry afterwards. Notification-template secrets are stripped the same way.
RBAC role assignments are migrated where the target framework allows it: user grants and team→object-role grants are applied; organization-member grants to teams (rejected by the access framework) are skipped with a warning rather than aborting the run.
Security hardening
- Superuser grant/revoke is now written to the dedicated audit log (independently of the activity stream).
- Audit records store a SHA-256 hash of the session key, never the raw key.
X-Forwarded-Foris trusted for the audit source IP only behind a configured trusted proxy (PROXY_IP_ALLOWED_LIST).- OAuth
refresh_tokenis redacted from activity-stream entries. - Tenant concurrency-quota errors are logged instead of silently swallowed.
Tenant isolation now fails closed
- The RLS middleware aborts the request (HTTP 500) if it cannot install the tenant scope, instead of continuing with global row visibility.
- The strict-isolation gate resolves the target organization with the caller's
RLS scope removed — previously the lookup ran inside the caller's scope, so a
cross-tenant object was invisible and the gate could essentially never fire. It
now denies by default when a covered resource's organization cannot be
determined, and records a
TenantIsolationEvent. - RLS coverage extended to
main_eventlog, and every policy now casts the tenant GUC viaNULLIF(current_setting(...), '')::intso the empty "no scope" sentinel cannot raise (migrations0209,0210). - The tenancy rate limiter logs Redis outages loudly and honours
TENANCY_RATE_LIMIT_FAIL_CLOSED(default open, for availability).
Trust boundaries around import and SSO
import_from_awxno longer carries privilege across the boundary implicitly: superuser / system-role promotion requires--grant-superusers, custom credential-type injectors are dropped for admin re-approval unless--trust-injectorsis passed, and secrets are read fromAWX_TOKEN/AWX_PASSWORDin preference to argv.- SSO account takeover fixed:
associate_by_emailwas removed from the auth pipeline — accounts associate by provider UID, never by matching email. - Tenant provisioning refuses to silently reuse an existing username (which
discarded the supplied password and cross-linked accounts) unless
attach_existing_adminis set. - The IaC scanner can no longer be pointed outside the project checkout via a job
template's
playbookfield (absolute paths /..).
Deployment defaults — Helm chart and Compose
Both deployment artifacts shipped working credentials and a privileged worker by default. That is over:
- Helm:
postgresPassword,forailSecretKeyandforailBroadcastWebsocketSecretare auto-generated on first install and reused across upgrades;forailAdminPasswordis required.forail-taskruns non-privileged with no host cgroup mount unless you opt in. Session cookies areSecure,allowedHostsis the ingress host plus loopback (not"*"), and an opt-inNetworkPolicyplus per-workloadsecurityContextknobs are available. - Compose:
FORAIL_TASK_PRIVILEGED/FORAIL_TASK_CGROUPdefault off,FORAIL_ALLOWED_HOSTSdefaults tolocalhost,127.0.0.1instead of*, andFORAIL_TAGpins to2026.07.0rather than:latest. - Operator: the manager no longer holds cluster-wide
get/list/watchon every Secret — the credential reconciler's access is a namespacedRole/RoleBindingin the operator's own namespace.
Use operator
2026.07.1. In2026.07.0that narrowing leftCredentialCRs working only in the operator's own namespace, because a Credential resolvesspec.inputsFromin its namespace and the Secret cache covered only one.2026.07.1addssecretNamespacesto the operator chart, which renders both the per-namespace SecretRoleand the matching cache scope:
sh helm install forail-operator ... --set 'secretNamespaces={team-a,team-b}'Still no cluster-wide secrets grant. Everything else in this release is unchanged at
2026.07.0. - Assistant: a wildcard CORS origin no longer combines with credentials, and/api/v1/chataccepts an optional shared bearer token (FORAIL_ASSISTANT_CHAT_TOKEN) with a concurrency cap (FORAIL_ASSISTANT_CHAT_MAX_CONCURRENCY, 429 on overload).
See Breaking changes — deployment defaults below for the upgrade actions.
⚠️ Breaking changes — SAML
Two SAML defaults changed. They affect installs that rely on the previous, weaker behavior.
1. Signed assertions + SHA-256 now required by default
SOCIAL_AUTH_SAML_SECURITY_CONFIG now defaults to:
{
"requestedAuthnContext": false,
"wantMessagesSigned": true,
"wantAssertionsSigned": true,
"rejectUnsolicitedResponsesWithInResponseTo": true,
"signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256"
}
Impact: If your IdP sends unsigned responses/assertions, or signs with SHA-1, logins will be rejected after upgrade.
Action:
- Preferred: reconfigure your IdP to sign responses and assertions with SHA-256.
- Temporary fallback: explicitly set SOCIAL_AUTH_SAML_SECURITY_CONFIG via
PATCH /api/v2/settings/saml/ for a legacy IdP (not recommended for
production).
⚠️ Setting this replaces the whole dict — it does not merge with the secure defaults. When the setting is unset, Forail's hardened defaults apply; the moment you set it, your value is used verbatim and any key you omit falls back to the weak python-saml/OneLogin default (unsigned assertions, SHA-1). So to relax a single key you must re-specify the full secure dict with only that key changed, e.g. to accept unsigned assertions from one legacy IdP while keeping every other protection:
json { "requestedAuthnContext": false, "wantMessagesSigned": true, "wantAssertionsSigned": false, "rejectUnsolicitedResponsesWithInResponseTo": true, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }Clearing the setting (back to null) restores the full hardened defaults.
2. SAML role-attribute grants require an explicit value
Granting is_superuser / is_system_auditor from a SAML attribute now requires
a non-empty is_superuser_value / is_system_auditor_value.
Impact: A configuration that sets only is_superuser_attr (with no value)
previously granted superuser to every user the IdP sent with that attribute.
That now fails safe (no grant) and logs a warning.
Action: Set the required attribute value(s) for the flags you intend to grant.
⚠️ Breaking changes — deployment defaults
1. helm install requires an admin password
secrets.forailAdminPassword has no default and no generated fallback; the chart
fails to render without it. The other three secrets (postgresPassword,
forailSecretKey, forailBroadcastWebsocketSecret) are generated on first
install and looked up on subsequent upgrades, so leave them empty unless you pin
them deliberately.
Action: pass --set secrets.forailAdminPassword='<strong-password>' on
install. Automation that renders the chart (CI helm lint / helm template)
needs a throwaway value for the same reason.
2. forail-task is no longer privileged by default
The podman-in-pod execution path needs a privileged container and the host cgroup namespace; both now default off, because a privileged pod with a host cgroup mount is a trivial container escape.
Action, Kubernetes: --set task.privileged=true --set task.hostCgroup=true
(ideally pinning those workers to dedicated, tainted nodes).
Action, Compose: FORAIL_TASK_PRIVILEGED=true FORAIL_TASK_CGROUP=host.
3. Allowed hosts and secure cookies
forail.allowedHosts / FORAIL_ALLOWED_HOSTS no longer default to "*", and
session cookies are Secure by default — a deployment served over plain HTTP
will not keep a session.
Action: set your real ingress host(s), and keep 127.0.0.1,localhost in the
list — the in-cluster health probes call the API on loopback. Terminate TLS in
front of the ingress, or set forail.cookieSecure: "false" for a lab install.
Fixed
- The task dispatcher crash-looped on 2026.06.0, so no job could finish.
The periodic schedule runs
update_active_jobs_gauge_taskevery 30 seconds unconditionally, but in 2026.06.0 that function carried Celery's@shared_taskinstead of Forail's own@task(). Dispatching it raisedValueError: ... is not decorated with @task(), the dispatcher exited, and whatever was running died with "Task was canceled due to receiving a shutdown signal" — typically surfacing as a failed project update and a job inerror. Measured on a fresh 2026.06.0 install: the dispatcher restarted roughly every 50 seconds, indefinitely. Anyone still on 2026.06.0 should upgrade; there is no configuration that avoids this, since the schedule entry is not conditional. Fixed by registering the task properly (@task(queue=get_task_queuename)). - In-cluster job execution. Two pieces were missing from the chart, and each
failed a launch on its own. Note this is not "out of the box": project updates
and control-plane jobs still run through podman inside the task pod, so they
also need the privileged opt-in from Breaking changes #2 above. Without it
every job dies moments after launch with
mount /var/lib/containers/storage/overlay: permission denied, and the only symptom in the UI is a project or job stuck inPending. The chart now prints a warning to that effect at install time when the flags are off. The two chart fixes were: - No pod RBAC. Jobs run as pods in a Kubernetes container group, and receptor
manages them with the task pod's ServiceAccount — which had no pod
permissions, so every launch failed with
pods is forbidden ... cannot list resource "pods"and the job hung pending. The chart now ships aforailServiceAccount plus a namespacedforail-job-runnerRole/RoleBinding(pods,pods/log|attach|exec) and aMY_POD_NAMESPACEdownward-API env so job pods land in the release namespace. - The receptor mesh config declared only the
localworktype, so launches errored at 0s withunknown work type kubernetes-incluster-auth. Thekubernetes-incluster-authworktype (authmethod: incluster) is now registered. forail-webcrash-loop after the allowed-hosts change. The liveness and readiness probes callhttp://127.0.0.1:8013/api/v2/ping/; with only the ingress host allowed, Django answered400 DisallowedHost, the probe failed and the pod restarted in a loop. The chart default keeps the loopback names.- Tenancy audit events were never persisted.
TenantQuotaEventandTenantIsolationEventinheritCreatedModifiedModel, which lacks thedescriptioncolumn that migration0205declaresNOT NULL— every insert raisedIntegrityError. Both models now declare the field (no new migration). - RBAC role assignment was broken in 2026.06.0.
ScanFindingandTenantIsolationEventwere registered with the defaultparent_field_name='organization', but neither model has anorganizationfield. The resultingFieldDoesNotExistaborted every role-assignment operation across the platform. They are now registered against their real parents (scan_resultandaccessed_organizationrespectively). Anyone on 2026.06.0 who relies on role assignment should upgrade. No data migration is required — the fix is in model registration only. pytest.inireferenced the pre-renameawx.main.tests.settings_for_test(no longer exists), which prevented the backend test suite from starting.- Tenant queue router referenced pre-rename
awx.main.tasks.*task names.
Upgrade
No schema migrations. Migrations 0209 and 0210 re-create RLS policies only
and are idempotent, so the standard image re-point applies — but the chart's new
required/secure defaults have to be supplied:
helm upgrade forail oci://ghcr.io/forail-platform/forail-helm \
--version 2026.7.0 -n forail \
--set secrets.forailAdminPassword='<strong-password>' \
--set 'forail.allowedHosts=forail.example.com\,127.0.0.1\,localhost' \
--set task.privileged=true --set task.hostCgroup=true # only if you run jobs in-pod
Escape the commas. Helm's
--setsplits unescaped commas into a list, so--set forail.allowedHosts='a,b,c'fails to parse. Either escape them as above (a\,b\,c, inside single quotes so the shell keeps the backslashes) or put the value in a values file, where no escaping is needed:
yaml forail: allowedHosts: "forail.example.com,127.0.0.1,localhost"
Before upgrading:
- Any deployment that runs jobs — read Known issue — upgrading breaks job execution at the top of these notes, and plan to apply the workaround (and re-apply role assignments) as part of the upgrade. Without it the platform comes up healthy but executes nothing.
- SAML deployments — review Breaking changes — SAML above and reconfigure the IdP if needed.
- Any deployment — review Breaking changes — deployment defaults; an upgrade that omits the admin password will not render, and one that drops the loopback hosts will fail its health probes.
- Multi-tenant deployments — tenant isolation now fails closed. A request whose tenant scope cannot be installed is rejected rather than served with global visibility; verify your tenants resolve correctly in a staging install first.