Skip to main content
Anubis uses cryptographic signing to secure challenge tokens and prevent tampering. This page covers key generation, JWT signing, and cookie security.

JWT Signing

Challenge solutions are stored in JWT (JSON Web Tokens) signed with either:
  • Ed25519 (recommended) - Elliptic curve signatures
  • HMAC-SHA512 - Symmetric key signing
Ed25519 provides fast, secure signatures with small key sizes.

Generate Key

# Generate 32-byte (256-bit) random key
openssl rand -hex 32 > /etc/anubis/ed25519.key

# Example output:
# a3f5d8c2e1b4f6a9c7d3e8f1a2b5c6d7e3f4a1b2c5d6e7f8a9b1c2d3e4f5a6b7

Configure Anubis

# Via command-line flag
anubis --ed25519-private-key-hex-file /etc/anubis/ed25519.key

# Via environment variable
export ED25519_PRIVATE_KEY_HEX_FILE=/etc/anubis/ed25519.key

# Or inline (not recommended)
export ED25519_PRIVATE_KEY_HEX=a3f5d8c2e1b4f6a9c7d3e8f1a2b5c6d7e3f4a1b2c5d6e7f8a9b1c2d3e4f5a6b7

HMAC-SHA512

Symmetric signing using a shared secret.
# Generate secret
openssl rand -base64 64

# Configure
export HS512_SECRET="your-secret-here"
Note: Ed25519 is preferred. HMAC-SHA512 is legacy.

Random Key (Development Only)

If no key is configured, Anubis generates a random key on startup:
WARN generating random key, Anubis will have strange behavior when multiple 
instances are behind the same load balancer target
Consequences:
  • Challenges invalidated on restart
  • Multi-instance deployments won’t work
  • Users must re-solve challenges after deployment
⚠️ Never use random keys in production

Key Management

Storage Requirements

Keys must be:
  1. Persistent - Survives container/pod restarts
  2. Secret - Not committed to version control
  3. Accessible - Readable by Anubis process

File Permissions

# Secure key file
chown anubis:anubis /etc/anubis/ed25519.key
chmod 400 /etc/anubis/ed25519.key

# Verify
ls -l /etc/anubis/ed25519.key
# -r-------- 1 anubis anubis 64 Jan 1 12:00 /etc/anubis/ed25519.key

Docker/Kubernetes

Docker Secret

# Create secret
docker secret create anubis_key /path/to/ed25519.key

# Use in service
docker service create \
  --secret anubis_key \
  --env ED25519_PRIVATE_KEY_HEX_FILE=/run/secrets/anubis_key \
  ghcr.io/techarohq/anubis:latest

Kubernetes Secret

# Create secret from file
kubectl create secret generic anubis-key \
  --from-file=ed25519.key=/path/to/ed25519.key
apiVersion: apps/v1
kind: Deployment
metadata:
  name: anubis
spec:
  template:
    spec:
      containers:
      - name: anubis
        image: ghcr.io/techarohq/anubis:latest
        env:
        - name: ED25519_PRIVATE_KEY_HEX_FILE
          value: /secrets/ed25519.key
        volumeMounts:
        - name: signing-key
          mountPath: /secrets
          readOnly: true
      volumes:
      - name: signing-key
        secret:
          secretName: anubis-key
          defaultMode: 0400

Key Rotation

  1. Generate new key
  2. Deploy Anubis with new key
  3. Existing JWTs remain valid until expiry
  4. New challenges use new key
No downtime required. Users with old cookies will re-solve challenges. Anubis sets multiple security flags on cookies.

Secure Flag

Requires HTTPS:
# Enable (default)
anubis --cookie-secure=true

# Disable (development only)
anubis --cookie-secure=false
If --cookie-secure=false, SameSite automatically downgrades from None to Lax.

SameSite

Controls cross-site cookie sending:
# Options: None, Lax, Strict, Default
anubis --cookie-same-site None
ValueBehaviorUse Case
NoneSent on all requests (requires Secure)Embedded/third-party use
LaxSent on top-level navigationMost sites (recommended)
StrictOnly sent on same-site requestsHigh-security apps
DefaultBrowser default (usually Lax)Legacy compatibility
Default: None (with Secure flag)

Partitioned (CHIPS)

Enable Cookies Having Independent Partitioned State:
anubis --cookie-partitioned
Required for third-party cookies in Chrome (2024+). Share cookies across subdomains:
# Static domain
anubis --cookie-domain example.com

# Dynamic (auto-detect from request)
anubis --cookie-dynamic-domain
Static domain:
  • Cookie valid for *.example.com
  • Set once, works everywhere
Dynamic domain:
  • Cookie valid for request domain only
  • Users on www.example.com and api.example.com get separate cookies
⚠️ Cannot use both: Setting both flags is an error.
# Default: 24 hours
anubis --cookie-expiration-time 24h

# Custom values
anubis --cookie-expiration-time 1h   # 1 hour
anubis --cookie-expiration-time 7d   # 7 days (not supported, use 168h)
anubis --cookie-expiration-time 168h # 7 days
After expiration, users must re-solve challenges. Customize cookie names:
# Default: "anubis"
anubis --cookie-prefix myapp

# Results in cookies:
# - myapp-auth
# - myapp-cookie-verification
Use different prefixes when running multiple Anubis instances on the same domain.

JWT Claims

JWT payload contains:
{
  "exp": 1735689600,          // Expiration timestamp
  "iat": 1735603200,          // Issued at timestamp
  "nbf": 1735603200,          // Not before timestamp
  "difficulty": 2,            // Optional: if --difficulty-in-jwt
  "X-Real-IP": "1.2.3.4"     // Optional: if --jwt-restriction-header set
}

Difficulty in JWT

Include challenge difficulty in token:
anubis --difficulty-in-jwt
Use cases:
  • Track which difficulty was solved
  • Audit challenge settings
  • Debug multi-difficulty policies

JWT IP Restriction

Bind JWT to specific IP address:
# Default: X-Real-IP
anubis --jwt-restriction-header X-Real-IP

# Custom header
anubis --jwt-restriction-header CF-Connecting-IP
If set, JWT is only valid when request comes from the same IP that solved the challenge. Limitations:
  • Breaks mobile users (IP changes)
  • Issues with carrier-grade NAT
  • Only use if your threat model requires it

Security Best Practices

Key Generation

Do:
  • Use cryptographically secure random number generator
  • Store keys in secrets management (Vault, AWS Secrets Manager)
  • Rotate keys periodically
  • Use Ed25519 (not HMAC-SHA512)
🚫 Don’t:
  • Commit keys to version control
  • Use the same key across environments
  • Share keys between services
  • Use predictable/weak keys
Do:
  • Use --cookie-secure in production (HTTPS only)
  • Set appropriate --cookie-same-site for your use case
  • Use short expiration times (balance security vs UX)
  • Enable --cookie-partitioned for third-party contexts
🚫 Don’t:
  • Disable --cookie-secure in production
  • Use SameSite=None without HTTPS
  • Set extremely long expiration times
  • Ignore browser warnings about cookie flags

Deployment

Do:
  • Always configure signing keys in production
  • Use persistent storage backends (bbolt, valkey, s3api)
  • Monitor for “generating random key” warnings
  • Test key rotation procedure
🚫 Don’t:
  • Rely on random key generation
  • Use memory storage backend in production
  • Forget to mount secrets in containers
  • Share signing keys across environments

Validation Errors

Key Validation

# Error: supplied key is not hex-encoded
ED25519_PRIVATE_KEY_HEX=not-hex-data

# Fix: Use hex-encoded key
ED25519_PRIVATE_KEY_HEX=$(openssl rand -hex 32)
# Error: supplied key is not 32 bytes long
ED25519_PRIVATE_KEY_HEX=abc123  # Too short

# Fix: Must be exactly 64 hex characters (32 bytes)
ED25519_PRIVATE_KEY_HEX=$(openssl rand -hex 32)

Conflicting Configuration

# Error: do not specify both HS512 and ED25519 secrets
HS512_SECRET=secret1
ED25519_PRIVATE_KEY_HEX=secret2

# Fix: Use only one
unset HS512_SECRET
# Error: can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN
anubis --cookie-domain example.com --cookie-dynamic-domain

# Fix: Choose one
anubis --cookie-domain example.com

Persistent Storage Warning

When using persistent storage without a configured key:
WARN [misconfiguration] persistent storage backend is configured, but no 
private key is set. Challenges will be invalidated when Anubis restarts. 
Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE
Impact:
  • All active users must re-solve challenges after restart
  • Degraded user experience
  • Potential spike in challenge traffic
Fix: Configure a signing key (see Key Generation).

Next Steps

Build docs developers (and LLMs) love