
End-to-end reproduction and cross-layer detection of CVE-2026-53576, the unauthenticated RCE in Kestra - taken past the base PoC to show how a common Docker-socket misconfiguration turns container-root into full host compromise.
Kestra, an open-source workflow orchestration platform, shipped an authentication filter that decided whether a request needed credentials by checking if the URL ended with /configs - a check meant to expose one harmless public endpoint.
Because the check only looked at the tail of the string, any request whose URL happened to end that way skipped authentication entirely, including endpoints that create and run arbitrary code. The result: anyone who can reach a vulnerable Kestra instance over the network can run commands as root with zero credentials, no phishing, no password guessing, no privilege escalation step required.
In this exercise, that single gap was carried end-to-end against a self-hosted lab instance:
Every stage was captured against defensive telemetry (network IDS, host runtime security, Linux audit, and system logs).
Business impact if unpatched: complete compromise of the host running Kestra, not just the application - and by extension anything else reachable from that host.
Fix: upgrade to Kestra 1.0.45 / 1.3.21 or later (patch alone closes the primary vulnerability; the escalation and persistence stages require a separate, independent fix - don't mount the Docker socket into application containers, see Section 8).
This report documents CVE-2026-53576, a CVSS 10.0 unauthenticated remote code execution vulnerability in Kestra ≤1.3.20, caused by a path-suffix authentication bypass (AuthenticationFilter.java, endsWith("/configs")).
An attacker who names a workflow's namespace and ID configs can create and execute arbitrary shell commands with no credentials, as root, inside the Kestra container. Fixed in 1.0.45 and 1.3.21. The identical bug was also reported independently under CVE-2026-49869, which is the CISA KEV-listed number tied to real in-the-wild exploitation. Both should be cited together, since public sources and scanners may reference either one.
| Vulnerable | Kestra ≤ 1.3.20 (and pre-1.0.45 on the 1.0.x line) |
| Fixed | 1.0.45 / 1.3.21+ |
| CVE | CVE-2026-53576 |
| Twin advisory | CVE-2026-49869 - same bug, CISA KEV-listed (in-the-wild) |
| Date | Event |
|---|---|
| 2026-06-02 | Kestra 1.3.21 released; changelog references a "potential authentication bypass in the authentication filter" |
| 2026-06-03 | Kestra 1.0.45 released on the 1.0.x line |
| 2026-09-02 | CVE-2026-49869 (the twin advisory) added to the CISA KEV catalog |
| 2026-09-22 to 09-24 | This lab reproduction, detection build, and cross-layer validation |
Self-hosted homelab:
- Debian host (hostname docker, kernel 6.12.107+deb13-amd64),
- Docker Engine 29.8.1.
- Kestra deployed as the official kestra/kestra:v1.3.20 image, reachable through a Caddy reverse proxy at kestra.int.atlasvec.com.
- Telemetry stack under test: Falco (runtime/eBPF, host-level),
- Suricata 8.0.3 IDS (passive SPAN mirror), forwarding toward Splunk.
Before touching anything, I confirmed the target was actually vulnerable and the telemetry stack was live. I also took a snapshot of the pre-attack state so I could revert once I was done.
Kestra running the vulnerable version:

The container itself, up and reachable:

Falco's units loaded on the host:

Falco actively emitting events (modern eBPF mode):

Suricata's service active:

And its eve.json writing live events:

This is two mistakes lining up, not one.
First, the authentication filter decides whether a request can skip auth by matching the end of the request path:
// AuthenticationFilter.java — the vulnerable check, confirmed against the self-hosted v1.3.20 source
if (path.endsWith("/configs")) {
// treated as a public, unauthenticated path
}
That check exists to expose a single harmless endpoint, the public instance config. Because endsWith only reads the tail of the string, it cannot tell that one safe path apart from any other path that happens to end the same way.
Second, Kestra's router still dispatches those look-alike paths to their real, sensitive handlers. /api/v1/main/flows/configs reaches the flow-create handler. /api/v1/main/executions/configs/configs reaches the execution handler. The filter waves both through as public, and the router runs them anyway. Name a flow's namespace and ID configs, and a code-executing endpoint now ends in /configs, so it inherits the public path's free pass.
The captured request pair shows it plainly. GET /api/v1/main/flows/search with no credentials returns 401. POST /api/v1/main/flows/configs with the same empty auth header returns 200 and stores the flow (see §5.1).
Every step below is captured with its own screenshot. All of it runs inside my own isolated homelab against a self-hosted Kestra instance behind the reverse proxy.