Enterprise Shield – The Evolution – Part 1

Written by

in

Over the years, I have tried to limit the amount of spurious or aggressive traffic against my small, yet mighty web server environment. It started with a list of UFW rules, but that became very unwieldy once the list hit over 25,000 unique CIDR blocks.

A few months ago, working alongside some AI partners, I asked how this system could be made more efficient and manage even larger rulesets without making the server scream in pain every time a large burst of traffic hit it.

That’s when the Enterprise Shield approach was born.

What Is Enterprise Shield?

Enterprise Shield is a custom Linux firewall management system protecting creaky2, a vintage 2008 MacBook Pro repurposed as a personal Ubuntu Server hosting performancezen.com. It is not a product — it is a purpose-built system, grown organically over years of dealing with real-world attacks against a small but publicly visible WordPress blog.

At its core, Enterprise Shield does three things:

  1. Blocks hostile IP space — autonomous systems (ASNs), country CIDR ranges, and individually flagged IPs — before they ever reach the web server.
  2. Rate-limits legitimate but noisy infrastructure — Microsoft Azure, AWS, Google Cloud, Oracle Cloud, Cloudflare — so they can’t hammer the server but aren’t outright blocked.
  3. Logs, enriches, and reports on everything that hits the firewall so you can understand the threat landscape over time.

The system wraps Linux’s ipset and iptables primitives with custom tooling to make them manageable: automating blocklist maintenance, surviving reboots, and providing a real-time terminal dashboard and HTML threat analysis reports.


A Brief History: How We Got Here

The Early Days: Hand-Crafted BASH (v11.x)

Enterprise Shield started as a BASH script. Over time it grew significantly:

  • ASN blocking via RADB WHOIS lookups, parallelised across threads
  • Country blocking via the ipverse/country-ip-blocks
    GitHub feed (43 countries)
  • AbuseIPDB integration — a penalty box of individually-flagged malicious IPs, refreshed 5× daily (constrained by the free API tier)
  • Azure and hyperscaler separation — Azure (AS8075) given its own rate-limit chain distinct from AWS/GCP/Oracle/Cloudflare
  • Atomic ipset swaps — the live blocklist is never empty during an update
  • Delta safety abort — if the new CIDR count drops >5% from the previous run, the script aborts rather than switching to a degraded ruleset
  • Systemd boot persistence — two services that restore ipsets and iptables chains after every reboot

By version 11.25-MT, the script was around 500 lines, handled parallel WHOIS with a 4-thread batch model, and managed a blocklist of over 500,000 CIDR entries.

It worked. But it had accumulated significant technical debt.


Why We Rewrote It

Problem 1: Every Run Was a Full Rebuild

The BASH system had no memory between runs. Every night at 2AM:

  • All ASN WHOIS data was re-fetched — 447 ASNs × a RADB query each, regardless of whether anything had changed
  • All 43 country CIDR files were re-downloaded from GitHub
  • All 500,000+ CIDR entries were re-inserted from scratch into a staging ipset

This wasn’t just slow — it was wasteful. ASN CIDR allocations change rarely. The ipverse country files change weekly at most. But the system had no way to say “I already know what AS3209 owns — skip it.”

BASH nightly run cost (approximate):
  447 ASN WHOIS lookups        ~35 minutes
  43 GitHub downloads          ~2 minutes
  ipset full rebuild            ~1 minute
  Total rebuild time:          ~38 minutes
  Total per week:              ~4.5 hours of server time

Problem 2: State Was Scattered Across Flat Files

Runtime state was stored in flat files:

  • /etc/ipset.conf — the full ipset dump (502,575 lines)
  • /etc/shield-iptables.rules — the iptables chain rules
  • /var/lib/shield/last_entry_count — just a number, for the delta check
  • /var/lib/shield/last_public_ip — the server’s external IP at last run

There was no audit trail, no history, no way to answer “what changed between last Tuesday and today?” The last_entry_count file was a single integer — useful for
the delta abort check, useless for anything else.

Problem 3: No Hit Data

The BASH system blocked things — but didn’t know what it blocked. There was no structured record of which IPs hit the firewall, when, or how many times. The iptables -L packet counters reset on every rebuild. A threat analysis report
was impossible.

Problem 4: AbuseIPDB Was Bolted On

The AbuseIPDB penalty box was a separate script (abuseipdb_penaltybox.sh) that maintained its own ipset (SHIELD_PENALTY), with its own boot persistence service,
its own log file, and completely separate state from the main system. It worked, but it was an island.

Problem 5: Boot Persistence Was Fragile

The two-service boot sequence (ipsets before UFW, chains after UFW) worked correctly but relied on /etc/ipset.conf — a 502,575-line flat file. If that file was corrupt or missing, the server came up with no firewall. There was no crash recovery, no verification that the restored state was sane.


The Solution: Python + SQLite

The rewrite was designed around a single guiding principle:

The database is the system. Everything else derives from it.

What Changed

┌────────────────────────────────────────────────────────────-─────────┐
│                    BASH v11.25-MT → Python v12                       │
├──────────────────────────┬────────────────────────────────-──────────┤
│ State storage            │ Flat files → SQLite database              │
│ Rebuild strategy         │ Full re-fetch → Delta (only changed data) │
│ WHOIS cadence            │ Every night for all ASNs → Only stale     │
│ Country blocks           │ Re-download always → ETag conditional GET │
│ AbuseIPDB                │ Separate script/ipset → Integrated table  │
│ Hit logging              │ None → rsyslog → hits_parser → SQLite     │
│ Boot persistence         │ Flat file restore → DB-driven restore.py  │
│ Crash recovery           │ None → rebuild_in_progress flag in DB     │
│ Config change detection  │ None → SHA-256 hash of config files       │
│ Penalty box              │ Separate ipset/service → shield_penalty   │
│ Allowlist                │ Hardcoded in script → shield_allow ipset  │
│ Dashboard                │ BASH/iptables poll → Python delta packets │
│ Reports                  │ None → HTML threat analysis reports       │
└──────────────────────────┴─────────────────────────────────-─────────┘

The Delta Rebuild Advantage

The most impactful change. Instead of re-fetching everything nightly, the Python system tracks the last successful WHOIS lookup for every ASN and the ETag of every country block file:

Python nightly run cost (approximate):
  ASN WHOIS lookups (only stale, 14-day TTL)    ~2-5 per night
  Country file downloads (only changed, ETag)   0-3 per night (usually 0)
  DB diff computation                           ~5 seconds
  ipset delta apply                             ~10 seconds
  Total rebuild time:                           ~30 seconds typical
  Total per week:                               ~3.5 minutes

That’s roughly a 700× reduction in rebuild time under normal conditions.


The Migration Process

The migration was executed in four phases with no loss of protection at any point.

Phase 1: Database Seeding
    ↓
    Python DB populated while BASH system continues running
    Both systems coexist. Python touches no live ipsets.

Phase 2: Shadow Mode (7+ days)
    ↓
    Python rebuild runs with --dry-run flag
    Logs all changes it WOULD make, modifies nothing
    Compared against BASH output each morning

Phase 3: Cutover
    ↓
    BASH cron entries commented out (not deleted)
    Python cron entries go live
    New systemd boot services enabled
    72-hour intensive monitoring period

Phase 4: Cleanup (30 days later)
    ↓
    BASH scripts archived to tar.gz
    Old flat files removed
    502,575-line /etc/ipset.conf deleted
    Orphan ipsets destroyed
    Old systemd services masked

What Was Decommissioned

The cutover removed a significant amount of legacy infrastructure:

  • /usr/local/bin/enterprise_shield.sh (the main BASH script)
  • /usr/local/bin/block_asn.sh (the injection tool)
  • /usr/local/bin/abuseipdb_penaltybox.sh
  • /usr/local/bin/shield-stat.sh and shield_display.sh
  • /etc/ipset.conf — 502,575 lines, gone
  • shield-ipset-restore.service (BASH-era)
  • shield-iptables-restore.service (BASH-era)
  • The orphan SHIELD_PENALTY ipset that had survived via netfilter-persistent

The new Python system replaced all of this with a clean installation under /usr/local/lib/enterprise_shield/, state in /var/lib/enterprise_shield/shield.db,
and configuration in /etc/enterprise_shield/.


Key Decisions Made Along the Way

Several non-obvious architectural choices shaped the Python system. These are worth documenting because they affected behaviour:

Hard DELETEs over soft deletes — Earlier design drafts used a soft-delete pattern (marking rows inactive rather than removing them). This was rejected: it causes the database to grow indefinitely, and there’s no useful recovery scenario where you’d
want to un-delete a stale CIDR entry. The Python system uses hard DELETEs.

AbuseIPDB gets its own table — The initial design merged AbuseIPDB IPs into the main cidr_blocks table. This was wrong: AbuseIPDB operates on a completely different lifecycle (5× daily refresh, IP-level not CIDR-level, confidence threshold filtering).
It got its own abuseipdb_entries table with independent process ownership.

ip-api.com not BGPView — Report enrichment (ASN name, country, city for attacker IPs) originally used BGPView. This was replaced with ip-api.com: 45 req/min free tier, no auth, returns ASN + org + country + city in a single call. Shodan’s InternetDB
(internetdb.shodan.io/{ip}) was added for open port data and threat tags — also no auth.

All runtime state in the database — No flat files for parser position, public IP cache, or rebuild flags. The system_state table in SQLite holds everything. This means the database backup is the complete system backup.

SHA-256 config change detection — The rebuild checks a SHA-256 hash of the config files at startup. If anything changed since the last run, a full re-import of config is triggered automatically. No manual --import step needed.


The Result

Enterprise Shield v12 reached verified production status on 2026-05-27, after extensive testing including multiple reboot verification cycles. All 16 items on the post-reboot checklist pass cleanly.

The system now:

  • Rebuilds in ~30 seconds instead of ~38 minutes
  • Has a complete history of every CIDR change
  • Logs every firewall hit to a structured database
  • Produces HTML threat analysis reports
  • Survives reboots with a DB-driven restore rather than a flat-file restore
  • Has crash recovery via the rebuild_in_progress flag
  • Provides a real-time terminal dashboard (shield_stat.py) with delta-based packet rate bars

The database contains ~500,000 active CIDR entries across 7 ipsets, ~10,000 AbuseIPDB entries, and growing hit log data as the rsyslog pipeline feeds it.

The next post will go into the architecture of the system so that if you’re considering implementing something similar, you can see how to configure the entire flow.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *