SMF - Just Installed!
Client
↓
HAProxy (:80 / :443)
↓
Maps routing:
├── hosts.map (HTTP backend)
├── passth.map (TLS SNI backend)
└── acme_backends.map (ACME HTTP backend)
↓
Backends (auto-generated)
/usr/local/bin/haproxy-map-update.sh
haproxy-map-update.sh set DOMAIN BACKEND MODE IP_LIST PORT [-y]
haproxy-map-update.sh del DOMAIN
haproxy-map-update.sh set ftp.cloud-life.site ftp http 10.10.1.37 80 -y
haproxy-map-update.sh set games.cloud-life.site games passth "10.10.1.46,10.10.1.47,10.10.1.48" 443 -y
haproxy-map-update.sh del games.cloud-life.site
Internet → HAProxy :80
→ /.well-known/acme-challenge/
→ 127.0.0.1:8888 (certbot standalone)
/etc/haproxy/maps/hosts.map
/etc/haproxy/maps/passth.map
/etc/haproxy/maps/acme_backends.map
IP_LIST="10.10.1.46,10.10.1.47,10.10.1.48"
PORT=443
server games1 10.10.1.46:443 ssl verify none check
server games2 10.10.1.47:443 ssl verify none check
server games3 10.10.1.48:443 ssl verify none check
-y
Internet
↓
:80 (HAProxy)
↓
ACME path → 127.0.0.1:8888 (certbot standalone)
↓
остальной трафик → maps → backend
frontend http_80
bind *:80
mode http
acl letsencrypt-acl path_beg /.well-known/acme-challenge/
use_backend letsencrypt-backend if letsencrypt-acl
http-request redirect scheme https code 301 if !letsencrypt-acl
backend letsencrypt-backend
mode http
server letsencrypt 127.0.0.1:8888
#!/bin/bash
set -euo pipefail
DOMAIN=${1:-}
if [ -z "$DOMAIN" ]; then
echo "[CERT] no domain!"
exit 1
fi
echo "[CERT] Issuing certificate for $DOMAIN"
certbot certonly --standalone \
--http-01-port=8888 \
-d "$DOMAIN" \
--non-interactive \
--agree-tos \
--email george@cloud-life.site \
--expand
echo "[CERT] Building PEM"
cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
/etc/letsencrypt/live/$DOMAIN/privkey.pem \
> /etc/ssl/haproxy/$DOMAIN.pem
echo "[CERT] Done"
#!/bin/bash
GREEN="\033[0;32m"
NC="\033[0m"
RED="\033[0;31m"
usage() {
echo -e "${GREEN}"
echo "Usage:"
echo " $0 set DOMAIN BACKEND MODE"
echo " $0 del DOMAIN"
echo ""
echo "Modes:"
echo " http - обычный HTTP (hosts.map)"
echo " passth - TLS passthrough (passth.map + acme_backends.map)"
echo -e "${NC}"
exit 1
}
if [ $# -lt 2 ]; then
echo -e "${RED}[ERROR] Invalid arguments${NC}"
usage
fi
ACTION=$1
DOMAIN=$2
BACKEND=${3:-}
MODE=${4:-}
if [ "$ACTION" == "set" ]; then
if [ -z "$BACKEND" ] || [ -z "$MODE" ]; then
echo -e "${RED}[ERROR] Invalid arguments${NC}"
usage
fi
fi
set -euo pipefail
HOSTS_MAP="/etc/haproxy/maps/hosts.map"
PASSTH_MAP="/etc/haproxy/maps/passth.map"
ACME_MAP="/etc/haproxy/maps/acme_backends.map"
ACME_BACKEND="acme_${BACKEND}"
issue_cert() {
echo "[CERT] Ensuring certificate for $DOMAIN"
if /usr/local/bin/haproxy-cert.sh "$DOMAIN"; then
echo -e "${GREEN}[CERT] OK${NC}"
else
echo -e "${RED}[CERT] FAILED → aborting${NC}"
exit 1
fi
}
update_file() {
local MAP=$1
local KEY=$2
local VALUE=$3
TMP=$(mktemp)
[ -f "$MAP" ] && cp "$MAP" "$TMP"
grep -v "^$KEY " "$TMP" > "$TMP.new" || true
if [ "$ACTION" == "set" ]; then
echo "$KEY $VALUE" >> "$TMP.new"
fi
mv "$TMP.new" "$MAP"
rm -f "$TMP"
}
delete_from_all() {
update_file "$HOSTS_MAP" "$DOMAIN" ""
update_file "$PASSTH_MAP" "$DOMAIN" ""
update_file "$ACME_MAP" "$DOMAIN" ""
}
engine() {
for MAP in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
if [ -f "$MAP" ]; then
awk '{ print length($1), $0 }' "$MAP" \
| sort -nr \
| cut -d" " -f2- \
> "$MAP.tmp"
mv "$MAP.tmp" "$MAP"
fi
done
}
echo "[MAP] Processing: $DOMAIN"
# 🔥 1. СНАЧАЛА сертификат
if [ "$ACTION" == "set" ]; then
issue_cert
fi
# 🔥 2. потом routing
delete_from_all
if [ "$ACTION" == "set" ]; then
if [ "$MODE" == "http" ]; then
update_file "$HOSTS_MAP" "$DOMAIN" "$BACKEND"
elif [ "$MODE" == "passth" ]; then
update_file "$PASSTH_MAP" "$DOMAIN" "$BACKEND"
update_file "$ACME_MAP" "$DOMAIN" "$ACME_BACKEND"
else
echo "Mode must be: http | passth"
exit 1
fi
fi
echo "[MAP] Sorting maps"
engine
echo "[MAP] Reloading HAProxy"
systemctl reload haproxy
echo -e "${GREEN}[MAP] DONE${NC}"
# HTTP сайт
haproxy-map-update.sh set site.com backend_name http
# TLS passthrough
haproxy-map-update.sh set site.com backend_name passth
# удалить
haproxy-map-update.sh del site.com
nano /usr/local/bin/haproxy-safe-reload.sh
#!/bin/bash
set -euo pipefail
VIP="10.10.10.10"
# Проверяем, что нода активная (есть VIP)
ip addr | grep -q "$VIP"
if [ $? -ne 0 ]; then
echo "[HAProxy] Not active node → skip reload"
exit 0
fi
LOCK=/var/lock/haproxy-reload.lock
flock -n "$LOCK" bash -c '
echo "[HAProxy] validating config..."
haproxy -c -f /etc/haproxy/haproxy.cfg
if [ $? -ne 0 ]; then
echo "[HAProxy] CONFIG INVALID → abort"
exit 1
fi
echo "[HAProxy] reloading..."
systemctl reload haproxy
echo "[HAProxy] reload OK"
'
chmod +x /usr/local/bin/haproxy-safe-reload.sh
apt install inotify-tools -y
nano /usr/local/bin/haproxy-watch.sh
#!/bin/bash
WATCH="/etc/haproxy"
LOCK=/var/lock/haproxy-watch.lock
LAST_RUN=0
inotifywait -m -r -e close_write --format '%f' $WATCH | while read file; do
```
# игнор временных файлов
case "$file" in
*.swp|*.swx|*.tmp|4913)
continue
;;
esac
# реагируем только на нужные файлы
if [[ "$file" != "haproxy.cfg" && "$file" != *.map ]]; then
continue
fi
NOW=$(date +%s)
# debounce (защита от повторов)
if (( NOW - LAST_RUN < 3 )); then
continue
fi
LAST_RUN=$NOW
echo "[WATCH] Change detected: $file"
flock -n $LOCK /usr/local/bin/haproxy-safe-reload.sh
```
done
chmod +x /usr/local/bin/haproxy-watch.sh
nano /etc/systemd/system/haproxy-watch.service
[Unit]
Description=HAProxy config watcher
After=network.target
[Service]
ExecStart=/usr/local/bin/haproxy-watch.sh
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now haproxy-watch
systemctl status haproxy-watch
nano /etc/letsencrypt/renewal-hooks/deploy/haproxy-deploy-cert.sh
#!/bin/bash
set -euo pipefail
CERT_DIR="/etc/haproxy/certs"
if [ -n "${RENEWED_LINEAGE:-}" ]; then
DOMAIN=$(basename "$RENEWED_LINEAGE")
```
cat "$RENEWED_LINEAGE/fullchain.pem" \
"$RENEWED_LINEAGE/privkey.pem" \
> "$CERT_DIR/$DOMAIN.pem"
```
else
for d in /etc/letsencrypt/live/*/; do
DOMAIN=$(basename "$d")
cat "$d/fullchain.pem" "$d/privkey.pem" > "$CERT_DIR/$DOMAIN.pem"
done
fi
/usr/local/bin/haproxy-safe-reload.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy-deploy-cert.sh
nano /etc/haproxy/haproxy.cfg
[WATCH] Change detected: haproxy.cfg
[HAProxy] reload OK