From fed7bd43c1ac88a3aff66cd1b8c0f2a9cece53ee Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 11:42:29 +0100 Subject: [PATCH] fix(haproxy): Combine fullchain + key for HAProxy certificates HAProxy requires certificate files to contain both the fullchain (cert + intermediate CA) and the private key concatenated together. Changes: - haproxyctl: Fix cert_add to create combined .pem files - haproxy-sync-certs: New script to sync ACME certs to HAProxy format - haproxy.sh: ACME deploy hook for HAProxy - init.d: Sync certs before starting HAProxy - Makefile: Install new scripts, add cron job for cert sync This fixes the "No Private Key found" error when HAProxy tries to load certificates that only contain the fullchain without the key. Co-Authored-By: Claude Opus 4.5 --- package/secubox/secubox-app-haproxy/Makefile | 19 +++++- .../files/etc/init.d/haproxy | 3 + .../files/usr/lib/acme/deploy/haproxy.sh | 59 +++++++++++++++++++ .../files/usr/sbin/haproxy-sync-certs | 47 +++++++++++++++ .../files/usr/sbin/haproxyctl | 7 ++- 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh create mode 100644 package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs diff --git a/package/secubox/secubox-app-haproxy/Makefile b/package/secubox/secubox-app-haproxy/Makefile index 6c3b204..78a6559 100644 --- a/package/secubox/secubox-app-haproxy/Makefile +++ b/package/secubox/secubox-app-haproxy/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-haproxy PKG_VERSION:=1.0.0 -PKG_RELEASE:=13 +PKG_RELEASE:=14 PKG_MAINTAINER:=CyberMind PKG_LICENSE:=MIT @@ -50,11 +50,28 @@ define Package/secubox-app-haproxy/install $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_BIN) ./files/usr/sbin/haproxyctl $(1)/usr/sbin/haproxyctl + $(INSTALL_BIN) ./files/usr/sbin/haproxy-sync-certs $(1)/usr/sbin/haproxy-sync-certs + + $(INSTALL_DIR) $(1)/usr/lib/acme/deploy + $(INSTALL_BIN) ./files/usr/lib/acme/deploy/haproxy.sh $(1)/usr/lib/acme/deploy/haproxy.sh $(INSTALL_DIR) $(1)/usr/share/haproxy/templates $(INSTALL_DATA) ./files/usr/share/haproxy/templates/* $(1)/usr/share/haproxy/templates/ $(INSTALL_DIR) $(1)/usr/share/haproxy/certs + + # Add cron job for certificate sync after ACME renewals + $(INSTALL_DIR) $(1)/etc/cron.d + echo "# Sync ACME certs to HAProxy after renewals" > $(1)/etc/cron.d/haproxy-certs + echo "15 3 * * * root /usr/sbin/haproxy-sync-certs >/dev/null 2>&1" >> $(1)/etc/cron.d/haproxy-certs +endef + +define Package/secubox-app-haproxy/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +# Sync existing ACME certificates on install +/usr/sbin/haproxy-sync-certs 2>/dev/null || true +exit 0 endef $(eval $(call BuildPackage,secubox-app-haproxy)) diff --git a/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy b/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy index 94cd476..6bed2b3 100644 --- a/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy +++ b/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy @@ -16,6 +16,9 @@ start_service() { [ "$enabled" = "1" ] || return 0 + # Sync ACME certificates to HAProxy format before starting + /usr/sbin/haproxy-sync-certs 2>/dev/null || true + procd_open_instance procd_set_param command "$PROG" service-run procd_set_param respawn 3600 5 0 diff --git a/package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh b/package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh new file mode 100644 index 0000000..eba5b5b --- /dev/null +++ b/package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# ACME deploy hook for HAProxy +# Combines fullchain + private key into single .pem file +# Usage: Called by acme.sh after certificate issuance/renewal + +HAPROXY_CERTS_DIR="/srv/haproxy/certs" + +# acme.sh passes these environment variables: +# DOMAIN - the domain name +# CERT_PATH - path to the domain certificate +# KEY_PATH - path to the domain private key +# CA_PATH - path to the intermediate CA certificate +# FULLCHAIN_PATH - path to the full chain certificate +# CERT_KEY_PATH - same as KEY_PATH + +deploy() { + local domain="$1" + local key_path="$2" + local cert_path="$3" + local ca_path="$4" + local fullchain_path="$5" + + [ -z "$domain" ] && { echo "Error: domain required"; return 1; } + + mkdir -p "$HAPROXY_CERTS_DIR" + + # Use fullchain if available, otherwise use cert + ca + local combined_cert="" + if [ -n "$fullchain_path" ] && [ -f "$fullchain_path" ]; then + combined_cert="$fullchain_path" + elif [ -n "$cert_path" ] && [ -f "$cert_path" ]; then + combined_cert="$cert_path" + else + echo "Error: No certificate file found for $domain" + return 1 + fi + + if [ -z "$key_path" ] || [ ! -f "$key_path" ]; then + echo "Error: No key file found for $domain" + return 1 + fi + + # Combine fullchain + private key for HAProxy + echo "Deploying certificate for $domain to HAProxy..." + cat "$combined_cert" "$key_path" > "$HAPROXY_CERTS_DIR/$domain.pem" + chmod 600 "$HAPROXY_CERTS_DIR/$domain.pem" + + echo "Certificate deployed: $HAPROXY_CERTS_DIR/$domain.pem" + + # Reload HAProxy if running + if [ -x /etc/init.d/haproxy ]; then + /etc/init.d/haproxy reload 2>/dev/null || true + fi + + return 0 +} + +# Entry point for acme.sh deploy hook +deploy "$Le_Domain" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH" diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs new file mode 100644 index 0000000..994321a --- /dev/null +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs @@ -0,0 +1,47 @@ +#!/bin/sh +# Sync ACME certificates to HAProxy format +# Combines fullchain + private key into .pem files +# Called by ACME renewal or manually via haproxyctl + +ACME_DIR="/etc/acme" +HAPROXY_CERTS_DIR="/srv/haproxy/certs" + +log_info() { echo "[haproxy-sync-certs] $*"; logger -t haproxy-sync-certs "$*"; } +log_error() { echo "[haproxy-sync-certs] ERROR: $*" >&2; logger -t haproxy-sync-certs -p err "$*"; } + +mkdir -p "$HAPROXY_CERTS_DIR" + +# Find all ACME certificates and deploy them +for domain_dir in "$ACME_DIR"/*/; do + [ -d "$domain_dir" ] || continue + + # Skip non-domain directories + case "$(basename "$domain_dir")" in + ca|*.ecc) continue ;; + esac + + domain=$(basename "$domain_dir") + fullchain="$domain_dir/fullchain.cer" + key="$domain_dir/${domain}.key" + + # Try alternate paths + [ -f "$fullchain" ] || fullchain="$domain_dir/fullchain.pem" + [ -f "$key" ] || key="$domain_dir/privkey.pem" + [ -f "$key" ] || key="$domain_dir/${domain}.key" + + if [ -f "$fullchain" ] && [ -f "$key" ]; then + log_info "Syncing certificate for $domain" + cat "$fullchain" "$key" > "$HAPROXY_CERTS_DIR/$domain.pem" + chmod 600 "$HAPROXY_CERTS_DIR/$domain.pem" + else + log_error "Missing cert or key for $domain (fullchain=$fullchain, key=$key)" + fi +done + +log_info "Certificate sync complete" + +# Reload HAProxy if running +if pgrep -x haproxy >/dev/null 2>&1 || lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING; then + log_info "Reloading HAProxy..." + /etc/init.d/haproxy reload 2>/dev/null || true +fi diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index c72ce7d..a94ee7b 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -630,8 +630,13 @@ cmd_cert_add() { --home "$LE_WORKING_DIR" \ --cert-file "$CERTS_PATH/$domain.crt" \ --key-file "$CERTS_PATH/$domain.key" \ - --fullchain-file "$CERTS_PATH/$domain.pem" \ + --fullchain-file "$CERTS_PATH/$domain.fullchain.pem" \ --reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true + + # HAProxy needs combined file: fullchain + private key + log_info "Creating combined PEM for HAProxy..." + cat "$CERTS_PATH/$domain.fullchain.pem" "$CERTS_PATH/$domain.key" > "$CERTS_PATH/$domain.pem" + chmod 600 "$CERTS_PATH/$domain.pem" fi # Restart HAProxy if it was running