@ -1,8 +1,10 @@
import { timingSafeEqual } from 'node:crypto' ;
/ *
* Trusted - header SSO ( Culpur Defense fork ) .
*
* Runs before route handlers . When TRUSTED_PROXY_ENABLED is set AND the request ' s
* real socket peer is in TRUSTED_PROXY_IPS , this reads the identity header that a
* Runs before route handlers . When TRUSTED_PROXY_ENABLED is set and the request
* proves it came from the trusted proxy , this reads the identity header that a
* trusted upstream ( an Authentik forward - auth outpost ) has asserted , resolves or
* auto - provisions the matching wg - easy user , and establishes the normal wg - easy
* session . Everything downstream ( getCurrentUser - > definePermissionEventHandler
@ -10,36 +12,69 @@
*
* SECURITY MODEL — why this is not an auth bypass :
* 1 . Off by default . Stock wg - easy never runs this branch .
* 2 . Source - IP allowlist . The identity header is honoured ONLY when the request
* arrives from the trusted proxy . We read the real connection peer via
* getRequestIP ( event , { xForwardedFor : false } ) — NOT X - Forwarded - For , which
* a client could spoof . In our topology the outpost also strips any
* client - supplied X - authentik - * headers and sets its own , so the header cannot
* arrive pre - set from a browser .
* 2 . Shared secret ( PRIMARY ) . The proxy injects TRUSTED_PROXY_SECRET_HEADER with
* a secret only it knows ; wg - easy verifies it with a constant - time compare . A
* direct - to - port attacker on the LAN cannot forge it . THIS is the real trust
* boundary in a shared - bridge topology , where Docker SNATs every inbound
* connection to the docker0 gateway so the source IP alone can ' t tell the
* proxy apart from any other host that can reach the published port .
* 3 . Source - IP allowlist ( OPTIONAL defense - in - depth ) . If TRUSTED_PROXY_IPS is
* set , the real socket peer must also match . Read via getRequestIP ( event ,
* { xForwardedFor : false } ) — the connection remoteAddress , NOT the spoofable
* X - Forwarded - For . Left empty when SNAT makes the peer non - discriminating .
*
* In our topology the outpost also strips any client - supplied X - authentik - * headers
* and sets its own , so the identity header cannot arrive pre - set from a browser .
*
* If the feature is disabled , the source is not allowlisted , or no header is
* present , this middleware does nothing and the request falls through to the
* existing password / session auth untouched .
* If the feature is disabled , the secret is missing / wrong , the ( optional ) IP check
* fails , or no identity header is present , this middleware does nothing and the
* request falls through to the existing password / session auth untouched .
* /
/** Constant-time string compare that never throws on length/'' mismatch. */
function secretMatches ( provided : string | undefined , expected : string ) : boolean {
if ( ! provided ) {
return false ;
}
const a = Buffer . from ( provided ) ;
const b = Buffer . from ( expected ) ;
if ( a . length !== b . length ) {
return false ;
}
return timingSafeEqual ( a , b ) ;
}
export default defineEventHandler ( async ( event ) = > {
if ( ! WG_ENV . TRUSTED_PROXY_ENABLED ) {
return ;
}
// Fail closed: enabling the feature without a secret must NOT trust anything.
if ( ! WG_ENV . TRUSTED_PROXY_SECRET ) {
return ;
}
// Only act on API calls and page loads; leave setup/i18n alone (mirrors setup.ts).
const url = getRequestURL ( event ) ;
if ( url . pathname . startsWith ( '/_i18n/' ) || url . pathname . startsWith ( '/setup/' ) ) {
return ;
}
// 1. Trust gate — the real socket peer must be an allowlisted proxy.
// xForwardedFor:false makes h3 return the connection remoteAddress, so a
// spoofed X-Forwarded-For cannot satisfy this check.
const peer = getRequestIP ( event , { xForwardedFor : false } ) ;
if ( ! peer || ! WG_ENV . TRUSTED_PROXY_IPS . includes ( peer ) ) {
// 1a. Trust gate — the proxy-injected shared secret must match (constant time).
const providedSecret = getHeader ( event , WG_ENV . TRUSTED_PROXY_SECRET_HEADER ) ;
if ( ! secretMatches ( providedSecret , WG_ENV . TRUSTED_PROXY_SECRET ) ) {
return ;
}
// 1b. Optional defense-in-depth — if an IP allowlist is configured, the real
// socket peer must also be in it. Skipped when the list is empty (SNAT case).
if ( WG_ENV . TRUSTED_PROXY_IPS . length > 0 ) {
const peer = getRequestIP ( event , { xForwardedFor : false } ) ;
if ( ! peer || ! WG_ENV . TRUSTED_PROXY_IPS . includes ( peer ) ) {
return ;
}
}
// 2. Read the identity the upstream verified.
const username = getHeader ( event , WG_ENV . TRUSTED_PROXY_HEADER ) ? . trim ( ) ;
if ( ! username ) {
@ -76,6 +111,6 @@ export default defineEventHandler(async (event) => {
await session . update ( { userId : user.id } ) ;
SERVER_DEBUG (
` Trusted-header SSO: session established for ${ user . id } ( ${ user . username } ) from ${ peer } `
` Trusted-header SSO: session established for ${ user . id } ( ${ user . username } ) `
) ;
} ) ;