Config
Auth
AI agents: fetch https://docs.erpc.cloud/config/auth.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /config/auth.llms.txt

Authentication

Every request eRPC receives can be checked before it ever touches an upstream. Stack as many strategies as you need — static token, signed JWT, Ethereum wallet signature, or IP allowlist — and the first one that passes wins. Tie each identity directly to a rate-limit budget so power users and free-tier callers never share the same quota.

Quick taste

Illustrative, not a tuned production config — one secret token, one budget:

projects[].auth
erpc.yaml
projects:  - id: main    auth:      strategies:        - type: secret          # bind this identity to its own rate-limit budget          rateLimitBudget: backend-tier          secret:            id: backend            value: ${MY_SECRET_VALUE}

Agent reference

Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly:

Prompt Example #1: lock down my project with a static token
Add a secret-token auth strategy to my eRPC project so that only callers
who supply the correct token in X-ERPC-Secret-Token (or ?secret=) can reach any
upstream. Bind the token to an existing rate-limit budget so the backend tier gets
its own quota. Work with my existing eRPC config. Read the full reference first:
https://docs.erpc.cloud/config/auth.llms.txt
Prompt Example #2: set up JWT auth with per-user rate-limit tiers
Configure eRPC to verify signed JWT bearer tokens (RS256) for my
public-facing project. Different token tiers should get different rate-limit
budgets — embed the budget ID in the token's rlm claim so a single strategy
handles all users without separate config entries. Include key rotation via
kid so I can add a new key without downtime. Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/auth.llms.txt
Prompt Example #3: migrate to database auth without a redeploy
Replace my static secret strategy in my eRPC config with a database
strategy backed by PostgreSQL so I can add and revoke API keys in the database
without redeploying eRPC. Enable fail-open with an emergency rate-limit budget
so the RPC endpoint stays reachable if the auth DB goes down. Reference:
https://docs.erpc.cloud/config/auth.llms.txt
Prompt Example #4: debug why callers are getting 401 or no-strategy-matched
I'm seeing 401 responses and "no auth strategy matched" errors in my
eRPC logs. Walk through the credential-extraction priority order and strategy
matching logic to explain why my config in my eRPC config might be failing, and
suggest fixes. Reference: https://docs.erpc.cloud/config/auth.llms.txt
Prompt Example #5: add IP allowlist as a credential-less fallback
My internal services call eRPC without any token. Add a network
strategy that allowlists my private CIDR ranges and localhost so they get
through, while still requiring a secret token for all other callers. Each
internal IP should get its own User.Id in metrics (ipAsUser mode). Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/auth.llms.txt
Authentication — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

How it works

At startup, three independent AuthRegistry instances can be created: one per project (consumer traffic at projects[*].auth), one for the admin API (admin.auth), and one for the healthcheck endpoint (healthCheck.auth). If a project has no auth config, its registry is nil and all requests are allowed. If admin.auth is nil, the admin endpoint hard-errors — configure admin.auth to protect it.

For each HTTP request, eRPC extracts credential signals from query params and headers in a fixed priority order, then calls AuthRegistry.Authenticate. The registry iterates strategies in declaration order, skipping any whose type does not match the credential type and any filtered out by method rules. The first strategy that both matches and succeeds wins; its returned User{Id, RateLimitBudget} is attached to the request for downstream rate-limiting and observability.

HTTP credential extraction order (first match wins):

  1. ?token= — deprecated alias for ?secret=AuthTypeSecret
  2. ?secret=AuthTypeSecret
  3. X-ERPC-Secret-Token header → AuthTypeSecret
  4. Authorization: Basic (password part of base64 user:pass) → AuthTypeSecret
  5. Authorization: Bearer <token>AuthTypeJwt
  6. ?jwt=AuthTypeJwt
  7. ?signature= + ?message=AuthTypeSiwe
  8. X-Siwe-Message + X-Siwe-Signature headers → AuthTypeSiwe
  9. No credential present → AuthTypeNetwork (fallback)

gRPC metadata uses a parallel order without query-param equivalents: x-erpc-secret-token, then authorization: Basic, authorization: Bearer, then x-siwe-message+x-siwe-signature, then network fallback. gRPC has no ?token= / ?secret= / ?jwt= equivalents — query params are not available in gRPC metadata.

Method filtering. Every strategy accepts ignoreMethods and allowMethods wildcard lists. ignoreMethods is evaluated first; allowMethods overrides it. The canonical pattern for restricting a strategy to one method is ignoreMethods: ["*"] plus allowMethods: ["eth_getLogs"].

Rate-limit budget cascade. A common.User{Id, RateLimitBudget} is returned from each strategy. Budget priority: (1) per-user budget from the strategy result (database record field, JWT claim, or strategy-level rateLimitBudget); (2) strategy-level AuthStrategyConfig.rateLimitBudget; (3) no budget → all requests pass. In acquireRateLimitPermit, the user's budget (if non-empty) overrides the strategy-level budget.

secret strategy

The simplest backend-to-backend gate. The client sends the token as ?secret=VALUE or the X-ERPC-Secret-Token header. Authorization: Basic is also accepted; only the password field is used — the username is silently discarded.

Authenticate: exact equality match (ap.Secret.Value != s.cfg.Value). Returns ErrAuthUnauthorized on mismatch. No timing-safe compare — plain Go string inequality. For public internet deployments, prefer database or jwt.

jwt strategy

Verifies a signed JWT bearer token. Supports RSA, EC, and HMAC keys with optional kid-based rotation. Rate-limit budget tiers can be embedded in any JWT claim, enabling per-user rate limiting without separate strategy entries.

Authenticate flow: ParseUnverified (kid extraction) → algorithm allowlist → key lookup by kid or type-compatibility → full Parse (signature + temporal claims) → issuer/audience/required claims → extract sub as User.Id → extract budget claim.

Critical footgun: when verificationKeys is nil or empty, s.keys is empty, key lookup finds nothing, and every JWT is rejected with "no suitable verification key found". An empty key map is deny-all, not allow-all.

siwe strategy

Verifies an EIP-4361 Sign-In-With-Ethereum message and signature. The recovered Ethereum address (lowercase) becomes User.Id. Requires an explicit allowedDomains list — an empty or absent list rejects all SIWE requests.

Authenticate flow: parse EIP-4361 message → EIP-191 signature verification → domain allowlist → ValidNow() temporal check → return User{Id: strings.ToLower(address)}.

Both raw and base64-encoded message forms are accepted. SIWE requires both ?signature= AND ?message= together — supplying only one causes silent fallthrough to network-strategy extraction.

network strategy

Authorizes requests by client IP address against an allowlist of exact IPs and CIDR ranges. This is the credential-less fallback — any request arriving without a recognizable token, JWT, or SIWE payload receives AuthTypeNetwork and is routed here if configured.

Authenticate: reads req.ClientIP() (proxy-resolved by HTTP ingress). Checks localhost → exact IP list → CIDR list. First match wins. ipAsUser=false (default): all IPs in a CIDR range share one identity (cidr.String()). ipAsUser=true: each IP gets its own identity (clientIP.String()).

database strategy

Looks up an API key against a connector-backed store (PostgreSQL, DynamoDB, Redis, memory, or gRPC). Includes an in-process Ristretto positive cache (default 1h TTL), a hardcoded 5-second negative cache for invalid/disabled keys, singleflight deduplication for concurrent misses, configurable retry/backoff, and a connector-down circuit-breaker with optional fail-open.

Authenticate fast paths (in evaluation order):

  1. Positive Ristretto cache hit → return cached User immediately.
  2. Negative Ristretto cache hit → return ErrAuthUnauthorized immediately.
  3. Connector-down fast path (tryFastFailOpen) → return emergency user if fail-open enabled.
  4. Singleflight deduplicated DB lookup → parse JSON → cache or negative-cache.

DB record format:

{
  "userId":          "string (required)",
  "enabled":         true,
  "rateLimitBudget": "budget-id (optional)"
}

enabled: falseErrAuthUnauthorized + 5-second negative-cache entry. Missing userId → auth error. Missing enabled → treated as true.

Connector-down circuit-breaker (added after 2026-05-13 production incident). The strategy tracks connectorDown bool and connectorDownSince int64. On transport/timeout/not-ready errors, markConnectorDown() sets the latch. While down, tryFastFailOpen() bypasses singleflight and the DB entirely — returning the emergency user (if failOpen.enabled=true) or rejecting immediately. One probe per second is elected via CAS to run the real DB path. A successful probe calls markConnectorUp().

Config schema

Auth attachment points

YAML pathTypeDefaultNotes
projects[*].auth*AuthConfignil (allow-all)Consumer auth per project. Nil = allow-all. common/config.go:L509
admin.auth*AuthConfignilAdmin endpoint auth. Nil = admin endpoint always returns error. common/config.go:L249
healthCheck.auth*AuthConfignilHealthcheck auth. Nil = unguarded. Do NOT set rateLimitBudget on healthcheck strategies — healthcheck auth registry has no rate-limiter registry and will panic. erpc/http_server.go:L201-205

AuthConfig

FieldTypeDefaultNotes
auth.strategies[]*AuthStrategyConfignil (allow-all)Ordered list. First strategy that matches credential type + method filter and authenticates successfully wins. common/config.go:L2444

AuthStrategyConfig — common fields

FieldTypeDefaultNotes
strategies[*].typestringInferred from sub-config block"secret", "jwt", "siwe", "network", "database". Block presence force-overwrites type for secret/database/jwt/siwe. For network, only type: "network" triggers auto-creation; the network block does not overwrite type. common/defaults.go:L2636-2666
strategies[*].ignoreMethods[]stringnilWildcard patterns (supports *, |, &, !). Applied before allowMethods. auth/authorizer.go:L86-98
strategies[*].allowMethods[]stringnilOverrides ignoreMethods; any matching allow re-enables the strategy for that method. auth/authorizer.go:L100-113
strategies[*].rateLimitBudgetstring""Strategy-level budget ID. Overridden by per-user budget when non-empty. auth/authorizer.go:L119-127

secret strategy — SecretStrategyConfig

YAML prefix: auth.strategies[*].secret

FieldTypeDefaultNotes
secret.idstring""Returned as User.Id on success. May be empty. common/config.go:L2461
secret.valuestringrequiredExact token to match. Plain string equality (not timing-safe). Redacted in JSON/YAML marshal. auth/strategy_secret.go:L24
secret.rateLimitBudgetstring""Attached to returned User.RateLimitBudget if non-empty. auth/strategy_secret.go:L29-31

Supports: AuthTypeSecret only. SetDefaults: no-op.

jwt strategy — JwtStrategyConfig

YAML prefix: auth.strategies[*].jwt

FieldTypeDefaultNotes
jwt.verificationKeysmap[string]stringnilMap of kid → keyData. Value is PEM string, "file:///path", or bare HMAC secret bytes. Empty map = ALL JWTs rejected (deny-all, not allow-all). auth/jwks.go
jwt.verificationJwksUrlstring""Absolute HTTP(S) URL returning a standard JWKS document ({"keys":[...]}). Fetched at startup and refreshed in the background. Static verificationKeys override JWKS entries with the same kid. Non-HTTP(S) or host-less values fail startup validation. common/validation.go
jwt.verificationJwksRefreshIntervalDuration1h when JWKS URL setBackground JWKS refresh interval. common/defaults.go
jwt.verificationJwksTlsInsecureSkipVerifyboolfalseSkip TLS certificate verification when fetching the JWKS URL. Do not enable in production — intended for local development with self-signed certificates only. auth/jwks.go
jwt.allowedAlgorithms[]stringnil (any algorithm)e.g. ["RS256","ES256"]. Prevents algorithm-confusion attacks. auth/strategy_jwt.go
jwt.allowedIssuers[]stringnil (any issuer)Exact match against iss claim. Non-empty list + missing iss → error. auth/strategy_jwt.go
jwt.allowedAudiences[]stringnil (any audience)Exact match against aud claim as scalar string. Array-form aud JWT claim always fails the cast. auth/strategy_jwt.go
jwt.requiredClaims[]stringnilClaim names that must be present (any value). auth/strategy_jwt.go
jwt.claimMatchersmap[string][]stringnilMap of claimName → allowedValues. Every key is an AND condition; within each key's value list any match passes (OR). Claim value may be a string, string array, or SCIM-style [{"value":"..."}]. Missing or empty claim → error. Omitting claimMatchers skips the check (backward-compatible). auth/strategy_jwt.go
jwt.rateLimitBudgetClaimNamestring"rlm" (set by SetDefaults)JWT claim name from which User.RateLimitBudget is extracted. Missing claim → empty budget (no rate-limit). Non-string value → silent no-op. To suppress, use a claim name never present in tokens. common/defaults.go:L2749-2751

Supports: AuthTypeJwt only. SetDefaults: sets rateLimitBudgetClaimName = "rlm" if empty.

siwe strategy — SiweStrategyConfig

YAML prefix: auth.strategies[*].siwe

FieldTypeDefaultNotes
siwe.allowedDomains[]stringnil (deny-all)Exact match against EIP-4361 domain field. Empty list = all SIWE rejected. Must supply at least one domain. auth/strategy_siwe.go:L59-67
siwe.rateLimitBudgetstring""Attached to User.RateLimitBudget if non-empty. auth/strategy_siwe.go:L53-55

Supports: AuthTypeSiwe only. SetDefaults: no-op.

network strategy — NetworkStrategyConfig

YAML prefix: auth.strategies[*].network

FieldTypeDefaultNotes
network.allowedIPs[]stringnilExact IPv4/IPv6 addresses. Invalid IP → startup error. auth/strategy_network.go:L26-33
network.allowedCIDRs[]stringnilCIDR ranges. Invalid CIDR → startup error. auth/strategy_network.go:L35-44
network.allowLocalhostboolfalseWhen true, any loopback address (127.0.0.1, ::1) is allowed unconditionally before IP/CIDR checks. User.Id = clientIP.String(). auth/strategy_network.go:L63-71
network.trustedProxies[]stringnilNO-OP. Never read by NetworkStrategy. Use server.trustedIPForwarders / server.trustedIPHeaders for X-Forwarded-For unwrapping. auth/strategy_network.go:L47-49
network.rateLimitBudgetstring""Attached to User.RateLimitBudget for all matching IPs. auth/strategy_network.go:L65-68
network.ipAsUserboolfalseFor CIDR matches: false → User.Id = cidr.String() (all IPs in range share one identity); true → User.Id = clientIP.String() (per-IP identity). Localhost and exact-IP matches always use clientIP.String(). auth/strategy_network.go:L63-93

Supports: AuthTypeNetwork only (credential-less fallback). SetDefaults: no-op.

database strategy — DatabaseStrategyConfig

YAML prefix: auth.strategies[*].database

FieldTypeDefaultNotes
database.connector*ConnectorConfigauto-createdDrivers: postgresql, dynamodb, redis, memory, grpc. Connector id defaults to "auth-<driver>". common/defaults.go:L2676-2678
database.maxWaitDuration1sPer-request context timeout for the full DB lookup (singleflight + retries). common/defaults.go:L2718-2720
database.cache.ttl*time.Duration1hPositive-cache TTL in Ristretto. common/defaults.go:L2691-2694
database.cache.maxSize*int6410000Max positive-cache entries. common/defaults.go:L2696-2699
database.cache.maxCost*int641073741824 (1 GiB)Ristretto MaxCost for positive cache. common/defaults.go:L2701-2704
database.cache.numCounters*int64100000Ristretto NumCounters (also reused for negative cache). common/defaults.go:L2706-2709
database.retry.maxAttemptsint3Max DB lookup attempts. common/defaults.go:L2726-2728
database.retry.baseBackoffDuration100msExponential backoff base; each attempt uses baseBackoff << (attempt-1). common/defaults.go:L2729-2731
database.failOpen.enabledboolfalseWhen true, DB errors grant an emergency user instead of rejecting. The connector-down circuit-breaker fast path also respects this flag. auth/strategy_database.go:L519-530
database.failOpen.userIdstring"emergency-failopen"User.Id for all fail-open authenticated requests. Flows into Prometheus labels and log fields. common/defaults.go:L2738-2740
database.failOpen.rateLimitBudgetstring""User.RateLimitBudget applied to fail-open traffic. auth/strategy_database.go:L526-528

Negative cache: hardcoded 5-second TTL, 1 MiB MaxCost. Not configurable. Disabled/invalid keys stay rejected up to 5 seconds after change.

database strategy Supports: accepts both AuthTypeSecret (token delivered via header/query) and AuthTypeDatabase. The AuthTypeDatabase enum value is never set by any current HTTP or gRPC payload extractor — it is reserved for future or programmatic use. auth/strategy_database.go:L118-120

database connector error classification — determines whether connectorDown latch is set (auth/strategy_database.go:L487-L517 (opens in a new tab)):

ErrorLabelSets connectorDown?
data.ErrConnectorNotReadydb_not_readyyes
context.DeadlineExceeded, substring "deadline exceeded" or "timeout"db_timeoutyes
substrings: "connection refused", "connection reset", "broken pipe", "no route to host", "EOF", "use of closed network connection"db_connectionyes
ErrRecordNotFound(treated separately; not error-classified)no — marks connector UP
everything else (parse errors, Postgres 53300 "too many connections", syntax errors)db_query_errorno

getWithRetries abort conditions (auth/strategy_database.go:L367-L417 (opens in a new tab)):

  • ErrRecordNotFound → abort immediately (DB is healthy; no retry needed).
  • data.ErrConnectorNotReady → abort immediately (connector's own reconnect loop handles recovery).
  • Context done → abort immediately.
  • Any other error → retry with baseBackoff << (attempt-1) up to maxAttempts.

Connector failsafe wrapping. Auth-scope connectors support the same failsafeForGets and failsafeForSets wrapping as cache-scope connectors (ConnectorConfig.failsafeForGets[*] and failsafeForSets[*]). Each entry is a FailsafeConfig with fields matchMethod (default "*"), matchFinality, retry, circuitBreaker, timeout, hedge, and consensus. common/config.go:L349-350

PostgreSQL auth-scope defaults (connector.postgresql.*):

FieldAuth-scope defaultNotes
table"erpc_auth"Differs from cache ("erpc_json_rpc_cache") and shared-state ("erpc_shared_state") scopes. common/defaults.go:L1028-1029
minConns1Differs from cache scope (4). common/defaults.go:L1034-1036
maxConns4Differs from cache scope (32). common/defaults.go:L1041-1043
initTimeout5scommon/defaults.go:L1048-1050
getTimeout1scommon/defaults.go:L1051-1053
setTimeout2scommon/defaults.go:L1054-1056

Memory connector (connector.memory.*):

FieldAuth-scope defaultNotes
maxItems100000Ristretto item count limit. common/defaults.go:L936-938
maxTotalSize"1GB"Max total size of values. common/defaults.go:L939-941
emitMetricsnil (disabled)When true, a background goroutine emits Ristretto metrics to Prometheus every 30 seconds. Must be explicitly set to true to enable. data/memory.go:L71

DynamoDB auth-scope defaults (connector.dynamodb.*):

FieldAuth-scope defaultNotes
table"erpc_auth"Differs from cache scope. common/defaults.go:L1068-1069
region""AWS region. Required unless resolved from environment/instance profile. common/config.go:L430
endpoint""Custom endpoint URL (for local DynamoDB or LocalStack). common/config.go:L431
authnilAWS credential config; see AwsAuthConfig table below. common/config.go:L432
partitionKeyName"groupKey"common/defaults.go:L1074-1076
rangeKeyName"requestKey"common/defaults.go:L1077-1079
reverseIndexName"idx_requestKey_groupKey"GSI name. common/defaults.go:L1080-1082
ttlAttributeName"ttl"common/defaults.go:L1083-1085
initTimeout5sSDK init timeout. common/defaults.go:L1086-1088
getTimeout1sPer-get operation timeout. common/defaults.go:L1089-1091
setTimeout2sPer-set operation timeout. common/defaults.go:L1092-1094
maxRetries0Max DynamoDB SDK retries. Zero means use SDK default (typically 3). common/config.go:L440
statePollInterval5sInterval for polling DynamoDB connection state. common/defaults.go:L1095-1097
lockRetryInterval0Footgun: zero means busy-spin retries; set to e.g. 100ms in production. common/config.go:L442

AwsAuthConfig (used by connector.dynamodb.auth):

FieldDefaultNotes
mode"" (SDK default chain)Valid values: "file" (reads credentials file), "env" (reads AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY), "secret" (uses accessKeyID/secretAccessKey fields directly). Empty = AWS SDK default chain (env → shared credentials file → EC2 instance profile). common/config.go:L479-485
credentialsFile""Path to AWS credentials file (used when mode="file").
profile""AWS named profile. Only meaningful when mode="file" — passed to credentials.NewSharedCredentials. Ignored for mode="env", mode="secret", and the SDK default chain. data/dynamodb.go:L177-187
accessKeyID""AWS access key ID (used when mode="secret").
secretAccessKey""AWS secret access key (used when mode="secret"). Redacted in JSON marshal. common/config.go:L487-495

Redis auth-scope defaults (connector.redis.*):

FieldAuth-scope defaultNotes
uri""Full Redis URI (e.g., redis://user:pass@host:6379/0). Mutually exclusive with addr — providing both causes a startup error. When set, addr/username/password/db are ignored entirely. common/defaults.go:L946-950
addr""Redis host:port. Folded into uri and cleared by SetDefaults. Port defaults to 6379 when missing. After folding, addr is set to "". common/defaults.go:L978-1015
username""Redis username. Folded into uri and cleared (same as addr). json:"-" does NOT apply to username; only password is marshal-suppressed.
password""Redis password. Folded into uri and cleared. Tagged json:"-" — never marshalled to JSON output. common/config.go:L386
db0Redis database index. Folded into uri path and cleared. Always appended as "/<db>" even for index 0.
connPoolSize8Connection pool size. common/defaults.go:L955-957
initTimeout5scommon/defaults.go:L958-960
getTimeout1scommon/defaults.go:L961-963
setTimeout3scommon/defaults.go:L964-966
lockRetryInterval500mscommon/defaults.go:L967-969
tlsnilOptional TLS config with sub-fields enabled (bool), certFile, keyFile, caFile, insecureSkipVerify. When tls.enabled=true and addr is set, SetDefaults sets the URI scheme to rediss://. When uri is provided with rediss:// scheme, tls.* fields are merged onto the baseline TLS config. data/redis.go:L149-179

gRPC connector (connector.grpc.*):

FieldAuth-scope defaultNotes
serversnilExplicit server addresses. common/config.go:L356
bootstrap""xDS bootstrap for server discovery. Appended to servers. data/grpc.go:L83-98
headersnilStatic headers on all outbound gRPC requests.
getTimeout100msMuch tighter than other drivers. common/defaults.go:L927-929

Worked examples

All patterns below are distilled from real production fleets; comments explain the non-obvious choices.

1. Production database auth with fail-open and extended cache TTL. The customer-facing edge fleet uses PostgreSQL-backed dynamic API keys. The cache TTL was raised from 5s to 1m after a 2026-05-13 incident where transient pgbouncer disruption caused a reconnect storm across all 32 replicas simultaneously — lower query rate means transient errors are far less likely to cascade. Trade-off: revoked/disabled keys take up to 1m to stop working:

projects[].auth.strategies[]
erpc.yaml
- type: database  database:    connector:      id: auth-postgresql      driver: postgresql      postgresql:        connectionUri: ${DATABASE_URL}        # table defaults to "erpc_auth" at auth scope (differs from cache scope)    cache:      # Raised from 5s to 1m to cut the Postgres query rate ~12× and prevent      # connector reconnect storms under transient pgbouncer disruptions.      # Trade-off: revoked keys stay valid for up to 1 minute.      ttl: 1m    failOpen:      # Keep traffic flowing under an emergency budget if the auth DB goes down;      # the connector-down circuit-breaker limits real DB probes to ~1/second.      enabled: true      userId: emergency-failopen      # Cap emergency traffic so a DB outage can't become a free-for-all      rateLimitBudget: emergency-tier

2. Admin endpoint secret. All production deployments share a single secret strategy on the admin scope; the token is injected from an environment variable at deploy time. Note: admin.auth being nil hard-errors the endpoint with no useful error code — always configure it even in internal-only deployments:

admin.auth.strategies[]
erpc.yaml
# admin.auth — nil = admin always returns 500, not 401admin:  auth:    strategies:      - type: secret        secret:          # admin endpoints only need identity; no rate-limit budget required          value: ${ADMIN_SECRET_TOKEN}

3. Multi-secret staging setup with per-identity budgets. The staging environment uses multiple named secrets bound to the same rate-limit budget — useful for smoke-testing different client identities without a database. Each id surfaces separately in Prometheus labels so you can see per-client RPS in dashboards:

projects[].auth.strategies[]
erpc.yaml
- type: secret  secret:    id: client-a    value: ${CLIENT_A_TOKEN}    # each secret gets its own budget so one client can't starve others    rateLimitBudget: edge-tier-60krpm-total-unlimited-per-ip- type: secret  secret:    id: client-b    value: ${CLIENT_B_TOKEN}    rateLimitBudget: edge-tier-60krpm-total-unlimited-per-ip

4. Network strategy as credential-less fallback for internal services. The staging deployment allows all internal traffic via CIDR while still accepting named secrets for external test callers. The network strategy must come first — it matches any request with no credential, so placing it later would shadow secrets on credential-less paths:

projects[].auth.strategies[]
erpc.yaml
# Place network strategy FIRST — it is the credential-less fallback;# any request without a token, JWT, or SIWE payload lands here.- type: network  network:    allowedCIDRs: ["10.0.0.0/8", "172.16.0.0/12"]    allowLocalhost: true    # ipAsUser: true gives each IP its own User.Id in Prometheus labels    ipAsUser: true    rateLimitBudget: internal-tier# External callers still use named secrets- type: secret  secret:    id: external-service    value: ${EXTERNAL_SECRET_TOKEN}    rateLimitBudget: external-tier

5. JWT with per-user rate-limit tiers and key rotation. Embed the budget ID in the token's rlm claim so a single strategy entry handles thousands of users. Rotate signing keys by adding a new kid entry — old tokens continue to verify against the old key until they naturally expire. Always set allowedAlgorithms to prevent algorithm-confusion attacks:

projects[].auth.strategies[]
erpc.yaml
- type: jwt  jwt:    verificationKeys:      # Add a new kid here when rotating; keep the old entry until tokens expire      rsa-2024: "file:///etc/erpc/public-2024.pem"      rsa-2025: "file:///etc/erpc/public-2025.pem"    # ALWAYS restrict algorithms — omitting this allows algorithm-confusion attacks    allowedAlgorithms: ["RS256"]    allowedIssuers: ["https://auth.myapp.com"]    allowedAudiences: ["https://rpc.myapp.com"]    # Extract User.RateLimitBudget from this JWT claim; missing claim = no budget    rateLimitBudgetClaimName: rlm

Request/response behavior

  • A successful auth attaches User{Id, RateLimitBudget} to the request; User.Id flows into log context and Prometheus labels on every downstream metric. [auth/registry.go:L74-76]
  • ErrAuthUnauthorized → HTTP 401. Carries {strategy} in Details. [common/errors.go:L524-542]
  • ErrAuthRateLimitRuleExceeded → HTTP 429. Carries {projectId, strategy, budget, rule, userId, clientIp} in Details. The rule value is formatted as "method:<rpc_method>". [common/errors.go:L545-568]
  • If no strategy's Supports returns true (e.g., a JWT token arrives but only network is configured), the response is "no auth strategy matched" — not a per-strategy auth failure. [auth/registry.go:L46-96]
  • For admin scope, when adminCfg != nil but adminAuthRegistry == nil, AdminAuthenticate returns a plain fmt.Errorf with no typed error code → HTTP 200 (not 401/500). When adminCfg is nil, the path returns ErrAuthUnauthorized → HTTP 401. [erpc/admin.go:L26-30]
  • erpc_rate_limits_total fires for auth-level budget exhaustion with origin="auth" and auth="<type>:<index>" (e.g. "secret:0", "database:1"). [auth/authorizer.go:L137]
  • ErrAuthRateLimitRuleExceeded is auth-scope only — it fires when the auth-strategy-level rate-limit budget is exceeded. It does NOT fire for upstream/network rate-limit budgets (those produce ErrProjectRateLimitRuleExceeded or ErrNetworkRateLimitRuleExceeded). Retryability: U:no (upstreams will not retry), N:yes (network propagates 429 to caller). [common/errors.go:L545-568]
  • gRPC required metadata: x-erpc-project (required; missing → codes.InvalidArgument "missing metadata"), x-erpc-chain-id (required), x-erpc-architecture (optional, defaults to "evm"). [erpc/grpc_server.go:L138-145]

Best practices

  • Use database for user-facing APIs — dynamic key management without redeploys, per-user rate-limit budgets via the DB record's rateLimitBudget field, and the built-in cache means sub-millisecond auth on cache hits.
  • Always set allowedAlgorithms on jwt — leaving it nil accepts any algorithm, opening the door to algorithm-confusion attacks (e.g., RS256 public key forged as HS256 secret).
  • Always list at least one allowedDomains on siwe — an empty or absent list silently rejects every SIWE request with no config-level indication.
  • Never set rateLimitBudget on a healthCheck.auth strategy — the healthcheck auth registry is created with a nil rate-limiter registry; reaching the budget-acquisition path causes a nil pointer panic.
  • Enable failOpen with a capped emergency budget on database — a DB outage should not make your RPC endpoint unreachable; the circuit-breaker bounds probe load to ~1 query/second per replica.
  • DynamoDB lockRetryInterval defaults to 0 — zero means busy-spin retries during lock contention; set to 100ms or higher in production to avoid rapid API calls and unexpected cost.
  • Use secret only for private, server-side callers — the plain string comparison is not timing-safe; for browser-reachable endpoints, prefer jwt or database.

Edge cases & gotchas

  1. No strategies = allow-all. AuthRegistry.Authenticate returns nil, nil when the strategy list is empty. All requests pass. Auth is opt-in.
  2. network strategy is the credential-less fallback. Requests without any recognizable credential get AuthTypeNetwork. If no network strategy is configured and only a secret strategy exists, credential-less requests get "no auth strategy matched", not "invalid secret".
  3. secret and database both consume AuthTypeSecret. If both are configured, declaration order determines which runs first. To use only database, omit the secret strategy.
  4. JWT allowedAudiences only supports scalar aud. The implementation casts claims["aud"] to string. Tokens with "aud": ["a","b"] (array form) always fail the cast and are rejected even if a matching audience is in the array.
  5. SIWE allowedDomains nil/empty = deny-all. Visual appearance of siwe: {} gives no indication it is a blanket deny.
  6. network.trustedProxies is an intentional no-op. Configure server.trustedIPForwarders / server.trustedIPHeaders instead.
  7. Healthcheck auth cannot use rateLimitBudget. The healthcheck auth registry is created with rateLimitersRegistry = nil. Setting a budget on a healthcheck strategy reaches a nil pointer dereference at acquireRateLimitPermit.
  8. secret uses non-constant-time comparison. Plain != is timing-side-channel vulnerable for public internet exposures.
  9. Database negative cache TTL is hardcoded at 5 seconds. Not configurable. Re-enabled keys are rejected for up to 5 seconds.
  10. Connector-down probe is per-process. One probe/second per replica, no cross-replica coordination.
  11. type inference conflict. Setting multiple sub-config blocks (e.g. both secret: and jwt:) in one strategy entry causes the last-evaluated block to silently overwrite type. Evaluation order: secret → database → jwt → siwe. Never set multiple sub-config blocks in one strategy entry.
  12. Authorization: Basic username is silently discarded. Only the password field is used as the secret value. alice:mysecret and bob:mysecret are treated identically.
  13. JWT requires keys. Configure verificationKeys and/or verificationJwksUrl. With neither, startup validation fails. An empty resolved key map rejects every JWT (deny-all, not allow-all).
  14. SIWE requires both signature and message together. Missing either one causes silent fallthrough to network-strategy payload extraction.
  15. ?token= query param is deprecated. Alias for ?secret=. Continues to work but may be removed; use ?secret= or X-ERPC-Secret-Token header in new implementations.
  16. Redis addr/username/password/db are cleared after SetDefaults. After initialization only uri is set. Config exports show only the URI (with credentials URL-encoded in it).
  17. DynamoDB lockRetryInterval defaults to 0. Zero = busy-spin during lock contention. Set to a non-zero value (e.g. 100ms) in production.
  18. database.cache, database.retry, and database.failOpen are always auto-created. Caching is always on with 1h TTL unless ttl is explicitly changed. There is no config to disable caching by omitting the block.
  19. type: "network" auto-creates the network {} sub-struct; the converse is asymmetric. Adding a network: block does NOT overwrite type — unlike secret/database/jwt/siwe which force-overwrite type when their sub-block is present.
  20. When admin.auth is nil the admin endpoint hard-errors with HTTP 500. AdminAuthenticate returns a plain fmt.Errorf (no typed error code) → HTTP 200 wire, not 401. Configure admin.auth to protect it properly.
  21. database strategy also accepts AuthTypeDatabase in Supports. The enum value AuthTypeDatabase exists but is never produced by any current HTTP or gRPC payload extractor. It is reserved for future or programmatic injection. In practice, database auth is triggered via AuthTypeSecret credentials (header/query token). [auth/strategy_database.go:L118-120]
  22. Singleflight scope is per API key. The singleflight group inside DatabaseStrategy uses the raw API key string as the deduplication key. Concurrent requests with the same API key during a cache miss are coalesced into a single DB lookup. Requests with different API keys run in parallel. [auth/strategy_database.go:L173-178]
  23. AuthConfig is shared across all three scopes. The same AuthConfig type is used for projects[*].auth, admin.auth, and healthCheck.auth. All five strategy types can be configured in any scope. The only scope-specific hazard is healthCheck.auth — the healthcheck auth registry is created with a nil rateLimitersRegistry, so setting rateLimitBudget on any healthcheck strategy causes a nil pointer panic.
  24. Redis addr/username/password/db are cleared after SetDefaults. After initialization, only uri is set; all discrete fields are zeroed. Config exports show only the URI (with credentials URL-encoded in it). Because password has json:"-", a JSON export shows the URI but not the password field. [common/defaults.go:L1012-1015]
  25. Admin auth registry DOES support rate-limit budgets. Unlike the healthcheck scope, the admin AuthRegistry is created with the same rateLimitersRegistry as projects. Rate-limit budgets on admin auth strategies are applied when configured.
  26. verificationJwksUrl must be absolute HTTP(S). Non-empty values are parsed at startup; scheme must be http or https and a host is required. Scheme-less hosts, file://, and other schemes fail validation even when static verificationKeys are also set. [common/validation.go]

Observability

MetricTypeLabelsWhen it fires
erpc_auth_failed_totalcounterproject, network, strategy, reason, agent_nameDatabase strategy only. Reason values: missing_secret, empty_secret, cached_unknown_api_key, db_fail_open_fast_path, db_not_ready, db_timeout, db_connection, db_query_error, invalid_api_key, disabled_key, db_record_parse_error, db_record_missing_user_id, internal_error.
erpc_rate_limits_totalcounterproject, network, vendor, upstream, category, finality, user, agent_name, budget, scope, auth, originWhen auth-level rate-limit budget is exhausted. origin="auth", auth="<type>:<index>" (e.g. "secret:0", "database:1").

Log messages (database strategy):

LevelMessageKey fields
warn"database connector marked DOWN; subsequent requests will fast-path to fail-open until next probe succeeds"connectorId
warn"database connector marked UP; resuming normal auth flow"connectorId
warn"singleflight error; fail-open enabled, granting emergency user"userId, err
warn"database authentication lookup failed; retrying"driver, connectorId, attempt, backoff, err
error"database query failed during authentication"apiKey, driver, connectorId, err
error"auth DB error; fail-open enabled, granting emergency user"userId
info"initialized API key cache for database authentication strategy"ttl, negTtl, maxSize, maxCost, numCounters
debug"API key found in cache" / "not found in cache"apiKey
debug"API key found in negative cache"apiKey
debug"cached API key data"apiKey, ttl
debug"user authenticated successfully"apiKey, userId, budget
debug"invalidated API key cache entry"apiKey

No dedicated auth trace spans — auth runs inline inside the HTTP handler goroutine within the OTel request span.

Source code entry points

Related pages

  • Rate limiters — define the budgets that auth strategies assign to each identity.
  • Projects — auth lives at projects[*].auth; project-level config context.
  • Use cases: Multi-tenant RPC — end-to-end example combining auth + rate limiters.