harold-guard
v0.1.0
Published
NestJS guard that enforces Harold's IP/route blocklist and reports violations back
Maintainers
Readme
harold-guard
NestJS guard that enforces Harold's IP/route blocklist inside your app and reports violations back.
Features
- Polls Harold's
/api/enforce/list(blocked IPs and routes) every 30s - Blocks by real client IP:
CF-Connecting-IP→ firstX-Forwarded-For→req.ip - Route matching: exact + prefix (
/admin/*) - Fail-open: traffic passes if Harold is unreachable
- Reports violations back to
/api/enforce/hits(batched, every 60s)
Note: Harold's API also returns blocked ASNs, but the guard cannot resolve client IPs to ASNs locally — ASN entries are not enforced by this module.
Usage
Configuration resolves in this order: explicit options → environment variables
(HAROLD_URL, HAROLD_TOKEN). With env vars set, no options are needed:
export HAROLD_URL=http://harold.internal:8501
export HAROLD_TOKEN=... # matches Harold's 'enforce_token' secret// app.module.ts — zero config when using env vars
import { HaroldGuardModule } from 'harold-guard';
@Module({
imports: [HaroldGuardModule.register()],
})
export class AppModule {}// or explicitly (overrides the env vars)
HaroldGuardModule.register({
url: 'http://harold.internal:8501',
token: process.env.HAROLD_TOKEN,
source: 'my-app/api', // identifier shown in Harold's hit reports
})// main.ts — register globally
import { HaroldGuard } from 'harold-guard';
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(app.get(HaroldGuard));Behavior
- Blocked IP →
403 Forbidden(reported askind: 'ip') - Blocked route →
404 Not Found(reported askind: 'route'; scanners shouldn't learn the route is protected) - Blocked requests are also logged locally (
warn), throttled to one line per IP+route per minute so a scan flood can't flood your logs. Disable withlogBlocked: false. - Prefix routes end with
*(e.g./admin/*) - Build:
npm run build(TypeScript →dist/)
Redis store (optional)
Pass a store to keep the blocklist in Redis: warm starts after restarts, a
shared copy across app instances, and a fallback when Harold is unreachable.
The store is a cache — Harold remains the source of truth.
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
HaroldGuardModule.register({
store: {
client: {
get: (key) => redis.get(key),
setEx: (key, ttl, value) => redis.set(key, value, 'EX', ttl),
},
// key: 'harold:blocklist', // default
// ttlSeconds: 900, // default
},
})With node-redis v4 the adapter is:
client: {
get: (key) => redis.get(key),
setEx: (key, ttl, value) => redis.set(key, value, { EX: ttl }),
}