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/*.ymlfiles. 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 ifVARis unset.${VAR:-default}— falls back todefaultifVARis unset. An empty-but-setVARis 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. |
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. |
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.
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. |
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.
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:
Other resource kinds¶
kind: IdP— the OIDC connector. Its fields, includingverificationOnlyfor 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 aRouteGroupis (selector+config). See developer: writing plugins.kind: StateProvider— the server-side session store's own module type, registered by a plugin and referenced fromGateway.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.