Skip to content

push ingress

Kadyapam edited this page Jun 12, 2026 · 2 revisions

Push ingress (POST /ingress/{listener})

Added in v3.3.0 — Phase 3 of the subscription/listener RFC (noetl/ai-meta#90).

The gateway terminates untrusted inbound push traffic — generic webhooks and Google Pub/Sub push deliveries — at POST /ingress/{listener}. It is a verify-and-forward gatekeeper: it authenticates each delivery, and only after verification passes applies the subscription's header directives and forwards one POST /api/execute per delivery to the server on the subscription's dedicated pool. The gateway never reads domain data or holds a DB connection on the ingress path (Data Access Boundary).

This is the push activation mode (Mode C) of a kind: Subscription — the pull mode (Mode B) is the worker's continuous runtime.

Flow (RFC §6, auth-gated directive trust §7.5)

POST /ingress/{listener}            (no session auth — a source, not a user)
  1. fetch ingress config (cached)  — verify scheme + Wallet secret + dispatch + directives
  2. verify the delivery            — HMAC / bearer / Pub-Sub OIDC
       fail → 401/403, noetl_ingress_rejected_total, NO forward, NO directives
  3. ON SUCCESS ONLY: parse + resolve header directives (allowlist, RFC §7)
  4. forward POST {server}/api/execute  — one execution per delivery, dedicated pool
  5. emit subscription.message.directives_applied (audit, RFC §7.6)
  6. return 202 so the source acks

The ordering is the security property: a request that fails verification is rejected before any directive is parsed, so an unauthenticated caller can never drive routing (redirect / pool / priority). In the code this is the verify_then_plan invariant — a failed verification yields no dispatch plan at all (src/ingress/mod.rs).

Verify schemes (RFC §6)

A push kind: Subscription declares its verify scheme + the Wallet alias of its secret in the catalog spec.ingress.verify block. The gateway resolves the config (including the decrypted secret for HMAC/bearer) from the server's GET /api/internal/ingress/{listener} endpoint — never from a gateway env var.

verify.type Check the gateway performs Secret source
hmac_sha256 Recompute HMAC-SHA256(secret, raw_body), constant-time compare against the header value (optional sha256= prefix). Wallet alias
bearer Constant-time compare the Authorization: Bearer token against the expected token. Wallet alias
pubsub_oidc Validate the Google-signed OIDC JWT: RS256 vs Google JWKS, aud == audience, email == service_account, email_verified, not expired. Config (JWKS public)
none Rejected at catalog registration — push ingress always verifies.

The verifiers live in src/ingress/verify.rs. Negative paths are unit-tested: bad signature, expired token, wrong audience, wrong service-account, unknown kid, tampered body.

Live OIDC validation against Google's real JWKS

The pubsub_oidc positive signature path is validated live against Google's real signing keys (noetl/ai-meta#91) — not just the self-minted RSA key the unit tests use. The #[ignore]d oidc_live_google_token_against_real_jwks test fetches the live JWKS (https://www.googleapis.com/oauth2/v3/certs) via the gateway's own fetch_google_jwks and validates a genuinely Google-signed OIDC token: valid (correct aud + SA) → verified; wrong audience → oidc_wrong_audience; wrong SA → oidc_wrong_sa; tampered signature → oidc_bad_signature. The token is minted out-of-band by impersonating a Google service account with a custom audience + --include-email, so it carries the exact claims a Pub/Sub push subscription sends. Reproduce with noetl/e2e scripts/live_validate_oidc_verify.sh. A full HTTP run (gateway → kind server, real token to /ingress/{listener}) confirmed one execution dispatched on a valid token and zero on the tampered / wrong-aud / missing-token deliveries.

Header directives (auth-gated)

After verification the gateway runs the same allowlist-based directive engine the worker's pull runtime uses (RFC §7), vendored serde-only into src/ingress/directives.rs so the edge stays free of the worker's heavy tool dependencies. Only keys in the subscription's spec.headers.directives allowlist act as instructions; a value allowlist (allowed: / map:) constrains the target. Directives can redirect the target playbook (dispatch.playbook), route to a different pool (dispatch.execution_pool / priority), supply an idempotency_key, hint content_type, and carry a W3C trace context into the execution.

For a webhook source the directive channel is the HTTP headers (auth headers are stripped before forwarding); for a Pub/Sub push source the gateway unwraps the message envelope and the directive channel is the message attributes (RFC §7.1).

Catalog shape

apiVersion: noetl.io/v1
kind: Subscription
metadata: { name: stripe-webhook, path: subscriptions/stripe-webhook }
spec:
  source: webhook
  mode: push
  ingress:
    gateway_path: /ingress/stripe          # trailing segment = the {listener}
    verify:
      type: hmac_sha256
      header: "Stripe-Signature"
      secret: "stripe_webhook_secret"       # Wallet alias — NOT an env var
  dispatch:
    playbook: domain/handle_stripe_event
    payload_from: message.body
    execution_pool: subscription
  headers:
    directives:
      - header: x-noetl-route
        controls: dispatch.playbook
        allowed: [domain/handle_stripe_event, domain/handle_fraud]

Configuration

Env var Purpose
NOETL_INTERNAL_API_TOKEN Service-account bearer the gateway presents to GET /api/internal/ingress/{listener}. Must match the server's value. Unset → /ingress/{listener} returns 503.
NOETL_BASE_URL The server the gateway forwards POST /api/execute to.

Observability

The gateway's first /metrics surface ships with this feature:

  • noetl_ingress_received_total{subscription} — deliveries received.
  • noetl_ingress_rejected_total{subscription,reason} — rejected at verification (no execution). reason is low-cardinality (bad_signature, oidc_expired, oidc_wrong_audience, …).
  • noetl_ingress_dispatched_total{subscription} — verified deliveries forwarded as one execution.

Each forward carries the execution_id and (best-effort) emits the subscription.message.directives_applied audit event.

Related

Clone this wiki locally