shadowdaoandClaude Sonnet 5.5 8787e0d99e feat(edge): static default splash for unrouted hosts, answered before the WAF (2026.11.1)
Visiting a server's own hostname (or its IP, or any unrouted name) on 80/443
could show the Coraza WAF block page. `use_backend default-backend` does not
short-circuit http-request rules, so the catch-all was WAF-inspected with the
full server-wide rule set like customer traffic; a browser on the server
hostname also carries the control panel's cookies (not port-scoped).

Unrouted requests are now answered by HAProxy itself with a tiny static page
(errors/default-splash.html via `http-request return ... file`; 200 on "/",
404 elsewhere) placed before the SPOE call. "Routed" comes from
routed_hosts.list / routed_wildcards.list, written by generate_config() from
the same rows as the routing ACLs, port-stripped + lowercased (superset of the
raw-Host routing ACLs), and part of the rollback set. ACME http-01, blocked
IPs and suspended hosts keep their existing handling; rate limits and gates
still run first. HAPROXY_DEFAULT_SPLASH=off restores the old behaviour.

Page and template live in the image outside the /etc/haproxy named volume;
the lists are generated on start, so image bump + recreate is the deploy.

Tests: scripts/test-default-splash.py (in the image build), new splash-off
scenario in validate-rendered-config.py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
2026-11-08 06:55:42 -08:00
2025-02-17 04:31:42 +00:00

HAProxy Manager

A Flask-based API service for managing HAProxy configurations, domains, and SSL certificates. A Flask-based API service for managing HAProxy configurations with dynamic SSL certificate management and health monitoring.

To run the container:

# Without API key authentication (default)
docker run -d -p 80:80 -p 443:443 -p 443:443/udp -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy --name haproxy-manager your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest

# With API key authentication (recommended for production)
docker run -d -p 80:80 -p 443:443 -p 443:443/udp -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy -e HAPROXY_API_KEY=your-secure-api-key-here --name haproxy-manager your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest

Features

  • RESTful API for HAProxy configuration management
  • Database-backed configuration storage using SQLite
  • Automatic HAProxy configuration generation from templates
  • Let's Encrypt SSL certificate integration with auto-renewal
  • Health monitoring endpoint
  • Dynamic backend server management
  • Template override support for custom backend configurations
  • Process monitoring and auto-restart capabilities
  • Socket-based HAProxy runtime API integration
  • NEW: API key authentication for secure access
  • NEW: Certificate renewal API endpoint
  • NEW: Certificate download endpoints for other services
  • NEW: Comprehensive error logging and alerting system
  • NEW: Certificate status monitoring with expiration dates
  • NEW: Default backend page for unmatched domains

Security

API Key Authentication

When the HAPROXY_API_KEY environment variable is set, all API endpoints (except /health and /) require authentication using a Bearer token:

# Example API call with authentication
curl -H "Authorization: Bearer your-secure-api-key-here" \
     http://localhost:8000/api/domains

If no API key is set, the service runs without authentication (useful for development).

Requirements

  • HAProxy
  • Python 3.x
  • Flask
  • SQLite3
  • Certbot (for Let's Encrypt certificates)
  • OpenSSL (for self-signed start-up certificate)

Web UI Interface

The HAProxy Manager includes a web-based user interface accessible at port 8000, providing:

  • Domain and backend server management interface
  • SSL certificate status monitoring

Do Not Expose port 8000 to the open internet If you need to have it exposed to the internet, restrict it to an IP Address via IPTABLES or other firewalls.

# Allow access from the specific IP address (replace 192.168.1.100 with your IP)
iptables -A INPUT -p tcp --dport 8000 -s {YOUR_PUBLIC_IP} -j ACCEPT

# Drop all other connections to port 8000
iptables -A INPUT -p tcp --dport 8000 -j DROP

If you need to be able to access the web interface from multiple locations, I recommend putting it behind an authenticated Proxy like Authentik

API Endpoints

Health Check

Check the status of the HAProxy Manager service.

GET /health

# Response
{
    "status": "healthy",
    "haproxy_status": "running",
    "database": "connected"
}

Get Domains

Retrieve all configured domains and their backend information.

GET /api/domains
Authorization: Bearer your-api-key

# Response
[
    {
        "id": 1,
        "domain": "example.com",
        "ssl_enabled": 1,
        "ssl_cert_path": "/etc/haproxy/certs/example.com.pem",
        "template_override": null,
        "backend_name": "example_backend"
    }
]

Add Domain

Add a new domain with backend servers configuration.

POST /api/domain
Authorization: Bearer your-api-key
Content-Type: application/json

{
    "domain": "example.com",
    "backend_name": "example_backend",
    "template_override": null,
    "servers": [
        {
            "name": "server1",
            "address": "10.0.0.1",
            "port": 8080,
            "options": "check"
        },
        {
            "name": "server2",
            "address": "10.0.0.2",
            "port": 8080,
            "options": "check backup"
        }
    ]
}

# Response
{
    "status": "success",
    "domain_id": 1
}

Enable SSL

Request and configure SSL certificate for a domain using Let's Encrypt.

POST /api/ssl
Authorization: Bearer your-api-key
Content-Type: application/json

{
    "domain": "example.com"
}

# Response
{
    "status": "success"
}

Remove Domain

Remove a domain and its associated backend configuration.

DELETE /api/domain
Authorization: Bearer your-api-key
Content-Type: application/json

{
    "domain": "example.com"
}

# Response
{
    "status": "success",
    "message": "Domain configuration removed"
}

Regenerate Configuration

Regenerate HAProxy configuration from database.

GET /api/regenerate
Authorization: Bearer your-api-key

# Response
{
    "status": "success"
}

Reload HAProxy

Reload HAProxy configuration without restart.

GET /api/reload
Authorization: Bearer your-api-key

# Response
{
    "status": "success"
}

New Certificate Management Endpoints

Request Certificate Generation

Request certificate generation for one or more domains.

POST /api/certificates/request
Authorization: Bearer your-api-key
Content-Type: application/json

{
    "domains": ["example.com", "api.example.com"],
    "force_renewal": false,
    "include_www": true
}

# Response
{
    "status": "completed",
    "summary": {
        "total": 2,
        "successful": 2,
        "failed": 0
    },
    "results": [
        {
            "domain": "example.com",
            "status": "success",
            "message": "Certificate obtained successfully",
            "cert_path": "/etc/haproxy/certs/example.com.pem",
            "domains_covered": ["example.com", "www.example.com"]
        },
        {
            "domain": "api.example.com",
            "status": "success",
            "message": "Certificate obtained successfully",
            "cert_path": "/etc/haproxy/certs/api.example.com.pem",
            "domains_covered": ["api.example.com"]
        }
    ]
}

Renew All Certificates

Trigger renewal of all Let's Encrypt certificates and reload HAProxy.

POST /api/certificates/renew
Authorization: Bearer your-api-key

# Response
{
    "status": "success",
    "message": "Certificates renewed and HAProxy reloaded"
}

Get Certificate Status

Get status of all certificates including expiration dates.

GET /api/certificates/status
Authorization: Bearer your-api-key

# Response
{
    "certificates": [
        {
            "domain": "example.com",
            "ssl_enabled": true,
            "cert_path": "/etc/haproxy/certs/example.com.pem",
            "expires": "2024-12-31T23:59:59",
            "days_until_expiry": 45
        }
    ]
}

Download Certificate Files

Download certificate files for use by other services.

# Download combined certificate (cert + key)
GET /api/certificates/example.com/download
Authorization: Bearer your-api-key

# Download private key only
GET /api/certificates/example.com/key
Authorization: Bearer your-api-key

# Download certificate only (no private key)
GET /api/certificates/example.com/cert
Authorization: Bearer your-api-key

Certificate Renewal

The HAProxy Manager includes automatic certificate renewal with multiple scheduling options:

Automatic Renewal (Container-based)

By default, a cron job runs inside the container every 12 hours to check and renew certificates:

  • Runs at minute 0 of every 12th hour (12:00 AM, 12:00 PM)
  • Automatically reloads HAProxy if certificates are renewed
  • Logs all renewal attempts to /var/log/haproxy-manager.log
  • Errors logged to /var/log/haproxy-manager-errors.log

Manual Renewal via API

Trigger certificate renewal manually using the API:

curl -X POST http://localhost:8000/api/certificates/renew \
  -H "Authorization: Bearer your-api-key"

For more control over scheduling, run renewals from the host machine using the provided script:

# Make the script executable
chmod +x scripts/host-renew-certificates.sh

# Add to host crontab (edit with: crontab -e)
0 */12 * * * /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh

# Or run manually
./scripts/host-renew-certificates.sh

The host-side script:

  • Executes the renewal process inside the running container
  • Maintains separate host-side logs at /var/log/haproxy-manager-host-renewal.log
  • Automatically detects if the container is running
  • Supports custom container names via CONTAINER_NAME environment variable

See scripts/host-crontab-example.txt for more crontab configuration examples.

Renewal Script Features

The renewal script (scripts/renew-certificates.sh) includes:

  • Comprehensive logging with timestamps
  • Retry logic for HAProxy reload (3 attempts with 5-second delays)
  • HAProxy socket health checks before reload
  • Proper error handling and exit codes
  • Detection of whether certificates actually needed renewal

Logging and Monitoring

The HAProxy Manager includes comprehensive logging and error tracking:

Log Files

  • /var/log/haproxy-manager.log - General application logs
  • /var/log/haproxy-manager-errors.log - Error logs for alerting
  • /var/log/haproxy-manager-host-renewal.log - Host-side renewal logs (when using host script)

Logged Operations

All API operations are logged with timestamps and success/failure status:

  • Domain management (add/remove)
  • SSL certificate operations
  • Configuration generation
  • HAProxy reload/restart operations
  • Certificate renewals

Error Alerting

Failed operations are logged to the error log file. You can monitor this file for alerting:

# Monitor error log for alerting
tail -f /var/log/haproxy-manager-errors.log

Environment Variables

Variable Description Default
HAPROXY_API_KEY API key for authentication (optional) None (no auth)
HAPROXY_DEFAULT_PAGE_TITLE Title for the default page Site Not Configured
HAPROXY_DEFAULT_MAIN_MESSAGE Main message on the default page This domain has not been configured yet. Please contact your system administrator to set up this website.
HAPROXY_DEFAULT_SECONDARY_MESSAGE Secondary message on the default page If you believe this is an error, please check the domain name and try again.
HAPROXY_DEFAULT_SPLASH off (or 0/false/no) disables the default splash for unrouted hosts and restores the default-backend page below on

Default Splash (unrouted hosts)

Since 2026.11.1, any request whose Host is not routed to a site -- the server's own hostname (registered cert-only, no backend), a bare IP, a stale or unknown name, a request with no Host at all -- is answered by HAProxy itself with the static page errors/default-splash.html (http-request return ... file): 200 for /, 404 with the same page for any other path, cache-control: no-store, x-content-type-options: nosniff. The page names no version, echoes nothing from the request (it is loaded with file, not lf-file), and links nowhere -- in particular not to the control panel.

Why. use_backend default-backend does not short-circuit HAProxy's http-request rules, so the old catch-all was inspected by Coraza with the full server-wide rule set like customer traffic. A browser on https://<server-hostname>/ also sends the control panel's cookies (cookies are not port-scoped, so :8443 cookies go to :443), and anything Coraza matched there showed the WAF block page for a host with no site behind it. The splash rules sit before the SPOE call, so unrouted requests never reach the WAF; there is nothing behind them to protect.

What still applies / is kept out: rate limits, the IP block (blocked IPs still get the blocked-ip page), the wp-* gates, and the multipart guard run first; /.well-known/acme-challenge/ still reaches certbot (the server hostname's own certificate is issued through it); hosts in suspended_domains.list still get the suspended page. Configured sites, wildcards and their WAF inspection are unchanged. Plain HTTP on :80 gets the splash too, not a redirect: an unknown name usually has no valid certificate, so a redirect would trade a clean page for a TLS warning. Over TLS the presented certificate is whatever SNI selects -- the hostname's own cert if one is issued, otherwise the default bundle.

How "routed" is decided. generate_config() writes /etc/haproxy/routed_hosts.list (exact names) and /etc/haproxy/routed_wildcards.list (.base suffixes) from the same domain rows, in the same call, as the routing ACLs, and both are in the rollback set next to haproxy.cfg. The splash ACLs compare the Host port-stripped and lowercased, a superset of the routing ACLs (which compare the raw Host), so a host that routes is never splashed. Never hand-edit the lists; they are rewritten on every regeneration (also when the splash is off, so turning it on never meets a missing -f file).

Deploying to an existing host. The page ships at /haproxy/errors/default-splash.html and the template under /haproxy/templates/ -- both in the image, OUTSIDE the /etc/haproxy named volume, so the volume cannot shadow them. The lists live in the volume but are generated, not copied. So an image bump plus a recreate of the container is the whole deployment: on start generate_config() renders the new listener and writes both lists, then HAProxy loads them. Nothing to copy into the volume, no WHP-side change. Verify after the recreate:

docker exec haproxy-manager grep -c default-splash.html /etc/haproxy/haproxy.cfg   # 2
docker exec haproxy-manager cat /etc/haproxy/routed_hosts.list                     # every site
curl -sk -o /dev/null -w '%{http_code}\n' https://<server-hostname>/                # 200, splash
curl -sk -o /dev/null -w '%{http_code}\n' https://<a-customer-site>/                 # unchanged

Rollback: recreate on the previous image tag, or set HAPROXY_DEFAULT_SPLASH=off and regenerate.

Covered by scripts/test-default-splash.py (template order/conditions, the page, list generation) and the splash-off scenario of scripts/validate-rendered-config.py.

Default Backend Configuration

With the default splash on (above), unrouted hosts no longer reach this page; it still serves the blocked-IP and suspended pages, and serves unrouted hosts again when HAPROXY_DEFAULT_SPLASH=off.

When a domain is accessed that hasn't been configured in HAProxy, the system will serve a default page instead of showing an error. This default page:

  • Informs visitors that the site is not configured
  • Displays the domain name and current timestamp
  • Is fully customizable through environment variables

Customizing the Default Page

You can customize the default page by setting environment variables:

docker run -d \
  -p 80:80 -p 443:443 -p 443:443/udp -p 8000:8000 \
  -v lets-encrypt:/etc/letsencrypt \
  -v haproxy:/etc/haproxy \
  -e HAPROXY_API_KEY=your-secure-api-key-here \
  -e HAPROXY_DEFAULT_PAGE_TITLE="Website Coming Soon" \
  -e HAPROXY_DEFAULT_MAIN_MESSAGE="This website is currently under construction and will be available soon." \
  -e HAPROXY_DEFAULT_SECONDARY_MESSAGE="Please check back later or contact us for more information." \
  --name haproxy-manager \
  your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest

Example Usage

Setting up with API key authentication:

# Start container with API key
docker run -d \
  -p 80:80 -p 443:443 -p 443:443/udp -p 8000:8000 \
  -v lets-encrypt:/etc/letsencrypt \
  -v haproxy:/etc/haproxy \
  -e HAPROXY_API_KEY=your-secure-api-key-here \
  --name haproxy-manager \
  your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest

# Add a domain
curl -X POST http://localhost:8000/api/domain \
  -H "Authorization: Bearer your-secure-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "example.com",
    "backend_name": "example_backend",
    "servers": [
      {"name": "server1", "address": "10.0.0.1", "port": 8080, "options": "check"}
    ]
  }'

# Request SSL certificate
curl -X POST http://localhost:8000/api/ssl \
  -H "Authorization: Bearer your-secure-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

# Renew certificates
curl -X POST http://localhost:8000/api/certificates/renew \
  -H "Authorization: Bearer your-secure-api-key-here"

# Request certificate generation for another service
curl -X POST http://localhost:8000/api/certificates/request \
  -H "Authorization: Bearer your-secure-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "domains": ["api.example.com"],
    "force_renewal": false,
    "include_www": false
  }'

# Download certificate for another service
curl -H "Authorization: Bearer your-secure-api-key-here" \
  http://localhost:8000/api/certificates/example.com/download \
  -o example.com.pem
S
Description
Base code for HAProxy Web Manager
Readme MIT
2.8 MiB
0 Stars 6 Watchers 0 Forks
Languages
Python 68.2%
Shell 16.3%
Go Template 10%
HTML 3.5%
Dockerfile 1.7%
Other 0.3%