Skip to content

Gateway/Datakit: Dynamically set upstream based on authenticating consumer #5548

@lena-larionova

Description

@lena-larionova

Jobs to be done

Create how-to guide to dynamically set upstream based on authenticating consumer.

Information

Use the OIDC plugin to validate the token to sets a header for the consumer.  Then Datakit reads the header and sets either the service or the upstream.

# =============================================================================
# Consumer-based upstream routing with Kong
# =============================================================================
# Goal: for a given caller (identified by a token claim), consistently route to
#       ONE specific backend among several — decided AFTER the route is matched.
#
# Why this shape:
#   Kong's router runs BEFORE auth, so it cannot match on a consumer/claim.
#   Instead we keep ONE route -> ONE service, let OIDC validate the token and
#   set the credential from a claim, then let Datakit read that credential and
#   OVERRIDE the backend in-place. No second route, no header re-routing, no
#   loopback / second proxy hop.
#
# Plugin order (all in the `access` phase) — by static plugin priority, OIDC
# runs before Datakit, so no explicit `ordering` is required:
#   openid-connect  -> validates token, sets credential.id from `client_id` claim
#   datakit         -> reads kong.client.credential, maps id -> backend, sets it
#
# Two backend-write options are shown:
#   VARIANT A (ACTIVE)   : kong.service.upstream  -> named Upstream entity (POOL)
#                          keeps load balancing, health checks, retries.
#   VARIANT B (COMMENTED): kong.service.target    -> single host:port
#                          bypasses LB, health checks, retries. Simpler, no pool.
#   Pick ONE datakit block. They are mutually exclusive on the same route.
#
# Requires: gateway version with DataKit + property/jwt nodes (3.14-era).
#           Confirm the OIDC field name `credential_claim` matches your version.
# =============================================================================

_format_version: "3.0"

# -----------------------------------------------------------------------------
# Backends for VARIANT A (set_upstream). Each Upstream is a POOL of targets, so
# load balancing / health checks / retries are preserved on the override.
# (Not used by VARIANT B — there the host:port comes from the Datakit mapping.)
# -----------------------------------------------------------------------------
upstreams:
  - name: upstream-a
    targets:
      - { target: a1.example:443 }
      - { target: a2.example:443 }   # HA pool

  - name: upstream-b
    targets:
      - { target: b1.example:443 }

  - name: upstream-default          # fallback for unmapped / missing claim
    targets:
      - { target: default.example:443 }

services:
  - name: claims-api
    protocol: https
    host: upstream-default          # default backend; Datakit overrides per-request
    port: 443
    routes:
      - name: claims-route
        paths:
          - /v1/claims

    plugins:
      # -----------------------------------------------------------------------
      # 1) OIDC — validate the bearer token and set the credential from a claim.
      #    `credential_claim` puts the claim value onto credential.id WITHOUT
      #    requiring a Kong Consumer entity (consumer_optional: true).
      # -----------------------------------------------------------------------
      - name: openid-connect
        config:
          issuer: <issuer-url>
          auth_methods:
            - bearer
          credential_claim:          # credential.id := value of this claim
            - client_id
          consumer_optional: true    # no Kong Consumer needed; don't 401 on absence
          # hide_credentials: true   # optional: strip Authorization before upstream

      # -----------------------------------------------------------------------
      # 2) DataKit — VARIANT A (ACTIVE): route to a named UPSTREAM (pool).
      #    Runs after openid-connect by static plugin priority (OIDC's priority is
      #    higher, so it executes first in the access phase; Datakit follows), so
      #    kong.client.credential is already populated. No `ordering` needed. If
      #    you add another access-phase plugin, re-verify with X-DataKit-Debug-Trace.
      #
      #    Flow:  GET_CREDENTIAL -> PICK_UPSTREAM (jq map) -> SET_UPSTREAM
      # -----------------------------------------------------------------------
      - name: datakit
        config:
          # debug: true             # enable only while validating; dev-only output
          nodes:
            # Read the authenticated credential (GET mode: no input connected).
            - name: GET_CREDENTIAL
              type: property
              property: kong.client.credential

            # Map credential.id (the claim value) -> Upstream entity name.
            # Unmapped callers fall through to "upstream-default".
            - name: PICK_UPSTREAM
              type: jq
              input: GET_CREDENTIAL
              jq: |
                {
                  "1234": "upstream-a",
                  "5678": "upstream-b"
                }[.id] // "upstream-default"

            # Apply it (SET mode: input connected). Keeps LB / health / retries.
            - name: SET_UPSTREAM
              type: property
              property: kong.service.upstream
              input: PICK_UPSTREAM

      # -----------------------------------------------------------------------
      # 2) DataKit — VARIANT B (ALTERNATIVE): route to a fixed host:port TARGET.
      #    Bypasses load balancing, health checks, and retries. Use only for
      #    stable single-host backends. To use this instead of Variant A:
      #      - comment out the Variant A `datakit` plugin block above
      #      - uncomment this block
      #      - the `upstreams:` section becomes optional
      #
      # - name: datakit
      #   config:
      #     nodes:
      #       - name: GET_CREDENTIAL
      #         type: property
      #         property: kong.client.credential
      #
      #       # Map credential.id -> "host:port".
      #       - name: PICK_TARGET
      #         type: jq
      #         input: GET_CREDENTIAL
      #         jq: |
      #           {
      #             "1234": "a1.example:443",
      #             "5678": "b1.example:443"
      #           }[.id] // "default.example:443"
      #
      #       # Apply it. NOTE: bypasses balancer (no health checks / retries).
      #       - name: SET_TARGET
      #         type: property
      #         property: kong.service.target
      #         input: PICK_TARGET
      #
      #       # If backends differ on scheme, also set it:
      #       # - name: SET_SCHEME
      #       #   type: property
      #       #   property: kong.service.request.scheme
      #       #   input: <jq node returning "https" | "http">
      # -----------------------------------------------------------------------

# =============================================================================
# Deploy & verify
# =============================================================================
#   deck gateway validate deck.yaml
#   deck gateway diff     deck.yaml
#   deck gateway sync     deck.yaml
#
# Smoke test (trace shows node execution order + each node's output):
#   curl -i https://<proxy>/v1/claims \
#     -H "Authorization: Bearer <token-with-client_id=1234>" \
#     -H "X-DataKit-Debug-Trace: true"
#
# Confirm in the trace:
#   - GET_CREDENTIAL runs AFTER openid-connect and returns the credential
#   - .id carries the claim value (e.g. "1234")
#   - PICK_* resolves to the expected backend (not the default)
#   - SET_* executes before proxying
# =======================================================================

Size

M

Metadata

Metadata

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions