Skip to main content

Shared Plugin Configs

Plugin instances that share the same configuration — the canonical case is one OIDC client used by many routes — are usually configured by copy-pasting the same config: block into every node. Updating a credential or a discovery URL then means editing every copy and hoping none is missed. A shared plugin config is a named, typed config entity stored once, top-level in gateway.yaml under plugin_configs:, and referenced by any number of plugin nodes across policies and supernode definitions via config_ref. Editing the shared config updates every referencing instance atomically — the same one-edit-updates-all promise supernodes make for subgraphs, applied to plugin config instead.

Defining a shared config

plugin_configs:
- name: corp-oidc
type: openid-connect
description: "Corporate IdP client"
config:
client_id: gateway
client_secret: ${OIDC_SECRET}
discovery: https://idp.example.com/.well-known/openid-configuration
scope: openid

type names the plugin this profile configures — it's what lets the Admin API and the UI validate a reference and pick the right config form. config is the shared key/value block, with the same ${VAR} interpolation as anywhere else in gateway.yaml.

Referencing a shared config

Any plugin node — in a policy or inside a supernode definition — can attach a shared config with config_ref, alongside its own config::

- id: auth
type: openid-connect
config_ref: corp-oidc
config:
scope: openid profile # local key wins over the shared one

Merge semantics

The effective config a node runs with is the shared config merged with the node's own config:shallow, top-level, local wins. Keys the node doesn't set are inherited from the shared config; keys it does set (including a key explicitly set to null) override the shared value.

Keycorp-oidc (shared)auth node (local)Effective
client_idgatewaygateway (inherited)
scopeopenidopenid profileopenid profile (local wins)
discoveryhttps://idp.example.com/...https://idp.example.com/... (inherited)

Setting a local key to null still counts as the node setting it — the merge is a plain overwrite, not a "skip if absent" merge, so null is a value like any other. V1 has no way to remove an inherited key. A local null overrides the shared value with a literal null; that only behaves like "unsetting" the field when the plugin declares the field Optional. For a required field, saving a node with a local null fails with a 400 (the plugin's config deserialization rejects the missing/null value) rather than falling back to anything. If you need a node to not carry an inherited value, define the value you want locally instead of inheriting it, or split it onto a second shared config.

Typing

A shared config's type must match the type of every node that references it — a key-auth node cannot reference an openid-connect profile. This is checked at save time (PUT, or a gateway.yaml reload), not deferred to the first request that hits the node, and it's what lets the web UI's node inspector offer only the profiles that fit the node it's editing.

Inside supernodes

Nodes inside a supernode definition may carry config_ref exactly like a policy node. Resolution runs before supernode expansion, so every instance of the supernode inherits the already-resolved config for its inner nodes — attach a shared config to the inner node once, in the definition, and every instance gets it.

Compile-time resolution

Resolution follows the same discipline as supernode expansion: config_ref is materialized in-memory at the compile choke point (before supernode expansion, so expanded instances carry already-resolved configs), and the resolved copy is never persisted. gateway.yaml, the Admin API, GET /api/config/export, and etcd always keep the reference form — a node's stored config still reads config_ref: corp-oidc, not the expanded key/value block. This also means the last-good guarantee holds the same way it does for policies and supernodes: an edit that breaks resolution (an unknown reference, a type mismatch) is rejected before the swap, and the previously compiled routes keep running.

Delete protection

Deleting a shared config that's still referenced anywhere — a policy node or a supernode-definition node — fails commit validation: the Admin API responds 400 and nothing changes, the same mechanism supernodes use to protect a definition still in use.

V1 limits

  • No nesting. A shared config cannot reference another shared config.
  • No deep merge. The merge is shallow and top-level only — if a key's value is itself an object, the node's value replaces the shared value wholesale; there's no field-by-field merge inside nested structures.
  • No way to remove an inherited key. A local null overrides the shared value with null rather than removing it, and that only reads as "unset" when the plugin's field is Optional — for a required field it's a 400 at save time. Define the value locally, or use a second shared config, instead.

Export and seeding

PluginConfigDef is part of GatewayConfig, so shared configs travel with the rest of the config wherever it does: GET /api/config/export (see the Admin API guide) includes a plugin_configs: section alongside routes:, policies:, and supernodes:, and a gateway.yaml that has one seeds it into a fresh instance or etcd cluster on first load.