Security hardening¶
A checklist for running HOG in production. Each item is grounded in real config or behavior — follow the links for the details.
Run the container non-root and read-only¶
hog-runtime/hog-static already run as the non-root hog user
(uid/gid 10001) by default. HOG is stateless, so also run the root
filesystem read-only:
Mount /etc/hog (config) and /srv/web (content) read-only if you aren't
baking them into the image. See
installation.
Keep the session key a secret¶
session.key is an AES-256-GCM key: anyone who has it can forge or decrypt
sessions. Always inject it with ${ENV} from a secret store, never as a
literal in a config file you commit:
Rotating the key invalidates every existing session (a deliberate trade-off — see authentication).
Give each instance only the credentials it uses¶
A secret is only as safe as the number of places it is stored. When a
deployment runs one instance for login and another for verification, only the
first performs the code exchange, so only the first needs the OAuth client
secret — and the second is usually the one exposed to the whole platform's
traffic. Declare its IdP verification-only and the secret never has to be
copied into that workload, or into a second secret store to feed it:
kind: IdP
metadata: { name: corp-oidc }
spec:
type: oidc
verificationOnly: true
issuer: https://idp.example.com
clientID: ${OIDC_CLIENT_ID}
HOG rejects clientSecret and redirectURL on such an IdP rather than
ignoring them, so a config copied from the login instance and left unedited
fails at startup instead of quietly carrying a credential that instance can
never spend. The same holds for the other secrets: an instance with no
session block needs no session.key, and only an instance that signs
identity assertions needs an identity.assertion.issue.key — the accepting
side holds the public half. See
an instance that only verifies tokens.
Terminate TLS at a trusted load balancer, and set trustedProxies¶
HOG does not terminate TLS itself. Deploy it behind a TLS-terminating load
balancer or ingress, and tell HOG which peer to trust with
Gateway.spec.trustedProxies: a gateway-wide forwarded layer 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 there, before routing and before OpenTelemetry
ever see the request. trustedProxies takes CIDRs or bare IPs; "*" trusts
every peer (only appropriate if HOG's listener is otherwise unreachable
except through your proxy); the default — an empty list — trusts no
peer, so those headers are stripped from every request until you configure
it. This is the secure default, but it also means X-Forwarded-Proto (used
to decide whether cookies are marked Secure), the client IP HOG logs and
projects, and the X-Forwarded-* chain forwarded to backends all silently
fall back to "no proxy" values (always-Secure cookies, the immediate peer
as client IP) until you set this field:
Network-level isolation (a private subnet, a service mesh, security groups)
is still what stops an untrusted party from reaching HOG directly in the
first place — trustedProxies decides which of the peers that can reach
HOG are allowed to set forwarded headers, it doesn't substitute for network
isolation. See the configuration reference note
on this field.
Leave SameSite=Lax alone¶
The session, login-state, and chunked cookies are all set SameSite=Lax,
HttpOnly, and Secure (when the request is HTTPS). This isn't
configurable, and that's intentional: Strict breaks the OIDC redirect back
to /auth/callback, and Lax still blocks the cross-site subrequests and
form submissions that matter for CSRF. See
design: authentication and sessions.
Don't forward credentials to backends unless you need to¶
Both reverse-proxy and api backends default to not forwarding the
caller's cookies, and not injecting the session's access token:
handler:
type: reverse-proxy
upstream: http://backend:9000
forwardCookies: false # default — HOG's own cookies never leak downstream
forwardAccessToken: false # default — enable only if the backend needs to call further upstream as the user
forwardCookies: true and forwardAccessToken: true are real, supported
options — only turn them on for backends that specifically need the raw
cookie or the ability to act as the user. The client-supplied Authorization
header is always stripped before HOG decides whether to inject its own,
regardless of these flags, so a client can never smuggle a bearer token
straight through to a backend.
The identity assertion header (X-Hog-Identity by default,
handing identity to a second HOG instance)
follows the same rule: it is stripped from every inbound request before
anything reads it, on both the issuing and the accepting instance, and set
only by HOG itself, only on the outbound hop of a route with
forwardIdentity: true — a client can never supply one directly. On the
accepting side, a valid assertion by default (requireBearer: true) only
enriches the principal a verified Bearer token already authenticated, so a
leaked or forged header alone authenticates nobody unless an operator has
explicitly opted into requireBearer: false.
Authorization is fail-closed¶
- A route with an empty
access.authorizeskips the authorization gate (default-allow — authorization is opt-in per route, not implicit). - A route with
access.authorizenames denies on any policy denying (deny-overrides), on an unsatisfiedrequire, and on a Rego evaluation error (never silently allows on error). - Denied requests get a generic
403 forbiddenwith no policy detail in the body — the reason is logged and recorded on the trace span, not returned to the client.
See authorization.
No credentials in logs or traces¶
- Access tokens and refresh tokens are never logged, never placed in span
attributes, and never included in the
inputpassed to authorization policies. - The access log redacts sensitive query parameters (
code,state,token,access_token,id_token,refresh_token,api_key,client_secret,password,assertion) and refuses to captureAuthorization,Cookie,Proxy-Authorization, orSet-CookieviaaccessLog.headers(a startup error if you try).
See observability.
Only allow insecureSkipVerify for a reason you can justify¶
reverse-proxy.insecureSkipVerify: true disables upstream TLS certificate
verification. It exists for internal backends with self-signed certs you
already trust by network position — don't set it for anything reachable
outside your own infrastructure.
CSRF protection and security headers are on by default¶
Gateway.spec.security is applied gateway-wide, wrapping the whole handler
just inside the forwarded layer and just outside OpenTelemetry/routing —
it covers every route and the raw /auth/* endpoints alike, not just some
routes.
- CSRF (
security.csrf, on by default). Built onnet/http.CrossOriginProtection: a token-less, Fetch-metadata-based defense that allowsGET/HEAD/OPTIONS, same-origin requests, and non-browser requests (noSec-Fetch-Site/Originheader — soAuthorization: BearerAPI clients are entirely unaffected), and rejects a cross-origin, state-changing browser request with403. This is defense-in-depth on top ofSameSite=Lax(above), not a replacement for it —SameSite=Laxis still what keeps the session cookie itself off cross-site requests in the first place. - Security response headers (
security.headers, on by default).X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin, andStrict-Transport-Security(1 year,includeSubDomains) are set on every response unless you override or blank them out.Content-Security-Policyis opt-in (unset by default) — HOG can't pick a safe default CSP without knowing your frontend's script/style/asset origins.
See the configuration reference for the full field table and defaults.
A same-site, cross-origin SPA needs csrf.trustedOrigins
If your frontend and HOG share a registrable domain but live on different
subdomains — e.g. app.example.com calling a HOG instance at
api.example.com — the browser sends Sec-Fetch-Site: same-site, which
CrossOriginProtection still treats as cross-origin. A state-changing
request (POST/PUT/PATCH/DELETE) from that frontend gets 403
until you add https://app.example.com to security.csrf.trustedOrigins.
bypassPatterns is a deliberate CSRF hole
security.csrf.bypassPatterns exempts specific ServeMux-style patterns
from CSRF checking entirely — for an endpoint that legitimately can't
send Origin/Sec-Fetch-Site (e.g. a third-party webhook receiver).
Scope each pattern as narrowly as possible; anything it matches accepts
cross-origin state-changing requests with no CSRF defense at all.