Adding an IP Firewall to Express Without Cloudflare

You don't need a CDN to block bad IPs. Here's how to wire a 500k-entry IP blocklist into Express in one line, plus the manual fallback for teams that want zero dependencies.

Lhoussine
May 9, 2026·7 min read

Adding an IP Firewall to Express Without Cloudflare

Cloudflare's WAF is excellent. It's also overkill if all you want is to block known-bad IPs at the application layer, and the DNS-level change required to put Cloudflare in front of your app is a real commitment.

Here's how to add a 500k-entry IP firewall to Express in one line — no DNS changes, no infrastructure modifications, no Cloudflare account.

For broader context on the firewall layer, see the SecureNow Firewall page.

The one-line approach

npm install securenow

# In your start command:
node -r securenow/firewall-only app.js

That's the entire setup. The firewall-only preload adds a Node.js HTTP server hook that checks every incoming request's IP against a 500k-entry blocklist. Bad IPs get a 403; good IPs pass through to your Express app exactly as before.

What you get out of the box:

  • 500k known-bad IPs (AbuseIPDB feed + community blocklists)
  • Hourly refresh
  • Auto-allowlisting for Googlebot, Bingbot, GPTBot, ClaudeBot, PerplexityBot
  • Sub-millisecond per-request overhead
  • Fail-open behavior (firewall unreachable → traffic passes)
  • Free — 500k IPs and the firewall itself remain free even on paid plans

The manual middleware approach

If you want zero dependencies and you're willing to manage your own blocklist, here's the hand-rolled version:

import fs from 'fs';

class IpFirewall {
  constructor() {
    this.exactSet = new Set();
    this.cidrRanges = [];
    this.allowlist = new Set();
  }

  addBlock(entry) {
    if (entry.includes('/')) {
      this.cidrRanges.push(this.parseCidr(entry));
    } else {
      this.exactSet.add(entry);
    }
  }

  addAllow(entry) {
    this.allowlist.add(entry);
  }

  parseCidr(cidr) {
    const [ip, bits] = cidr.split('/');
    const network = this.ipToInt(ip);
    const mask = (~((1 << (32 - parseInt(bits))) - 1)) >>> 0;
    return { network: (network & mask) >>> 0, mask };
  }

  ipToInt(ip) {
    return ip.split('.').reduce((acc, x) => (acc << 8) + parseInt(x), 0) >>> 0;
  }

  isBlocked(ip) {
    if (this.allowlist.has(ip)) return false;
    if (this.exactSet.has(ip)) return true;
    const ipInt = this.ipToInt(ip);
    return this.cidrRanges.some(({ network, mask }) => ((ipInt & mask) >>> 0) === network);
  }

  loadFromFile(path) {
    const lines = fs.readFileSync(path, 'utf8').split('\n');
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed && !trimmed.startsWith('#')) this.addBlock(trimmed);
    }
  }
}

const firewall = new IpFirewall();
firewall.loadFromFile('./blocklist.txt');

export function firewallMiddleware(req, res, next) {
  const ip = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress;
  if (firewall.isBlocked(ip)) {
    return res.status(403).send('Forbidden');
  }
  next();
}

Wire it in:

import { firewallMiddleware } from './firewall.js';
app.use(firewallMiddleware);

Now you need a blocklist. Some free public sources:

Refresh nightly via cron. Most teams use 2-3 free feeds combined and stay under 50k entries, which is fine for memory but won't catch the long tail that the SecureNow firewall covers.

Where the manual approach falls short

Three issues at scale:

Refresh latency. Bad IPs are bad for hours, not days. A nightly refresh means you're 12+ hours behind on new threats. The SecureNow firewall refreshes hourly.

Allowlist maintenance. Googlebot's IP ranges change. So do Bingbot, GPTBot, ClaudeBot. Maintaining the allowlist is ongoing work most teams underdo.

Distributed deployment. If you have N Express instances, all of them load the same 50k-entry blocklist into memory and each does its own refresh. With the SecureNow firewall, the blocklist sync is shared and cached — same list, less network.

For a side project these don't matter. For a production SaaS they add up.

What about iptables?

For really high traffic where even sub-millisecond per-request overhead matters, you can drop bad IPs at the kernel level via iptables. The IP never makes it to your Express process at all — it's dropped before TCP handshake completes.

This is opt-in for SecureNow users (SECURENOW_FIREWALL_IPTABLES=1 env var) and requires CAP_NET_ADMIN on the container. For most apps it's overkill — the HTTP-layer blocking is fast enough.

Verifying it works

Hit your endpoint from a known-bad IP and verify you get 403:

curl -i https://yourapp.com/ -H "X-Forwarded-For: 185.220.101.42"
# expects: HTTP/1.1 403 Forbidden

To check what's currently blocked:

npx securenow firewall status
# Shows count of blocked IPs, last update, allowlist size

To test if a specific IP is blocked:

npx securenow firewall test-ip 1.2.3.4
# Returns: BLOCKED or ALLOWED, with the matching entry

When to upgrade past application-layer firewall

If you start seeing:

  • Layer-3/4 attacks (SYN floods, UDP amplification)
  • Application-aware bot traffic that mimics browsers perfectly
  • Sustained high-volume scraping that survives IP-rotation

Move up the stack. Cloudflare or AWS Shield handle network-layer attacks; DataDome and similar handle sophisticated bots. The IP firewall handles the long tail of known-bad — which is 80% of the volume for most apps.

The honest recommendation

For most Express apps: install SecureNow, preload firewall-only, done. Free, 5-minute setup, sub-millisecond per request.

For teams that want zero vendor dependencies: hand-roll the middleware above with 2-3 free blocklist sources, refresh nightly, accept that you'll miss some recent threats.

For teams already on Cloudflare: don't add another firewall layer. Cloudflare's WAF and bot management cover this and more.

Related

Frequently Asked Questions

Why not just use Cloudflare?

Cloudflare is great if you're already using it. If you're not, the setup overhead, DNS changes, and pricing complexity for the bot-blocking tier is non-trivial. An npm-package firewall is a one-line install with no DNS changes.

Where does the IP blocklist come from?

The SecureNow firewall pulls 500k+ known-bad IPs from AbuseIPDB plus user-contributed blocklists, refreshed hourly. You can also bring your own list.

Will this slow down my requests?

Sub-millisecond. The lookup is an in-memory hash check with O(1) for exact IPs and O(N) for CIDR ranges. For 500k entries, the cold-start memory is ~50 MB and per-request overhead is negligible.

What if the IP service is unreachable?

The firewall fails open by default — if the blocklist sync fails, your app keeps serving traffic without the firewall layer. The current cached list stays active until reachability is restored.

Recommended reading

Adding Backend Tracing to a Sentry Stack with OpenTelemetry

If your team uses Sentry for frontend errors and needs backend distributed tracing without doubling the Sentry bill, here's the OpenTelemetry path that doesn't make you choose.

May 9
How to Block Bot Traffic in Express With No Extra Infra

Five approaches to bot blocking in Express, ranked by effort vs. effectiveness. From a 5-line allowlist to a full IP-reputation firewall — all without Cloudflare, AWS WAF, or any new infrastructure.

May 9
How to Block Bot Traffic in Fastify With No Extra Infra

Fastify hooks (onRequest) and the SecureNow preload both work cleanly. Here's the production setup for IP blocking and user-agent filtering.

May 9