The Problem
Raj leads security at a 25-person API company. Credential stuffing attacks from rotating IPs bypass their per-IP rate limiter. A botnet of 10K IPs each sends 5 requests (below the limit) but together they make 50K malicious requests. VPN and proxy IPs get false-positive blocked, affecting legitimate users. They need reputation scoring: track IP behavior over time, detect patterns across IPs, integrate with threat intelligence feeds, and apply risk-based authentication (CAPTCHA for medium risk, block for high risk).
Step 1: Build the Reputation Engine
typescript
import { Redis } from "ioredis";
import { pool } from "../db";
const redis = new Redis(process.env.REDIS_URL!);
interface IPReputation { ip: string; score: number; riskLevel: "low" | "medium" | "high" | "critical"; factors: Array<{ name: string; impact: number; details: string }>; lastUpdated: number; }
// Calculate IP reputation score (0 = good, 100 = bad)
export async function getReputation(ip: string): Promise<IPReputation> {
const cached = await redis.get(`ip:rep:${ip}`);
if (cached) return JSON.parse(cached);
const factors: IPReputation["factors"] = [];
let score = 0;
// Factor 1: Recent auth failures
const failKey = `ip:fails:${ip}`;
const fails = parseInt(await redis.get(failKey) || "0");
if (fails > 5) { score += Math.min(30, fails * 3); factors.push({ name: "auth_failures", impact: Math.min(30, fails * 3), details: `${fails} failed auth attempts` }); }
// Factor 2: Request pattern analysis
const minute = Math.floor(Date.now() / 60000);
const reqs = parseInt(await redis.get(`ip:reqs:${ip}:${minute}`) || "0");
if (reqs > 50) { score += 20; factors.push({ name: "high_request_rate", impact: 20, details: `${reqs} requests/min` }); }
// Factor 3: User-Agent anomalies
const uaCount = await redis.scard(`ip:uas:${ip}`);
if (uaCount > 10) { score += 15; factors.push({ name: "ua_rotation", impact: 15, details: `${uaCount} different User-Agents` }); }
// Factor 4: Known blocklist
const blocked = await redis.sismember("ip:blocklist", ip);
if (blocked) { score += 40; factors.push({ name: "blocklist", impact: 40, details: "IP on threat blocklist" }); }
// Factor 5: Datacenter/proxy detection
const isProxy = await isDatacenterIP(ip);
if (isProxy) { score += 10; factors.push({ name: "datacenter_ip", impact: 10, details: "Datacenter/proxy IP range" }); }
// Factor 6: Geographic anomaly
const countries = await redis.scard(`ip:countries:${ip}`);
if (countries > 3) { score += 10; factors.push({ name: "geo_anomaly", impact: 10, details: `Requests from ${countries} countries` }); }
score = Math.min(100, score);
const riskLevel = score >= 70 ? "critical" : score >= 40 ? "high" : score >= 20 ? "medium" : "low";
const rep: IPReputation = { ip, score, riskLevel, factors, lastUpdated: Date.now() };
await redis.setex(`ip:rep:${ip}`, 300, JSON.stringify(rep));
return rep;
}
// Record request for reputation tracking
export async function recordRequest(ip: string, context: { userAgent: string; path: string; statusCode: number; country?: string }): Promise<void> {
const pipeline = redis.pipeline();
const minute = Math.floor(Date.now() / 60000);
pipeline.incr(`ip:reqs:${ip}:${minute}`); pipeline.expire(`ip:reqs:${ip}:${minute}`, 300);
pipeline.sadd(`ip:uas:${ip}`, context.userAgent); pipeline.expire(`ip:uas:${ip}`, 86400);
if (context.statusCode === 401 || context.statusCode === 403) { pipeline.incr(`ip:fails:${ip}`); pipeline.expire(`ip:fails:${ip}`, 3600); }
if (context.country) { pipeline.sadd(`ip:countries:${ip}`, context.country); pipeline.expire(`ip:countries:${ip}`, 86400); }
await pipeline.exec();
}
// Risk-based middleware
export function reputationMiddleware() {
return async (c: any, next: any) => {
const ip = c.req.header("CF-Connecting-IP") || c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() || "unknown";
const rep = await getReputation(ip);
c.header("X-IP-Risk", rep.riskLevel);
if (rep.riskLevel === "critical") return c.json({ error: "Access denied" }, 403);
if (rep.riskLevel === "high") { c.set("requireCaptcha", true); }
await next();
await recordRequest(ip, { userAgent: c.req.header("User-Agent") || "", path: c.req.path, statusCode: c.res.status });
};
}
async function isDatacenterIP(ip: string): Promise<boolean> {
const dcRanges = ["34.", "35.", "52.", "54.", "104."]; // simplified AWS/GCP/Azure ranges
return dcRanges.some((r) => ip.startsWith(r));
}
Results
- Credential stuffing blocked — 10K botnet IPs each get low individual score but auth_failures + ua_rotation + datacenter flags combine to high risk; attack mitigated
- Legitimate VPN users not blocked — VPN IP gets +10 for datacenter, but 0 for auth failures and normal UA; total score 10 = low risk; no false positive
- Risk-based auth — medium risk: show CAPTCHA; high risk: require MFA; critical: block; graduated response instead of binary block
- Real-time scoring — reputation calculated in <5ms from Redis counters; no external API calls; works at 10K req/sec
- Behavioral patterns — IP rotating User-Agents = bot signal; IP with 100% auth failures = credential stuffing; multi-factor scoring catches sophisticated attacks