Server Proxy — Unified Caddy Reverse Proxy
Purpose
The Proxy submodule provides a unified interface for exposing internal services through Caddy. It handles virtual host configuration, automatic SSL via ACME, OAuth2 authentication with Kanidm, static API key authentication, and public exposure through Cloudflared tunnels. It abstracts the complexity of reverse proxying by allowing services to define their proxy requirements within their own module configuration, automatically coordinating between backend hosts and the primary IO host to ensure ports are open and traffic is correctly routed.
Entry Point
- Main file:
modules/nixos/server/proxy/default.nix - Supporting file:
modules/nixos/server/proxy/options.nix - Supporting file:
modules/nixos/server/proxy/config.nix - Supporting file:
modules/nixos/server/proxy/kanidm.nix - Supporting file:
modules/nixos/server/proxy/extensions.nix - Supporting file:
modules/nixos/server/proxy/extensions/l4.nix
Options
server.proxy.domain
| Type | string |
The base domain for all virtual hosts.
server.proxy.extensions
| Type | attribute set of (submodule) |
| Default | { } |
Registry of proxy extensions. Each extension provides config functions that are injected into vhost Caddy blocks, sorted by priority.
server.proxy.extensions.<name>.config
| Type | function that evaluates to a(n) function that evaluates to a(n) function that evaluates to a(n) string |
Function: vhostName -> vhostAttrSet -> hostConfig -> string. Returns Caddy directives to inject, or ‘’ for no-op. The vhostAttrSet includes the resolved extraConfig (already localhost-replaced) as _resolvedExtraConfig.
server.proxy.extensions.<name>.consumesExtraConfig
| Type | boolean |
| Default | false |
Whether this extension embeds extraConfig inside its output. When true, config.nix skips the post-extension extraConfig append for this vhost.
server.proxy.extensions.<name>.enable
| Type | boolean |
| Default | false |
Whether this extension is globally enabled.
Each extension SHOULD auto-detect whether it has work to do and set this to true via mkDefault in its module config.
User can explicitly override to force-disable (higher merge priority than mkDefault).
server.proxy.extensions.<name>.globalConfig
| Type | function that evaluates to a(n) string |
| Default | <function> |
Function: hostConfig -> string. Returns Caddy directives to inject into the top-level globalConfig block. Only called on the IO primary host. Sorted by priority across extensions.
server.proxy.extensions.<name>.priority
| Type | signed integer |
| Default | 100 |
Lower values = earlier in Caddy config. Priority ranges: 0-49 reserved, 50-99 auth, 100-199 general, 200+ post-processing.
server.proxy.extensions.<name>.vhostModule
| Type | null or module |
| Default | null |
Optional module to inject into each vhost submodule. Use options.<extensionName> (relative to vhost scope) to declare per-vhost options.
server.proxy.kanidmContexts
| Type | attribute set of (submodule) |
| Default | { } |
Shared Kanidm OAuth2 context configurations.
server.proxy.kanidmContexts.<name>.allowGroups
| Type | list of string |
| Default | [ ] |
| Example | [ "idm_all_persons@auth.racci.dev" "admins@auth.racci.dev" ] |
Default list of Kanidm groups allowed to access virtualHosts using this context.
server.proxy.kanidmContexts.<name>.authDomain
| Type | null or string |
| Default | null |
| Example | "auth.example.com" |
The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.
server.proxy.kanidmContexts.<name>.scopes
| Type | list of string |
| Default | [ "openid" "email" "profile" "groups" ] |
OAuth scopes to request from Kanidm.
server.proxy.kanidmContexts.<name>.tokenLifetime
| Type | signed integer |
| Default | 3600 |
Token lifetime in seconds for the authentication portal.
server.proxy.virtualHosts
| Type | attribute set of (submodule) |
| Default | { } |
Virtual hosts to be handled by the IO server and forwarded to the respective backend.
server.proxy.virtualHosts.<name>.aliases
| Type | list of string |
| Default | [ ] |
A list of virtual host names that should be routed using this configuration. Options added here will inherit the base domain specified in <server.proxy.domain>.
server.proxy.virtualHosts.<name>.baseUrl
| Type | string |
| Default | ${subdomain}.${getIOPrimaryHostAttr "server.proxy.domain"} |
The base url including the configured base domain name.
server.proxy.virtualHosts.<name>.extensions
| Type | null or (list of string) |
| Default | null |
List of extension names to enable for this virtual host. When null (default), all globally enabled extensions apply. When set to a list, only those named extensions apply. Set to [] to disable all extensions for this vhost.
server.proxy.virtualHosts.<name>.extraConfig
| Type | string |
| Default | "" |
Configuration to be placed in the caddy virtualHost extraConfig.
server.proxy.virtualHosts.<name>.kanidm
| Type | null or (submodule) |
| Default | null |
Enable Kanidm OAuth2 authentication for this virtual host.
server.proxy.virtualHosts.<name>.kanidm.allowGroups
| Type | list of string |
| Default | [ ] |
| Example | [ "idm_all_persons@auth.racci.dev" "admins@auth.racci.dev" ] |
Default list of Kanidm groups allowed to access virtualHosts using this context.
server.proxy.virtualHosts.<name>.kanidm.authDomain
| Type | null or string |
| Default | null |
| Example | "auth.example.com" |
The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.
server.proxy.virtualHosts.<name>.kanidm.bypassPaths
| Type | list of string |
| Default | [ ] |
| Example | [ "/health" "/api/webhooks/*" ] |
List of path patterns that should bypass authentication.
server.proxy.virtualHosts.<name>.kanidm.context
| Type | string |
| Default | "‹name›" |
The OAuth context name for this virtual host.
server.proxy.virtualHosts.<name>.kanidm.scopes
| Type | list of string |
| Default | [ "openid" "email" "profile" "groups" ] |
OAuth scopes to request from Kanidm.
server.proxy.virtualHosts.<name>.kanidm.tokenLifetime
| Type | signed integer |
| Default | 3600 |
Token lifetime in seconds for the authentication portal.
server.proxy.virtualHosts.<name>.l4
| Type | null or (submodule) |
| Default | null |
This option has no description.
server.proxy.virtualHosts.<name>.l4.config
| Type | string |
| Default | "" |
Configuration for the L4 plugin.
server.proxy.virtualHosts.<name>.l4.listenPort
| Type | 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
Port to listen on for L4 traffic.
server.proxy.virtualHosts.<name>.l4.protocol
| Type | one of "tcp", "udp" |
| Default | "tcp" |
Protocol for L4 listener.
server.proxy.virtualHosts.<name>.listenPorts
| Type | non-empty (list of 16 bit unsigned integer; between 0 and 65535 (both inclusive)) |
| Default | [ 443 ] |
Port(s) to listen on for incoming traffic for this virtual host. If multiple ports are specified, the virtual host will be accessible on all of them.
server.proxy.virtualHosts.<name>.ports
| Type | list of 16 bit unsigned integer; between 0 and 65535 (both inclusive) |
| Default | [ ] |
Ports to be opened from the host for IO Hosts to forward traffic to.
server.proxy.virtualHosts.<name>.public
| Type | boolean |
| Default | false |
When enabled this service will be accessible to the public via Cloudflared Tunnels.
server.proxy.virtualHosts.<name>.requireApiKey
| Type | null or (submodule) |
| Default | null |
This option has no description.
server.proxy.virtualHosts.<name>.requireApiKey.bypassPaths
| Type | list of string |
| Default | [ ] |
| Example | [ "/health" "/api/webhooks/*" ] |
List of path patterns that bypass API key authentication.
server.proxy.virtualHosts.<name>.requireApiKey.enable
| Type | boolean |
| Default | false |
Enable API key authentication for this virtual host.
server.proxy.virtualHosts.<name>.useAcmeCerts
| Type | boolean |
| Default | true |
Whether to generate and use ACME certificates for this virtual host. If false, you must provide your own TLS configuration in extraConfig via the caddy tls directive.
Architecture / Services / Scope
File Layout
default.nix— Logic and helpers: resolving OAuth contexts and mapping local addresses to backend hostnames.options.nix— Option definitions for virtual hosts and shared contexts.config.nix— Caddy integration: generation ofservices.caddy.virtualHostsand ACME certificate requests. L4 (TCP/UDP) forwarding is handled by thel4extension, not byconfig.nix.kanidm.nix— Authentication security: generates the Caddysecurityblock, including identity providers, portals, and authorization policies.extensions.nix— System integration: connects the proxy to the dashboard, Cloudflared tunnels, and automates Kanidm client provisioning.
Extension System
The proxy module supports a registry-based extension system. Extensions are self-contained modules that inject Caddy directives into virtual host configurations — without modifying proxy internals.
Extension Registry
Extensions register themselves via server.proxy.extensions.<name>, an attribute set of submodules. Each extension has:
| Field | Type | Default | Description |
|---|---|---|---|
priority | int | 100 | Lower values = earlier Caddy config placement. Ranges: 0-49 reserved, 50-99 auth, 100-199 general, 200+ post-processing |
enable | bool | false | Globally enabled. Set via mkDefault based on detected config |
consumesExtraConfig | bool | false | When true, the extension embeds vh._resolvedExtraConfig in its output. config.nix skips appending raw extraConfig |
config | vhostName -> vhostAttrSet -> hostConfig -> str | required | Per-vhost Caddy directive generator |
globalConfig | hostConfig -> str | _ → "" | Top-level Caddy globalConfig directives |
vhostModule | nullOr deferredModule | null | Per-vhost option declarations |
Per-Vhost Extension Selection
Each vhost has server.proxy.virtualHosts.<name>.extensions (default null = all enabled extensions). Set to a list of extension names for selective enablement, or [] to disable all extensions.
Config Function Signature
config :: vhostName -> vhostAttrSet -> hostConfig -> string
Arguments:
vhostName(str): The vhost’s attribute name (e.g.,"grafana").vhostAttrSet: The full vhost attribute set, including_resolvedExtraConfig(user’sextraConfigwithreplaceLocalHostapplied) and_name.hostConfig: Full host-level NixOS config.
GlobalConfig Function Signature
globalConfig :: hostConfig -> string
Called once per enabled extension on the IO primary host. Output concatenated into services.caddy.globalConfig, sorted by extension priority.
Auto-Enable Pattern
Extensions auto-detect whether they have work to do using mkDefault; users can force-disable with explicit enable = false.
Priority Ordering
Extensions sort by priority ascending. Equal priorities break alphabetically by extension name. Extensions with lower priority numbers generate config earlier.
Authoring a New Extension
- Create file:
modules/nixos/server/proxy/extensions/<name>.nix - Import in
proxy/default.nix. - Set
server.proxy.extensions.<name>with priority, config function, etc. - Declare per-vhost options via
options.server.proxy.virtualHostswithattrsOf (submodule ...). - Use
proxyLibfor helpers:replaceLocalHost,resolveKanidmContext,hasAnyKanidm.
API Key Auth Extension
The api-key-auth extension provides static API key authentication for virtual hosts. When enabled, requests must include a valid Req-API-Key header matching a securely generated secret. Bypass paths are supported per vhost. Mutual exclusivity with Kanidm on the same vhost is enforced by the existing consumesExtraConfig assertion.
Migrated Extensions
| Extension | Priority | Purpose |
|---|---|---|
l4 | 10 | L4 TCP/UDP forwarding (layer4 Caddy block + firewall ports) |
kanidm | 50 | Kanidm OAuth2 authentication per vhost |
api-key-auth | 50 | Static API key authentication per vhost (with bypass paths) |
dashboard | 200 | Auto-generate dashboard items |
cloudflared | 200 | Cloudflared tunnel ingress |
Secrets
Kanidm OAuth2 Context
Authentication requires specific secrets per context, managed via sops-nix:
KANIDM/OAUTH2/<UPPER_CONTEXT>_SECRET: Provisioning secret for Kanidm systems.OAUTH_<PREFIX>_CLIENT_SECRET: The OAuth2 client secret for Caddy.<PREFIX>_SHARED_KEY: A shared key used by Caddy to sign and verify authentication tokens.
These are automatically managed if Kanidm provisioning is enabled on the same host.
API Key Auth
Secrets are auto-generated via sops at PROXY_AUTH/<VHOST_NAME>_API_KEY, injected via systemd LoadCredential.
Operational Notes / Assumptions
- Caddy Integration: The module assumes the existence of a
defaultCaddy snippet for common headers and security settings. Whenpublicis enabled, it also expects apublicsnippet. - Dashboard Integration: Services defined in
server.proxy.virtualHostsare automatically added to the server dashboard with default titles and icons derived from the host name. - Layer 4 Forwarding: L4 forwarding uses the
caddy.layer4plugin for non-HTTP traffic like database connections or SSH. Managed by thel4extension (modules/nixos/server/proxy/extensions/l4.nix), which auto-enables when any vhost hasl4 != null.