Skip to main content

A production-grade API key and rate limiting library for Python.

Project description

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

Python 3.9+ FastAPI License: MIT Redis PostgreSQL


Overview

KeyGuard is a lightweight, production-ready Python library that gives your FastAPI application a full API gateway layer in under 10 lines of code.

It handles the hard stuff that every SaaS backend needs:

  • ๐Ÿ”‘ API Key Lifecycle โ€” Generate, validate, and revoke keys with one-way hashed storage
  • โฑ๏ธ Sliding Window Rate Limiting โ€” Per-key quotas backed by Redis for millisecond precision
  • ๐Ÿ›ก๏ธ IP Abuse Prevention โ€” Automatic blacklisting for repeated unauthorized requests
  • ๐Ÿ“Š Request Logging โ€” Per-request latency and usage tracking stored in PostgreSQL
  • ๐Ÿข Multi-Tenant โ€” Organize keys under organizations for SaaS-style access control

Inspired by how Stripe, Cloudflare, and AWS API Gateway work โ€” simplified for real Python backends.


Table of Contents


Quick Start

from fastapi import FastAPI
from keyguard import KeyGuard, KeyGuardConfig, KeyGuardMiddleware

app = FastAPI()

# 1. Configure KeyGuard
config = KeyGuardConfig(
    database_url="postgresql+asyncpg://user:pass@localhost/mydb",
    redis_url="redis://localhost:6379/0",
    secret_key="your-secret-pepper-key"
)

# 2. Initialize the core instance
kg = KeyGuard(config)

# 3. Register middleware to protect your /api routes
app.add_middleware(KeyGuardMiddleware, kg_instance=kg, protected_path="/api")

# 4. Initialize database tables on startup
@app.on_event("startup")
async def startup():
    await kg.init_db()

# Any route under /api is now protected
@app.get("/api/data")
async def protected_data():
    return {"message": "Authorized!"}
# Access a protected route
curl http://localhost:8000/api/data -H "X-API-KEY: kg_live_your_key_here"

# Missing key โ†’ 401
# Wrong key  โ†’ 401
# Too many   โ†’ 429 with X-RateLimit-Remaining: 0

Installation

# From source (recommended for now)
git clone https://github.com/yourusername/keyguard
cd keyguard
pip install -e .

Dependencies automatically installed:

  • fastapi โ€” Web framework
  • sqlalchemy[asyncio] + asyncpg โ€” Async PostgreSQL
  • redis โ€” Rate limiting backend
  • pydantic โ€” Configuration validation
  • passlib โ€” Password/secret utilities

Architecture

KeyGuard is designed around two core concepts: a hot path (executed for every request) and a cold path (management and analytics).

Incoming Request
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚        KeyGuardMiddleware        โ”‚  โ† Hot Path
โ”‚                                  โ”‚
โ”‚  1. IP Blacklist check (Redis)   โ”‚
โ”‚  2. Extract X-API-KEY header     โ”‚
โ”‚  3. Hash & validate key (DB)     โ”‚
โ”‚  4. Sliding window rate limit    โ”‚
โ”‚  5. Attach key to request.state  โ”‚
โ”‚  6. Log usage (Postgres, async)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ–ผ
  Your Route Handler


โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           KeyGuard Core          โ”‚  โ† Cold Path
โ”‚                                  โ”‚
โ”‚  โ€ข AuthService (key generation)  โ”‚
โ”‚  โ€ข RateLimitService              โ”‚
โ”‚  โ€ข DB session factory            โ”‚
โ”‚  โ€ข init_db() utility             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Configuration

All configuration is passed via a KeyGuardConfig object. No .env file is required.

from keyguard import KeyGuardConfig

config = KeyGuardConfig(
    # Required
    database_url="postgresql+asyncpg://user:pass@localhost/db",
    redis_url="redis://localhost:6379/0",
    secret_key="a-long-random-secret-for-key-hashing",

    # Optional โ€” with sensible defaults
    default_rate_limit_per_minute=60,   # Default quota for new keys
    ip_block_threshold=100,             # Failed attempts before IP ban
    auto_init_db=True                   # Auto-create tables on init_db()
)
Parameter Default Description
database_url postgresql+asyncpg://... Async PostgreSQL connection string
redis_url redis://localhost:6379/0 Redis connection string
secret_key (required) Pepper used when hashing keys
default_rate_limit_per_minute 60 Default new key quota
ip_block_threshold 100 Auth failures before IP block

Integration Guide

1. Adding Middleware

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

# Or a more specific prefix
app.add_middleware(KeyGuardMiddleware, kg_instance=kg, protected_path="/api/v1")

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


2. Initializing the Database

KeyGuard creates its own tables inside your database without interfering with your app's existing schema.

@app.on_event("startup")
async def startup():
    await kg.init_db()  # Creates organizations, api_keys, usage_logs tables

3. Creating Organizations & Keys

KeyGuard's management interface is entirely programmatic โ€” ideal for embedding into your own admin panel, CLI, or setup scripts.

from keyguard.models import Organization, APIKey

async def create_org_and_key(session):
    # Create an Organisation
    org = Organization(name="Acme Corp")
    session.add(org)
    await session.flush()

    # Generate an API Key
    raw_key, key_hash = kg.auth.generate_api_key(prefix="kg_live_")

    key = APIKey(
        org_id=org.id,
        label="Production App Key",
        prefix=raw_key[:8],
        key_hash=key_hash,
        rate_limit_per_minute=120,      # Custom quota
        scopes=["read", "write"]        # Extensible scopes
    )
    session.add(key)
    await session.commit()

    print(f"API Key (show once): {raw_key}")
    return raw_key

# Use the session factory from KeyGuard
async with kg.session_factory() as session:
    await create_org_and_key(session)

Security note: The raw_key is only available at creation time. After hashing, it cannot be recovered. Store it securely and show it to the user once.


4. Protecting Routes

Once the 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 for use in your handler
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": str(key.org_id),
        "key_label": key.label,
        "scopes": key.scopes
    }

Response Headers on every authorized request:

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

Rate Limiting Algorithm

KeyGuard uses the Sliding Window Log algorithm, implemented with Redis Sorted Sets (ZSET).

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

Why Sliding Window Log over Fixed Window?

Algorithm Accuracy Redis Cost Burst Tolerance
Fixed Window Low (burst at boundary) Very low Poor
Sliding Window Counter Medium Low Good
Sliding Window Log High (exact) Medium Excellent

For most SaaS use cases, the precision of the Sliding Window Log is worth the slightly higher Redis memory cost.


Security Model

Key Generation

Keys are generated using Python's secrets.token_urlsafe(32) โ€” cryptographically secure random bytes encoded in URL-safe Base64.

Final key format: {prefix}{random_32_bytes_urlsafe_base64}
Example:          kg_live_4Gk9mBX3pLqRsW...

Key Storage

Raw keys are never stored. Keys are hashed with SHA-256 + secret pepper before being written to the database:

stored_hash = SHA-256(raw_key + SECRET_KEY)

Even if your database is fully compromised, the raw keys cannot be recovered without the SECRET_KEY.

IP Abuse Prevention

Every failed authentication attempt (wrong key, missing key) is tracked per IP in Redis. Once an IP exceeds the ip_block_threshold, it is blocked for 24 hours.

Abuse tracking key:   abuse:{ip}   (Counter, 1hr TTL)
Blacklist key:        block:{ip}   (Flag, 24hr TTL)

Database Schema

KeyGuard creates three tables in your database:

organizations
โ”œโ”€โ”€ id          UUID (PK)
โ”œโ”€โ”€ name        VARCHAR
โ”œโ”€โ”€ status      VARCHAR  -- 'active' | 'suspended'
โ””โ”€โ”€ created_at  TIMESTAMPTZ

api_keys
โ”œโ”€โ”€ id                    UUID (PK)
โ”œโ”€โ”€ org_id                UUID (FK โ†’ organizations)
โ”œโ”€โ”€ label                 VARCHAR
โ”œโ”€โ”€ prefix                VARCHAR     -- e.g. 'kg_live_'
โ”œโ”€โ”€ key_hash              VARCHAR     -- SHA-256 hash, indexed
โ”œโ”€โ”€ is_active             BOOLEAN
โ”œโ”€โ”€ scopes                JSONB       -- ["read", "write"]
โ”œโ”€โ”€ rate_limit_per_minute INTEGER
โ”œโ”€โ”€ monthly_limit         BIGINT
โ”œโ”€โ”€ created_at            TIMESTAMPTZ
โ”œโ”€โ”€ expires_at            TIMESTAMPTZ
โ””โ”€โ”€ last_used_at          TIMESTAMPTZ

usage_logs
โ”œโ”€โ”€ id          UUID (PK)
โ”œโ”€โ”€ key_id      UUID (FK โ†’ api_keys)
โ”œโ”€โ”€ path        VARCHAR
โ”œโ”€โ”€ method      VARCHAR
โ”œโ”€โ”€ status_code INTEGER
โ”œโ”€โ”€ latency_ms  INTEGER
โ”œโ”€โ”€ ip_address  VARCHAR
โ””โ”€โ”€ timestamp   TIMESTAMPTZ

Scaling Considerations

KeyGuard is designed to scale horizontally without any changes.

Scale Level Architecture
Small (< 1K req/s) Single FastAPI + Postgres + Redis instance
Medium (< 50K req/s) Multiple FastAPI instances behind a load balancer. Redis handles shared rate limit state.
Large (> 50K req/s) Cache key metadata in Redis to eliminate per-request DB reads. Push usage_logs to a queue (Kafka/SQS) instead of writing synchronously.

Recommended optimizations for high traffic:

  1. Redis Key Cache: Cache the API key object in Redis with a short TTL (e.g., 30s) to avoid PostgreSQL on every hot path request.
  2. Async Logging: Push UsageLog entries to a background job queue to prevent database writes from adding latency to the hot path.
  3. Read Replicas: Point the admin queries (stats, logs) to a Postgres read replica.

Development Setup

# Clone the repo
git clone https://github.com/yourusername/keyguard
cd keyguard

# Create and activate virtual environment
python -m venv venv
source venv/bin/activate

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

# Start infrastructure
docker compose up -d  # Starts Postgres + Redis

# Run the example integration
uvicorn example_integration:app --reload

License

MIT License โ€” see LICENSE for details.


Built with precision. Designed for production.

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.1.0.tar.gz (15.4 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.1.0-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: keyguard_python-0.1.0.tar.gz
  • Upload date:
  • Size: 15.4 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.1.0.tar.gz
Algorithm Hash digest
SHA256 7f51020f86ab0d5aedf517f19d8e34aff8193eb26419b653571ccc24783c69d4
MD5 6d5fdd36ea13d6c12b31aad336b703c6
BLAKE2b-256 07e9b049ab7fe28a2946ab0011cf58c3e20453ba72139f8e3a51a5637af933ef

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for keyguard_python-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 567ea7b0f198cb66e34b653d70d70e3fc35ece368f9385b59280e68cc9881f32
MD5 bbf8ac0f3e5cd6d6382fec66a2d0744e
BLAKE2b-256 3ac044175d99150113230db9da8adb5e51482e6ef48b185074ec694de702a604

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