Skip to content

Dynamic path templating for webhook destinations #993

Description

@ambroziepaval

Problem / use case.

When a tenant initiates an operation, say a action.initiated event, tey often want the resulting webhook delivered to a per-operation callback path chosen at initiation time, e.g. https://app.example.com/actions/<uuid>/callback

From a developer-experience standpoint this is a very natural thing to reach for: "I just want Outpost to handle the retries, queuing and delivery of this one HTTP request, so why can't I just hand it the destination URL on the fly?" It's an intuitive ask, which is part of why it keeps coming up.

Today a webhook destination's url is fully static per destination. The only way to achieve a per-operation path is to create a throwaway destination for each operation and publish with destination_id. That means unbounded destination churn plus lifecycle and cleanup burden, which does not fit a high-volume "one callback per action" pattern.
It also fragments the portal/UI experience: instead of thousands of single-use destinations, each with its own isolated logs, delivery attempts, and metrics, templating keeps everything under a single stable destination. All the usual actions (viewing delivery history, retrying, inspecting attempts, editing config) then operate on one coherent destination rather than being scattered across per-operation throwaways.

Proposal

Allow the path portion of a webhook URL to be templated from event fields, while the scheme + host remain fixed static config. This reuses the Go text/template mechanism already used for signing secrets and signature content in the webhook provider.

Example destination confog:

config.url               = "https://app.example.com"
config.url_path_template = "/actions/{{.metadata.callback_id}}/callback"

Publishing an event with metadata.callback_id = "abc-123" delivers to:

https://app.example.com/actions/abc-123/callback

Retries, queuing and signing all continue to work exactly as they do today.

Why templated path, not arbitrary URL

A fully event-controlled URL, while much easier to integrate, is deliberately not proposed, because it introduces three problems:

  1. SSRF. Event data an attacker can influence could redirect delivery to internal or cloud-metadata endpoints (169.254.169.254, localhost, private ranges).
  2. Rate-limiting / concurrency fragmentation. Outpost caches publishers and reasons about destinations by a stable config identity. A URL that changes every event fragments that model.
  3. Fragmented telemetry. Per-destination metrics and logs assume a stable target host; a moving host makes per-destination dashboards meaningless.

Pinning scheme + host to operator-controlled config while templating only the path neutralizes all three: the host (the SSRF-relevant part) is never event-controlled, the destination identity stays stable, and telemetry stays attributed per destination.

Design choices (proposed, open to input)

  1. Separate field url_path_template rather than template markers inside url, so the base URL stays a plain, validatable URL and scheme + host can never be templated.
  2. Template inputs: metadata (and possibly data). The event's metadata string map is a small, safe surface; parsed data could also be exposed if there's appetite. One open point: metadata keys are already emitted as X-Outpost-* headers, so a value used to shape the path would also go out as a header; if that's undesirable, a reserved metadata namespace (e.g. keys prefixed _path.) could be usable in the template but filtered out of the headers.
  3. Guardrails: percent-encode resolved path segments; reject templates/results containing ://, .., @, or a leading //; enforce the rendered result is still same-origin as the configured base URL.

Open questions

  1. Template-resolution failure at delivery time — hard-fail the attempt (visible in the attempt log) vs. fall back to the base URL? Proposal: hard-fail.
  2. Where template inputs should come from — metadata only, also parsed data, or is there a different source altogether that would be a better fit than either metadata or data? Open to suggestions if neither is the right home for a path-shaping value.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions