Adding an IP Firewall to Express Without Cloudflare

You don't need a CDN to block bad IPs in Express. Add a managed IP blocklist in one line, or use a manual middleware fallback with zero dependencies.

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 an IP-reputation 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 known-bad IP feed plus your own blocklist. Bad IPs get a 403; good IPs pass through to your Express app exactly as before.

What you get out of the box:

  • A known-bad IP feed of up to 10,000 IPs (SecureNow AI IPDB feed + community blocklists)
  • Hourly refresh
  • Sub-millisecond per-request overhead
  • Fail-open behavior (firewall unreachable → traffic passes)
  • 14-day free trial, then $99 per app per month with the rest of SecureNow included

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 (config.firewall.iptables: true in .securenow/runtime.json) and requires root or 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 as a known-bad IP and verify you get 403. Run this on the server itself: SecureNow only trusts X-Forwarded-For on loopback, private-network or trusted-proxy connections, so a client on the internet can't pick its address this way:

curl -i http://localhost:3000/ -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. 14-day free trial (then $99 per app per month), 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.

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 a known-bad IP feed (up to 10,000 IPs) from SecureNow AI IPDB plus user-contributed blocklists, refreshed hourly and applied above a confidence threshold (75 by default). 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. The feed is capped at 10,000 IPs, so memory use is small 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

Block an IP, a Range or One Route Without a Deploy (Node.js)

Block one IP, a CIDR range, or a single route and method in a Node.js app within seconds, with no code change: setup, scoping, CLI, MCP, verification and undo.

Oct 27
11 Signs Your AI-Built App Needs a Security Audit

Built your app with a coding agent? 11 warning signs it needs a security audit, each checkable in five minutes, and how to run the full audit yourself.

Oct 26
AI Security Audit Part 1: Set Up Your Agent to Audit Code

Part 1 of the AI security audit series: set up an AI coding agent to audit your app locally, connect SecureNow, and run your first threat-model pass.

Jul 24