API Key Authentication for Caddy Services¶
Last Updated: November 28, 2025
This document describes the API key authentication patterns available for services protected by caddy-security.
Overview¶
Two types of API key authentication are available:
- Static API Keys - Pre-defined keys for S2S (system-to-system) automation
- User-Generated API Keys - Keys generated by users through the portal UI
Static API Keys (S2S Authentication)¶
Static API keys bypass caddy-security entirely and use native Caddy header matching. They're ideal for:
- GitHub Actions CI/CD pipelines
- Monitoring systems (Prometheus, Gatus)
- Webhook receivers
- Automated scripts
How It Works¶
- Key is stored in SOPS and loaded as an environment variable
- Caddy matches requests with the
X-Api-Keyheader (or custom header) - Matching requests bypass caddy-security and go directly to the backend
- Optional
X-Auth-Sourceheader is injected for audit logging
Configuration¶
Per-Service Static API Keys¶
Add static API keys to individual service configurations:
let
forgeDefaults = import ../lib/defaults.nix { inherit config lib; };
in
{
modules.services.myapp.reverseProxy = {
enable = true;
hostName = "myapp.holthome.net";
backend = {
host = "127.0.0.1";
port = 8080;
};
caddySecurity = {
enable = true;
portal = "pocketid";
policy = "admin";
staticApiKeys = [
# Simple: all paths, any network
(forgeDefaults.mkStaticApiKey "github-actions" "MYAPP_GITHUB_API_KEY")
# Path-restricted
(forgeDefaults.mkStaticApiKeyWithPaths "webhook" "MYAPP_WEBHOOK_KEY" [ "/api/webhook" ])
# Network-restricted
(forgeDefaults.mkStaticApiKeyWithNetworks "internal" "MYAPP_INTERNAL_KEY" [ "10.0.0.0/8" ])
# Full control
(forgeDefaults.mkStaticApiKeyFull {
name = "monitoring";
envVar = "MYAPP_MONITORING_KEY";
headerName = "Authorization"; # Custom header
paths = [ "/api/health" "/metrics" ];
allowedNetworks = [ "10.0.0.0/8" ];
injectAuthHeader = true;
})
];
};
};
}
forgeDefaults Helpers¶
| Helper | Description |
|---|---|
mkStaticApiKey name envVar |
Simple key, all paths, any network |
mkStaticApiKeyWithPaths name envVar paths |
Key restricted to specific path prefixes |
mkStaticApiKeyWithNetworks name envVar networks |
Key restricted to CIDR ranges |
mkStaticApiKeyFull { ... } |
Full control over all options |
SOPS Secret Setup¶
- Generate a secure API key:
- Add to your SOPS secrets file:
- Configure SOPS in NixOS:
- Add to Caddy's environment file:
systemd.services.caddy.serviceConfig.EnvironmentFile = [
config.sops.secrets."caddy/api-keys-env".path
];
Generated Caddyfile¶
The static API key configuration generates:
myapp.holthome.net {
# Static API key: github-actions
@static_api_key_github_actions_myapp_holthome_net {
header X-Api-Key {$MYAPP_GITHUB_API_KEY}
}
route @static_api_key_github_actions_myapp_holthome_net {
header_up X-Auth-Source "static-api-key:github-actions"
reverse_proxy http://127.0.0.1:8080
}
# ... normal caddy-security routes follow ...
}
User-Generated API Keys (Local Identity Store)¶
⚠️ Note: The caddy-security "Portal Settings" page for self-service API key generation requires the Profile UI, which is a separate React application not currently deployed.
The infrastructure below is configured and ready, but users cannot yet generate their own API keys through the web interface. For now, use static API keys (see above) for automation.
For human users who need API access (CLI scripts, personal automation), the local identity store pattern will be available once Profile UI is deployed.
What's Already Configured¶
The following infrastructure is in place (in hosts/forge/services/pocketid.nix):
modules.services.caddy.security = {
# Local identity store for user-generated API keys
localIdentityStores.localdb = {
realm = "local";
path = "/var/lib/caddy/auth/users.json";
};
authenticationPortals.pocketid = {
identityProviders = [ "pocketid" ];
identityStores = [ "localdb" ]; # Enables user API key generation
# ...
};
authorizationPolicies = {
default = {
apiKeyAuth = {
enable = true;
portal = "pocketid";
realm = "local";
};
};
# admins and media policies also have apiKeyAuth enabled
};
};
Future: Portal-Based Key Generation¶
Once Profile UI is deployed, users will be able to:
- Log in via OIDC (PocketID) to any protected service
- Click "Profile" or "Settings" link
- Navigate to API Keys section and generate keys
- Keys are validated via
with api key auth portal <name> realm local
Using User-Generated API Keys¶
# Access a protected service with your personal API key
curl -H "X-Api-Key: $MY_API_KEY" https://prom.holthome.net/api/v1/query?query=up
# Access Grafana API
curl -H "X-Api-Key: $MY_API_KEY" https://grafana.holthome.net/api/dashboards
Policies with API Key Auth Enabled¶
| Policy | Description | API Key Auth |
|---|---|---|
default |
Authenticated users | ✅ Enabled |
admins |
Admin group members | ✅ Enabled |
media |
Media group members | ✅ Enabled |
lan-only |
Automation/internal | ❌ Static keys only |
Comparison¶
| Feature | Static API Keys | User-Generated Keys |
|---|---|---|
| Creation | Admin via SOPS | Users via portal UI |
| Storage | Environment variables | Local JSON file |
| Rotation | Config change + deploy | User self-service |
| Audit | X-Auth-Source header | Portal user claims |
| Use Case | S2S automation | Human CLI/scripts |
| Revocation | Remove from SOPS | User deletes in portal |
Best Practices¶
Static Key Best Practices¶
- Use descriptive names:
github-actions,prometheus-scraper,webhook-receiver - Restrict paths when possible: Only allow access to needed endpoints
- Restrict networks when possible: Limit to expected source IPs
- Enable audit headers: Keep
injectAuthHeader = truefor logging - Rotate regularly: Update SOPS secrets periodically
User Key Best Practices¶
- Use for human access only: CLI tools, personal scripts
- Don't share: Each user should generate their own key
- Revoke unused keys: Clean up keys no longer needed
Security Considerations¶
Static Key Security¶
- Keys bypass caddy-security entirely
- No MFA or session management
- Suitable for trusted automation only
- Log
X-Auth-Sourceheaders for audit trail
User Key Security¶
- Validated against portal session context
- User must have authenticated via OIDC first
- Keys tied to user identity for auditing
- Subject to role-based access control
Troubleshooting¶
Static Key Not Working¶
- Check environment variable is set:
- Verify header is being sent:
- Check Caddy logs for matcher evaluation
User Key Generation Not Available¶
- Verify local identity store is enabled in portal
- Check
/settingsendpoint is accessible - Verify user has authenticated via OIDC first