📚 Configuration Reference

Complete guide to configuring Bad IPs

⚠️ ALPHA SOFTWARE - Currently in testing. Configuration format may change in future releases.

Table of Contents

Main Configuration

File: /usr/local/etc/badips.conf

Global Settings

Parameter Description Default
log_level Logging verbosity: debug, info, warn, error debug
block_duration How long to block IPs (seconds) 691200 (8 days)
never_block_cidrs Comma-separated IPv4 CIDRs that are never blocked (highest priority) Required
never_block_cidrs_v6 Comma-separated IPv6 CIDRs that are never blocked (highest priority) ::1/128,fe80::/10,fc00::/7
always_block_cidrs Comma-separated IPv4 CIDRs that are always blocked (static firewall rules) 224.0.0.0/4,240.0.0.0/4
always_block_cidrs_v6 Comma-separated IPv6 CIDRs that are always blocked (static firewall rules) (empty)
auto_mode Auto-discover services (1=enabled, 0=disabled) 1
cleanup_every_seconds How often to clean expired blocks 3600
sleep_time Seconds between log scans 10
initial_journal_lookback Initial journal history to scan (seconds) 200000
central_db_batch_size Max batch size for PostgreSQL inserts before timeout 20
central_db_queue_timeout Max seconds to wait before processing queued IPs (regardless of batch size) 5
public_blocklist_urls Comma-separated list of URLs to fetch blocklists from (HTTP/HTTPS/file://)
⚠️ DEPRECATED in v3.5+ - Use PublicBlocklistPlugins instead
Optional
public_blocklist_refresh How often to refresh public blocklists (seconds)
⚠️ DEPRECATED in v3.5+ - Use PublicBlocklistPlugins instead
900 (15 minutes)
heartbeat Status logging interval (seconds) 10
graceful_shutdown_timeout Max seconds to wait for threads to finish during shutdown 10

Example Configuration

# Bad IPs Configuration # Generated during installation on Tue Dec 9 03:50:28 PM CST 2025 [global] # Logging log_level = debug # How long to block an IP (seconds) block_duration = 691200 # 8 days # Network filtering (IPv4) never_block_cidrs = 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,169.254.0.0/16,23.116.91.64/29 always_block_cidrs = 224.0.0.0/4,240.0.0.0/4 # Network filtering (IPv6) never_block_cidrs_v6 = ::1/128,fe80::/10,fc00::/7 always_block_cidrs_v6 = # Performance tuning auto_mode = 1 # Cleanup intervals cleanup_every_seconds = 3600 # Initial lookback -> how far to initally look back at journal # Files are always read in entirety on initial loading initial_journal_lookback = 200000 # Sleep time: number of seconds between looking at journalct or log files sleep_time = 10 # central_db_batch_size: the max batch size to insert into central database of new IPs blocked # As new IPs are found, after they have been blocked, each IP is added to a queue (sync_to_central_db_queue) # Once the queue has at least central_db_batch_size in it, then that many IPs will be saved to the central database # The lower the number, the quicker IPs will be saved the the database and can then be used by other systems # The higher the number, the less frequent there are inserts to the database # See central_db_queue_timeout to see max wait time. central_db_batch_size = 20 # central_db_queue_timeout: the timeout to wait for to be in sync_to_central_db_queue # After seconds, no matter how many IPs are in sync_to_central_db_queue, they will be removed and processed # The lower the numer, the faster a low count of items will be processed # The higher the number, the less stress on the database with low count inserts central_db_queue_timeout = 5 # ⚠️ DEPRECATED in v3.5+ - Use [PublicBlocklistPlugins:Name] sections instead # public_blocklist_urls (comma separated list) # List of sites where a static text list can be retrieved of IPs to block # Public lists are cached to a local file (see public_blocklist_refresh) # You can use a local file, too, doing something like: file:///block/these/annoying_ips.txt # That way you can generate your own list of IPs and IP+Subnets # public_blocklist_urls = https://www.spamhaus.org/drop/drop.txt, https://feodotracker.abuse.ch/downloads/ipblocklist.txt # public_blocklist_refresh (seconds): # How often to refresh above list. Honors ETag and not-before headers. # If you are kinda doing some debug and constantly stopping and starting, then the cached file will be used until # it is seconds old # public_blocklist_refresh = 900 # heartbeat (seconds): # How often to produce a log entry with some cursory info heartbeat = 10 # graceful_shutdown_timeout (seconds): # How long to give each thread an opportunity to be cleared before bypassing the queue and shutting down # 10 seconds is plenty of time for each thread graceful_shutdown_timeout = 10
⚠️ Critical: Always configure never_block_cidrs with your management networks to prevent lockouts!

IP Filtering Precedence

Bad IPs uses three nftables sets with the following precedence order (highest to lowest):

  1. never_block (static) - IPs/CIDRs that are always allowed, regardless of detections
  2. always_block (static) - IPs/CIDRs that are always blocked at the firewall level
  3. badipv4 (dynamic with timeout) - IPs dynamically blocked based on detections

Use cases for always_block:

Note: IPs in always_block are still logged to the database for reporting purposes. Different servers can have different always_block configurations based on their role (e.g., block on DNS/mail but not web).

Logging Configuration

File: /usr/local/etc/badips/log4perl.conf

Bad IPs uses Log::Log4perl for flexible logging configuration.

Default Configuration

# Root logger
log4perl.rootLogger = INFO, SYS

# File appender
log4perl.appender.SYS = Log::Log4perl::Appender::File
log4perl.appender.SYS.filename = /var/log/bad_ips/bad_ips.log
log4perl.appender.SYS.mode = append
log4perl.appender.SYS.layout = Log::Log4perl::Layout::PatternLayout
log4perl.appender.SYS.layout.ConversionPattern = %d|%P|%p|%l|%X{THREAD}|%m%n

Log Levels

Changing Log Level

To enable debug logging, edit /usr/local/etc/badips/log4perl.conf:

# Change INFO to DEBUG
log4perl.rootLogger = DEBUG, SYS

Then reload the service:

sudo systemctl reload bad_ips

Reload Timing

Log configuration changes are applied gradually:

Note: The 5-second interval is hardcoded in /usr/local/sbin/bad_ips. If you need immediate changes, modify the script and run sudo systemctl restart bad_ips.

Log Format

The default log format includes:

Log Rotation

Logs are automatically rotated by logrotate:

Database Configuration

File: /usr/local/etc/badips.d/database.conf

Permissions: 600 (contains passwords)

Database Parameters

Parameter Description Example
db_host Database hostname or IP 10.10.0.116
db_port Database port 5432
db_name Database name bad_ips
db_user Database username bad_ips_admin
db_password Database password secret
db_ssl_mode SSL mode: disable, require, verify-ca, verify-full disable

Example Configuration

[global] db_host = 10.10.0.116 db_port = 5432 db_name = bad_ips db_user = bad_ips_admin db_password = your_secure_password_here db_ssl_mode = require

Database Schema

Bad IPs uses the jailed_ips table with PostgreSQL's native inet type for IPv4/IPv6 support:

CREATE TABLE jailed_ips ( id SERIAL PRIMARY KEY, ip inet NOT NULL, originating_server VARCHAR(255) NOT NULL, originating_service VARCHAR(255), detector_name VARCHAR(255), pattern_matched TEXT, matched_log_line TEXT, first_blocked_at BIGINT NOT NULL, last_seen_at BIGINT NOT NULL, expires_at BIGINT NOT NULL, block_count INTEGER DEFAULT 1, UNIQUE(ip, originating_server) ); CREATE INDEX idx_jailed_ips_expires ON jailed_ips(expires_at); CREATE INDEX idx_jailed_ips_ip ON jailed_ips(ip);

Detector Configuration

Directory: /usr/local/etc/badips.d/

Naming: NN-service.conf (e.g., 10-sshd.conf)

Detector Format

[detector:name] units = service1.service, service2.service pattern1 = regex pattern to match pattern2 = another pattern pattern3 = yet another pattern

Pre-configured Detectors

SSH (10-sshd.conf)

[detector:sshd] units = ssh.service, sshd.service pattern1 = Failed password for invalid user pattern2 = Failed password for root pattern3 = Connection closed by authenticating user

Postfix Mail (20-postfix.conf)

[detector:postfix] units = postfix@-.service, postfix.service pattern1 = Relay access denied pattern2 = Illegal address syntax from pattern3 = SASL LOGIN authentication failed pattern4 = SSL_accept error from pattern5 = non-SMTP command from unknown

Dovecot IMAP/POP3 (30-dovecot.conf)

[detector:dovecot] units = dovecot.service pattern1 = Aborted login pattern2 = Disconnected.*rip= pattern3 = Auth failed pattern4 = Invalid user

Nginx Web (40-nginx.conf)

[detector:nginx] units = nginx.service pattern1 = 404.*GET /wp-admin pattern2 = 404.*GET /wp-login pattern3 = 400 Bad Request

BIND DNS (70-bind.conf)

[detector:bind] units = named.service, bind9.service pattern1 = query failed \(REFUSED\) pattern2 = rate limiting pattern3 = query failed \(SERVFAIL\)

Pattern Matching

Pattern Syntax

Patterns are Perl-compatible regular expressions (PCRE).

Common Patterns

Pattern Matches
Failed password Literal string "Failed password"
Failed password.*root "Failed password" followed by "root"
404.*wp-admin 404 errors accessing wp-admin
Disconnected.*rip= Disconnection messages with IP

IP Extraction

Bad IPs automatically extracts IPv4 addresses from matched log lines. No special configuration needed.

Testing Patterns

bad_ips --dry-run

Runs Bad IPs in detection-only mode. IPs are identified but not blocked.

Public Blocklist Plugins

Bad IPs supports extensible public blocklist plugins that fetch and process IP blocklists from external sources. A Spamhaus plugin is included by default, and you can develop custom plugins for any blocklist source.

Configuration

Plugins are configured in /usr/local/etc/badips.conf using the [PublicBlocklistPlugins:PluginName] section format:

[PublicBlocklistPlugins:Spamhaus] urls = https://www.spamhaus.org/drop/drop.txt, https://www.spamhaus.org/drop/edrop.txt fetch_interval = 3600 use_cache = 1 cache_path = /var/cache/badips/ active = 1 [PublicBlocklistPlugins:Feodotracker] urls = https://feodotracker.abuse.ch/downloads/ipblocklist.txt fetch_interval = 7200 use_cache = 1 cache_path = /var/cache/badips/ active = 0
⚠️ Configuration Requirements:
  • A configuration section [PublicBlocklistPlugins:<plugin_name>] must exist for each plugin
  • The <plugin_name> must exactly match the package name and module file name
  • The only required parameter is active
  • If active does not exist or is set to 0, the plugin is not loaded
  • All other parameters are optional and plugin-specific

Built-in Plugins

The Spamhaus plugin is included with Bad IPs and provides access to the Spamhaus DROP and EDROP lists. Additional plugins can be found in the template configuration or developed custom.

Developing Custom Plugins

You can develop your own blocklist plugins to integrate any external IP list source. Custom plugins must follow the BadIPs plugin architecture:

Package Structure

⚠️ Important: Plugin package names must follow the format: BadIPs::PublicBlocklistPlugins::<PluginName>

For example, a custom plugin named "MyBlocklist" would be:

Required Methods

1. Constructor: new()

The new() method must instantiate your plugin class and will receive the following arguments:

Argument Type Description
conf HashRef Complete configuration hash including your plugin's [PublicBlocklistPlugins:Name] section
reload_check CodeRef Function reference to check if reload has been requested. Returns boolean.
shutdown_check CodeRef Function reference to check if shutdown has been requested. Returns boolean.
enqueue_ip CodeRef Function reference to enqueue discovered IPs for blocking. Accepts an IP item hashref.
log Object Log::Log4perl logger object for your plugin
2. Run Method: run()

The run() method is where your plugin's main logic executes. This method:

Example Plugin Skeleton

package BadIPs::PublicBlocklistPlugins::MyBlocklist; use strict; use warnings; use LWP::UserAgent; sub new { my ($class, %args) = @_; my $self = { conf => $args{conf}, reload_check => $args{reload_check}, shutdown_check => $args{shutdown_check}, enqueue_ip => $args{enqueue_ip}, log => $args{log}, }; bless $self, $class; return $self; } sub run { my ($self) = @_; my $log = $self->{log}; my $plugin_conf = $self->{conf}{PublicBlocklistPlugins}{MyBlocklist}; $log->info("MyBlocklist plugin started"); while (!$self->{shutdown_check}->()) { # Check for reload request if ($self->{reload_check}->()) { $log->info("Reload requested, restarting..."); return; } # Fetch blocklist my $ua = LWP::UserAgent->new; my $response = $ua->get($plugin_conf->{url}); if ($response->is_success) { my @ips = split /\n/, $response->content; foreach my $ip (@ips) { next if $ip =~ /^#/; # Skip comments $ip =~ s/\s.*//; # Remove trailing comments # Enqueue IP for blocking $self->{enqueue_ip}->({ ip => $ip, originating_service => 'MyBlocklist', detector_name => 'MyBlocklist', pattern_matched => 'Public blocklist', }); } $log->info("Processed blocklist, found " . scalar(@ips) . " IPs"); } else { $log->error("Failed to fetch blocklist: " . $response->status_line); } # Sleep before next fetch sleep($plugin_conf->{fetch_interval} || 3600); } $log->info("MyBlocklist plugin shutting down"); return 1; } 1;

Plugin Configuration

After creating your plugin, add its configuration to /usr/local/etc/badips.conf. The section name must match your plugin name, and active = 1 is the only required parameter:

[PublicBlocklistPlugins:MyBlocklist] # Required parameter - plugin will not load without this active = 1 # Optional plugin-specific parameters url = https://example.com/blocklist.txt fetch_interval = 3600
💡 Notes:
  • The section name [PublicBlocklistPlugins:MyBlocklist] must exactly match your module file name (MyBlocklist.pm)
  • active = 1 is the only required parameter. If missing or set to 0, the plugin will not load.
  • All other parameters are optional and defined by your plugin
  • Access parameters via $self->{conf}{PublicBlocklistPlugins}{MyBlocklist}{param_name}

Best Practices

Service Management

Common Commands

# Check status systemctl status bad_ips # Reload configuration (no restart needed) systemctl reload bad_ips # Restart service systemctl restart bad_ips # View logs journalctl -u bad_ips.service -f # Test configuration bad_ips --test-config # View blocked IPs sudo nft list set inet filter badipv4
💡 Tip: After making configuration changes, use systemctl reload bad_ips instead of restart to apply changes without clearing blocked IPs.