Blog

Kestra Security: CVE-2026-49869 and Webhooks Without Exposure

Kestra security guide for CVE-2026-49869: trigger Kestra flows from webhooks without exposing the API or UI, using an outbound-only Webhook Relay agent.

8 min read
Karolis Rusenas
On this page

To trigger Kestra flows from webhooks without exposing Kestra, run a Webhook Relay agent beside it and give the provider the relay's public input URL. The agent connects outbound and forwards each delivery to the flow's private webhook URL, so Kestra's UI and API are never on the internet. That matters because CVE-2026-49869, a CVSS 10 authentication bypass that is being exploited in the wild, let an unauthenticated attacker run commands as root through the API.

The incident

CVEWhat an attacker getsAuth neededExploited in the wildFixed in
CVE-2026-49869, the Kestra /configs auth bypassCreate and run a flow, commands as root in the worker container, SSRF to cloud metadata addressesNoneYes, added to CISA KEV on 2026-09-021.3.21 and 1.0.45

Kestra's authentication filter exempted the public configuration endpoint with a path-suffix check, so any API path whose last segment was configs skipped authentication. An attacker could create a flow named configs with a shell or Python script task, run it, and read the output from the logs. Versions up to 1.3.20 are affected. Kestra released the fix in 1.3.21 on 2026-06-02 and published the advisory GHSA-5vc5-wxxq-3fjx with a working proof of concept the next day. The Threat Watch record notes that CISA gives no victim or method details and lists ransomware use as unknown.

The vulnerable routes are the flow-creation and execution routes. The webhook trigger path is not involved.

Why exposure is the multiplier

Kestra is a workflow orchestrator. It holds secrets, cloud credentials and the ability to run code, which is the point of the product. A bypass of the API's authentication therefore lands on a machine that can already reach your databases and cloud accounts.

The advisory states that instances using the default basic-auth mode are affected whether or not they face the internet, so the patch is not optional on internal networks either. But the internet-facing instances are the ones any scanner can reach, and a public proof of concept lowers the work to a single request.

A common reason to put Kestra on the internet is that a GitHub, Stripe or SaaS webhook has to reach a flow. The provider needs one path. Exposing Kestra directly gives it the login page, the whole REST API and every plugin endpoint as well. (The same trade-off, for CI servers, is in securing Jenkins.)

Kestra webhook trigger: what to expose and what not to

Kestra's webhook trigger documentation gives the URL as:

https://{kestra_domain}/api/v1/main/executions/webhook/{namespace}/{flowId}/{key}

The key is set on the trigger, and the page says anyone with the URL can call it without signing in, so the key is the only check. It supports GET, POST and PUT. A minimal flow, with the key read from Kestra Secrets as the docs recommend:

id: webhook_example
namespace: company.team
tasks:
  - id: hello
    type: io.kestra.plugin.core.log.Log
    message: "Hello from the webhook"
triggers:
  - id: webhook
    type: io.kestra.plugin.core.trigger.Webhook
    key: "{{ secret('WEBHOOK_KEY') }}"

The main segment is what the current documentation shows. Our Threat Watch record writes the same position as a tenant segment, so confirm the exact path for your version before you configure a forwarder.

ExposeDo not expose
The one webhook path per flow, reachable only through the relayThe UI, /api/v1/ flow and execution routes, /configs, plugin endpoints

With a relay you do not publish the Kestra hostname at all. The provider talks to a Webhook Relay URL and only the relay output knows the Kestra address.

Step by step: Webhook Relay next to Kestra

This follows the localhost forwarding guide and the Docker Compose installation page.

1. Create a bucket and an internal output. In the dashboard, create a bucket (for example kestra-hooks). Its public input URL is the address the provider will use. Add an output whose destination is the full Kestra webhook URL, key included, as seen from the agent. Or with the CLI, as in the guide:

relay bucket create kestra-hooks
relay input create --bucket kestra-hooks "public endpoint"
relay output create kestra-flow --bucket kestra-hooks \
  --destination http://kestra:8080/api/v1/main/executions/webhook/company.team/webhook_example/YOUR_KEY

Use your own Kestra port and a long random key. Only buckets with an internal output are streamed to agents.

Lock the destination path. By default an output appends any extra path the sender adds to the input URL (a POST to <input URL>/anything goes to <destination>/anything). Outputs created in the dashboard have Destination URL path locked switched on; outputs created with the CLI or API do not, so open the output and turn it on (API/MCP field lock_path: true). With the path locked, every delivery goes to exactly the destination URL and nothing else on Kestra is reachable through the relay. It matters here because CVE-2026-49869 came down to which path suffix the authentication filter trusted, so do not let the sender choose any part of the path Kestra receives.

2. Create an access token on the tokens page and put the key and secret in an .env file next to your compose file:

RELAY_KEY="your-access-token-key"
RELAY_SECRET="your-access-token-secret"
BUCKETS=kestra-hooks

3. Add the agent as a compose service. The docs' compose example uses webhookrelay/webhookrelayd:latest with RELAY_KEY, RELAY_SECRET and BUCKETS. Add it to the same file as Kestra, and do not publish Kestra's port:

services:
  kestra:
    image: kestra/kestra:latest
    # no "ports:" entry: Kestra is only reachable on the compose network
    # ... your existing Kestra configuration ...

  relay:
    container_name: webhookrelay
    image: webhookrelay/webhookrelayd:latest
    restart: always
    environment:
      - RELAY_KEY=${RELAY_KEY}
      - RELAY_SECRET=${RELAY_SECRET}
      - BUCKETS=${BUCKETS}

The Docker Compose page's own example uses network_mode: host so the agent can reach services on the host's localhost. The agent can deliver to any destination it can reach, so keep it on the compose network when Kestra is a sibling service, and use host networking only if Kestra is bound to the host.

4. Start it with docker compose up -d, then put the bucket's public input URL into the provider's webhook settings.

5. Test with curl -X POST <input URL> -d '{"hello":"world"}' and check the execution in Kestra and the request in the bucket's logs.

The firewall rule you need is the outbound HTTPS you already allow. The provider can no longer reach Kestra's UI, /configs or flow-creation routes, because it never learns where Kestra is.

Hardening that still matters

  • Patch. Upgrade to 1.3.21 (1.3.x) or 1.0.45 (1.0.x), or later. The relay does not fix the bug.
  • A long random key. The trigger key is the whole authentication on the webhook route. Generate it, store it as a Kestra Secret, and rotate it if it appears in a log or a ticket. With the relay, the provider never sees it: only the output destination holds it.
  • Verify signatures. GitHub, Stripe and others sign their deliveries. Check the HMAC at the relay with a function that stops forwarding on a mismatch, or in the flow. See how to verify a webhook signature and webhook security best practices.
  • Filter. Filter or drop webhooks before forwarding lets you forward only the events and methods a flow wants, for example only pushes to main. Forwarding rules can also match on URL path, JSON fields and IP address. Dropped requests stay in the logs. Filtering reduces what reaches the key-protected route; it is not a replacement for the key.
  • Private network. Restrict the Kestra port to trusted hosts. Do not publish it in compose.
  • Assume compromise if it was exposed unpatched. Look for flows or executions named configs, unexpected script-task executions and deleted logs, outbound calls to cloud metadata endpoints, and rotate secrets and cloud credentials the worker could reach.

For the CI-server version of this pattern, see receiving webhooks in CI/CD.

Honest limits

A relay is not a patch, and it does not fix everything.

  • The webhook path itself. If a future bug sits in the webhook trigger, the relay still forwards the request to it. Verify the provider's signature at the relay with an HMAC function: unsigned requests get a 401 there and never reach Kestra, so only genuine deliveries can touch the trigger. With the key and filtering, that is your defence there; a flaw triggered by content inside a genuine delivery still needs the patch.
  • Humans who need the UI from outside. Use a VPN or an identity-aware proxy for them. A webhook relay carries webhooks, not a browser session.
  • Workers or clients that connect from outside. If remote workers or API clients must reach Kestra, they need a path to it, and that path has to be authenticated and restricted on its own terms.
  • Internal attackers. The advisory says network access is enough. The relay does nothing against an attacker already on your network.

If your Kestra is public only so a provider can reach a flow, create a bucket and close the port.

Frequently asked questions

How do I trigger Kestra flows from webhooks without exposing Kestra?

Run a Webhook Relay agent next to Kestra and point the provider at the relay's public input URL. The agent connects outbound and forwards each delivery to the flow's private webhook URL, so Kestra needs no public IP, no port forward and no inbound firewall rule, and its UI and API are never reachable from the internet.

What is CVE-2026-49869?

A CVSS 10 authentication bypass in Kestra OSS. The authentication filter exempted any path ending in /configs, so an unauthenticated attacker could create and run a flow as root in the worker container. It affects versions up to 1.3.20, is fixed in 1.3.21 and 1.0.45, and is in the CISA Known Exploited Vulnerabilities catalog.

What is the Kestra webhook URL?

The Kestra documentation gives the webhook trigger URL as /api/v1/main/executions/webhook/{namespace}/{flowId}/{key} on your Kestra host. The key is set on the Webhook trigger of the flow and is the only check on the request; no Kestra login is required.

Is the Kestra webhook trigger authenticated?

Only by the secret key in the URL. Kestra's documentation states that anyone who has the webhook URL can call it without signing in. Use a long random key stored in Kestra Secrets, and add signature checks and filtering in front of it.

Does patching Kestra make it safe to expose to the internet?

Patching closes this bug but not the next one. A public Kestra still exposes the login page, the whole API and every plugin endpoint. If the only reason it is public is webhooks, forwarding just the webhook path through an outbound relay removes that exposure.

Does a relay fix CVE-2026-49869?

No. Upgrade to 1.3.21 or 1.0.45 or later. The relay only stops internet clients from reaching the vulnerable routes. The advisory notes that exploitation needs network access rather than an internet-facing instance, so internal networks still need the patch.

When does a relay not help with Kestra security?

When the webhook path itself is vulnerable to content in a genuine, signed delivery (signature checks at the relay stop unsigned requests), when users need the Kestra UI from outside your network (use a VPN or identity-aware proxy for that), or when workers connect from outside and need the API.