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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-25 11:42:29 +01:00
parent 785ba9eb4c
commit fed7bd43c1
5 changed files with 133 additions and 2 deletions

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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