Technology

ACME Protocol Home Lab Setup

ACME Protocol Home Lab Setup | SiamCafe Blog
2025-10-31· อ. บอม — SiamCafe.net· 1,567 คำ

ACME Protocol Home Lab Setup คืออะไร

ACME (Automatic Certificate Management Environment) เป็น protocol สำหรับขอ SSL/TLS certificates อัตโนมัติ ใช้โดย Let's Encrypt และ CA อื่นๆ Home Lab คือ server infrastructure ที่ตั้งที่บ้านสำหรับเรียนรู้ ทดลอง และ host services ส่วนตัว การรวมสองแนวคิดนี้ช่วยให้ Home Lab มี HTTPS certificates อัตโนมัติ ทำให้ services ปลอดภัย เข้าถึงจากภายนอกได้ และเรียนรู้ระบบ PKI จริง บทความนี้อธิบายการ setup ACME certificates สำหรับ Home Lab ครบทุกขั้นตอน

ACME Protocol Overview

# acme_overview.py — ACME protocol fundamentals
import json

class ACMEOverview:
    PROTOCOL = {
        "purpose": "ขอ SSL/TLS certificates อัตโนมัติ — ไม่ต้อง manual verification",
        "standard": "RFC 8555",
        "providers": {
            "lets_encrypt": "Let's Encrypt (ฟรี, ยอดนิยมที่สุด)",
            "zerossl": "ZeroSSL (ฟรี + paid options)",
            "buypass": "Buypass Go SSL (ฟรี, 180 วัน)",
            "google_trust": "Google Trust Services (ฟรี)",
        },
        "certificate_types": {
            "DV": "Domain Validation — ยืนยันว่าเป็นเจ้าของ domain",
            "wildcard": "Wildcard (*.example.com) — ต้องใช้ DNS challenge",
        },
    }

    CHALLENGES = {
        "http01": {
            "name": "HTTP-01 Challenge",
            "how": "ACME server เข้า http://domain/.well-known/acme-challenge/token",
            "requirement": "Port 80 ต้องเปิด, domain ชี้มาที่ server",
            "best_for": "Single domain, web servers ที่เข้าถึงได้จาก internet",
        },
        "dns01": {
            "name": "DNS-01 Challenge",
            "how": "สร้าง TXT record: _acme-challenge.domain = token",
            "requirement": "ต้อง control DNS records (API หรือ manual)",
            "best_for": "Wildcard certs, internal-only servers, Home Lab",
        },
        "tlsalpn01": {
            "name": "TLS-ALPN-01 Challenge",
            "how": "ตอบ TLS handshake ด้วย self-signed cert ที่มี acme identifier",
            "requirement": "Port 443 ต้องเปิด",
            "best_for": "เมื่อ port 80 ใช้ไม่ได้",
        },
    }

    def show_protocol(self):
        print("=== ACME Protocol ===\n")
        print(f"  Purpose: {self.PROTOCOL['purpose']}")
        print(f"  Standard: {self.PROTOCOL['standard']}")
        print(f"\n  Providers:")
        for key, desc in self.PROTOCOL['providers'].items():
            print(f"    [{key}] {desc}")

    def show_challenges(self):
        print(f"\n=== Challenge Types ===")
        for key, ch in self.CHALLENGES.items():
            print(f"\n  [{ch['name']}]")
            print(f"  How: {ch['how']}")
            print(f"  Best for: {ch['best_for']}")

acme = ACMEOverview()
acme.show_protocol()
acme.show_challenges()

Home Lab Architecture

# homelab.py — Home Lab architecture with ACME
import json

class HomeLabSetup:
    ARCHITECTURE = {
        "network": {
            "router": "Router/Firewall (pfSense/OPNsense) — port forwarding 80/443",
            "dns": "Local DNS (Pi-hole/AdGuard) + split DNS สำหรับ internal resolution",
            "reverse_proxy": "Traefik/Nginx Proxy Manager — terminate SSL + route traffic",
        },
        "servers": {
            "proxmox": "Proxmox VE — hypervisor สำหรับ VMs + containers",
            "docker": "Docker host — run services เป็น containers",
            "nas": "TrueNAS/Synology — storage + backup",
        },
        "services": {
            "web": "Nextcloud, Gitea, Wiki.js, Jellyfin",
            "monitoring": "Grafana, Prometheus, Uptime Kuma",
            "home_auto": "Home Assistant, Node-RED",
        },
    }

    DOMAIN_OPTIONS = {
        "own_domain": {
            "name": "ซื้อ Domain เอง (แนะนำ)",
            "cost": "~300-500 บาท/ปี (.com, .net)",
            "setup": "ชี้ A record ไป public IP หรือใช้ DDNS",
            "ssl": "Let's Encrypt DNS-01 challenge → wildcard cert",
        },
        "ddns": {
            "name": "Dynamic DNS (DDNS)",
            "cost": "ฟรี (DuckDNS, No-IP)",
            "setup": "ติดตั้ง DDNS client บน router/server",
            "ssl": "Let's Encrypt HTTP-01 หรือ DNS-01 (DuckDNS support)",
        },
        "cloudflare_tunnel": {
            "name": "Cloudflare Tunnel (Zero Trust)",
            "cost": "ฟรี",
            "setup": "ไม่ต้องเปิด port — tunnel จาก server ออก Cloudflare",
            "ssl": "Cloudflare จัดการ SSL ให้ — ง่ายสุด",
        },
    }

    def show_architecture(self):
        print("=== Home Lab Architecture ===\n")
        for category, items in self.ARCHITECTURE.items():
            print(f"[{category.upper()}]")
            for key, desc in items.items():
                print(f"  {key}: {desc}")
            print()

    def show_domain_options(self):
        print("=== Domain Options ===")
        for key, opt in self.DOMAIN_OPTIONS.items():
            print(f"\n  [{opt['name']}] Cost: {opt['cost']}")
            print(f"  SSL: {opt['ssl']}")

lab = HomeLabSetup()
lab.show_architecture()
lab.show_domain_options()

Traefik + Let's Encrypt Setup

# traefik_setup.py — Traefik with ACME
import json

class TraefikACME:
    DOCKER_COMPOSE = """
# docker-compose.yml — Traefik with Let's Encrypt
version: '3.8'
services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      CF_API_EMAIL: ""
      CF_DNS_API_TOKEN: ""
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/acme.json:/acme.json
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/dynamic:/dynamic:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.home.example.com`)"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"

  whoami:
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.home.example.com`)"
      - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
"""

    TRAEFIK_CONFIG = """
# traefik.yml — Static configuration
api:
  dashboard: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    exposedByDefault: false
  file:
    directory: /dynamic
    watch: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: your@email.com
      storage: /acme.json
      # DNS-01 challenge (Cloudflare) — wildcard support
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"
"""

    def show_compose(self):
        print("=== Docker Compose ===")
        print(self.DOCKER_COMPOSE[:500])

    def show_config(self):
        print(f"\n=== Traefik Config ===")
        print(self.TRAEFIK_CONFIG[:500])

    def dns_providers(self):
        print(f"\n=== Supported DNS Providers ===")
        providers = [
            ("Cloudflare", "cloudflare", "CF_DNS_API_TOKEN"),
            ("Route53 (AWS)", "route53", "AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY"),
            ("Google Cloud DNS", "gcloud", "GCE_SERVICE_ACCOUNT_FILE"),
            ("DigitalOcean", "digitalocean", "DO_AUTH_TOKEN"),
            ("DuckDNS", "duckdns", "DUCKDNS_TOKEN"),
            ("Namecheap", "namecheap", "NAMECHEAP_API_KEY"),
        ]
        for name, provider, env in providers:
            print(f"  [{name}] provider: {provider} | env: {env}")

traefik = TraefikACME()
traefik.show_compose()
traefik.show_config()
traefik.dns_providers()

Certbot CLI Setup

# certbot.py — Certbot for manual ACME setup
import json

class CertbotSetup:
    COMMANDS = {
        "install": {
            "name": "Install Certbot",
            "commands": [
                "# Ubuntu/Debian",
                "sudo apt update && sudo apt install -y certbot",
                "",
                "# With Cloudflare DNS plugin",
                "sudo apt install -y python3-certbot-dns-cloudflare",
                "",
                "# With Nginx plugin",
                "sudo apt install -y python3-certbot-nginx",
            ],
        },
        "http_challenge": {
            "name": "HTTP-01 Challenge (port 80 required)",
            "commands": [
                "# Standalone mode (certbot runs its own server)",
                "sudo certbot certonly --standalone -d home.example.com",
                "",
                "# Nginx mode (auto-configure Nginx)",
                "sudo certbot --nginx -d home.example.com",
                "",
                "# Webroot mode (existing web server)",
                "sudo certbot certonly --webroot -w /var/www/html -d home.example.com",
            ],
        },
        "dns_challenge": {
            "name": "DNS-01 Challenge (wildcard support)",
            "commands": [
                "# Cloudflare DNS",
                "sudo certbot certonly --dns-cloudflare \\",
                "  --dns-cloudflare-credentials /etc/cloudflare.ini \\",
                "  -d '*.home.example.com' -d home.example.com",
                "",
                "# cloudflare.ini:",
                "# dns_cloudflare_api_token = YOUR_API_TOKEN",
                "",
                "# Manual DNS (any provider)",
                "sudo certbot certonly --manual --preferred-challenges dns \\",
                "  -d '*.home.example.com'",
            ],
        },
        "renewal": {
            "name": "Auto Renewal",
            "commands": [
                "# Test renewal",
                "sudo certbot renew --dry-run",
                "",
                "# Cron job (auto renewal every 12 hours)",
                "echo '0 */12 * * * root certbot renew --quiet' | sudo tee /etc/cron.d/certbot",
                "",
                "# Renewal hook (restart services after renewal)",
                "sudo certbot renew --deploy-hook 'systemctl reload nginx'",
            ],
        },
    }

    def show_commands(self):
        print("=== Certbot Commands ===\n")
        for key, section in self.COMMANDS.items():
            print(f"[{section['name']}]")
            for cmd in section["commands"][:4]:
                print(f"  {cmd}")
            print()

certbot = CertbotSetup()
certbot.show_commands()

Python ACME Client

# acme_client.py — Python ACME automation
import json
import random

class ACMEAutomation:
    CODE = """
# acme_manager.py — Python ACME certificate manager
import subprocess
import json
import datetime
import os

class CertificateManager:
    def __init__(self, domain, email, dns_provider='cloudflare'):
        self.domain = domain
        self.email = email
        self.dns_provider = dns_provider
        self.cert_dir = f"/etc/letsencrypt/live/{domain}"
    
    def request_certificate(self, wildcard=True):
        '''Request new certificate via ACME'''
        domains = [f"-d {self.domain}"]
        if wildcard:
            domains.append(f"-d *.{self.domain}")
        
        cmd = [
            "certbot", "certonly",
            f"--dns-{self.dns_provider}",
            f"--dns-{self.dns_provider}-credentials",
            f"/etc/{self.dns_provider}.ini",
            "--email", self.email,
            "--agree-tos",
            "--non-interactive",
        ] + " ".join(domains).split()
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return {
            "success": result.returncode == 0,
            "output": result.stdout,
            "error": result.stderr,
        }
    
    def check_expiry(self):
        '''Check certificate expiry date'''
        cert_path = f"{self.cert_dir}/fullchain.pem"
        cmd = ["openssl", "x509", "-in", cert_path, "-noout", "-enddate"]
        result = subprocess.run(cmd, capture_output=True, text=True)
        
        # Parse date
        date_str = result.stdout.strip().split("=")[1]
        expiry = datetime.datetime.strptime(date_str, "%b %d %H:%M:%S %Y %Z")
        days_left = (expiry - datetime.datetime.utcnow()).days
        
        return {
            "domain": self.domain,
            "expiry": expiry.isoformat(),
            "days_left": days_left,
            "needs_renewal": days_left < 30,
        }
    
    def renew(self):
        '''Renew certificate'''
        result = subprocess.run(
            ["certbot", "renew", "--cert-name", self.domain],
            capture_output=True, text=True
        )
        return result.returncode == 0
    
    def list_certificates(self):
        '''List all managed certificates'''
        result = subprocess.run(
            ["certbot", "certificates"],
            capture_output=True, text=True
        )
        return result.stdout

manager = CertificateManager("home.example.com", "admin@example.com")
# manager.request_certificate(wildcard=True)
# print(manager.check_expiry())
"""

    def show_code(self):
        print("=== ACME Manager ===")
        print(self.CODE[:600])

    def cert_dashboard(self):
        print(f"\n=== Certificate Dashboard ===")
        certs = [
            {"domain": "home.example.com", "days": random.randint(60, 89), "wildcard": True},
            {"domain": "git.home.example.com", "days": random.randint(30, 89), "wildcard": False},
            {"domain": "media.home.example.com", "days": random.randint(10, 89), "wildcard": False},
        ]
        for c in certs:
            status = "OK" if c["days"] > 30 else "RENEW SOON" if c["days"] > 7 else "EXPIRED!"
            wc = " (wildcard)" if c["wildcard"] else ""
            print(f"  [{status:>11}] {c['domain']}{wc} — {c['days']} days left")

auto = ACMEAutomation()
auto.show_code()
auto.cert_dashboard()

FAQ - คำถามที่พบบ่อย

Q: Home Lab ใช้ Let's Encrypt ฟรีจริงไหม?

A: ฟรี 100% — Let's Encrypt ออก certificates ฟรีไม่จำกัดจำนวน ข้อจำกัด: certificate อายุ 90 วัน (ต้อง renew อัตโนมัติ), rate limits (50 certs/domain/week) สำหรับ Home Lab เพียงพอมาก — ใช้ wildcard cert ใบเดียวครอบทุก subdomain

Q: DNS-01 กับ HTTP-01 ใช้อันไหนดี?

A: Home Lab แนะนำ DNS-01: รองรับ wildcard certs (*.home.example.com), ไม่ต้องเปิด port 80, ทำงานกับ internal-only services HTTP-01: ง่ายกว่า แต่ต้องเปิด port 80, ไม่รองรับ wildcard ถ้าใช้ Cloudflare → DNS-01 ง่ายมาก (API token + auto verification)

Q: Traefik กับ Nginx Proxy Manager อันไหนดี?

A: Traefik: Docker-native, auto-discovery, config-as-code, เหมาะ advanced users Nginx Proxy Manager: GUI, ง่ายมาก, เหมาะมือใหม่, built-in Let's Encrypt มือใหม่: เริ่มจาก Nginx Proxy Manager (GUI ง่าย) → ย้ายไป Traefik เมื่อ comfortable

Q: IP เปลี่ยนบ่อย (dynamic IP) ทำอย่างไร?

A: ใช้ DDNS: DuckDNS (ฟรี), Cloudflare DDNS, No-IP ติดตั้ง DDNS client บน router หรือ server → อัพเดท IP อัตโนมัติ ทางเลือก: Cloudflare Tunnel — ไม่ต้อง expose IP เลย, ไม่ต้อง port forward, ปลอดภัยกว่า

📖 บทความที่เกี่ยวข้อง

IS-IS Protocol Home Lab Setupอ่านบทความ → Falco Runtime Security Home Lab Setupอ่านบทความ → React Suspense Home Lab Setupอ่านบทความ → Redis Cluster Home Lab Setupอ่านบทความ → DNS over TLS Home Lab Setupอ่านบทความ →

📚 ดูบทความทั้งหมด →