kanopi / firewall
Evaluate the requests for malicious items.
Requires
- php: >=8.2
- amphp/dns: ^2.4
- doctrine/dbal: ^4.2
- geoip2/geoip2: ^3.2
- guzzlehttp/guzzle: ^7.9
- matomo/device-detector: ^6.4
- monolog/monolog: ^3.9
- symfony/cache: ^7.3
- symfony/http-foundation: ^7.3
- symfony/property-access: ^7.3
- symfony/uid: ^7.3
- symfony/yaml: ^7.3
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- dg/bypass-finals: ^1.9
- phpcompatibility/php-compatibility: ^9.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- rector/rector: ^2.0
- squizlabs/php_codesniffer: ^3.13
- symfony/dotenv: ^7.3
- symfony/var-dumper: ^7.3
This package is auto-updated.
Last update: 2025-06-26 04:29:31 UTC
README
Simple Firewall is a powerful, extensible request-evaluation library for PHP-based systems. It provides comprehensive protection by analyzing HTTP requests and applying configurable rules to either allow or block access based on IP addresses, geolocation, user agents, URLs, ASN (Autonomous System Numbers), and rate limits. The library is designed to work seamlessly with popular frameworks like Drupal, WordPress, Symfony, or any standalone PHP application.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Configuration Overview
- Storage Configuration
- Plugin Architecture
- Available Plugins
- Conditional Logic
- Logging Configuration
- Dynamic Configuration Overrides
- Platform Integration
- Advanced Examples
- Testing
- Contributing
Features
- Flexible Plugin System: Modular architecture allows for easy extension and customization
- Multiple Storage Backends: Support for in-memory, file-based, database, and Redis storage
- Comprehensive Request Analysis: Evaluate requests based on IP, location, user agent, URL patterns, and more
- Rate Limiting: Built-in rate limiting with configurable storage backends
- GeoIP Integration: Full support for MaxMind GeoIP2 databases (both local and web service)
- Advanced Conditional Logic: Support for simple, complex, and grouped conditional rules
- PSR-3 Compatible Logging: Integration with Monolog for flexible logging
- Framework Agnostic: Works with any PHP application or framework
Requirements
- PHP 8.0 or higher
- Composer
- Optional: MaxMind GeoIP2 databases for geolocation features
- Optional: Redis for distributed rate limiting
Installation
Install via Composer:
composer require kanopi/firewall
For geolocation features, also install:
composer require geoip2/geoip2
Quick Start
Basic Implementation
Place the following code in your application's entry point (e.g., index.php
, wp-config.php
, or Drupal's settings.php
):
<?php // Include composer autoloader if not already loaded require_once __DIR__ . '/vendor/autoload.php'; // Initialize and evaluate the firewall if (class_exists('\Kanopi\Firewall\Firewall')) { \Kanopi\Firewall\Firewall::create([__DIR__ . '/config/firewall.yml'])->evaluate(); }
Minimal Configuration Example
Create a config/firewall.yml
file:
# Storage configuration - where blocked IPs are stored storage: type: \Kanopi\Firewall\Storage\FileStorage config: file: /var/log/firewall/blocked.data # Block malicious IPs block: \Kanopi\Firewall\Plugins\IpAddress: enable: true config: - 192.168.1.100 - 10.0.0.0/24 # Optional: Configure logging logger: - class: Monolog\Handler\StreamHandler args: - /var/log/firewall/firewall.log - Monolog\Level::Info
Configuration Overview
The firewall configuration consists of four main sections:
Section | Purpose | Required |
---|---|---|
storage |
Defines where blocked IP addresses are persisted | Yes |
bypass |
Plugins that allow trusted traffic through | No |
block |
Plugins that deny harmful or suspicious traffic | No |
logger |
Monolog handlers for logging firewall events | No |
Storage Configuration
Storage defines how the firewall persists blocked IP addresses across requests.
Available Storage Classes
1. In-Memory Storage
Non-persistent storage that resets with each request. Useful for testing.
storage: type: \Kanopi\Firewall\Storage\InMemoryStorage
2. File Storage
Persists blocked IPs to the filesystem.
storage: type: \Kanopi\Firewall\Storage\FileStorage config: file: /var/log/firewall/blocked_ips.data
3. Database Storage
Stores blocked IPs in a SQL database using Doctrine DBAL.
storage: type: \Kanopi\Firewall\Storage\DatabaseStorage config: storage-table: firewall_blocked_ips connection: # Option 1: Using DSN (recommended) dsn: "mysql://user:password@localhost:3306/database?serverVersion=8.0" # Option 2: Individual parameters # dbname: 'my_database' # user: 'db_user' # password: 'db_password' # host: 'localhost' # port: 3306 # driver: 'pdo_mysql'
Plugin Architecture
Plugins are the core components that evaluate incoming requests. They can be configured in two sections:
bypass
: Plugins here allow requests to pass through without further evaluationblock
: Plugins here can block requests based on configured rules
Common Plugin Configuration
All plugins share these configuration options:
PluginNamespace: enable: true # Whether the plugin is active priority: 0 # Execution order (-100 runs before 100) metadata: {} # Plugin-specific configuration config: [] # Rules or conditions for the plugin
Available Plugins
IP Address Plugin
Namespace: \Kanopi\Firewall\Plugins\IpAddress
Evaluates requests based on IP addresses, supporting IPv4, IPv6, CIDR blocks, and IP ranges.
Configuration Example
# In bypass section - whitelist trusted IPs bypass: \Kanopi\Firewall\Plugins\IpAddress: enable: true priority: -100 # Run early config: # Single IPv4 address - 192.168.1.1 # Single IPv6 address - ::1 - 2001:db8::1 # CIDR notation - 10.0.0.0/8 - 172.16.0.0/12 # IP range (start-end) - 192.168.1.100-192.168.1.200 # In block section - blacklist malicious IPs block: \Kanopi\Firewall\Plugins\IpAddress: enable: true priority: -100 config: - 192.168.1.50 - 10.10.10.0/24
GeoLocation Plugin
Namespace: \Kanopi\Firewall\Plugins\GeoLocation
Evaluates requests based on geographic location using MaxMind GeoIP2 databases.
Configuration Example
block: \Kanopi\Firewall\Plugins\GeoLocation: enable: true priority: 0 metadata: reader: # Option 1: Local database file type: reader db: /path/to/GeoLite2-City.mmdb # Option 2: MaxMind web service # type: client # accountId: 123456 # licenseKey: your_license_key # languages: ['en', 'es'] # options: [] config: # Block specific countries - "country:CN" - "country:RU" - "country.isoCode:KP" # Block entire continents - "continent:AS" - "continent.code:AF" # Block specific cities - "city:Moscow" - "city.name@contains:Beijing" # Complex location rules - variable: location.timeZone operator: equals value: "Asia/Shanghai"
Available Variables
country
- Returns country ISO code (e.g., "US")country.isoCode
- Country ISO codecountry.name
- Full country namecontinent
- Returns continent code (e.g., "NA")continent.code
- Continent codecontinent.name
- Full continent namecity
- Returns city namecity.name
- City namelocation.latitude
- Latitude coordinatelocation.longitude
- Longitude coordinatelocation.timeZone
- Time zonepostal
- Returns postal codepostal.code
- Postal/ZIP code
URL Plugin
Namespace: \Kanopi\Firewall\Plugins\Url
Evaluates requests based on URL components and request parameters.
Configuration Example
block: \Kanopi\Firewall\Plugins\Url: enable: true priority: 0 config: # Block all POST requests - "method:POST" # Block specific paths - "path:/wp-admin" - "path@starts_with:/admin" - "path@contains:phpmyadmin" - "path@regex:/\.(sql|bak|old)$/i" # Block based on host - "host:malicious.example.com" - "host@ends_with:.suspicious.com" # Block based on query parameters - "query.cmd@exists" - "query.action:delete" # Block based on POST data - "post.username:admin" - "post.action@in:drop,truncate,delete" # Block based on headers - "header.user-agent@contains:bot" - "header.x-forwarded-for@exists" # Complex URL rules - type: AND rules: - "method:POST" - "path@starts_with:/api" - "!header.authorization@exists"
Available Variables
method
- HTTP method (GET, POST, PUT, DELETE, etc.)host
- Hostname from the requestpath
- URI path (e.g., /admin/users)scheme
- URL scheme (http or https)port
- Port numberquery.*
- Query parameters (e.g., query.page, query.id)post.*
- POST body parametersheader.*
- HTTP headers (e.g., header.user-agent)cookie.*
- Cookie values
User Agent Plugin
Namespace: \Kanopi\Firewall\Plugins\UserAgent
Analyzes user agent strings to identify bots, devices, browsers, and operating systems.
Configuration Example
block: \Kanopi\Firewall\Plugins\UserAgent: enable: true priority: 0 config: # Block all bots - "bot:true" # Block specific device types - "device.type:desktop" - "device.type@in:smartphone,tablet" # Block specific browsers - "client.name:Internet Explorer" - "client.type:browser" - "client.version@less_than:10" # Block specific operating systems - "os.name:Windows XP" - "os.short_name:WIN" - "os.version@less_than:10" # Block specific brands or models - "brand:Huawei" - "model@contains:Galaxy" # Complex user agent rules - type: AND rules: - "bot:false" - "client.name:Chrome" - "client.version@less_than:80"
Available Variables
bot
- Whether the user agent is a bot ("true" or "false")device.type
- Device type (desktop, smartphone, tablet, etc.)client.name
- Browser or client nameclient.type
- Client type (browser, mobile app, etc.)client.version
- Client version numberos.name
- Operating system nameos.short_name
- OS short name (WIN, MAC, LIN, etc.)os.version
- OS version numberbrand
- Device brand (Apple, Samsung, etc.)model
- Device model
ASN Plugin
Namespace: \Kanopi\Firewall\Plugins\Asn
Evaluates requests based on Autonomous System Numbers (ASN) using MaxMind's GeoIP2 ASN database.
Configuration Example
block: \Kanopi\Firewall\Plugins\Asn: enable: true priority: 0 metadata: reader: type: reader db: /path/to/GeoLite2-ASN.mmdb config: # Block specific ASN numbers - "asn:13335" # Cloudflare - "asn:15169" # Google # Block by organization name - "asn_org:CLOUDFLARENET" - "asn_org@contains:AMAZON" - "asn_org@starts_with:DIGITAL"
Available Variables
asn
- Autonomous System Numberasn_org
- Organization name associated with the ASN
Rate Limit Plugin
Namespace: \Kanopi\Firewall\Plugins\RateLimit
Implements rate limiting to prevent abuse and DDoS attacks.
Configuration Example
block: \Kanopi\Firewall\Plugins\RateLimit: enable: true priority: 100 # Run after other plugins metadata: # Default settings for all paths default_rate: 60 # Requests allowed default_sample: 60 # Time window in seconds default_expiration_time: 300 # Block duration in seconds # Storage backend for rate limit data storage: # Option 1: Redis (recommended for production) type: \Kanopi\Firewall\RateLimitStorage\RedisRateLimitStorage config: redis: host: localhost port: 6379 # Authentication options: # auth: "password" # auth: ["password"] # auth: ["username", "password"] # Option 2: File storage # type: \Kanopi\Firewall\RateLimitStorage\FileRateLimitStorage # config: # file: /var/log/firewall/ratelimit.data # Option 3: Database storage # type: \Kanopi\Firewall\RateLimitStorage\DatabaseRateLimitStorage # config: # storage-table: firewall_ratelimit # connection: # dsn: "mysql://user:pass@localhost/db" # Option 4: In-memory (testing only) # type: \Kanopi\Firewall\RateLimitStorage\InMemoryRateLimitStorage config: # Strict rate limit for homepage - path: "/" rate: 10 sample: 60 # API endpoints with higher limits - path: "/api/*" rate: 100 sample: 60 # Admin area with moderate limits - path: "/admin/*" rate: 30 sample: 60 # Login endpoint with strict limits - path: "/login" rate: 5 sample: 300 # 5 attempts per 5 minutes # Use regex for complex patterns - path: "/\.(php|asp|aspx)$/i" rate: 1 sample: 3600 # Block direct script access
Path Patterns
- Exact match:
/login
- Wildcard:
/api/*
(matches /api/users, /api/posts/123, etc.) - Regex:
/^\/api\/v[0-9]+\//
(matches /api/v1/, /api/v2/, etc.)
Conditional Logic
The firewall supports three formats for defining conditions:
1. Simple Format
Quick and readable syntax for common conditions:
# Basic equality - "variable:value" # With operator - "variable@operator:value" # Negation - "!variable:value" - "!variable@operator:value" # Numeric comparisons - "rate > 100" - "client.version <= 10" # Array matching - "tags@contains:spam,malware#all" # Must contain all - "tags@contains:bot,crawler#any" # Must contain at least one
Supported Operators
equals
(default)not_equals
contains
starts_with
ends_with
regex
in
greater_than
(>)less_than
(<)greater_than_or_equal
(>=)less_than_or_equal
(<=)exists
2. Complex Format
Detailed configuration with full control:
- variable: method operator: in value: [GET, POST] negate: false case_sensitive: true matches: any # For array values: any, all, none, some
3. Grouped Format
Combine multiple conditions with logical operators:
- type: AND rules: - "method:POST" - "path@starts_with:/api" - type: OR rules: - "header.authorization@exists" - "query.api_key@exists"
Logging Configuration
The firewall uses Monolog for flexible logging. Multiple handlers can be configured:
logger: # File logging - class: Monolog\Handler\StreamHandler args: - /var/log/firewall/firewall.log - Monolog\Level::Info formatter: class: Monolog\Formatter\LineFormatter args: - "[%datetime%] [%level_name%] [%context.plugin%] %message% %context% %extra%\n" - "Y-m-d H:i:s" # Syslog - class: Monolog\Handler\SyslogHandler args: - firewall - LOG_USER - Monolog\Level::Warning # Email alerts for critical events - class: Monolog\Handler\NativeMailerHandler args: - security@example.com - "Firewall Alert" - noreply@example.com - Monolog\Level::Critical
Dynamic Configuration Overrides
For dynamic environments (Docker, multi-site installations), you can override YAML configuration with PHP arrays:
<?php $overrides = [ // Override storage location '[storage][config][file]' => $_ENV['FIREWALL_STORAGE_PATH'] ?? '/tmp/firewall.data', // Override GeoIP database path '[block][\Kanopi\Firewall\Plugins\GeoLocation][metadata][reader][db]' => $_ENV['GEOIP_DB_PATH'], // Override Redis connection '[block][\Kanopi\Firewall\Plugins\RateLimit][metadata][storage][config][redis][host]' => $_ENV['REDIS_HOST'] ?? 'localhost', // Disable a plugin '[block][\Kanopi\Firewall\Plugins\UserAgent][enable]' => false, ]; \Kanopi\Firewall\Firewall::create([__DIR__ . '/config.yml'], $overrides)->evaluate();
Platform Integration
Drupal
Add to settings.php
before the container configuration:
// Load composer autoloader if not already loaded if (file_exists(__DIR__ . '/../vendor/autoload.php')) { require_once __DIR__ . '/../vendor/autoload.php'; } // Initialize firewall if (class_exists('\Kanopi\Firewall\Firewall')) { $firewall_config = __DIR__ . '/firewall.yml'; if (file_exists($firewall_config)) { \Kanopi\Firewall\Firewall::create([$firewall_config])->evaluate(); } }
WordPress
Add to wp-config.php
after ABSPATH
is defined but before wp-settings.php
:
// Firewall integration if (file_exists(__DIR__ . '/vendor/autoload.php')) { require_once __DIR__ . '/vendor/autoload.php'; if (class_exists('\Kanopi\Firewall\Firewall')) { $firewall_config = __DIR__ . '/firewall/config.yml'; if (file_exists($firewall_config)) { \Kanopi\Firewall\Firewall::create([$firewall_config])->evaluate(); } } }
Symfony
Add to public/index.php
before the kernel boot:
use App\Kernel; use Kanopi\Firewall\Firewall; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { // Initialize firewall if (class_exists(Firewall::class)) { $configPath = dirname(__DIR__) . '/config/firewall.yml'; if (file_exists($configPath)) { Firewall::create([$configPath])->evaluate(); } } return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); };
Laravel
Add to public/index.php
after the autoloader:
require __DIR__.'/../vendor/autoload.php'; // Firewall integration if (class_exists('\Kanopi\Firewall\Firewall')) { $firewall_config = __DIR__ . '/../config/firewall.yml'; if (file_exists($firewall_config)) { \Kanopi\Firewall\Firewall::create([$firewall_config])->evaluate(); } } $app = require_once __DIR__.'/../bootstrap/app.php';
Advanced Examples
Multi-layered Security Configuration
# High-performance storage storage: type: \Kanopi\Firewall\Storage\DatabaseStorage config: storage-table: firewall_blocked connection: dsn: "mysql://firewall:secure@localhost/security" # Whitelist trusted sources bypass: # Office IPs \Kanopi\Firewall\Plugins\IpAddress: enable: true priority: -200 config: - 203.0.113.0/24 # Office network - 198.51.100.50 # VPN endpoint # Comprehensive blocking rules block: # Geographic restrictions \Kanopi\Firewall\Plugins\GeoLocation: enable: true priority: -100 metadata: reader: type: reader db: /usr/share/GeoIP/GeoLite2-City.mmdb config: # Block high-risk countries - type: OR rules: - "country@in:CN,RU,KP,IR" - "continent:AF" # Block suspicious user agents \Kanopi\Firewall\Plugins\UserAgent: enable: true priority: -50 config: # Block all bots except Google and Bing - type: AND rules: - "bot:true" - "!client.name@in:Googlebot,Bingbot" # Block outdated browsers - type: OR rules: - variable: client.name operator: equals value: "Internet Explorer" - type: AND rules: - "client.name:Chrome" - "client.version < 80" # URL-based protection \Kanopi\Firewall\Plugins\Url: enable: true priority: 0 config: # Protect admin areas - type: AND rules: - "path@starts_with:/admin" - "!header.authorization@exists" # Block vulnerability scanners - "path@regex:/(\.git|\.env|\.htaccess|wp-config\.php|phpmyadmin)/i" # Block SQL injection attempts - "query@regex:/(union.*select|select.*from|insert.*into|drop.*table)/i" # Aggressive rate limiting \Kanopi\Firewall\Plugins\RateLimit: enable: true priority: 100 metadata: default_rate: 120 default_sample: 60 storage: type: \Kanopi\Firewall\RateLimitStorage\RedisRateLimitStorage config: redis: host: redis.internal port: 6379 auth: ["default", "redis_password"] config: # API rate limits by endpoint - path: "/api/v1/auth/*" rate: 5 sample: 300 - path: "/api/v1/public/*" rate: 100 sample: 60 - path: "/api/v1/private/*" rate: 30 sample: 60 # Comprehensive logging logger: # General log file - class: Monolog\Handler\RotatingFileHandler args: - /var/log/firewall/firewall.log - 7 # Keep 7 days - Monolog\Level::Info formatter: class: Monolog\Formatter\JsonFormatter # Security alerts - class: Monolog\Handler\StreamHandler args: - /var/log/firewall/security-alerts.log - Monolog\Level::Warning formatter: class: Monolog\Formatter\LineFormatter args: - "[%datetime%] %level_name%: %message% %context%\n"
Custom Plugin Implementation
Create a custom plugin to implement specific business logic:
<?php namespace App\Security\Firewall\Plugins; use Kanopi\Firewall\Plugins\AbstractPluginBase; use Symfony\Component\HttpFoundation\Request; class ApiKeyValidator extends AbstractPluginBase { private array $validApiKeys; public function __construct(array $metadata = [], array $config = []) { parent::__construct($metadata, $config); // Load API keys from configuration or database $this->validApiKeys = $metadata['api_keys'] ?? []; } public function getName(): string { return 'API Key Validator'; } public function getDescription(): string { return 'Validates API keys for authenticated endpoints'; } public function evaluate(Request $request): bool { // Only check API endpoints if (!str_starts_with($request->getPathInfo(), '/api/')) { return false; } // Check for API key in header or query $apiKey = $request->headers->get('X-API-Key') ?? $request->query->get('api_key'); if (!$apiKey) { $this->logger?->warning('Missing API key', [ 'ip' => $request->getClientIp(), 'path' => $request->getPathInfo(), ]); return true; // Block request } if (!in_array($apiKey, $this->validApiKeys, true)) { $this->logger?->warning('Invalid API key', [ 'ip' => $request->getClientIp(), 'api_key' => substr($apiKey, 0, 8) . '...', ]); return true; // Block request } return false; // Allow request } public function getStatusCode(): int { return 401; // Unauthorized } }
Register the custom plugin in your configuration:
block: \App\Security\Firewall\Plugins\ApiKeyValidator: enable: true priority: -150 # Run before rate limiting metadata: api_keys: - "sk_live_abcd1234567890" - "sk_live_efgh0987654321"
Testing
The firewall includes a comprehensive test suite. Run tests with:
# Run all tests composer test # Run with coverage composer test:coverage # Run specific test suite ./vendor/bin/phpunit tests/Unit/Plugins/ # Run integration tests ./vendor/bin/phpunit tests/Integration/
Example Test Case
<?php use PHPUnit\Framework\TestCase; use Kanopi\Firewall\Firewall; use Symfony\Component\HttpFoundation\Request; class FirewallTest extends TestCase { public function testBlocksMaliciousIp(): void { $config = [ 'storage' => [ 'type' => 'Kanopi\Firewall\Storage\InMemoryStorage' ], 'block' => [ 'Kanopi\Firewall\Plugins\IpAddress' => [ 'enable' => true, 'config' => ['192.168.1.100'] ] ] ]; $firewall = Firewall::create([$config]); // Create a request from the blocked IP $request = Request::create('/', 'GET', [], [], [], [ 'REMOTE_ADDR' => '192.168.1.100' ]); // The firewall should block this request $this->expectException(\Exception::class); $firewall->evaluate($request); } }
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
- Clone the repository
- Install dependencies:
composer install
- Run tests:
composer test
- Check code style:
composer cs
- Run static analysis:
composer stan
License
This project is licensed under the MIT License. See the LICENSE file for details.
Support
- Documentation: https://github.com/kanopi/firewall/wiki
- Issues: https://github.com/kanopi/firewall/issues
- Discussions: https://github.com/kanopi/firewall/discussions
Credits
Simple Firewall is developed and maintained by Kanopi Studios.
Special thanks to:
- The Symfony team for the excellent HttpFoundation component
- MaxMind for the GeoIP2 databases
- The Monolog team for the flexible logging library
- All our contributors and users