package

github.com/kursadaltan/kemalwaf

V1.2.0 / published Dec 5, 2025 / repository

Kemal Waf

kemal-waf

CI/CD License: Apache 2.0 Crystal Kemal

A Web Application Firewall (WAF) Proof-of-Concept application built with Kemal framework that supports OWASP CRS rules.

πŸš€ Quick Setup: Use the setup.sh script to prepare rules files. See the Running from Docker Hub section for details.

Features

Core WAF Features

  • βœ… YAML format rule loading with multiple operator support
  • βœ… LibInjection integration - Real SQLi/XSS detection
  • βœ… Request variables: REQUEST_LINE, ARGS, ARGS_NAMES, HEADERS, BODY, COOKIE, COOKIE_NAMES, REQUEST_FILENAME, REQUEST_BASENAME
  • βœ… Advanced transformations: none, url_decode, url_decode_uni, lowercase, utf8_to_unicode, remove_nulls, replace_comments
  • βœ… Multiple operator support: regex, libinjection_sqli, libinjection_xss, contains, starts_with
  • βœ… OWASP CRS SQL Injection rules (942xxx series)
  • βœ… Hot rule reloading (every 5 seconds)
  • βœ… Observe mode - logging without blocking to test rules
  • βœ… Prometheus metrics (/metrics endpoint)
  • βœ… Upstream proxy support
  • βœ… TLS/HTTPS support with certificate files or auto-generated self-signed certificates
  • βœ… SNI (Server Name Indication) - Per-domain TLS certificates
  • βœ… Let's Encrypt integration - Automatic certificate generation and renewal
  • βœ… HTTP and HTTPS can run simultaneously

πŸŽ‰ New: Web Admin Panel

  • βœ… Cloudflare-like UI - Modern, user-friendly interface
  • βœ… Single Docker Image - WAF + Admin Panel integrated
  • βœ… Domain Management - Add/edit/delete proxy hosts via GUI
  • βœ… SSL/TLS Management - Configure Let's Encrypt or custom certificates
  • βœ… Real-time Config - Changes apply immediately
  • βœ… Secure Authentication - JWT-based with Argon2 password hashing
  • βœ… Setup Wizard - Easy first-time configuration

Quick Start

The easiest way to get started with both WAF and Admin Panel:

# Clone the repository
git clone https://github.com/kursadaltan/kemalwaf.git
cd kemalwaf

# Build and start (includes WAF + Admin Panel)
docker compose up -d

# Access the services:
# - Admin Panel: http://localhost:8888
# - WAF HTTP: http://localhost:80
# - WAF HTTPS: https://localhost:443

On first access, the admin panel will guide you through setup wizard to create your admin user.

See DOCKER_ADMIN.md for detailed admin panel documentation.

Running from Docker Hub

Quick Start (Default Rules)

Default rules are already included in the Docker image, so volume mount is optional:

# Pull the image from Docker Hub
docker pull kursadaltan/kemalwaf:latest

# Minimal run (with default rules)
docker run -d \
  -p 3030:3030 \
  -v $(pwd)/config/waf.yml:/app/config/waf.yml:ro \
  kursadaltan/kemalwaf:latest

Running with Custom Rules

If you want to use your own rules files:

1. Preparation with setup script (Recommended):

# Run the setup script (downloads rules and config files)
curl -L https://raw.githubusercontent.com/kursadaltan/kemalwaf/main/setup.sh | bash

# Or manually:
chmod +x setup.sh
./setup.sh

2. Mounting custom rules with docker run:

docker run -d \
  -p 3030:3030 \
  -v $(pwd)/config/waf.yml:/app/config/waf.yml:ro \
  -v $(pwd)/rules:/app/rules:ro \
  kursadaltan/kemalwaf:latest

Note: If the rules volume is mounted, the mounted rules will be used instead of the default rules in the image.

Running with Docker Compose

# Clone the project
git clone https://github.com/kursadaltan/kemalwaf.git
cd kemal-waf

# Start with Docker Compose
docker-compose up --build

# WAF is now running at http://localhost:3030

Direct Build on macOS (Without Docker)

Prerequisite: Crystal must be installed

# To install Crystal
brew install crystal

Build and Run:

# Build
./build.sh

# Run
./run.sh

Or manually:

# Install dependencies
shards install

# Compile the application
crystal build --release --no-debug src/waf.cr -o bin/kemal-waf

# Run
UPSTREAM=http://localhost:8080 ./bin/kemal-waf

Note: To run on macOS, you need an upstream server. If you're not using Docker Compose, start the upstream server in another terminal or set the UPSTREAM environment variable to a real upstream URL.

Configuration

Configuration is done via config/waf.yml file. See config/waf.yml.example for a complete example.

The configuration file supports:

  • Multi-domain upstream routing
  • WAF mode (enforce, observe, disabled)
  • Rate limiting settings
  • IP filtering (whitelist/blacklist)
  • GeoIP blocking
  • Rule directory and reload interval
  • Logging configuration
  • Metrics settings
  • TLS/HTTPS configuration (new)
  • HTTP/HTTPS server settings (new)

Environment variables (for backward compatibility):

| Variable | Default | Description | |----------|---------|-------------| | RULE_DIR | rules | Directory containing YAML rule files | | UPSTREAM | http://localhost:8080 | Upstream URL for proxy target (if not in config) | | OBSERVE | false | If true, rules will log but not block when matched | | BODY_LIMIT_BYTES | 1048576 | Request body read limit (1MB) | | RELOAD_INTERVAL_SEC | 5 | Interval for checking rule files (seconds) | | HTTP_ENABLED | true | Enable HTTP server | | HTTPS_ENABLED | false | Enable HTTPS server | | HTTP_PORT | 3030 | HTTP port number | | HTTPS_PORT | 3443 | HTTPS port number | | TLS_CERT_FILE | - | Path to TLS certificate file | | TLS_KEY_FILE | - | Path to TLS private key file | | TLS_AUTO_GENERATE | false | Auto-generate self-signed certificate (testing only) |

TLS/HTTPS Configuration

Kemal WAF supports TLS (HTTPS) with multiple certificate options and SNI (Server Name Indication) support for multi-domain deployments.

Option 1: Global Certificate Files

For a single domain or wildcard certificate:

waf:
  server:
    https_enabled: true
    https_port: 3443
    tls:
      cert_file: /path/to/cert.pem
      key_file: /path/to/key.pem

Option 2: Auto-Generated Self-Signed Certificate

For testing and development:

waf:
  server:
    https_enabled: true
    https_port: 3443
    tls:
      auto_generate: true
      auto_cert_dir: config/certs

⚠️ Warning: Self-signed certificates are for testing/development only. Do not use in production!

Option 3: SNI - Per-Domain Certificates

For multi-domain deployments, each domain can have its own certificate:

waf:
  server:
    https_enabled: true
    https_port: 3443
    
  domains:
    "example.com":
      default_upstream: "http://localhost:8080"
      cert_file: /etc/letsencrypt/live/example.com/fullchain.pem
      key_file: /etc/letsencrypt/live/example.com/privkey.pem
      
    "api.example.com":
      default_upstream: "http://localhost:8081"
      cert_file: /etc/letsencrypt/live/api.example.com/fullchain.pem
      key_file: /etc/letsencrypt/live/api.example.com/privkey.pem

Option 4: Let's Encrypt Auto-Certificate

Kemal WAF can automatically obtain and renew Let's Encrypt certificates:

waf:
  server:
    https_enabled: true
    http_enabled: true   # Required for HTTP-01 challenge
    http_port: 80        # Must be accessible on port 80
    https_port: 443
    
  domains:
    "example.com":
      default_upstream: "http://localhost:8080"
      letsencrypt_enabled: true
      letsencrypt_email: admin@example.com
      
    "api.example.com":
      default_upstream: "http://localhost:8081"
      letsencrypt_enabled: true
      letsencrypt_email: admin@example.com

Requirements for Let's Encrypt:

  • Domain must point to your server (DNS A/AAAA record)
  • Port 80 must be accessible for HTTP-01 challenge
  • Certbot should be installed (brew install certbot or apt-get install certbot)
  • Email address for certificate expiry notifications

Environment Variables for Let's Encrypt:

  • LETSENCRYPT_STAGING=true - Use staging environment for testing (avoids rate limits)

Certificate Priority:

  1. Custom cert_file / key_file (highest priority)
  2. letsencrypt_enabled: true
  3. Global TLS configuration (fallback)

HTTP and HTTPS Together

You can enable both HTTP and HTTPS simultaneously:

waf:
  server:
    http_enabled: true
    http_port: 3030
    https_enabled: true
    https_port: 3443
    tls:
      auto_generate: true

Automatic Certificate Renewal

When using Let's Encrypt, certificates are automatically renewed 30 days before expiry. The renewal process runs in the background every 12 hours.

HTTP/2.0 Support

HTTP/2.0 support is planned but not yet implemented. The configuration option http2_enabled is available in the config file for future use. Currently, the WAF uses HTTP/1.1.

Rule Format

Simple Format (Backward Compatible)

---
id: 942100
msg: "SQL Injection Attack Detected"
variables:
  - ARGS
  - BODY
  - REQUEST_LINE
pattern: "(?i)(union.*select|select.*from|insert.*into)"
action: deny
transforms:
  - url_decode
  - lowercase

Advanced Format (OWASP CRS)

---
id: 942100
name: "SQL Injection - LibInjection Detection"
msg: "SQL Injection Attack Detected via libinjection"
category: "sqli"
severity: "CRITICAL"
paranoia_level: 1
operator: "libinjection_sqli"  # or "regex", "libinjection_xss", "contains", "starts_with"
pattern: null  # null for LibInjection, pattern for regex
variables:
  - type: COOKIE
  - type: ARGS
  - type: ARGS_NAMES
  - type: HEADERS
    names: ["User-Agent", "Referer"]  # Filter for specific headers
  - type: BODY
transforms:
  - none
  - utf8_to_unicode
  - url_decode_uni
  - remove_nulls
action: "deny"
tags:
  - "OWASP_CRS"
  - "attack-sqli"
  - "paranoia-level/1"

Rule Fields

  • id: Unique rule identifier (integer, required)
  • msg: Rule description (string, required)
  • name: Rule name (string, optional)
  • category: Rule category: sqli, xss, lfi, rce, etc. (string, optional)
  • severity: Severity level: CRITICAL, HIGH, MEDIUM, LOW (string, optional)
  • paranoia_level: Paranoia level (integer, optional, default: 1)
  • operator: Matching operator (string, optional, default: "regex")
    • regex: Regex pattern matching
    • libinjection_sqli: LibInjection SQLi detection
    • libinjection_xss: LibInjection XSS detection
    • contains: String contains check
    • starts_with: String starts with check
  • pattern: Regex pattern or string pattern (string?, optional - null for LibInjection)
  • variables: List of variables to check (array, required)
    • Simple format: ["ARGS", "BODY"]
    • Advanced format: [{type: "HEADERS", names: ["User-Agent"]}]
    • Supported variables:
      • REQUEST_LINE: HTTP request line (METHOD PATH PROTOCOL)
      • REQUEST_FILENAME: Request path
      • REQUEST_BASENAME: Basename of path
      • ARGS: Query string parameters (in key=value format)
      • ARGS_NAMES: Parameter names only
      • HEADERS: HTTP headers (in Header-Name: value format)
      • BODY: Request body
      • COOKIE: Cookie header
      • COOKIE_NAMES: Cookie names only
  • action: deny (block) or log (log only) (string, required)
  • transforms: Optional transformation list (array, optional)
    • none: No transform
    • url_decode: Apply URL decode
    • url_decode_uni: Unicode-aware URL decode
    • lowercase: Convert to lowercase
    • utf8_to_unicode: UTF-8 to Unicode conversion
    • remove_nulls: Remove null bytes
    • replace_comments: Remove SQL/HTML comments
  • tags: Rule tags (array, optional)

Adding New Rules

Manual YAML Creation

  1. Create a new .yaml file in the rules/ directory (or subdirectories)
  2. Define the rule using the format above
  3. WAF will automatically load the new rule within 5 seconds (recursive directory scanning)

OWASP CRS Rules

The project includes OWASP CRS SQL Injection rules (in the rules/owasp-crs/ folder):

  • 942100: LibInjection SQLi Detection
  • 942140: Common DB Names Detection
  • 942151: SQL Function Names Detection
  • 942160: Sleep/Benchmark Detection
  • 942170: Benchmark and Sleep Injection

These rules have been manually converted from OWASP CRS to YAML format. To add new rules:

  1. Reference the OWASP CRS documentation
  2. Copy regex patterns from OWASP CRS
  3. Map transforms correctly
  4. Create a rule file in YAML format

LibInjection Installation

The LibInjection C library must be installed on the system or built from source:

# To build LibInjection from source
git clone https://github.com/libinjection/libinjection.git
cd libinjection
make
sudo make install

It is linked during Crystal build with the -linjection flag.

Endpoints

/health

Health check endpoint

curl http://localhost:3000/health

Response:

{
  "status": "healthy",
  "rules_loaded": 2,
  "observe_mode": false
}

/metrics

Prometheus format metrics

curl http://localhost:3000/metrics

Metrics:

  • waf_requests_total: Total number of requests processed
  • waf_blocked_total: Number of blocked requests
  • waf_observed_total: Number of requests matched in observe mode
  • waf_rules_loaded: Number of loaded rules

Test Scenarios

1. Normal Request (Allowed)

curl -i http://localhost:3000/api/users

Expected: 200 OK (response from upstream)

2. SQL Injection Attack (Blocked)

curl -i "http://localhost:3000/api/users?id=1' OR '1'='1"

Expected: 403 Forbidden

{
  "error": "Request blocked by WAF",
  "rule_id": 942100,
  "message": "SQL Injection Attack Detected via libinjection"
}

3. XSS Attack (Blocked)

curl -i "http://localhost:3000/search?q=<script>alert('xss')</script>"

Expected: 403 Forbidden

{
  "error": "Request blocked by WAF",
  "rule_id": 941100,
  "message": "XSS Attack Detected"
}

4. SQL Injection in POST Body (Blocked)

curl -i -X POST http://localhost:3000/api/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "pass OR 1=1"}'

Expected: 403 Forbidden

5. Observe Mode (Logs but Doesn't Block)

# Enable observe mode
docker-compose down
OBSERVE=true docker-compose up -d

# Try SQL injection
curl -i "http://localhost:3000/api/users?id=1' OR '1'='1"

Expected: 200 OK (proxied to upstream, but logged)

Check logs:

docker-compose logs waf | grep "OBSERVE MODE"

Test Plan

Unit Tests

  1. Transformation Tests

    • URL decode: %27OR%271%27%3D%271 β†’ 'OR'1'='1
    • Lowercase: SELECT * FROM β†’ select * from
    • Combined: URL decode + lowercase
  2. Regex Matching Tests

    • SQLi patterns: union select, ' or '1'='1, --, /**/
    • XSS patterns: <script>, javascript:, onerror=
  3. Variable Snapshot Tests

    • ARGS parsing: Correct parsing of query string parameters
    • HEADERS parsing: Capturing all headers
    • BODY parsing: Reading POST body content
    • COOKIE parsing: Parsing cookie header

Integration Tests

  1. Rule Loading

    • Loading all YAML files at startup
    • Gracefully skipping invalid YAML files
    • Catching regex compilation errors
  2. Hot Reload

    • Adding new rule file β†’ loaded within 5s
    • Updating existing rule file β†’ reloaded within 5s
    • Deleting rule file β†’ removed within 5s
  3. Proxy Functionality

    • Forwarding GET requests to upstream
    • Forwarding POST requests with body
    • Forwarding upstream response headers to client
    • Catching upstream connection errors (502)
  4. Metric Accuracy

    • Each request increments waf_requests_total
    • Blocked requests increment waf_blocked_total
    • Observe mode matches increment waf_observed_total
    • Rule reloading updates waf_rules_loaded

Performance Tests

# Load test with Apache Bench
ab -n 10000 -c 100 http://localhost:3000/api/test

# Check metrics
curl http://localhost:3000/metrics

Architecture

This WAF is built on top of the Kemal web framework for Crystal, which provides fast HTTP server capabilities and middleware support.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Client  │─────▢│  kemal-waf   │─────▢│ Upstream β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚              β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚ - Rule Load  β”‚
                 β”‚ - Evaluate   β”‚
                 β”‚ - Proxy      β”‚
                 β”‚ - Metrics    β”‚
                 β”‚ - Hot Reload β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Components

  • rule_loader.cr: YAML rule loading, file watching, hot-reload, recursive directory scanning
  • evaluator.cr: Request evaluation, variable snapshot, transformations, multiple operator support
  • libinjection.cr: LibInjection C binding and wrapper functions
  • proxy_client.cr: Upstream HTTP proxy client
  • metrics.cr: Prometheus metric management
  • waf.cr: Main Kemal server application with middleware and routes

Security Notes

⚠️ This is a PoC application. For production use:

Contributing

Pull requests are welcome. For major changes, please open an issue first.

For detailed information, see the CONTRIBUTING.md file.

Changelog

All notable changes are documented in the CHANGELOG.md file.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

API