Skip to content

Authentication

Chapkit provides simple API key authentication for service-to-service communication in Docker Compose and Kubernetes environments.

Quick Start

The simplest and most secure approach for production deployments:

from chapkit.api import ServiceBuilder, ServiceInfo

app = (
    ServiceBuilder(info=ServiceInfo(display_name="My Service"))
    .with_auth()  # Reads from CHAPKIT_API_KEYS environment variable
    .with_config(MyConfig)
    .build()
)

Set the environment variable:

export CHAPKIT_API_KEYS="sk_prod_abc123,sk_prod_xyz789"
fastapi run your_file.py

Docker Secrets (Most Secure for Production)

For Docker Swarm or Kubernetes deployments:

app = (
    ServiceBuilder(info=ServiceInfo(display_name="My Service"))
    .with_auth(api_key_file="/run/secrets/api_keys")
    .build()
)

docker-compose.yml:

version: '3.8'

services:
  app:
    image: your-app
    secrets:
      - api_keys

secrets:
  api_keys:
    file: ./secrets/api_keys.txt

secrets/api_keys.txt:

sk_prod_abc123
sk_prod_xyz789

Direct Keys (Development Only)

WARNING: Never use this in production!

app = (
    ServiceBuilder(info=ServiceInfo(display_name="My Service"))
    .with_auth(api_keys=["sk_dev_test123"])  # NOT for production
    .build()
)

Configuration Options

The .with_auth() method accepts these parameters:

.with_auth(
    api_keys=None,                      # Direct list (dev only)
    api_key_file=None,                  # File path (Docker secrets)
    env_var="CHAPKIT_API_KEYS",         # Environment variable name
    header_name="X-API-Key",            # HTTP header for API key
    unauthenticated_paths=None,         # Paths without auth
)

Priority

Chapkit uses the first non-None value in this order: 1. api_keys (direct list) 2. api_key_file (file path) 3. env_var (environment variable, default: CHAPKIT_API_KEYS)

Parameters

  • api_keys (List[str] | None): Direct list of API keys. Only for examples and local development.
  • api_key_file (str | None): Path to file containing keys (one per line). For Docker secrets.
  • env_var (str): Environment variable name to read keys from. Default: CHAPKIT_API_KEYS.
  • header_name (str): HTTP header name for API key. Default: X-API-Key.
  • unauthenticated_paths (List[str] | None): Paths that don't require authentication.

Key Format Conventions

Recommended format: sk_{environment}_{random}

Examples

sk_prod_a1b2c3d4e5f6g7h8     # Production
sk_staging_x1y2z3a4b5c6d7e8  # Staging
sk_dev_test123               # Development

Why This Format?

  • sk_ prefix: Easily identifiable as a secret key
  • environment: Know which environment the key belongs to
  • random: Unique identifier (16+ characters recommended)

Chapkit logs only the first 7 characters (sk_prod_****) for security.


Key Rotation

To rotate API keys without downtime:

  1. Add new key (keep old key active)
  2. Update clients to use new key
  3. Remove old key after all clients updated

Example Rotation

# Step 1: Both keys active
export CHAPKIT_API_KEYS="sk_prod_old123,sk_prod_new456"

# Deploy and verify service restarts
fastapi run your_file.py

# Step 2: Update all clients to use sk_prod_new456
# Test that clients work with new key

# Step 3: Remove old key (after confirming all clients updated)
export CHAPKIT_API_KEYS="sk_prod_new456"

# Restart service
fastapi run your_file.py

Unauthenticated Paths

By default, these paths don't require authentication:

  • /docs - Swagger UI
  • /redoc - ReDoc
  • /openapi.json - OpenAPI schema
  • /health - Health check
  • / - Landing page

Custom Unauthenticated Paths

.with_auth(
    unauthenticated_paths=["/health", "/public", "/status"]
)

This replaces the default list. To add to the default list:

default_paths = ["/docs", "/redoc", "/openapi.json", "/health", "/"]
custom_paths = default_paths + ["/public", "/status"]

.with_auth(unauthenticated_paths=custom_paths)

Testing Authenticated APIs

With cURL

# Valid request
curl -H "X-API-Key: sk_dev_test123" http://localhost:8000/api/v1/configs

# Missing key (returns 401)
curl http://localhost:8000/api/v1/configs

# Invalid key (returns 401)
curl -H "X-API-Key: invalid_key" http://localhost:8000/api/v1/configs

# Unauthenticated path (no key needed)
curl http://localhost:8000/health

With Python requests

import requests

headers = {"X-API-Key": "sk_dev_test123"}

# Authenticated request
response = requests.get(
    "http://localhost:8000/api/v1/configs",
    headers=headers
)

# Check response
assert response.status_code == 200

With httpx (async)

import httpx

headers = {"X-API-Key": "sk_dev_test123"}

async with httpx.AsyncClient() as client:
    response = await client.get(
        "http://localhost:8000/api/v1/configs",
        headers=headers
    )
    assert response.status_code == 200

Docker Deployment

Docker Compose

docker-compose.yml:

version: '3.8'

services:
  chapkit-service:
    image: your-chapkit-app
    ports:
      - "8000:8000"
    environment:
      # Option 1: Environment variable
      CHAPKIT_API_KEYS: sk_prod_abc123,sk_prod_xyz789

      # Option 2: Point to secrets file
      # CHAPKIT_API_KEY_FILE: /run/secrets/api_keys

    # Option 2 (continued): Mount secrets
    # secrets:
    #   - api_keys

# secrets:
#   api_keys:
#     file: ./secrets/api_keys.txt

secrets/api_keys.txt:

# Production API keys
# One key per line, comments allowed
sk_prod_abc123
sk_prod_xyz789

.gitignore:

# Never commit secrets!
secrets/api_keys.txt

secrets/api_keys.txt.example:

# Example API keys file
# Copy to api_keys.txt and replace with real keys
sk_prod_example1
sk_prod_example2

Docker Swarm

# Create secret
echo -e "sk_prod_abc123\nsk_prod_xyz789" | \
  docker secret create chapkit_api_keys -

# Deploy service
docker service create \
  --name my-chapkit-service \
  --secret chapkit_api_keys \
  -e CHAPKIT_API_KEY_FILE=/run/secrets/chapkit_api_keys \
  -p 8000:8000 \
  your-chapkit-app

Kubernetes Deployment

secret.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: chapkit-api-keys
type: Opaque
stringData:
  api_keys.txt: |
    sk_prod_abc123
    sk_prod_xyz789

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: chapkit-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: chapkit-service
  template:
    metadata:
      labels:
        app: chapkit-service
    spec:
      containers:
      - name: app
        image: your-chapkit-app
        ports:
        - containerPort: 8000
        env:
        - name: CHAPKIT_API_KEY_FILE
          value: /etc/secrets/api_keys.txt
        volumeMounts:
        - name: api-keys
          mountPath: /etc/secrets
          readOnly: true
      volumes:
      - name: api-keys
        secret:
          secretName: chapkit-api-keys


Logging

Chapkit automatically logs authentication events with masked keys for security.

Successful Authentication

{
  "event": "auth.success",
  "key_prefix": "sk_prod",
  "path": "/api/v1/configs"
}

Failed Authentication

{
  "event": "auth.invalid_key",
  "key_prefix": "sk_unkn",
  "path": "/api/v1/configs",
  "method": "GET"
}

Missing Key

{
  "event": "auth.missing_key",
  "path": "/api/v1/configs",
  "method": "GET"
}

Only the first 7 characters of keys are logged. Full keys are never logged.


Security Best Practices

  • Use environment variables or Docker secrets in production
  • Use sk_env_random format for easy identification in logs
  • Rotate keys regularly (quarterly recommended)
  • Use different keys for different services/environments
  • Keep .env files in .gitignore
  • Use minimum 16 characters for key randomness
  • Monitor authentication logs for failed attempts

Avoid

  • Committing API keys to git (use .gitignore)
  • Using api_keys= parameter in production (only for examples)
  • Reusing keys across environments (dev/staging/prod)
  • Using weak/short keys (minimum 16 characters)
  • Sharing keys via email/Slack (use secrets management)
  • Hardcoding keys in source code

Error Responses

All authentication errors follow RFC 9457 Problem Details format.

Missing API Key (401)

{
  "type": "urn:chapkit:error:unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Missing authentication header: X-API-Key",
  "instance": "/api/v1/configs"
}

Invalid API Key (401)

{
  "type": "urn:chapkit:error:unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid API key",
  "instance": "/api/v1/configs"
}

Advanced Usage

Custom Header Name

.with_auth(
    header_name="X-Custom-API-Key"
)

Test with:

curl -H "X-Custom-API-Key: sk_dev_test123" http://localhost:8000/api/v1/configs

Multiple Environments

Development:

# dev.py
.with_auth(api_keys=["sk_dev_test123"])

Production:

# prod.py
.with_auth()  # Reads from CHAPKIT_API_KEYS env var

Service-to-Service Communication

# Service A (client)
import httpx

headers = {"X-API-Key": os.getenv("SERVICE_B_API_KEY")}
async with httpx.AsyncClient() as client:
    response = await client.get(
        "http://service-b:8000/api/v1/data",
        headers=headers
    )

# Service B (server)
app = ServiceBuilder(info=info).with_auth().build()

Troubleshooting

"No API keys configured" Error

Problem: Service fails to start with error message.

Solution: Ensure you've provided keys via one of these methods:

# Environment variable
export CHAPKIT_API_KEYS="sk_dev_test123"

# Or in Python (dev only)
.with_auth(api_keys=["sk_dev_test123"])

# Or via file
.with_auth(api_key_file="/path/to/keys.txt")

401 Unauthorized on Health Check

Problem: Health check returns 401 instead of 200.

Solution: Health checks are unauthenticated by default. If you customized unauthenticated_paths, add /health back:

.with_auth(
    unauthenticated_paths=[
        "/docs", "/redoc", "/openapi.json",
        "/health",  # Add this
        "/", "/custom/path"
    ]
)

Keys Not Loading from File

Problem: FileNotFoundError: API key file not found

Solution: 1. Verify file path is absolute: /run/secrets/api_keys (not relative) 2. Check file exists: ls -la /run/secrets/api_keys 3. Verify container has access (Docker secrets mount at /run/secrets/)

Keys Not Loading from Environment

Problem: "No API keys found in CHAPKIT_API_KEYS"

Solution: 1. Verify env var is set: echo $CHAPKIT_API_KEYS 2. Check for typos in variable name 3. Ensure keys are comma-separated: key1,key2,key3 4. No spaces around commas: sk_dev_1,sk_dev_2 (not sk_dev_1, sk_dev_2)


Next Steps

  • ML Services: Combine with .with_ml() for authenticated ML endpoints
  • Rate Limiting: See roadmap for per-key rate limiting (P2)
  • Key Scoping: See roadmap for endpoint-specific keys (P2)
  • Monitoring: Track authentication metrics with Prometheus (P1)

For more examples, see: - examples/auth_basic.py - Basic authentication example - CLAUDE.md - Comprehensive development guide