From e79a643134741a0ccad43200912e4d0e8b5c2b89 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 26 Jan 2026 08:49:04 +0100 Subject: [PATCH] feat(haproxy): Add certificate staging/production validation - Add cert_is_production() to detect Let's Encrypt staging certificates - Add cert_validate_public() to verify certificate publicly via curl/openssl - Add cert_info() to display certificate details (domain, issuer, dates) - Add cmd_cert_verify command for on-demand certificate verification - Update cmd_cert_list to show staging/production status with icons - Update cmd_cert_add to warn about staging mode and verify after issuance - Bump package release to r16 Co-Authored-By: Claude Opus 4.5 --- package/secubox/secubox-app-haproxy/Makefile | 2 +- .../files/usr/sbin/haproxyctl | 161 +++++++++++++++++- 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/package/secubox/secubox-app-haproxy/Makefile b/package/secubox/secubox-app-haproxy/Makefile index 40e1d9d4..52ef93de 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:=15 +PKG_RELEASE:=16 PKG_MAINTAINER:=CyberMind PKG_LICENSE:=MIT diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index 18edd02b..c9986fb3 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -555,6 +555,111 @@ _add_server_to_backend() { # Certificate Management # =========================================== +# Check if certificate is from Let's Encrypt Production (not Staging) +cert_is_production() { + local cert_file="$1" + [ -f "$cert_file" ] || return 1 + + # Check the issuer - staging certs have "(STAGING)" in the issuer + local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null) + if echo "$issuer" | grep -qi "staging\|test\|fake"; then + return 1 # Staging certificate + fi + + # Check for Let's Encrypt production issuers + if echo "$issuer" | grep -qiE "Let's Encrypt|R3|R10|R11|E1|E2|ISRG"; then + return 0 # Production certificate + fi + + # Check if it's a self-signed or other CA + return 0 # Assume production for other CAs +} + +# Validate certificate publicly using external service +cert_validate_public() { + local domain="$1" + local timeout=10 + + # Try to connect and verify the certificate + if command -v curl >/dev/null 2>&1; then + if curl -sS --max-time "$timeout" -o /dev/null "https://$domain" 2>/dev/null; then + return 0 + fi + fi + + # Fallback: use openssl s_client + if command -v openssl >/dev/null 2>&1; then + local result=$(echo | timeout "$timeout" openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -dates 2>/dev/null) + if [ -n "$result" ]; then + return 0 + fi + fi + + return 1 +} + +# Get certificate info +cert_info() { + local cert_file="$1" + [ -f "$cert_file" ] || return 1 + + local subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | sed 's/subject=//') + local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | sed 's/issuer=//') + local not_after=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2) + local not_before=$(openssl x509 -in "$cert_file" -noout -startdate 2>/dev/null | cut -d= -f2) + + echo "Subject: $subject" + echo "Issuer: $issuer" + echo "Valid From: $not_before" + echo "Valid Until: $not_after" + + if cert_is_production "$cert_file"; then + echo "Type: PRODUCTION (publicly trusted)" + else + echo "Type: STAGING/TEST (NOT publicly trusted!)" + fi +} + +# Verify and report certificate status +cmd_cert_verify() { + load_config + + local domain="$1" + if [ -z "$domain" ]; then + echo "Usage: haproxyctl cert verify " + return 1 + fi + + local cert_file="$CERTS_PATH/$domain.pem" + if [ ! -f "$cert_file" ]; then + log_error "Certificate not found: $cert_file" + return 1 + fi + + echo "Certificate Information for $domain:" + echo "======================================" + cert_info "$cert_file" + echo "" + + # Check if it's production + if ! cert_is_production "$cert_file"; then + log_warn "This is a STAGING certificate - NOT trusted by browsers!" + log_warn "To get a production certificate, ensure staging='0' in config and re-issue" + return 1 + fi + + # Try public validation + echo "Public Validation:" + if cert_validate_public "$domain"; then + log_info "Certificate is publicly valid and accessible" + return 0 + else + log_warn "Could not verify certificate publicly" + log_warn "Ensure DNS points to this server and port 443 is accessible" + return 1 + fi +} + cmd_cert_list() { load_config @@ -566,11 +671,25 @@ cmd_cert_list() { [ -f "$cert" ] || continue local name=$(basename "$cert" .pem) local expiry=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2) - echo " $name - Expires: ${expiry:-Unknown}" + local type_icon="✅" + if ! cert_is_production "$cert"; then + type_icon="⚠️ STAGING" + fi + echo " $name - Expires: ${expiry:-Unknown} $type_icon" done else echo " No certificates found" fi + + # Show current mode + local staging=$(uci -q get haproxy.acme.staging) + echo "" + if [ "$staging" = "1" ]; then + echo "⚠️ ACME Mode: STAGING (certificates will NOT be trusted by browsers)" + echo " To use production: uci set haproxy.acme.staging='0' && uci commit haproxy" + else + echo "✅ ACME Mode: PRODUCTION (certificates will be publicly trusted)" + fi } cmd_cert_add() { @@ -593,6 +712,18 @@ cmd_cert_add() { [ -z "$email" ] && { log_error "ACME email not configured. Set in LuCI > Services > HAProxy > Settings"; return 1; } + # Warn about staging mode + if [ "$staging" = "1" ]; then + log_warn "==========================================" + log_warn "STAGING MODE ENABLED!" + log_warn "Certificate will NOT be trusted by browsers" + log_warn "To use production: uci set haproxy.acme.staging='0' && uci commit haproxy" + log_warn "==========================================" + sleep 2 + else + log_info "Using Let's Encrypt PRODUCTION (certificates will be publicly trusted)" + fi + log_info "Requesting certificate for $domain..." local staging_flag="" @@ -690,6 +821,24 @@ cmd_cert_add() { chmod 600 "$CERTS_PATH/$domain.pem" + # Verify certificate type (production vs staging) + echo "" + if cert_is_production "$CERTS_PATH/$domain.pem"; then + log_info "✅ Certificate is from PRODUCTION CA (publicly trusted)" + else + log_warn "⚠️ Certificate is from STAGING CA (NOT publicly trusted!)" + log_warn " Browsers will show security warnings for this certificate" + log_warn " To get a production certificate:" + log_warn " 1. uci set haproxy.acme.staging='0'" + log_warn " 2. uci commit haproxy" + log_warn " 3. haproxyctl cert remove $domain" + log_warn " 4. haproxyctl cert add $domain" + fi + + # Show certificate info + echo "" + cert_info "$CERTS_PATH/$domain.pem" + # Add to UCI local section="cert_$(echo "$domain" | tr '.-' '__')" uci set haproxy.$section=certificate @@ -699,6 +848,13 @@ cmd_cert_add() { uci commit haproxy log_info "Certificate installed for $domain" + + # Offer to verify publicly if production + if cert_is_production "$CERTS_PATH/$domain.pem"; then + echo "" + log_info "To verify the certificate is working publicly, run:" + log_info " haproxyctl cert verify $domain" + fi } cmd_cert_import() { @@ -1042,8 +1198,9 @@ case "${1:-}" in add) shift; cmd_cert_add "$@" ;; import) shift; cmd_cert_import "$@" ;; renew) shift; cmd_cert_add "$@" ;; + verify) shift; cmd_cert_verify "$@" ;; remove) shift; rm -f "$CERTS_PATH/$1.pem"; uci delete haproxy.cert_${1//[.-]/_} 2>/dev/null ;; - *) echo "Usage: haproxyctl cert {list|add|import|renew|remove}" ;; + *) echo "Usage: haproxyctl cert {list|add|import|renew|verify|remove}" ;; esac ;;