Skip to content

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:

  1. Static API Keys - Pre-defined keys for S2S (system-to-system) automation
  2. 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

  1. Key is stored in SOPS and loaded as an environment variable
  2. Caddy matches requests with the X-Api-Key header (or custom header)
  3. Matching requests bypass caddy-security and go directly to the backend
  4. Optional X-Auth-Source header 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

  1. Generate a secure API key:
openssl rand -base64 32
  1. Add to your SOPS secrets file:
# secrets/forge.sops.yaml
caddy/api-keys/myapp-github: "your-secure-key-here"
  1. Configure SOPS in NixOS:
sops.secrets."caddy/api-keys/myapp-github" = {
  owner = "caddy";
  group = "caddy";
};
  1. 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:

  1. Log in via OIDC (PocketID) to any protected service
  2. Click "Profile" or "Settings" link
  3. Navigate to API Keys section and generate keys
  4. 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

  1. Use descriptive names: github-actions, prometheus-scraper, webhook-receiver
  2. Restrict paths when possible: Only allow access to needed endpoints
  3. Restrict networks when possible: Limit to expected source IPs
  4. Enable audit headers: Keep injectAuthHeader = true for logging
  5. Rotate regularly: Update SOPS secrets periodically

User Key Best Practices

  1. Use for human access only: CLI tools, personal scripts
  2. Don't share: Each user should generate their own key
  3. 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-Source headers 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

  1. Check environment variable is set:
sudo systemctl show caddy -p Environment | grep API_KEY
  1. Verify header is being sent:
curl -H "X-Api-Key: your-key" https://service.example.com/api/test
  1. Check Caddy logs for matcher evaluation

User Key Generation Not Available

  1. Verify local identity store is enabled in portal
  2. Check /settings endpoint is accessible
  3. Verify user has authenticated via OIDC first

References