From 91c92dd07e3b1337add83f0084c062aff31e1624 Mon Sep 17 00:00:00 2001 From: jknapp Date: Thu, 19 Feb 2026 13:04:32 -0800 Subject: [PATCH] Add wildcard domain support with DNS-01 ACME challenge flow Support wildcard domains (*.domain.tld) in HAProxy config generation with exact-match ACLs prioritized over wildcard ACLs. Add DNS-01 challenge endpoints that coordinate with certbot via auth/cleanup hook scripts for wildcard SSL certificate issuance. Co-Authored-By: Claude Opus 4.6 --- __pycache__/haproxy_manager.cpython-310.pyc | Bin 34934 -> 0 bytes haproxy_manager.py | 248 ++++++++++++++++++-- scripts/dns-challenge-auth-hook.sh | 29 +++ scripts/dns-challenge-cleanup-hook.sh | 10 + templates/hap_backend.tpl | 6 +- templates/hap_wildcard_acl.tpl | 4 + 6 files changed, 272 insertions(+), 25 deletions(-) delete mode 100644 __pycache__/haproxy_manager.cpython-310.pyc create mode 100755 scripts/dns-challenge-auth-hook.sh create mode 100755 scripts/dns-challenge-cleanup-hook.sh create mode 100644 templates/hap_wildcard_acl.tpl diff --git a/__pycache__/haproxy_manager.cpython-310.pyc b/__pycache__/haproxy_manager.cpython-310.pyc deleted file mode 100644 index 69463da52aca1f2a5e796ba3da990b5035ab96eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34934 zcmd6QdvqMvdEf5r>=TQ{V(|t+a`=+C6akQwEXlN96bVR_DUc>W%YyB!mbe3O!No2( zGZ2M23r4lv+Ofm0Q0X=+#! z`}@9oXLfe6kmR_3l;GZ(x$n96yWjiXsdsm0BKUXDkN@PA)E`G8e?|}Cf0X(de)dy( zBqAarT8+%hYji$}XRWHO#O7lw`n%m)*B~9dS#-b5C9Wwio^DfaV3LlNk-z{Dc{bJyw(fPw-Pz)iaD2Bxdu1CbE*oEso zVodDD^{CmkIc^T2?1b61JZVmdJvXEC$IOA{<6^HcmZD=y?<8hI&-9-rW} zgMOMgB<`Y=`FlD3Za*Gn4vV5Hb04Q238md5j;geGaoV_;2-=zy$5h(==D_B0ar{

Xa%aN5~WU!D^0RcViM+PF9ujKR}lMx{N* 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

7|iHuu?bkqZQ42aGfFfJ#G|m)IF( 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 PIACho5oiWt`tSc=pjRQ< diff --git a/haproxy_manager.py b/haproxy_manager.py index 77435d7..3588f53 100644 --- a/haproxy_manager.py +++ b/haproxy_manager.py @@ -13,6 +13,9 @@ import json import ipaddress import shutil import tempfile +import threading +import time +import re app = Flask(__name__) @@ -118,6 +121,12 @@ def init_db(): blocked_by TEXT ) ''') + # Migration: add is_wildcard column if it doesn't exist + try: + cursor.execute("ALTER TABLE domains ADD COLUMN is_wildcard BOOLEAN DEFAULT 0") + except sqlite3.OperationalError: + pass # Column already exists + conn.commit() def validate_ip_address(ip_string): @@ -273,6 +282,7 @@ def add_domain(): template_override = data.get('template_override') backend_name = data.get('backend_name') servers = data.get('servers', []) + is_wildcard = data.get('is_wildcard', False) if not domain or not backend_name: log_operation('add_domain', False, 'Domain and backend_name are required') @@ -294,9 +304,9 @@ def add_domain(): cursor.execute(''' UPDATE domains - SET template_override = ? + SET template_override = ?, is_wildcard = ? WHERE id = ? - ''', (template_override, domain_id)) + ''', (template_override, 1 if is_wildcard else 0, domain_id)) # Update backend or create if doesn't exist cursor.execute('SELECT id FROM backends WHERE domain_id = ?', (domain_id,)) @@ -317,7 +327,8 @@ def add_domain(): logger.info(f"Updated existing domain {domain} (preserved SSL: enabled={ssl_enabled}, cert={ssl_cert_path})") else: # New domain - insert it - cursor.execute('INSERT INTO domains (domain, template_override) VALUES (?, ?)', (domain, template_override)) + cursor.execute('INSERT INTO domains (domain, template_override, is_wildcard) VALUES (?, ?, ?)', + (domain, template_override, 1 if is_wildcard else 0)) domain_id = cursor.lastrowid # Add backend @@ -1114,6 +1125,167 @@ def clear_expired_blocks(): log_operation('clear_expired_blocks', False, str(e)) return jsonify({'status': 'error', 'message': str(e)}), 500 +@app.route('/api/ssl/dns-challenge/request', methods=['POST']) +@require_api_key +def dns_challenge_request(): + """Start DNS-01 challenge for wildcard certificate""" + data = request.get_json() + domain = data.get('domain') + + if not domain: + return jsonify({'success': False, 'error': 'Domain is required'}), 400 + + # Extract base domain (strip *. prefix if present) + base_domain = domain + if base_domain.startswith('*.'): + base_domain = base_domain[2:] + + # Validate base_domain format + if not re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$', base_domain): + return jsonify({'success': False, 'error': 'Invalid domain format'}), 400 + + # Clean up any previous challenge files + token_file = f'/tmp/dns-challenge-{base_domain}.token' + proceed_file = f'/tmp/dns-challenge-{base_domain}.proceed' + for f in [token_file, proceed_file]: + if os.path.exists(f): + os.remove(f) + + # Start certbot in background thread + def run_certbot(): + try: + auth_hook = '/app/scripts/dns-challenge-auth-hook.sh' + cleanup_hook = '/app/scripts/dns-challenge-cleanup-hook.sh' + result = subprocess.run([ + 'certbot', 'certonly', '-n', + '--manual', '--preferred-challenges', 'dns-01', + '-d', f'*.{base_domain}', + '--manual-auth-hook', auth_hook, + '--manual-cleanup-hook', cleanup_hook + ], capture_output=True, text=True, timeout=600) + if result.returncode == 0: + logger.info(f"DNS-01 certbot completed successfully for *.{base_domain}") + else: + logger.error(f"DNS-01 certbot failed for *.{base_domain}: {result.stderr}") + except subprocess.TimeoutExpired: + logger.error(f"DNS-01 certbot timed out for *.{base_domain}") + except Exception as e: + logger.error(f"DNS-01 certbot error for *.{base_domain}: {e}") + + certbot_thread = threading.Thread(target=run_certbot, daemon=True) + certbot_thread.start() + + # Poll for the auth hook to write the challenge token + max_wait = 30 + poll_interval = 0.5 + elapsed = 0 + while elapsed < max_wait: + if os.path.exists(token_file): + try: + with open(token_file, 'r') as f: + challenge_token = f.read().strip() + if challenge_token: + log_operation('dns_challenge_request', True, f'Challenge token obtained for *.{base_domain}') + return jsonify({ + 'success': True, + 'data': { + 'challenge_token': challenge_token, + 'base_domain': base_domain + } + }) + except Exception as e: + logger.warning(f"Error reading token file: {e}") + time.sleep(poll_interval) + elapsed += poll_interval + + log_operation('dns_challenge_request', False, f'Timed out waiting for challenge token for *.{base_domain}') + return jsonify({'success': False, 'error': 'Timed out waiting for challenge token from certbot'}), 504 + +@app.route('/api/ssl/dns-challenge/verify', methods=['POST']) +@require_api_key +def dns_challenge_verify(): + """Signal certbot to proceed after DNS record is set, wait for cert""" + data = request.get_json() + domain = data.get('domain') + + if not domain: + return jsonify({'success': False, 'error': 'Domain is required'}), 400 + + # Extract base domain (strip *. prefix if present) + base_domain = domain + if base_domain.startswith('*.'): + base_domain = base_domain[2:] + + # Validate base_domain format + if not re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$', base_domain): + return jsonify({'success': False, 'error': 'Invalid domain format'}), 400 + + # Create proceed signal file so the auth hook can continue + proceed_file = f'/tmp/dns-challenge-{base_domain}.proceed' + try: + with open(proceed_file, 'w') as f: + f.write('proceed') + except Exception as e: + log_operation('dns_challenge_verify', False, f'Failed to create proceed file: {e}') + return jsonify({'success': False, 'error': f'Failed to signal certbot: {e}'}), 500 + + # Wait for certbot to finish and produce the certificate + cert_path = f'/etc/letsencrypt/live/{base_domain}/fullchain.pem' + key_path = f'/etc/letsencrypt/live/{base_domain}/privkey.pem' + max_wait = 120 + poll_interval = 1 + elapsed = 0 + + while elapsed < max_wait: + if os.path.exists(cert_path) and os.path.exists(key_path): + # Check that files were recently modified (within last 5 minutes) + cert_mtime = os.path.getmtime(cert_path) + if (time.time() - cert_mtime) < 300: + break + time.sleep(poll_interval) + elapsed += poll_interval + + if elapsed >= max_wait: + log_operation('dns_challenge_verify', False, f'Timed out waiting for certificate for *.{base_domain}') + return jsonify({'success': False, 'error': 'Timed out waiting for certificate from certbot'}), 504 + + # Combine fullchain + privkey into HAProxy cert + try: + os.makedirs(SSL_CERTS_DIR, exist_ok=True) + combined_path = f'{SSL_CERTS_DIR}/*.{base_domain}.pem' + + with open(combined_path, 'w') as combined: + with open(cert_path, 'r') as cf: + combined.write(cf.read()) + with open(key_path, 'r') as kf: + combined.write(kf.read()) + + # Update database + with sqlite3.connect(DB_FILE) as conn: + cursor = conn.cursor() + # Match wildcard domain entry (stored as *.domain.tld) + cursor.execute(''' + UPDATE domains + SET ssl_enabled = 1, ssl_cert_path = ? + WHERE domain = ? OR domain = ? + ''', (combined_path, f'*.{base_domain}', base_domain)) + + # Regenerate config and reload HAProxy + generate_config() + + log_operation('dns_challenge_verify', True, f'Wildcard certificate obtained for *.{base_domain}') + return jsonify({ + 'success': True, + 'data': { + 'domain': f'*.{base_domain}', + 'cert_path': combined_path, + 'message': 'Wildcard certificate obtained and HAProxy updated' + } + }) + except Exception as e: + log_operation('dns_challenge_verify', False, str(e)) + return jsonify({'success': False, 'error': str(e)}), 500 + def generate_config(): try: conn = sqlite3.connect(DB_FILE) @@ -1128,6 +1300,7 @@ def generate_config(): d.ssl_enabled, d.ssl_cert_path, d.template_override, + d.is_wildcard, b.id as backend_id, b.name as backend_name FROM domains d @@ -1167,25 +1340,12 @@ def generate_config(): default_rule = " # Default backend for unmatched domains\n default_backend default-backend\n" config_parts.append(default_rule) - # Add domain configurations - for domain in domains: - if not domain['backend_name']: - logger.warning(f"Skipping domain {domain['domain']} - no backend name") - continue + # Split domains into exact and wildcard for ACL ordering + exact_domains = [d for d in domains if not d.get('is_wildcard')] + wildcard_domains = [d for d in domains if d.get('is_wildcard')] - # Add domain ACL - try: - domain_acl = template_env.get_template('hap_subdomain_acl.tpl').render( - domain=domain['domain'], - name=domain['backend_name'] - ) - config_acls.append(domain_acl) - logger.info(f"Added ACL for domain: {domain['domain']}") - except Exception as e: - logger.error(f"Error generating domain ACL for {domain['domain']}: {e}") - continue - - # Add backend configuration + # Helper to generate backend config for a domain + def generate_backend_for_domain(domain): try: cursor.execute(''' SELECT * FROM backend_servers WHERE backend_id = ? @@ -1194,7 +1354,7 @@ def generate_config(): if not servers: logger.warning(f"No servers found for backend {domain['backend_name']}") - continue + return if domain['template_override'] is not None: logger.info(f"Template Override is set to: {domain['template_override']}") @@ -1202,7 +1362,6 @@ def generate_config(): backend_block = template_env.get_template(template_file).render( name=domain['backend_name'], servers=servers - ) else: backend_block = template_env.get_template('hap_backend.tpl').render( @@ -1214,8 +1373,51 @@ def generate_config(): logger.info(f"Added backend block for: {domain['backend_name']}") except Exception as e: logger.error(f"Error generating backend block for {domain['backend_name']}: {e}") + + # First pass: exact domain ACLs (higher priority - evaluated first) + for domain in exact_domains: + if not domain['backend_name']: + logger.warning(f"Skipping domain {domain['domain']} - no backend name") continue + try: + domain_acl = template_env.get_template('hap_subdomain_acl.tpl').render( + domain=domain['domain'], + name=domain['backend_name'] + ) + config_acls.append(domain_acl) + logger.info(f"Added ACL for domain: {domain['domain']}") + except Exception as e: + logger.error(f"Error generating domain ACL for {domain['domain']}: {e}") + continue + + generate_backend_for_domain(domain) + + # Second pass: wildcard domain ACLs (lower priority - evaluated after exact matches) + for domain in wildcard_domains: + if not domain['backend_name']: + logger.warning(f"Skipping wildcard domain {domain['domain']} - no backend name") + continue + + try: + # Strip *. prefix to get base domain for hdr_end matching + base_domain = domain['domain'] + if base_domain.startswith('*.'): + base_domain = base_domain[2:] + + domain_acl = template_env.get_template('hap_wildcard_acl.tpl').render( + domain=domain['domain'], + name=domain['backend_name'], + base_domain=base_domain + ) + config_acls.append(domain_acl) + logger.info(f"Added wildcard ACL for domain: {domain['domain']}") + except Exception as e: + logger.error(f"Error generating wildcard ACL for {domain['domain']}: {e}") + continue + + generate_backend_for_domain(domain) + # Add ACLS config_parts.append('\n' .join(config_acls)) # Add LetsEncrypt Backend diff --git a/scripts/dns-challenge-auth-hook.sh b/scripts/dns-challenge-auth-hook.sh new file mode 100755 index 0000000..0c87f48 --- /dev/null +++ b/scripts/dns-challenge-auth-hook.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Certbot DNS-01 auth hook +# Called by certbot with CERTBOT_DOMAIN and CERTBOT_VALIDATION env vars +# Writes the validation token for the API to read, then waits for proceed signal + +TOKEN_FILE="/tmp/dns-challenge-${CERTBOT_DOMAIN}.token" +PROCEED_FILE="/tmp/dns-challenge-${CERTBOT_DOMAIN}.proceed" + +# Write the challenge token so the API can return it to the caller +echo "${CERTBOT_VALIDATION}" > "${TOKEN_FILE}" + +# Wait for the proceed signal (PHP side sets DNS record, then calls verify endpoint) +MAX_WAIT=300 +ELAPSED=0 + +while [ ${ELAPSED} -lt ${MAX_WAIT} ]; do + if [ -f "${PROCEED_FILE}" ]; then + # Give DNS a moment to propagate after the signal + sleep 5 + exit 0 + fi + sleep 1 + ELAPSED=$((ELAPSED + 1)) +done + +echo "Timed out waiting for proceed signal for ${CERTBOT_DOMAIN}" >&2 +exit 1 diff --git a/scripts/dns-challenge-cleanup-hook.sh b/scripts/dns-challenge-cleanup-hook.sh new file mode 100755 index 0000000..8c9b256 --- /dev/null +++ b/scripts/dns-challenge-cleanup-hook.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Certbot DNS-01 cleanup hook +# Removes temporary challenge files after certbot finishes + +TOKEN_FILE="/tmp/dns-challenge-${CERTBOT_DOMAIN}.token" +PROCEED_FILE="/tmp/dns-challenge-${CERTBOT_DOMAIN}.proceed" + +rm -f "${TOKEN_FILE}" "${PROCEED_FILE}" diff --git a/templates/hap_backend.tpl b/templates/hap_backend.tpl index b6c9153..925f3e7 100644 --- a/templates/hap_backend.tpl +++ b/templates/hap_backend.tpl @@ -7,7 +7,8 @@ backend {{ name }}-backend http-request add-header X-CLIENT-IP %[var(txn.real_ip)] http-request set-header X-Real-IP %[var(txn.real_ip)] http-request set-header X-Forwarded-For %[var(txn.real_ip)] - {% if ssl_enabled %}http-request set-header X-Forwarded-Proto https if { ssl_fc }{% endif %} + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } {% for server in servers %} server {{ server.server_name }} {{ server.server_address }}:{{ server.server_port }} {{ server.server_options }} @@ -31,7 +32,8 @@ backend {{ name }}-sse-backend http-request add-header X-CLIENT-IP %[var(txn.real_ip)] http-request set-header X-Real-IP %[var(txn.real_ip)] http-request set-header X-Forwarded-For %[var(txn.real_ip)] - {% if ssl_enabled %}http-request set-header X-Forwarded-Proto https if { ssl_fc }{% endif %} + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } {% for server in servers %} server {{ server.server_name }} {{ server.server_address }}:{{ server.server_port }} {{ server.server_options }} diff --git a/templates/hap_wildcard_acl.tpl b/templates/hap_wildcard_acl.tpl new file mode 100644 index 0000000..8f03ec0 --- /dev/null +++ b/templates/hap_wildcard_acl.tpl @@ -0,0 +1,4 @@ + + #Wildcard method {{ domain }} + acl {{ name }}-acl hdr_end(host) -i .{{ base_domain }} + use_backend {{ name }}-backend if {{ name }}-acl