📦 HAProxy + Certbot (standalone через 8888) + dynamic maps

Автор George, Сегодня в 02:11

« назад - далее »

George

Задача:
Организовать автоматическое:
- получение SSL (Let's Encrypt)
- обновление HAProxy maps
- без остановки HAProxy
- без webroot
- без ломания routing

---

📌 Архитектура:

Код: bash
Internet
   ↓
:80 (HAProxy)
   ↓
ACME path → 127.0.0.1:8888 (certbot standalone)
   ↓
остальной трафик → maps → backend

---

🔧 1. HAProxy (ACME routing)

В frontend на 80:

Код: bash
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:

Код: bash
backend letsencrypt-backend
    mode http
    server letsencrypt 127.0.0.1:8888

---

⚙️ 2. Скрипт получения сертификата

/usr/local/bin/haproxy-cert.sh

Код: bash
#!/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"

Важно:
- HAProxy НЕ останавливается
- порт 80 остаётся занят HAProxy
- certbot работает через 8888

---

⚙️ 3. Основной скрипт управления maps

/usr/local/bin/haproxy-map-update.sh

Код: bash
#!/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}"

---

🚀 Использование:

Код: bash
# 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

---

💡 Важные моменты:

  • Certbot вызывается ДО изменения maps
  • Это критично (иначе будет 503)
  • ACME идёт через HAProxy → 8888
  • HAProxy не перезапускается дважды
  • maps сортируются по длине (чтобы поддомены не ломались)

---

🏁 Результат:

  • Никаких 503 при выпуске сертификатов
  • Нет остановки HAProxy
  • Полностью автоматизированный onboarding доменов
  • Production-ready схема
  •  

🡱 🡳

Отметьте интересные вам фрагменты текста и они станут доступны по уникальной ссылке в адресной строке браузера.