Caddy token based authentication. Supports static tokens from files, signed API keys, JWT tokens, client certificate authentication, and SPIFFE JWT SVIDs. Handles millions of daily request in production environments across the globe.
{
order token first
}
:3000 {
token {
jwt {
issuer https://dex.issuer.lan
group admin
}
}
reverse_proxy https://some.service.internal {
header_up Host {http.reverse_proxy.upstream.hostport}
}
}Read Extending Caddy to get an overview of what interfaces you need to implement.
You first need to build a new caddy executable with this plugin. The easiest way is to do this with xcaddy.
Install xcaddy:
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latestAfter xcaddy installation you can build caddy with this plugin by executing:
xcaddy build v2.11.2 --with github.com/loafoe/caddy-tokenThe token directive supports multiple authentication methods and configuration options.
token {
file <token_file>
jwt {
issuer <issuer_url>
verify <true|false>
group <group_name>
}
signed {
key <signing_key>
scope <scope_name>
}
client_ca {
debug <true|false>
default_org <organization_name>
}
injectOrgHeader <true|false>
allowUpstreamAuth <true|false>
tenantOrgClaim <claim_name>
debug <true|false>
}Specifies a file containing static API tokens.
Syntax: file <path_to_token_file>
Example:
token {
file /etc/caddy/tokens.txt
}Configures JWT token validation using an OIDC issuer.
Sub-directives:
issuer <url>- OIDC issuer URL for token validationverify <true|false>- Enable/disable token verification (default: true)group <name>- Required group claim (can be specified multiple times)
Example:
token {
jwt {
issuer https://auth.example.com
verify true
group admin
group developers
}
}Configures signed API key validation.
Sub-directives:
key <signing_key>- The signing key for API key validationscope <name>- Required scope (can be specified multiple times)
Example:
token {
signed {
key "your-signing-key-here"
scope read
scope write
}
}Enables client certificate authentication.
Sub-directives:
debug <true|false>- Enable debug logging for client CA operationsdefault_org <organization_name>- Organization name to set in X-Scope-OrgID header (default: "anonymous")
⚠️ Security requirement:client_caauthenticates a request only when the client certificate chains to a trusted CA, i.e. the TLS listener must be configured withclient_auth { mode require_and_verify }and atrusted_ca_cert/trusted_ca_cert_file. Withoutrequire_and_verify, the plugin rejects the request rather than trusting an unverified, possibly self-signed certificate.
Example:
token {
client_ca {
debug true
default_org "my-organization"
}
}Configures SPIFFE JWT SVID authentication. See SPIFFE Integration for detailed documentation and examples.
Controls whether to inject the X-Scope-OrgID header based on token claims.
Syntax: injectOrgHeader <true|false>
Default: true
Example:
token {
injectOrgHeader false
}Allows upstream services to set authentication headers.
Syntax: allowUpstreamAuth <true|false>
Default: false
Example:
token {
allowUpstreamAuth true
}Specifies which JWT claim to use for tenant organization mapping.
Syntax: tenantOrgClaim <claim_name>
Options: ort (observability read tenants), owt (observability write tenants)
Example:
token {
tenantOrgClaim ort
}
### `debug`
Enables top-level debug logging.
**Syntax:** `debug <true|false>`
**Default:** `false`
**Example:**
```caddyfile
token {
debug true
}
## Complete Configuration Examples
### Static Token File Authentication
```caddyfile
{
order token first
}
:8080 {
token {
file /etc/caddy/api-tokens.txt
injectOrgHeader true
}
respond "Authenticated with static token"
}
{
order token first
}
:8080 {
token {
jwt {
issuer https://auth.example.com
verify true
group admin
}
tenantOrgClaim ort
injectOrgHeader true
}
reverse_proxy backend:3000
}{
order token first
}
:8080 {
token {
signed {
key "your-secret-signing-key"
scope api:read
scope api:write
}
}
reverse_proxy api-server:8000
}{
order token first
}
:8080 {
token {
client_ca {
debug true
default_org "secure-clients"
}
allowUpstreamAuth false
}
reverse_proxy secure-service:9000
}{
order token first
}
:8080 {
token {
file /etc/caddy/tokens.txt
jwt {
issuer https://sso.company.com
group employees
}
signed {
key "api-signing-key"
scope service:access
}
client_ca {
debug false
default_org "combined-clients"
}
injectOrgHeader true
allowUpstreamAuth false
tenantOrgClaim ort
}
reverse_proxy internal-service:5000
}SPIFFE (Secure Production Identity Framework For Everyone) provides a standard for service identity in distributed systems. This plugin supports SPIFFE JWT SVIDs for workload authentication.
- Workloads present JWT SVIDs in the
Authorization: Bearer <token>header - The plugin extracts the SPIFFE ID from the JWT's
subclaim (e.g.,spiffe://cluster.local/ns/prod/sa/api) - The trust domain is looked up from the SPIFFE ID to determine key source:
- Trust domains with
jwks_url: Keys fetched via HTTP from the JWKS endpoint - Trust domains without
jwks_url: Keys fetched from the local Workload API
- Trust domains with
- JWT signature is verified against the retrieved public key
- The
X-Scope-OrgIDheader is set based on trust domain configuration
token {
spiffe {
workload_socket <socket_path>
trust_domain <domain> {
jwks_url <url>
audience <audience>
org <organization>
org_from_path <true|false>
org_path_index <index>
org_claim <claim_name>
}
allowed_ids <pattern>
default_org <organization>
debug <true|false>
}
}| Directive | Description |
|---|---|
workload_socket |
SPIFFE Workload API socket path (e.g., unix:///run/spire/sockets/agent.sock). Also reads from SPIFFE_ENDPOINT_SOCKET env var. |
trust_domain |
Configure a trust domain (repeatable). Contains sub-directives below. |
jwks_url |
JWKS endpoint for JWT verification. If omitted, uses Workload API. |
audience |
Required audience claim value. |
org |
Static organization name for this trust domain. |
org_from_path |
Extract organization from SPIFFE ID path segments. |
org_path_index |
Path segment index to use as organization (0-indexed). |
org_claim |
Extract organization from a JWT claim. |
allowed_ids |
SPIFFE ID pattern to allow (repeatable). Supports * and ** wildcards. |
default_org |
Fallback organization when extraction fails (default: anonymous). |
debug |
Enable debug logging. |
Organization Extraction Priority:
- Static
orgvalue (if configured) - JWT claim via
org_claim(if configured and claim exists) - Path segment via
org_from_pathandorg_path_index(if configured) default_orgfallback
token {
spiffe {
trust_domain example.org {
jwks_url https://spire.example.org/.well-known/jwks.json
audience myapi
org_from_path true
org_path_index 1
}
allowed_ids spiffe://example.org/tenant/*/service/*
default_org anonymous
}
}token {
spiffe {
workload_socket unix:///run/spire/sockets/agent.sock
trust_domain cluster.local {
audience myapi
org_from_path true
org_path_index 1
}
allowed_ids spiffe://cluster.local/ns/*/sa/*
default_org anonymous
}
}token {
spiffe {
trust_domain prod.example.org {
jwks_url https://spire-prod.example.org/keys
audience prod-api
org production
}
trust_domain staging.example.org {
jwks_url https://spire-staging.example.org/keys
audience staging-api
org staging
}
trust_domain partners.example.org {
jwks_url https://spire-partners.example.org/keys
audience partner-api
org_claim partner_id
}
default_org anonymous
}
}Authenticate workloads from both local (Workload API) and remote (JWKS URL) trust domains:
token {
spiffe {
workload_socket unix:///run/spire/sockets/agent.sock
# Local - uses Workload API (no jwks_url)
trust_domain cluster.local {
audience myapi
org_from_path true
org_path_index 1
}
# Remote - uses JWKS URL
trust_domain remote-cluster.example.org {
jwks_url https://oidc-discovery.remote-cluster.example.org/keys
audience myapi
org_from_path true
org_path_index 1
}
allowed_ids spiffe://cluster.local/ns/*/sa/*
allowed_ids spiffe://remote-cluster.example.org/ns/*/sa/*
default_org anonymous
}
}The allowed_ids directive supports glob patterns:
| Pattern | Matches |
|---|---|
spiffe://example.org/service/api |
Exact match only |
spiffe://example.org/service/* |
Any single segment: /service/api, /service/web |
spiffe://example.org/ns/** |
Any path under /ns/: /ns/prod/sa/api, /ns/dev/sa/web/v2 |
spiffe://*/service/* |
Any trust domain with /service/{name} path |
For a SPIFFE ID like spiffe://cluster.local/ns/production/sa/api-service:
| Configuration | Extracted Org |
|---|---|
org production |
production (static) |
org_from_path true, org_path_index 0 |
ns |
org_from_path true, org_path_index 1 |
production |
org_from_path true, org_path_index 2 |
sa |
org_from_path true, org_path_index 3 |
api-service |
org_claim tenant |
Value of tenant claim in JWT |
- SPIRE Server deployed in your cluster
- SPIRE Agent running as a DaemonSet
- Workloads registered with SPIRE
apiVersion: apps/v1
kind: Deployment
metadata:
name: caddy-gateway
spec:
template:
spec:
containers:
- name: caddy
image: your-caddy-image:latest
volumeMounts:
- name: spire-agent-socket
mountPath: /run/spire/sockets
readOnly: true
volumes:
- name: spire-agent-socket
hostPath:
path: /run/spire/sockets
type: DirectoryapiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: caddy-gateway
spec:
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
podSelector:
matchLabels:
app: caddy-gateway
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: gateway| Variable | Description |
|---|---|
SPIFFE_ENDPOINT_SOCKET |
Default Workload API socket path (used if workload_socket not configured) |
The plugin checks for authentication in the following order:
-
Upstream Authentication - When
allowUpstreamAuthis enabled, allows upstreamX-Scope-OrgIDheaders -
Client Certificate Authentication - When
client_cais configured, checks for TLS client certificates and setsX-Scope-OrgIDto the configureddefault_orgvalue -
SPIFFE JWT SVID Authentication - When
spiffeis configured, validates Bearer tokens as SPIFFE JWT SVIDs:- Extracts trust domain from the
subclaim (SPIFFE ID) - Verifies signature against the trust domain's JWKS
- Validates audience claim
- Matches SPIFFE ID against allowed patterns
- Sets
X-Scope-OrgIDbased on trust domain configuration
- Extracts trust domain from the
-
API Key Authentication - Checks for API keys in:
X-Api-Keyheader- Basic Auth password field
Authorization: Bearer <token>header
-
JWT Token Authentication - Validates JWT tokens from:
X-Id-Tokenheader- Verifies against configured OIDC issuer
A companion CLI tool is available to generate static tokens for use with this plugin.
go install github.com/loafoe/caddy-token/cmd/caddy-token-gen@latestcaddy-token-gen g -e client-test -r us-east -p fake -o fake -k "your-secret-signing-key"Use --ttl to set a token lifetime (the value is any Go duration, e.g. 720h). The
token is rejected after it expires. When --ttl is omitted (or zero), the token
never expires — prefer setting one.
caddy-token-gen g -e client-test -r us-east -p fake -o fake -k "your-secret-signing-key" --ttl 720hThe Docker images published to GitHub Container Registry (ghcr.io/loafoe/caddy-token) are signed using Cosign with keyless signing (OIDC via GitHub Actions). We also generate and attest a Software Bill of Materials (SBOM) in SPDX format for each image.
You can verify the image signatures and the SBOM attestations using the instructions below.
To verify the signature of a container image:
cosign verify ghcr.io/loafoe/caddy-token:<tag> \
--certificate-identity-regexp '^https://github.com/loafoe/caddy-token/\.github/workflows/build_docker.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comTo verify the SBOM attestation on the container image:
cosign verify-attestation --type spdxjson \
--certificate-identity-regexp '^https://github.com/loafoe/caddy-token/\.github/workflows/build_docker.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/loafoe/caddy-token:<tag>To extract and save the SBOM in SPDX JSON format:
cosign verify-attestation --type spdxjson \
--certificate-identity-regexp '^https://github.com/loafoe/caddy-token/\.github/workflows/build_docker.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/loafoe/caddy-token:<tag> \
| jq -r '.payload | @base64d | fromjson | .predicate' > sbom.spdx.jsonX-Scope-OrgID/X-Grafana-Org-Idare stripped from inbound requests before authentication unlessallowUpstreamAuth trueis set. This prevents a client from spoofing a tenant. Only enableallowUpstreamAuthwhen this plugin sits behind another trusted authenticator that sets these headers.- Client certificate auth requires a verified chain. See the
client_canote — configure the TLS listener withrequire_and_verify. jwt { verify false }does not disable signature verification. A token whose signature cannot be verified is always rejected (fail closed). The option only relaxes issuer/expiry strictness within the verifier; it never authorizes on claims from an unverified JWT.- API key expiry is enforced. Keys minted with a TTL (
caddy-token-gen g --ttl ...) are rejected after expiry. Keys minted without a TTL never expire — prefer setting one. default_orgis a fail-open fallback. When org extraction fails (JWT/SPIFFE), requests are admitted underdefault_org(defaultanonymous). Ensure the upstream treats this tenant as unprivileged, or scopeallowed_ids/claims so extraction cannot silently fall back.- Container image runs as a non-root user and pins its base images by digest.
License is Apache 2.0