Caddy token based authentication. Supports static tokens from files, signed API keys, JWT tokens, client certificate authentication, and SPIFFE JWT SVIDs.
{
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.8.4 --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>
}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")
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
}{
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 fakeLicense is Apache 2.0