Skip to main content

API key authentication, rate limiting, and abuse prevention as a drop-in Python library. Works with zero infrastructure.

Project description

API key authentication, rate limiting, and abuse prevention
as a drop-in Python library.

Python 3.9+ FastAPI License: MIT Zero Config Redis


Why KeyGuard?

Every API needs authentication and rate limiting. But setting up PostgreSQL, Redis, and Docker just to protect a few routes is overkill for most projects.

KeyGuard works with zero infrastructure — install it, add 3 lines of code, and your API is protected. When you're ready for production, swap in PostgreSQL and Redis with a config change.


Zero-Infrastructure Quick Start

pip install -e .
from fastapi import FastAPI
from keyguard import KeyGuard, KeyGuardConfig, KeyGuardMiddleware

app = FastAPI()

# That's it — SQLite database + in-memory rate limiting
kg = KeyGuard(KeyGuardConfig(secret_key="my-secret"))

app.add_middleware(KeyGuardMiddleware, kg_instance=kg, protected_path="/api")

@app.on_event("startup")
async def startup():
    await kg.init_db()  # Creates a local keyguard.db file

@app.get("/api/data")
async def protected():
    return {"message": "Authorized!"}

No Docker. No PostgreSQL. No Redis. Just pip install and go.

# Create your first API key
python -m keyguard init
python -m keyguard create-org "My Project"
python -m keyguard create-key --org "My Project" --label "dev-key"

# Test it
curl http://localhost:8000/api/data -H "X-API-KEY: kg_live_..."

Table of Contents


CLI Tool

Manage everything from the command line — no code required.

# Initialize the database
python -m keyguard init

# Create an organization
python -m keyguard create-org "Acme Corp"

# Generate an API key
python -m keyguard create-key --org "Acme Corp" --label "production"

# List everything
python -m keyguard list-orgs
python -m keyguard list-keys

# Revoke a key
python -m keyguard revoke-key kg_live_4Gk9

# View usage stats
python -m keyguard stats

Custom database and Redis:

# Use PostgreSQL instead of SQLite
python -m keyguard --db "postgresql+asyncpg://user:pass@localhost/db" list-keys

# With Redis for rate limiting
python -m keyguard --redis "redis://localhost:6379/0" stats

CLI Output Examples

$ python -m keyguard list-keys

Label                     Prefix          Org                  Status     Rate/min   Last Used
──────────────────────────────────────────────────────────────────────────────────────────────
production                kg_live_4Gk9    Acme Corp            active     120        2026-04-22 14:30
staging-key               kg_live_xR2m    Acme Corp            active     60         never
test-key                  kg_live_9pLq    Dev Team             revoked    30         2026-04-21 09:15

Total: 3 key(s)
$ python -m keyguard stats

╔══════════════════════════════════════╗
║        KeyGuard Statistics           ║
╠══════════════════════════════════════╣
║  Organizations:    2                 ║
║  Total Keys:       3                 ║
║  Active Keys:      2                 ║
║  Total Requests:   1,247             ║
║  Requests (1h):    83                ║
║  Error Rate:       2.4%              ║
╚══════════════════════════════════════╝

Admin API

Mount a built-in admin router to manage KeyGuard via HTTP:

from keyguard.api.admin import create_admin_router

app.include_router(
    create_admin_router(kg),
    prefix="/admin",
    tags=["KeyGuard Admin"]
)

All admin endpoints are protected by the X-Admin-Key header (your secret_key).

Method Endpoint Description
POST /admin/orgs Create organization
GET /admin/orgs List organizations
POST /admin/keys Create API key (returns raw key once)
GET /admin/keys List all keys (masked)
DELETE /admin/keys/{id} Revoke a key
GET /admin/stats Usage statistics
# Create an org
curl -X POST http://localhost:8000/admin/orgs \
  -H "X-Admin-Key: my-secret" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme Corp"}'

# Create a key
curl -X POST http://localhost:8000/admin/keys \
  -H "X-Admin-Key: my-secret" \
  -H "Content-Type: application/json" \
  -d '{"org_name": "Acme Corp", "label": "prod-key", "rate_limit_per_minute": 120}'

# List keys
curl http://localhost:8000/admin/keys -H "X-Admin-Key: my-secret"

# View stats
curl http://localhost:8000/admin/stats -H "X-Admin-Key: my-secret"

Tip: Visit http://localhost:8000/docs to get an interactive Swagger UI for all admin endpoints.


Production Setup

When you're ready for production, add PostgreSQL and Redis:

# Install production drivers
pip install -e ".[all]"

# Start infrastructure (optional Docker helper)
cd docker && docker compose up -d
config = KeyGuardConfig(
    database_url="postgresql+asyncpg://user:pass@localhost/mydb",
    redis_url="redis://localhost:6379/0",
    secret_key="a-long-random-secret",
    default_rate_limit_per_minute=120,
)
Setup Database Rate Limiter Best For
Zero Config SQLite (file) In-memory Prototyping, small projects, single-process
Production PostgreSQL Redis Multi-process, horizontal scaling, SaaS

Configuration

All configuration is passed via a KeyGuardConfig object:

from keyguard import KeyGuardConfig

config = KeyGuardConfig(
    # Database — SQLite (default) or PostgreSQL
    database_url="sqlite+aiosqlite:///keyguard.db",

    # Redis — None (default, uses in-memory) or a Redis URL
    redis_url=None,

    # Security — pepper for key hashing
    secret_key="change-me",

    # Rate limiting defaults
    default_rate_limit_per_minute=60,
    ip_block_threshold=100,
)
Parameter Default Description
database_url sqlite+aiosqlite:///keyguard.db SQLite or PostgreSQL connection string
redis_url None Redis URL. None = in-memory rate limiting
secret_key (required) Secret pepper for key hashing
default_rate_limit_per_minute 60 Default quota for new keys
ip_block_threshold 100 Failed attempts before IP block

Architecture

Incoming Request
       │
       ▼
┌──────────────────────────────────┐
│        KeyGuardMiddleware        │  ← Hot Path
│                                  │
│  1. IP Blacklist check           │  ← Redis or In-Memory
│  2. Extract X-API-KEY header     │
│  3. Hash & validate key          │  ← SQLite or PostgreSQL
│  4. Sliding window rate limit    │  ← Redis or In-Memory
│  5. Attach key to request.state  │
│  6. Log usage (async)            │  ← SQLite or PostgreSQL
└──────────────────────────────────┘
       │
       ▼
  Your Route Handler


┌──────────────────────────────────┐
│           KeyGuard Core          │  ← Cold Path
│                                  │
│  • AuthService (key generation)  │
│  • RateLimitService (auto-pick)  │
│  • DB session factory            │
│  • CLI + Admin API               │
└──────────────────────────────────┘

KeyGuard automatically picks the right backend:

  • No Redis URL → MemoryRateLimitService (in-memory deques)
  • Redis URL set → RateLimitService (Redis sorted sets)
  • sqlite:// URL → SQLite via aiosqlite
  • postgresql:// URL → PostgreSQL via asyncpg

Integration Guide

1. Adding Middleware

# Protect all routes under /api
app.add_middleware(KeyGuardMiddleware, kg_instance=kg, protected_path="/api")

Routes outside protected_path (e.g., /health, /docs, /admin) are unaffected.

2. Protecting Routes

Once middleware is applied, all routes under protected_path automatically:

  • Return 401 if no key is provided
  • Return 401 if the key is invalid or revoked
  • Return 429 when the rate limit is exceeded
  • Attach the key object to request.state.api_key
from fastapi import Request

@app.get("/api/profile")
async def get_profile(request: Request):
    key = request.state.api_key   # Populated by KeyGuard
    return {
        "org_id": key.org_id,
        "key_label": key.label,
        "scopes": key.scopes
    }

3. IP-Based Rate Limiting & Lockouts

For routes that don't use API keys (like login, signup, or heavy tasks), use the rate_limit_by_ip dependency. It supports sliding windows, hard lockouts, and granular scoping.

from keyguard import rate_limit_by_ip

# 1. Simple Rate Limit (5 per minute)
@app.post("/api/search", dependencies=[Depends(rate_limit_by_ip(kg, limit=5))])

# 2. Hard Lockout (24-hour block after 3 failures)
@app.post("/login", dependencies=[Depends(rate_limit_by_ip(kg, limit=3, lockout=86400))])

# 3. Scheduled Lockout (Block until 4:00 PM)
@app.post("/signup", dependencies=[Depends(rate_limit_by_ip(kg, limit=2, lockout="4:00 PM"))])

# 4. Global vs. Path Scope (default is "path")
# scope="path":   Only blocks access to THIS specific endpoint.
# scope="global": Blocks the IP from ALL KeyGuard-protected routes.
@app.post("/heavy-task", dependencies=[Depends(rate_limit_by_ip(kg, limit=1, scope="path"))])

4. Manual Logic-Based Blocking

You can manually trigger a lockout from within your route handlers based on your own business logic (e.g., failed payment or fraud detection) using kg.block_request.

@app.post("/withdraw")
async def withdraw(request: Request):
    if fraud_detected:
        # Manually block this IP from /withdraw for 1 hour
        await kg.block_request(request, duration="1 hour", scope="path")
        return {"error": "Suspicious activity. Endpoint locked for 1 hour."}

5. Response Headers

Response headers on every authorized request:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 43

6. Three Ways to Manage Keys

Method Best For
CLI (python -m keyguard) Quick setup, scripting, CI/CD
Admin API (/admin/keys) Web dashboards, frontends
Programmatic (Python code) Custom logic, migrations

Rate Limiting Algorithm

KeyGuard uses the Sliding Window Log algorithm.

Redis mode: Implemented with Redis Sorted Sets (ZSET) for distributed rate limiting across multiple processes.

In-memory mode: Implemented with Python deque + asyncio.Lock for single-process deployments.

Window: 60 seconds, Limit: 5 req/min

Timeline:
──────────────────────────────────────────▶ time
  t=0   t=10   t=40  t=59  t=61  t=62
  [req]  [req] [req] [req] [req] [req]
                                  ↑       ↑
                              5th hit   t=61: t=0 now out of window
                              BLOCKED   count = 4 → ALLOWED

Security Model

Key Generation

Keys use secrets.token_urlsafe(32) — cryptographically secure random bytes.

Format:  {prefix}{random_32_bytes_urlsafe_base64}
Example: kg_live_4Gk9mBX3pLqRsW...

Key Storage

Raw keys are never stored. They're hashed with SHA-256 + secret pepper:

stored_hash = SHA-256(raw_key + SECRET_KEY)

IP Abuse Prevention

Failed auth attempts are tracked per IP. After exceeding ip_block_threshold, the IP is blocked for 24 hours.


Database Schema

KeyGuard creates three tables (works identically in SQLite and PostgreSQL):

organizations
├── id          VARCHAR(36) (PK)
├── name        VARCHAR
├── status      VARCHAR      -- 'active' | 'suspended'
└── created_at  DATETIME

api_keys
├── id                    VARCHAR(36) (PK)
├── org_id                VARCHAR(36) (FK  organizations)
├── label                 VARCHAR
├── prefix                VARCHAR
├── key_hash              VARCHAR (indexed)
├── is_active             BOOLEAN
├── scopes                JSON
├── rate_limit_per_minute  INTEGER
├── monthly_limit         BIGINT
├── created_at            DATETIME
├── expires_at            DATETIME
└── last_used_at          DATETIME

usage_logs
├── id          VARCHAR(36) (PK)
├── key_id      VARCHAR(36) (FK  api_keys)
├── path        VARCHAR
├── method      VARCHAR
├── status_code INTEGER
├── latency_ms  INTEGER
├── ip_address  VARCHAR
└── timestamp   DATETIME

Scaling Considerations

Scale Level Setup
Hobby (< 100 req/s) SQLite + in-memory. Single process.
Small (< 1K req/s) PostgreSQL + Redis. Single server.
Medium (< 50K req/s) PostgreSQL + Redis. Multiple workers behind a load balancer.
Large (> 50K req/s) Add Redis key caching, push logs to a queue (Kafka/SQS).

Development Setup

# Clone
git clone https://github.com/The-honoured1/keyguard
cd keyguard

# Create venv
python -m venv venv
source venv/bin/activate

# Install with dev dependencies
pip install -e ".[dev]"

# Run the example (zero infrastructure!)
python example_integration.py

# Or use the CLI
python -m keyguard init
python -m keyguard create-org "Test"
python -m keyguard create-key --org "Test" --label "my-key"

For production drivers:

# PostgreSQL support
pip install -e ".[postgres]"

# Redis support
pip install -e ".[redis]"

# Everything
pip install -e ".[all]"

Project Structure

keyguard/
├── keyguard/
│   ├── __init__.py          # Package exports
│   ├── __main__.py          # CLI entry point
│   ├── cli.py               # CLI commands
│   ├── config.py            # Configuration
│   ├── core.py              # KeyGuard core class
│   ├── middleware.py         # FastAPI middleware
│   ├── models.py            # Model re-exports
│   ├── api/
│   │   └── admin.py         # Admin API router
│   ├── db/
│   │   └── models.py        # SQLAlchemy models
│   ├── schemas/
│   │   └── admin.py         # Pydantic schemas
│   └── services/
│       ├── auth_service.py       # Key generation & hashing
│       ├── rate_limit_service.py # Redis rate limiter
│       └── memory_rate_limit.py  # In-memory rate limiter
├── docker/
│   └── docker-compose.yml   # Optional production infra
├── example_integration.py   # Working example
├── pyproject.toml
└── README.md

License

MIT License — see LICENSE for details.


Built with precision. Designed for simplicity.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keyguard_python-0.3.0.tar.gz (24.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

keyguard_python-0.3.0-py3-none-any.whl (23.6 kB view details)

Uploaded Python 3

File details

Details for the file keyguard_python-0.3.0.tar.gz.

File metadata

  • Download URL: keyguard_python-0.3.0.tar.gz
  • Upload date:
  • Size: 24.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for keyguard_python-0.3.0.tar.gz
Algorithm Hash digest
SHA256 4b4a12ed5381715926e7541582ed1844bce2414ad8681bd533b73c598745cae6
MD5 7dbac3712725c22fc3b5a92bda3f6e67
BLAKE2b-256 aec64b112d9b642b8e6196a002858a7090b1cd47900f63840bbcfc1e5b2fb10e

See more details on using hashes here.

File details

Details for the file keyguard_python-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for keyguard_python-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 204caf84ceb3a031cafe25d4665da8568ac3842651c1f539496c98a006af1dde
MD5 462074e554a363cc9c7229ce1346246d
BLAKE2b-256 b696f02ef27fa84bac62bc144ade66347220f1f798342c7e777924b0fd041194

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page