Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Typestring

The base domain for all virtual hosts.


server.proxy.extensions

Typeattribute 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

Typefunction 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

Typeboolean
Defaultfalse

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

Typeboolean
Defaultfalse

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

Typefunction 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

Typesigned integer
Default100

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

Typenull or module
Defaultnull

Optional module to inject into each vhost submodule. Use options.<extensionName> (relative to vhost scope) to declare per-vhost options.


server.proxy.kanidmContexts

Typeattribute set of (submodule)
Default{ }

Shared Kanidm OAuth2 context configurations.


server.proxy.kanidmContexts.<name>.allowGroups

Typelist 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

Typenull or string
Defaultnull
Example"auth.example.com"

The domain where Kanidm is hosted. Defaults to auth.<server.proxy.domain> if not specified.


server.proxy.kanidmContexts.<name>.scopes

Typelist of string
Default[ "openid" "email" "profile" "groups" ]

OAuth scopes to request from Kanidm.


server.proxy.kanidmContexts.<name>.tokenLifetime

Typesigned integer
Default3600

Token lifetime in seconds for the authentication portal.


server.proxy.virtualHosts

Typeattribute set of (submodule)
Default{ }

Virtual hosts to be handled by the IO server and forwarded to the respective backend.


server.proxy.virtualHosts.<name>.aliases

Typelist 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

Typestring
Default${subdomain}.${getIOPrimaryHostAttr "server.proxy.domain"}

The base url including the configured base domain name.


server.proxy.virtualHosts.<name>.extensions

Typenull or (list of string)
Defaultnull

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

Typestring
Default""

Configuration to be placed in the caddy virtualHost extraConfig.


server.proxy.virtualHosts.<name>.kanidm

Typenull or (submodule)
Defaultnull

Enable Kanidm OAuth2 authentication for this virtual host.


server.proxy.virtualHosts.<name>.kanidm.allowGroups

Typelist 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

Typenull or string
Defaultnull
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

Typelist of string
Default[ ]
Example[ "/health" "/api/webhooks/*" ]

List of path patterns that should bypass authentication.


server.proxy.virtualHosts.<name>.kanidm.context

Typestring
Default"‹name›"

The OAuth context name for this virtual host.


server.proxy.virtualHosts.<name>.kanidm.scopes

Typelist of string
Default[ "openid" "email" "profile" "groups" ]

OAuth scopes to request from Kanidm.


server.proxy.virtualHosts.<name>.kanidm.tokenLifetime

Typesigned integer
Default3600

Token lifetime in seconds for the authentication portal.


server.proxy.virtualHosts.<name>.l4

Typenull or (submodule)
Defaultnull

This option has no description.


server.proxy.virtualHosts.<name>.l4.config

Typestring
Default""

Configuration for the L4 plugin.


server.proxy.virtualHosts.<name>.l4.listenPort

Type16 bit unsigned integer; between 0 and 65535 (both inclusive)

Port to listen on for L4 traffic.


server.proxy.virtualHosts.<name>.l4.protocol

Typeone of "tcp", "udp"
Default"tcp"

Protocol for L4 listener.


server.proxy.virtualHosts.<name>.listenPorts

Typenon-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

Typelist 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

Typeboolean
Defaultfalse

When enabled this service will be accessible to the public via Cloudflared Tunnels.


server.proxy.virtualHosts.<name>.requireApiKey

Typenull or (submodule)
Defaultnull

This option has no description.


server.proxy.virtualHosts.<name>.requireApiKey.bypassPaths

Typelist of string
Default[ ]
Example[ "/health" "/api/webhooks/*" ]

List of path patterns that bypass API key authentication.


server.proxy.virtualHosts.<name>.requireApiKey.enable

Typeboolean
Defaultfalse

Enable API key authentication for this virtual host.


server.proxy.virtualHosts.<name>.useAcmeCerts

Typeboolean
Defaulttrue

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 of services.caddy.virtualHosts and ACME certificate requests. L4 (TCP/UDP) forwarding is handled by the l4 extension, not by config.nix.
  • kanidm.nix — Authentication security: generates the Caddy security block, 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:

FieldTypeDefaultDescription
priorityint100Lower values = earlier Caddy config placement. Ranges: 0-49 reserved, 50-99 auth, 100-199 general, 200+ post-processing
enableboolfalseGlobally enabled. Set via mkDefault based on detected config
consumesExtraConfigboolfalseWhen true, the extension embeds vh._resolvedExtraConfig in its output. config.nix skips appending raw extraConfig
configvhostName -> vhostAttrSet -> hostConfig -> strrequiredPer-vhost Caddy directive generator
globalConfighostConfig -> str_ → ""Top-level Caddy globalConfig directives
vhostModulenullOr deferredModulenullPer-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’s extraConfig with replaceLocalHost applied) 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

  1. Create file: modules/nixos/server/proxy/extensions/<name>.nix
  2. Import in proxy/default.nix.
  3. Set server.proxy.extensions.<name> with priority, config function, etc.
  4. Declare per-vhost options via options.server.proxy.virtualHosts with attrsOf (submodule ...).
  5. Use proxyLib for 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

ExtensionPriorityPurpose
l410L4 TCP/UDP forwarding (layer4 Caddy block + firewall ports)
kanidm50Kanidm OAuth2 authentication per vhost
api-key-auth50Static API key authentication per vhost (with bypass paths)
dashboard200Auto-generate dashboard items
cloudflared200Cloudflared tunnel ingress

Secrets

Kanidm OAuth2 Context

Authentication requires specific secrets per context, managed via sops-nix:

  1. KANIDM/OAUTH2/<UPPER_CONTEXT>_SECRET: Provisioning secret for Kanidm systems.
  2. OAUTH_<PREFIX>_CLIENT_SECRET: The OAuth2 client secret for Caddy.
  3. <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 default Caddy snippet for common headers and security settings. When public is enabled, it also expects a public snippet.
  • Dashboard Integration: Services defined in server.proxy.virtualHosts are automatically added to the server dashboard with default titles and icons derived from the host name.
  • Layer 4 Forwarding: L4 forwarding uses the caddy.layer4 plugin for non-HTTP traffic like database connections or SSH. Managed by the l4 extension (modules/nixos/server/proxy/extensions/l4.nix), which auto-enables when any vhost has l4 != null.

References