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.
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
- Installation
- Architecture
- Configuration
- Integration Guide
- Rate Limiting Algorithm
- Security Model
- Database Schema
- Scaling Considerations
- Development Setup
- License
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 frameworksqlalchemy[asyncio]+asyncpgโ Async PostgreSQLredisโ Rate limiting backendpydanticโ Configuration validationpasslibโ 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_keyis 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
401if no key is provided - Return
401if the key is invalid or revoked - Return
429when the rate limit is exceeded - Attach the key object to
request.state.api_keyfor 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:
- 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.
- Async Logging: Push
UsageLogentries to a background job queue to prevent database writes from adding latency to the hot path. - 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f51020f86ab0d5aedf517f19d8e34aff8193eb26419b653571ccc24783c69d4
|
|
| MD5 |
6d5fdd36ea13d6c12b31aad336b703c6
|
|
| BLAKE2b-256 |
07e9b049ab7fe28a2946ab0011cf58c3e20453ba72139f8e3a51a5637af933ef
|
File details
Details for the file keyguard_python-0.1.0-py3-none-any.whl.
File metadata
- Download URL: keyguard_python-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
567ea7b0f198cb66e34b653d70d70e3fc35ece368f9385b59280e68cc9881f32
|
|
| MD5 |
bbf8ac0f3e5cd6d6382fec66a2d0744e
|
|
| BLAKE2b-256 |
3ac044175d99150113230db9da8adb5e51482e6ef48b185074ec694de702a604
|