Skip to main content
Anubis operates as a reverse proxy that sits between clients and your upstream application, evaluating every request against configured policy rules before allowing access.

Request Flow

When a request arrives at Anubis, it follows this processing pipeline:

1. Initial Request Check

Every request is evaluated in lib/anubis.go:maybeReverseProxy(). The system:
  1. Extracts the client IP from X-Real-Ip header (configured by your reverse proxy)
  2. Checks for an existing Anubis JWT cookie
  3. If found, validates the cookie’s integrity and expiration
Anubis requires the X-Real-Ip header to be set by your upstream reverse proxy (nginx, Caddy, etc). Missing this header results in a misconfiguration error.
If a cookie exists, Anubis performs JWT validation:
// From lib/anubis.go:248
token, err := jwt.ParseWithClaims(
    ckie.Value, 
    jwt.MapClaims{}, 
    s.getTokenKeyfunc(), 
    jwt.WithExpirationRequired(), 
    jwt.WithStrictDecoding()
)
The JWT validation checks:
  • Signature integrity: Using Ed25519 or HMAC-SHA512
  • Expiration: Tokens expire based on ANUBIS_COOKIE_EXPIRATION
  • Policy rule hash: Ensures the token was issued for the currently matching rule
  • Restriction header (optional): Binds the token to specific request properties

3. Policy Evaluation

When no valid cookie exists, the request goes through policy evaluation in lib/anubis.go:check():
// From lib/anubis.go:596
func (s *Server) check(r *http.Request, lg *slog.Logger) (
    policy.CheckResult, 
    *policy.Bot, 
    error
) {
    weight := 0
    
    // Evaluate bot rules sequentially
    for _, b := range s.policy.Bots {
        match, err := b.Rules.Check(r)
        if match {
            switch b.Action {
            case config.RuleDeny, config.RuleAllow, 
                 config.RuleBenchmark, config.RuleChallenge:
                return cr("bot/"+b.Name, b.Action, weight), &b, nil
            case config.RuleWeigh:
                weight += b.Weight.Adjust
            }
        }
    }
    
    // Evaluate thresholds
    for _, t := range s.policy.Thresholds {
        // CEL expression evaluation
        result, _, err := t.Program.ContextEval(
            r.Context(), 
            &policy.ThresholdRequest{Weight: weight}
        )
        if matches {
            return cr("threshold/"+t.Name, t.Action, weight), ...
        }
    }
    
    // Default: allow
    return cr("default/allow", config.RuleAllow, weight), ...
}

Bot Rules

Evaluated first. Each rule can match on IP, user agent, headers, path, ASN, or GeoIP.

Thresholds

Evaluated after bot rules. Use accumulated weight from WEIGH actions to trigger challenges.

Challenge Issuance

When a policy rule returns CHALLENGE, Anubis:
  1. Generates a unique challenge ID using UUID v7
  2. Creates random data (64 bytes) for proof-of-work
  3. Stores challenge metadata in the configured store backend:
// From lib/anubis.go:119
chall := challenge.Challenge{
    ID:             id.String(),
    Method:         rule.Challenge.Algorithm,  // "fast" or "slow"
    RandomData:     fmt.Sprintf("%x", randomData),
    IssuedAt:       time.Now(),
    Difficulty:     rule.Challenge.Difficulty,  // 0-64
    PolicyRuleHash: rule.Hash(),
    Metadata: map[string]string{
        "User-Agent": r.Header.Get("User-Agent"),
        "X-Real-Ip":  r.Header.Get("X-Real-Ip"),
    },
}
  1. Renders the challenge page with embedded JavaScript solver
  2. Sets a test cookie to verify cookie support
Challenges expire after 30 minutes and can only be solved once (double-spend protection).

JWT Token Generation

After successful challenge validation in lib/anubis.go:PassChallenge(), a JWT is created:
claims := jwt.MapClaims{
    "challenge":  chall.ID,
    "method":     rule.Challenge.Algorithm,
    "policyRule": rule.Hash(),  // Critical for invalidation
    "action":     string(cr.Rule),
    "iat":        time.Now().Unix(),
    "nbf":        time.Now().Add(-1 * time.Minute).Unix(),
    "exp":        time.Now().Add(cookieExpiration).Unix(),
}

Policy Rule Hash

The policyRule claim contains a hash of the bot rule configuration:
// From lib/policy/bot.go:19
func (b Bot) Hash() string {
    return internal.FastHash(fmt.Sprintf("%s::%s", b.Name, b.Rules.Hash()))
}
This ensures that if you update your policy (change difficulty, add new checks, etc.), all existing JWTs become invalid and users must re-solve challenges.

Proxying to Upstream

Once validated, the request is forwarded to your upstream application with additional headers:
X-Anubis-Rule: bot/verified-googlebot
X-Anubis-Action: ALLOW
X-Anubis-Status: PASS
These headers allow your application to log or make decisions based on how Anubis evaluated the request. Anubis uses two cookies:
Contains the signed JWT proving the client passed a challenge.
  • Expires based on ANUBIS_COOKIE_EXPIRATION (default: 24 hours)
  • Can be scoped to a specific domain with ANUBIS_COOKIE_DOMAIN
  • Supports SameSite policies and Partitioned cookies

Performance Characteristics

  • Policy evaluation: O(n) where n is the number of bot rules
  • JWT validation: Constant time cryptographic operations
  • Challenge validation: Constant time hash comparison with crypto/subtle
  • Store operations: Depends on backend (memory: O(1), bbolt: O(log n), Valkey: network latency)
Bot rules are evaluated in order. Place your most specific or most frequently matched rules first for optimal performance.

DNSBL Integration

If dnsbl: true is configured, Anubis queries DroneBL before policy evaluation:
// From lib/anubis.go:333
if s.policy.DNSBL && ip != "" {
    resp, err := dnsbl.Lookup(ip)
    if resp != dnsbl.AllGood {
        // Deny immediately with DNSBL reason
    }
}
Results are cached in the store for 24 hours to minimize DNS lookups.

Next Steps

Challenges

Learn about challenge types and proof-of-work mechanisms

Policies

Understand bot detection rules and threshold configuration

Architecture

Explore component interactions and deployment patterns

Build docs developers (and LLMs) love