Skip to content

Configuration reference

HOG is configured with Kubernetes-style YAML resources. This page is the authoritative field-by-field reference for every resource kind. For the concepts behind resources, routes, and the middleware chain, see core concepts.

Loading model

--config accepts a single file or a directory:

  • A directory is read in lexical filename order over its *.yaml/*.yml files. That order becomes the document order, which decides plugin and policy layering when several resources apply to the same route.
  • A file (or each file in a directory) may contain multiple ----separated YAML documents.
  • Every resource has the same envelope:
apiVersion: v1     # accepted but not currently validated
kind: Gateway
metadata:
  name: my-gateway
  labels: {}       # optional; used by RouteGroup/plugin selectors
spec:
  # kind-specific fields — documented below
  • Every string value supports ${ENV} interpolation, resolved once at startup against the process environment:
    • ${VAR} — required; startup fails if VAR is unset.
    • ${VAR:-default} — falls back to default if VAR is unset. An empty-but-set VAR is used as-is (the default does not apply).
spec:
  session:
    key: ${SESSION_KEY}                # fails fast if unset
  otlp:
    endpoint: ${OTLP_ENDPOINT:-http://localhost:4318}

A config must contain exactly one Gateway resource and at most one Telemetry resource; any number of Route, RouteGroup, Policy, RequestPlugin, and ResponsePlugin resources; and at most one IdP resource (multi-IdP is not yet supported).


Gateway

The root resource. listen is the only field with a default; everything else is opt-in.

Field Type Default Description
listen string :8080 The address net/http listens on.
trustedProxies []string — (trusts no peer) CIDR/IP list scoping trust of X-Forwarded-*/X-Real-Ip/Forwarded headers to your ingress. Enforced — see the note below.
security mapping — CSRF protection and security response headers, applied gateway-wide. See Gateway: security and security hardening.
plugins []string — Build-time module manifest for hog-build: <import-path>[@version] entries. Consumed by the build tool, not at runtime. See installation and building a custom binary.
session mapping — The session/cookie block. See Gateway: session and authentication.
identity mapping — The shared identity/passport model, used by both cookie and Bearer auth. See Gateway: identity.
auth mapping — Login/logout endpoint paths. See Gateway: auth.
stateProvider mapping — Server-side session state backend (opt-in). See Gateway: stateProvider and scaling.
kind: Gateway
metadata: { name: my-gateway }
spec:
  listen: ":8080"

trustedProxies is enforced

A gateway-wide forwarded layer — the outermost wrapper around the whole handler, applied before routing and before OpenTelemetry — strips X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port, X-Real-Ip, and Forwarded from any request whose immediate peer isn't listed in trustedProxies. Entries are CIDRs or bare IPs; the literal "*" trusts every peer; the default (empty list) trusts no peer, so those headers are stripped from every request until you configure this field — the secure default. Set it to your load balancer's/ingress's CIDR to get the real client IP, correct cookie Secure handling, and a correct X-Forwarded-* chain to backends. See security hardening and architecture: deployment.

Gateway: security

CSRF protection and security response headers, applied as a gateway-wide outermost wrapper around every response — routes and the raw /auth/* endpoints alike, not a per-route setting.

Field Type Default Description
csrf.enabled bool true Enables net/http.CrossOriginProtection (Fetch-metadata based CSRF defense). Set false to disable entirely.
csrf.trustedOrigins []string — Origins (e.g. https://app.example.com) to trust for cross-origin, same-site-or-not requests. Required for a same-site-but-cross-origin browser SPA — see the note below.
csrf.bypassPatterns []string — ServeMux-style patterns exempted from CSRF checking (e.g. a webhook endpoint that can't send Origin/Sec-Fetch-Site). A deliberate, per-pattern CSRF hole — scope it as narrowly as possible.
headers.frameOptions string DENY X-Frame-Options value. Set to "" to omit the header.
headers.contentTypeOptions string nosniff X-Content-Type-Options value. Set to "" to omit the header.
headers.referrerPolicy string strict-origin-when-cross-origin Referrer-Policy value. Set to "" to omit the header.
headers.hsts.enabled bool true Sets Strict-Transport-Security.
headers.hsts.maxAge int 31536000 max-age in seconds.
headers.hsts.includeSubDomains bool true Adds the includeSubDomains directive.
headers.hsts.preload bool false Adds the preload directive.
headers.contentSecurityPolicy string — (unset) Content-Security-Policy value. Opt-in — HOG sets no default CSP, since it depends on the specific frontend served.
spec:
  security:
    csrf:
      enabled: true
      trustedOrigins: [https://app.example.com]
      bypassPatterns: []
    headers:
      frameOptions: DENY
      contentTypeOptions: nosniff
      referrerPolicy: strict-origin-when-cross-origin
      hsts: { enabled: true, maxAge: 31536000, includeSubDomains: true }
      contentSecurityPolicy: ""

CSRF protection allows GET/HEAD/OPTIONS, same-origin requests, and non-browser requests (no Sec-Fetch-Site/Origin, so Bearer/API clients are unaffected); it rejects a cross-origin, state-changing (unsafe-method) browser request with 403 unless the origin is in csrf.trustedOrigins. SameSite=Lax session cookies remain the primary CSRF control — CSRF protection here is defense-in-depth on top of them.

A same-site, cross-origin SPA needs csrf.trustedOrigins

A browser sends Sec-Fetch-Site: same-site (not same-origin) when your SPA and HOG share a registrable domain but differ by subdomain — e.g. a frontend at app.example.com calling a HOG instance at api.example.com. CrossOriginProtection treats that as cross-origin: a state-changing request (POST/PUT/PATCH/DELETE) gets 403 until you add https://app.example.com to csrf.trustedOrigins.

Gateway: session

Configures the encrypted session cookie. Omit the block entirely to run without sessions (a pure reverse-proxy/aggregation gateway). See authentication for the full picture, including how this interacts with identity, auth, and the IdP resource.

Field Type Default Description
key string — Required if session is present. Must be exactly 32 bytes (used as an AES-256-GCM key). Use ${ENV} — never commit it.
cookieName string hog_session The session cookie's name (chunked as <name>.0, <name>.1, … if the sealed value exceeds one cookie).
ttl duration string 8h Session lifetime, e.g. "8h", "30m".
fingerprintHeaders []string ["User-Agent"] Request headers hashed into a server-side fingerprint, checked on every read; a mismatch invalidates the session.
infoPath string /auth/session Path of the SPA-facing session-info endpoint (GET → the public session view; only mounted when both session and an IdP are configured).
postLogoutRedirect string / Where logoutPath redirects to after clearing the session.
spec:
  session:
    key: ${SESSION_KEY}
    ttl: 8h
    fingerprintHeaders: ["User-Agent"]

Gateway: identity

The claim/group projection model shared by the cookie session and Bearer auth. Omitting the block uses the defaults below.

Field Type Default Description
claims []string ["email", "name", "given_name", "family_name"] Allowlist of ID-token/userinfo claims persisted into the passport (sub is always kept separately).
groups mapping — Optional group-DN projection (see below).
groups.source string — The userinfo/token claim holding the group-DN array (e.g. isMemberOf).
groups.match []string — Case-insensitive substring patterns; only DNs containing at least one are kept. A group with no match entries never matches anything — set at least one pattern for groups to have any effect.
groups.render string cn cn (extract the cn= component) or dn (keep the whole DN).
groups.as string groups The session field / default projected-header name for the rendered group list.
groups.strip []string — Case-insensitive prefixes removed from each rendered group value; the first matching prefix wins. An empty list leaves values unchanged.
userInfo string auto auto (fetch userinfo only if the token is missing a configured claim/group source), always, or never.
subjectClaim string sub Claim that becomes the principal subject (X-User-Id) on both the cookie and the Bearer paths. Userinfo wins over the token when both carry the claim; if the claim is absent or not a string, the token's own sub is used instead — the subject is never silently empty. Set this for providers whose access tokens carry no sub.
assertion mapping — Identity hand-off to a second HOG instance (see below).
spec:
  identity:
    claims: [email, name]
    groups:
      source: isMemberOf
      match: ["ou=engineering"]
      render: cn
      as: groups
      strip: ["role-staff-"]
    subjectClaim: uid

identity.assertion

Lets an upstream HOG instance mint a short-lived, signed statement of the principal it resolved, and a downstream HOG instance accept it instead of resolving the identity against the IdP a second time. issue, accept, or both may be configured on the same instance. See authentication.

Field Type Default Description
issue.name string — Required. The iss claim identifying this instance to the peer that accepts it.
issue.keyId string — Required. The kid header identifying the signing key.
issue.key string — Required. A 32-byte Ed25519 seed, base64-encoded (standard or raw-URL-safe).
issue.ttl duration string 60s How long a minted assertion is valid. Must be > 0.
issue.header string X-Hog-Identity Header the assertion is written to.
accept.issuer string — Required. Expected iss claim — the peer instance's assertion.issue.name.
accept.header string X-Hog-Identity Header the assertion is read from.
accept.requireBearer bool true When true, the assertion only enriches a principal a verified Bearer token already authenticated, and only when the asserted subject matches the token's; a mismatched or unaccompanied assertion is ignored rather than rejecting the request. When false, a valid assertion authenticates a request on its own.
accept.keys []mapping — Required, at least one. Verification keys, one per accepted signer: keyId (matches the peer's issue.keyId) and publicKey (a 32-byte Ed25519 public key, base64-encoded).
spec:
  identity:
    assertion:
      issue:
        name: go-app
        keyId: go-app-2026-09
        key: ${ASSERTION_SEED}
      accept:
        issuer: hog-edge
        keys:
          - keyId: hog-edge-2026-09
            publicKey: ${HOG_EDGE_PUBLIC_KEY}

Gateway: auth

Endpoint paths for the browser login flow. Only meaningful (and only mounted) when both session and an IdP resource are configured.

Field Type Default Description
loginPath string /auth/login Starts the OIDC flow; redirects to the IdP.
logoutPath string /auth/logout POST-only endpoint that clears the HOG session and redirects to session.postLogoutRedirect. A non-POST returns 405; a cross-origin POST is rejected by the security stage.

The callback path is not set here — it's derived from the IdP resource's redirectURL path (default /auth/callback if none is configured or it can't be parsed). See authentication.

spec:
  auth:
    loginPath: /auth/login
    logoutPath: /auth/logout

Gateway: stateProvider

Opt-in server-side session storage, for silent access-token refresh across a multi-instance deployment. Requires session to also be configured (the session key encrypts the at-rest record). See scaling for when you need this.

Field Type Default Description
type string — Required. The registered StateProvider module name. valkey is the state provider HOG ships (plugins/statestore-valkey, a nested Go module added via the gateway's plugins: list and hog-build so its dependency stays out of the core binary); no other built-in types ship in HOG itself.
refreshSkew duration string 60s How early before the access token's expiry to silently refresh it.
keyPrefix string hog:sess: Prefix applied to the store key derived from the session ID.
config mapping — Opaque; passed verbatim to the registered StateProvider factory. For type: valkey: address (required), user (default default), password, db (default 0), tls (default false), timeout (default 3s) — see scaling.
spec:
  session:
    key: ${SESSION_KEY}
  stateProvider:
    type: valkey
    refreshSkew: 60s
    config:
      address: valkey:6379
      user: hog
      password: ${VALKEY_PASSWORD}
      db: 0
      tls: true
      timeout: 3s

Route

A single routable endpoint. match and handler.type are required.

Field Type Default Description
match string — Required. A net/http.ServeMux pattern, e.g. /api/users/{id}, GET /health, /app/ (trailing slash = subtree). Must be unique across all routes.
type string inferred app or service. If unset, inferred from handler.type: reverse-proxy/api → service; everything else (static, …) → app. Determines the default auth and how the auth gate responds (redirect vs. 401).
handler mapping — Required. { type: <name>, ...handler-specific fields } — see Handler types.
access mapping — This route's own auth/authorize/projection (see Access below). Always wins over a matching RouteGroup.
kind: Route
metadata: { name: dashboard, labels: { tier: api } }
spec:
  match: /api/dashboard
  type: service
  handler:
    type: api
    backends: [...]
  access:
    auth: required
    authorize: [admins-only]

Access (route auth + authorize + projection)

Not to be confused with the kind: Policy authorization resource — this is the inline access: block on a Route/RouteGroup.

Field Type Default Description
auth string inferred required or public. Unset defaults from the route's type: service → required, app → public.
authorize []string — Names of kind: Policy (authorization) resources to enforce on this route, unioned with any matching RouteGroup's access.authorize. See authorization.
projection mapping derive from passport Customizes the X-User-* headers injected for the backend. See below.
onDeny mapping — What an authorization denial answers instead of the default 403. Honoured on app routes only — a service route always keeps the 403. See authorization.

onDeny:

Field Type Description
redirect string Same-origin path an authorization denial redirects to (302), e.g. /utils/overview. Must start with / and must not start with // or /\, or contain :// — validated at config load.
spec:
  access:
    auth: required
    authorize: [admins-only]
    onDeny:
      redirect: /

projection:

Field Type Description
session.claims map[string]string Explicit claim → header overrides. When set, only these claims are projected (replaces the default "one X-User-<Claim> per passport claim" behavior).
session.groups.header string Overrides the groups header name (default derives from identity.groups.as, e.g. X-User-Groups).
request mapping Reserved; decodes but has no effect yet.
spec:
  access:
    auth: required
    authorize: [admins-only]
    projection:
      session:
        claims:
          email: X-User-Email
        groups:
          header: X-User-Roles

RouteGroup

Applies a shared access block to every Route whose labels match selector — the same pattern as a Kubernetes Service selecting Pods. A route's own spec.access always wins over a matching group field-by-field (its own access.authorize is unioned with, not replaced by, a matching group's); when several groups match, later groups (document order) override earlier ones field-by-field.

Field Type Default Description
type string — Default route type (app/service) for matching routes that don't set their own.
selector mapping matches everything matchLabels (exact-match map) and/or matchExpressions (see below).
access mapping — Same shape as Route's access.

selector.matchExpressions[]:

Field Type Description
key string The label to test.
operator string In, NotIn, Exists, or DoesNotExist.
values []string Comparison set for In/NotIn.
kind: RouteGroup
metadata: { name: app-auth }
spec:
  selector:
    matchLabels: { tier: api }
  access:
    auth: required
    authorize: [require-admins]

Policy (authorization)

A named, reusable authorization unit referenced from Route/RouteGroup access.authorize:. See authorization for how policies combine and evaluate; this table is the field reference.

Field Type Default Description
require mapping — Built-in group/claim rule. At least one of require/rego is required; an empty (no groups/claims) require block is a config error.
require.groups []string — Any-of: the principal must belong to at least one listed group.
require.claims map[string]string|[]string — All-of across keys; each value is a scalar or list (any-of within that claim). An empty value list is a config error.
rego mapping — Embedded OPA/Rego rule.
rego.path string — Path (file or directory) to .rego source, resolved relative to where the config is loaded. Must define deny under package hog.authz.
kind: Policy
metadata: { name: admins-only }
spec:
  require:
    groups: [admins]
    claims:
      tier: [gold, platinum]

Handler types

Set via handler.type; the remaining fields under handler are specific to that type. Built-in types: static, reverse-proxy, api, health.

static

Traversal-safe file serving (built on os.Root) with single-page-app (SPA) fallback.

Field Type Default Description
dir string — Required. Directory to serve.
index string index.html Filename served for the directory root and, on a miss, the SPA fallback shell.
spaFallback bool true On a miss for an extensionless path, serve index instead of 404.
stripPrefix string — Prefix trimmed from the request path before resolving a file.
cacheControl string — Cache-Control value applied to non-index files. The index file is always served Cache-Control: no-cache.

Dotfiles and path-traversal segments (anything starting with ., including ..) are always rejected, independent of spaFallback.

spec:
  match: /
  handler:
    type: static
    dir: /srv/web
  access: { auth: public }

reverse-proxy

A transparent, single-backend proxy (net/http/httputil.ReverseProxy). Streams responses (SSE, WebSockets).

Field Type Default Description
upstream string — Required. Base URL, e.g. http://users-svc:9000.
stripPrefix string — Prefix trimmed from the outbound path.
preserveHost bool false Forward the inbound Host header instead of the upstream's.
forwardAccessToken bool false Inject Authorization: Bearer <access token> from the session principal. Off by default — see security hardening.
forwardCookies bool false Pass the inbound Cookie header through. Off by default — HOG's own session/login cookies are never meant to reach a backend.
forwardIdentity bool false Forward the identity assertion minted by identity.assertion.issue to this upstream — for handing a verified identity to a second HOG instance. See authentication.
timeout duration string none Per-request timeout. A timed-out request returns 504; any other proxy error returns 502.
insecureSkipVerify bool false Disable upstream TLS certificate verification. Use only for trusted internal backends with self-signed certs.
spec:
  match: /users/
  type: service
  handler:
    type: reverse-proxy
    upstream: http://users-svc:9000
    stripPrefix: /users
    timeout: 10s

api

Fans out to 1..N backends concurrently and merges their JSON responses under a key per backend.

Field Type Default Description
timeout duration string none Overall request timeout, applied to all backend calls via a shared context.
backends []mapping — Required, at least one. See below.

backends[]:

Field Type Default Description
group string — Required, unique per handler. The JSON key the backend's response is merged under.
upstream string — Required. Base URL.
path string — Required. Request path, joined onto the upstream. Supports {name} placeholders resolved from the route's own path parameters (e.g. a route matched as /api/orders/{id} can use path: /orders/{id}).
method string GET HTTP method for the backend call.
required bool true If true, a failed/timed-out call fails the whole request (502/504); if false, the backend is omitted and its group name is listed in the X-Hog-Partial response header.
forwardQuery bool false Forward the inbound request's query string to this backend.
forwardAccessToken bool false Inject Authorization: Bearer <access token> for this backend only.
forwardIdentity bool false Forward the identity assertion minted by identity.assertion.issue to this backend only. See authentication.

A 2xx response that isn't valid JSON (including an empty body) is treated as a backend failure. A single backend response is capped at 10 MiB.

spec:
  match: /api/dashboard
  type: service
  handler:
    type: api
    timeout: 5s
    backends:
      - group: profile
        upstream: http://users-svc:9000
        path: /me
      - group: notifications
        upstream: http://notif-svc:9100
        path: /unread
        required: false

health

A built-in liveness/readiness handler. No config fields — it always returns 200 {"status":"ok"}. Mount it on whatever path and labels you want:

spec:
  match: /healthz
  handler: { type: health }
  access: { auth: public }

Other resource kinds

  • kind: IdP — the OIDC connector. Its fields, including verificationOnly for an instance that only verifies tokens someone else issued, are documented in authentication.
  • kind: Telemetry — OpenTelemetry + access-log settings. See observability.
  • kind: RequestPlugin / kind: ResponsePlugin — third-party middleware slots, selected onto routes the same way a RouteGroup is (selector + config). See developer: writing plugins.
  • kind: StateProvider — the server-side session store's own module type, registered by a plugin and referenced from Gateway.spec.stateProvider.type.

Complete example

The annotated document below combines every resource kind covered on this page — Gateway (session, identity including the subject claim, group strip and identity assertion, security, the Valkey stateProvider), IdP (including the bearer: block), Telemetry, four Routes (static ×2 — one of them gated with onDeny.redirect — reverse-proxy and api, both with forwardIdentity set), a RouteGroup, and both policy tiers (require and rego) — into one config that actually loads and parses (it's exercised by a Go test in the repo, app/full_config_test.go, so it can't silently drift from the field names above). The source file lives at website/docs/examples/full-config.yaml.

# yaml-language-server: $schema=https://paulopiriquito.github.io/hog/hog.schema.json
# =============================================================================
# HOG — complete, annotated configuration example
# =============================================================================
# A single multi-document config that touches every major resource kind, with
# every field commented. It is designed to PARSE standalone — see
# app/full_config_test.go, which loads this exact file through config.Load and
# app.Parse as a drift test — even with no environment variables set:
# secret-shaped fields use `${VAR:-}` (an explicit empty default) purely so
# this file is self-contained for docs/testing. A real deployment MUST set
# SESSION_KEY and OIDC_CLIENT_SECRET; see operations/configuration.md.
#
# `apiVersion: hog.dev/v1` appears on every resource below. HOG itself never
# reads or validates this field — it exists only so tools that DO care about
# apiVersion/kind (kustomize, kubectl-style tooling) can identify and patch
# HOG resources. See operations/kustomize.md for rendering this kind of
# config from a base + per-environment overlays.
# =============================================================================

# -----------------------------------------------------------------------------
# Gateway — the root resource. A config must contain exactly one.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Gateway
metadata:
  name: hog
spec:
  # Address net/http listens on. Defaults to ":8080" if omitted.
  listen: ":8080"

  # CIDRs/bare IPs trusted to set X-Forwarded-For/-Proto/-Host/-Port,
  # X-Real-Ip, and Forwarded. A request from any other peer has those headers
  # stripped before routing/tracing ever see it. The default (empty) trusts
  # no peer — set this to your load balancer's/ingress's CIDR.
  trustedProxies:
    - 10.0.0.0/8

  # Build-time module manifest: Go import paths of plugin packages linked
  # into the binary by the hog-build tool. Here, the Valkey state-provider
  # plugin used by `stateProvider` below — it lives in its own Go module
  # (plugins/statestore-valkey) so its dependency never reaches the core
  # hog module.
  plugins:
    - github.com/paulopiriquito/hog/plugins/statestore-valkey

  # The encrypted, HttpOnly session cookie (stateless BFF mode). Omit this
  # whole block to run HOG as a pure reverse-proxy/aggregation gateway with
  # no login flow.
  session:
    # Must be exactly 32 bytes — used as an AES-256-GCM key. `${SESSION_KEY:-}`
    # resolves to an empty value when the env var is unset, purely so this
    # example parses standalone; a real deployment MUST set SESSION_KEY
    # (e.g. `openssl rand -hex 16`) and never commit the value.
    key: ${SESSION_KEY:-}

  # The claim/group projection model shared by cookie sessions and Bearer
  # auth. Omitting this block uses HOG's defaults (claims:
  # [email, name, given_name, family_name], no groups, userInfo: auto,
  # subjectClaim: sub).
  identity:
    # Claim that becomes the principal subject (X-User-Id) on both the
    # cookie and the Bearer paths. Our IdP (see the OIDC bearer: block
    # below) is one of those providers whose access tokens carry no sub,
    # so the subject comes from uid instead. Defaults to "sub".
    subjectClaim: uid
    groups:
      # The userinfo/ID-token claim holding the group-DN array.
      source: isMemberOf
      # Case-insensitive substrings; only DNs containing at least one of
      # these are kept. A `groups` block with no `match` entries never
      # matches anything.
      match:
        - "ou=staff"
      # "cn" extracts the cn= component of each kept DN; "dn" keeps the
      # whole DN string.
      render: cn
      # Session field name / default projected-header name for the
      # rendered group list (e.g. X-User-Groups).
      as: groups
      # Case-insensitive prefixes removed from each rendered group value;
      # the first matching prefix wins. Turns "role-staff-admin" into
      # "admin".
      strip:
        - "role-staff-"

    # Identity hand-off to a second HOG instance: this gateway both mints
    # assertions for routes with handler.forwardIdentity: true (below) and
    # accepts assertions minted by an upstream HOG instance for its own
    # inbound requests.
    assertion:
      issue:
        # The iss claim identifying this instance to the peer that accepts it.
        name: go-app
        # The kid header identifying the signing key below.
        keyId: go-app-2026-09
        # 32-byte Ed25519 seed, base64-encoded. The fallback below is all
        # zero bytes, a placeholder that lets this example parse on its own.
        # It is not a key: signing with it is signing with a key everyone
        # has. A real deployment sets ASSERTION_SEED (`openssl rand -base64
        # 32`) and never commits the value.
        key: ${ASSERTION_SEED:-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=}
        # How long a minted assertion is valid. Defaults to "60s".
        ttl: 60s
        # Header carrying the assertion. Defaults to "X-Hog-Identity".
        header: X-Hog-Identity
      accept:
        # Expected iss claim: the upstream instance's assertion.issue.name.
        issuer: hog-edge
        header: X-Hog-Identity
        # Defaults to true: the assertion only enriches a principal a
        # verified Bearer token already authenticated, and only when the
        # subject matches — a header alone never authenticates on its own.
        requireBearer: true
        keys:
          # kid → the upstream instance's Ed25519 public key, base64.
          - keyId: hog-edge-2026-09
            publicKey: kqPVKwZZ6E3fLpbM8cU7d4ANRUGhKtMt0TrQ3YFbnkY=

  # CSRF protection and security response headers, applied gateway-wide as
  # the outermost wrapper around every response (routes and the raw
  # /auth/* endpoints alike).
  security:
    csrf:
      # Enables net/http.CrossOriginProtection (Fetch-metadata CSRF
      # defense). On by default; shown explicitly here. Set false to
      # disable entirely.
      enabled: true
      # Origins to trust for a same-site-but-cross-origin browser SPA —
      # e.g. a frontend on a different subdomain than this gateway, which
      # a browser sends as Sec-Fetch-Site: same-site (not same-origin).
      trustedOrigins:
        - https://app.example.com
    headers:
      # X-Frame-Options value. Set to "" to omit the header.
      frameOptions: DENY
      # X-Content-Type-Options value. Set to "" to omit the header.
      contentTypeOptions: nosniff
      # Referrer-Policy value. Set to "" to omit the header.
      referrerPolicy: strict-origin-when-cross-origin
      hsts:
        # Sets Strict-Transport-Security. On by default.
        enabled: true
        # max-age in seconds.
        maxAge: 31536000
        # Adds the includeSubDomains directive.
        includeSubDomains: true
        # Adds the preload directive.
        preload: false
      # Content-Security-Policy value. HOG sets no default CSP (it depends
      # on the frontend served) — opt in explicitly.
      contentSecurityPolicy: "default-src 'self'"

  # Opt-in server-side session storage (silent access-token refresh across a
  # multi-instance deployment). Requires `session` above to also be set.
  # "valkey" is the state provider HOG ships (plugins/statestore-valkey,
  # registered above via `plugins:`); omit this whole block to run in pure
  # stateless-cookie mode.
  stateProvider:
    type: valkey
    # How early before the access token's expiry to silently refresh it.
    refreshSkew: 60s
    # Prefix applied to the store key derived from the session ID.
    keyPrefix: "hog:sess:"
    # Opaque; passed verbatim to the valkey plugin's factory.
    config:
      # host:port of the Valkey/Redis server. Required.
      address: valkey.example.com:6379
      # ACL username. Defaults to "default" (Valkey's default ACL user).
      user: hog
      # `${VALKEY_PASSWORD:-}` resolves to an empty value when unset, purely
      # so this example parses standalone; set a real secret for actual use.
      password: ${VALKEY_PASSWORD:-}
      # Logical database index. Defaults to 0.
      db: 0
      # Use TLS to connect. Defaults to false.
      tls: true
      # Per-command timeout, a Go duration string. Defaults to "3s".
      timeout: 3s

---
# -----------------------------------------------------------------------------
# IdP — the OIDC connector. HOG supports exactly one active IdP today.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: IdP
metadata:
  name: default
spec:
  # Only "oidc" is built in today.
  type: oidc
  # Set verificationOnly: true on an instance that ONLY verifies access tokens
  # another party issued — the one in front of the APIs, in a deployment that
  # runs one instance for login and another for verification. It mounts no
  # login, logout or callback route, so it needs just issuer and clientID, and
  # clientSecret/redirectURL are then rejected rather than ignored — the client
  # secret never has to be copied into that workload. Commented out here
  # because this example configures a session, which such an IdP cannot serve.
  # verificationOnly: true
  # The provider's issuer URL, used for OIDC discovery
  # (<issuer>/.well-known/openid-configuration).
  issuer: https://idp.example.com
  # The registered OAuth2 client ID.
  clientID: hog-example
  # The registered OAuth2 client secret. `${OIDC_CLIENT_SECRET:-}` resolves
  # to an empty value when unset, purely so this example parses standalone;
  # set it via a real secret for actual use.
  clientSecret: ${OIDC_CLIENT_SECRET:-}
  # Must match a redirect URI registered with the provider. Its PATH becomes
  # HOG's callback route automatically — no separate Route is declared for it.
  redirectURL: http://localhost:8080/auth/callback
  # Requested scopes. Defaults to [openid, profile, email] if omitted.
  scopes:
    - openid
    - profile
    - email

  # Tunes Bearer access-token verification for providers whose access
  # tokens use their own key set and carry no aud — such a provider puts
  # the client identity in client_id and the subject in a
  # provider-specific claim like uid instead of sub (see
  # identity.subjectClaim above).
  bearer:
    # Key set for access tokens. Defaults to the discovery document's
    # jwks_access_token_uri, else jwks_uri; set explicitly when a provider
    # doesn't advertise jwks_access_token_uri.
    jwksURL: https://idp.example.com/ext/oauth/jwks
    # Claim compared with bearerAudience (defaults to clientID) instead of
    # the standard aud.
    audienceClaim: client_id
    # Algorithms accepted for access tokens. Defaults to the discovery
    # document's ID-token algorithm list.
    signingAlgs:
      - RS256

---
# -----------------------------------------------------------------------------
# Telemetry — OpenTelemetry export + access-log settings. At most one.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Telemetry
metadata:
  name: default
spec:
  # Required: the OpenTelemetry service.name resource attribute.
  service:
    name: hog-example
  otlp:
    # The OTLP collector endpoint. Falls back to a local default when the
    # env var is unset.
    endpoint: ${OTLP_ENDPOINT:-http://localhost:4318}
    # "http/protobuf" or "grpc".
    protocol: http/protobuf
  accessLog:
    # debug|info|warn|error — level the access-log line is emitted at.
    level: info

---
# -----------------------------------------------------------------------------
# Route — static frontend, publicly reachable.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Route
metadata:
  name: app
spec:
  # A net/http.ServeMux pattern. Trailing slash = subtree match. Must be
  # unique across all routes.
  match: /
  # Inferred as "app" from handler.type: static; set explicitly for clarity.
  type: app
  handler:
    type: static
    # Directory served (traversal-safe, built on os.Root).
    dir: /srv/web
    # Filename served for the directory root and, on a miss, the SPA
    # fallback shell.
    index: index.html
    # On a miss for an extensionless path, serve `index` instead of 404 —
    # lets client-side routing own its own URLs.
    spaFallback: true
  access:
    # "app" routes default to public anyway; shown explicitly.
    auth: public

---
# -----------------------------------------------------------------------------
# Route — a second, gated app frontend: a denied visitor is redirected to a
# same-origin page instead of shown a bare 403.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Route
metadata:
  name: admin
spec:
  match: /admin/
  type: app
  handler:
    type: static
    dir: /srv/admin
    index: index.html
    spaFallback: true
  access:
    auth: required
    authorize:
      - staff
    # What an authorization denial answers instead of the default 403.
    # Honoured on app routes only — a service route always keeps the 403.
    onDeny:
      # Same-origin path; must start with "/" and not "//"/"/\", and must
      # not contain "://" — validated at config load.
      redirect: /

---
# -----------------------------------------------------------------------------
# Route — authenticated reverse-proxy to a single backend service.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Route
metadata:
  name: account
  # Labels matched by the RouteGroup selector below. A route's own `access`
  # always wins field-by-field over a matching group; `authorize` unions
  # instead of being replaced.
  labels:
    tier: api
spec:
  match: /account/
  # Inferred as "service" from handler.type: reverse-proxy; set explicitly.
  type: service
  handler:
    type: reverse-proxy
    # Base URL of the backend.
    upstream: http://account-svc:9000
    # Trimmed from the outbound path before proxying.
    stripPrefix: /account
    # Forward the inbound Host header instead of the upstream's.
    preserveHost: false
    # Inject Authorization: Bearer <access token> from the session
    # principal onto the outbound request. Off by default.
    forwardAccessToken: true
    # Pass the inbound Cookie header through. Off by default — HOG's own
    # session/login cookies are never meant to reach a backend.
    forwardCookies: false
    # Forward the identity assertion minted by identity.assertion.issue to
    # this upstream — account-svc is itself a second HOG instance, which
    # verifies the assertion (identity.assertion.accept) instead of calling
    # userinfo again. Off by default.
    forwardIdentity: true
    # Per-request timeout; a timeout returns 504, any other proxy error 502.
    timeout: 10s
    # Disable upstream TLS certificate verification. Use only for trusted
    # internal backends with self-signed certs.
    insecureSkipVerify: false
  access:
    auth: required
    # Names of `kind: Policy` resources to enforce, unioned with any
    # matching RouteGroup's access.authorize.
    authorize:
      - staff
    # Customizes the X-User-* headers injected for the backend instead of
    # the default "one X-User-<Claim> per passport claim" projection.
    projection:
      session:
        claims:
          email: X-User-Email
        groups:
          header: X-User-Roles

---
# -----------------------------------------------------------------------------
# Route — API aggregation, fanning out to two backends concurrently and
# merging their JSON responses under one key per backend.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Route
metadata:
  name: dashboard
  labels:
    tier: api
spec:
  match: /api/dashboard
  type: service
  handler:
    type: api
    # Overall request timeout, applied to every backend call via a shared
    # context.
    timeout: 5s
    backends:
      - # JSON key this backend's response is merged under. Required,
        # unique per handler.
        group: profile
        # Base URL of this backend.
        upstream: http://users-svc:9000
        # Request path, joined onto the upstream. Supports {name}
        # placeholders resolved from the route's own path parameters.
        path: /me
        # HTTP method for the backend call. Defaults to GET.
        method: GET
        # If true (default), a failed/timed-out call fails the whole
        # request (502/504).
        required: true
        # Forward the inbound request's query string to this backend.
        forwardQuery: false
        # Inject Authorization: Bearer <access token> for this backend only.
        forwardAccessToken: true
        # Forward the identity assertion minted by identity.assertion.issue
        # to this backend only — users-svc is a second HOG instance. Off by
        # default.
        forwardIdentity: true
      - group: notifications
        upstream: http://notif-svc:9100
        path: /unread
        method: GET
        # false: on failure this backend is omitted from the merged body
        # and its group name listed in the X-Hog-Partial response header,
        # instead of failing the whole request.
        required: false
        forwardQuery: true
        forwardAccessToken: false
  access:
    auth: required
    authorize:
      - staff

---
# -----------------------------------------------------------------------------
# RouteGroup — applies a shared access block to every Route whose labels
# match `selector`, the same pattern as a Kubernetes Service selecting Pods.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: RouteGroup
metadata:
  name: api-tier
spec:
  selector:
    # Exact-match label set. matchExpressions (key/operator/values, with
    # operator In|NotIn|Exists|DoesNotExist) is also available for set-based
    # matching.
    matchLabels:
      tier: api
  access:
    auth: required
    authorize:
      - staff

---
# -----------------------------------------------------------------------------
# Policy — built-in group/claim rule (Tier A), referenced from
# access.authorize on a Route or RouteGroup.
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Policy
metadata:
  name: staff
spec:
  require:
    # Any-of: the principal must belong to at least one listed group.
    groups:
      - staff
    # All-of across keys; each value is a scalar or list (any-of within
    # that claim). Uncomment to additionally require a claim match:
    # claims:
    #   tier: [gold, platinum]

---
# -----------------------------------------------------------------------------
# Policy — embedded OPA/Rego rule (Tier B), an alternative (or addition) to
# `require`. A policy is satisfied iff require passes (when present) AND the
# rego evaluation produces no deny (when present).
# -----------------------------------------------------------------------------
apiVersion: hog.dev/v1
kind: Policy
metadata:
  name: business-hours
spec:
  rego:
    # File or directory of .rego source, resolved relative to where the
    # config is loaded from. Must define `deny` under `package hog.authz`.
    # (Parsing this resource never reads the file — only building the
    # runtime handler does — so this path does not need to exist for the
    # config itself to load and validate.)
    path: policies/business-hours.rego

If you're managing this file (or several environment-specific variants of it) with kustomize — patching the upstream, trusted proxies, or IdP settings per environment without hand-editing each copy — see Rendering config with kustomize for base/overlay layout and a worked example.