Web Application Firewall built with Kemal framework
How to run kemal-waf in production: Docker, Compose, Nginx, and Kubernetes.
Before deploying to production:
# Create volumes
docker volume create waf-certs
docker volume create admin-data
docker volume create waf-logs
# Run with production config
docker run -d \
--name kemal-waf \
--restart unless-stopped \
-p 80:3030 \
-p 443:3443 \
-p 8888:8888 \
-v $(pwd)/config/waf.yml:/app/config/waf.yml:ro \
-v $(pwd)/rules:/app/rules:ro \
-v waf-certs:/app/config/certs \
-v admin-data:/app/admin/data \
-v waf-logs:/app/logs \
kursadaltan/kemalwaf:latest
Create docker-compose.prod.yml:
version: '3.8'
services:
waf:
image: kursadaltan/kemalwaf:latest
container_name: kemal-waf
restart: unless-stopped
ports:
- "80:3030"
- "443:3443"
- "8888:8888"
volumes:
- ./config/waf.yml:/app/config/waf.yml:ro
- ./rules:/app/rules:ro
- waf-certs:/app/config/certs
- admin-data:/app/admin/data
- waf-logs:/app/logs
environment:
- LOG_LEVEL=info
- LOG_RETENTION_DAYS=90
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
waf-certs:
admin-data:
waf-logs:
Deploy:
docker compose -f docker-compose.prod.yml up -d
See Nginx Setup Guide for detailed Nginx configuration.
# docker-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v2.10
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
waf:
image: kursadaltan/kemalwaf:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.waf.rule=Host(`example.com`)"
- "traefik.http.routers.waf.entrypoints=websecure"
- "traefik.http.routers.waf.tls.certresolver=letsencrypt"
- "traefik.http.services.waf.loadbalancer.server.port=3030"
apiVersion: apps/v1
kind: Deployment
metadata:
name: kemal-waf
spec:
replicas: 3
selector:
matchLabels:
app: kemal-waf
template:
metadata:
labels:
app: kemal-waf
spec:
containers:
- name: waf
image: kursadaltan/kemalwaf:latest
ports:
- containerPort: 3030
name: http
- containerPort: 3443
name: https
- containerPort: 8888
name: admin
volumeMounts:
- name: config
mountPath: /app/config/waf.yml
subPath: waf.yml
- name: rules
mountPath: /app/rules
- name: certs
mountPath: /app/config/certs
- name: admin-data
mountPath: /app/admin/data
livenessProbe:
httpGet:
path: /health
port: 3030
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3030
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
volumes:
- name: config
configMap:
name: waf-config
- name: rules
configMap:
name: waf-rules
- name: certs
secret:
secretName: waf-certs
- name: admin-data
persistentVolumeClaim:
claimName: waf-admin-data
---
apiVersion: v1
kind: Service
metadata:
name: kemal-waf
spec:
type: LoadBalancer
selector:
app: kemal-waf
ports:
- port: 80
targetPort: 3030
protocol: TCP
name: http
- port: 443
targetPort: 3443
protocol: TCP
name: https
- port: 8888
targetPort: 8888
protocol: TCP
name: admin
apiVersion: v1
kind: ConfigMap
metadata:
name: waf-config
data:
waf.yml: |
waf:
mode: enforce
domains:
"example.com":
default_upstream: "http://backend:8080"
curl http://localhost:3030/health
Response:
{
"status": "healthy",
"rules_loaded": 42,
"observe_mode": false
}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Access metrics at: http://localhost:9090/metrics
Key metrics:
waf_requests_total - Total requests processedwaf_blocked_total - Blocked requestswaf_observed_total - Observed matches (observe mode)waf_rules_loaded - Number of loaded rulesSee Monitoring Guide for Grafana dashboard setup.
Configure log rotation in config/waf.yml:
logging:
log_dir: logs
max_size_mb: 100
retention_days: 30
audit_file: logs/audit.log
For production, consider:
# Backup config and rules
tar -czf waf-backup-$(date +%Y%m%d).tar.gz \
config/waf.yml \
rules/ \
config/ip_whitelist.txt \
config/ip_blacklist.txt
# Backup admin data (if using SQLite)
docker exec kemal-waf sqlite3 /app/admin/data/admin.db .dump > admin-backup.sql
# Restore files
tar -xzf waf-backup-YYYYMMDD.tar.gz
# Restart container
docker restart kemal-waf
Configure connection pool size in config/waf.yml:
waf:
upstream:
pool_size: 100
timeout: 30s
Tune rate limits based on your traffic:
rate_limiting:
enabled: true
default_limit: 200 # Adjust based on expected traffic
window: 60s
block_duration: 300s
For Docker:
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
Run multiple WAF instances behind a load balancer:
# docker-compose.yml
services:
waf1:
image: kursadaltan/kemalwaf:latest
# ...
waf2:
image: kursadaltan/kemalwaf:latest
# ...
waf3:
image: kursadaltan/kemalwaf:latest
# ...
Use sticky sessions if needed, or ensure stateless operation.