WIUNIX ACADEMY
آموزش HAProxy و Let’s Encrypt با تمدید امن گواهی
صفحهٔ 3 از 4؛ مراحل قبلی و پیشنیازها را پیش از ادامه مرور کن.
آموزش متناسب با محیط تو
کجا اجرا میکنی؟
سیستمعامل و نسخه را انتخاب کن تا مراحل، دستورها و راهحلها برای همان محیط نمایش داده شوند.
آموزش متناسب با مقادیر تو
تنظیمات این آموزش
مقادیر نمونه را برای محیط خودت تنظیم کن؛ متن و دستورهای مرتبط با هم تغییر میکنند. رمز، کلید خصوصی و توکن وارد نکن. مقادیر فقط در همین زبانه نگهداری میشوند و به سرور یا لینک اشتراکگذاری فرستاده نمیشوند. پیش از اجرا، DNS، گواهیها و سیاست شبکهٔ واقعی را بررسی کن.
آیپی، دامنه یا پورت مثالها با محیط تو فرق دارد؟ دکمهٔ زیر را باز کن، مقادیر را تغییر بده و «اعمال در آموزش» را بزن؛ سپس دستورهای بهروزشده را کپی کن.
تنظیم مقادیر برای محیط من7 مقدار قابلتغییر
قدم بعدی
اعتبارسنجی، جایگذاری و reload
نسخهٔ پشتیبان اولیه همان bootstrap فقط HTTP است. اگر اعتبارسنجی کاندیدا شکست خورد، به فایل فعال دست نزنید. جایگذاری با rename در همان فایلسیستم انجام میشود. شکست reload نیازمند بازگرداندن صریح bootstrap است؛ فرمان آن در بخش بازیابی آمده. موفقیت reload جای آزمون مستقل TLS را نمیگیرد.
Bash روی VM اختصاصی؛ مورد بیرونی در متن مشخص است
set -eu
sudo haproxy -c -f '/srv/academy-haproxy-tls/haproxy.tls.cfg'
sudo cp -a '/srv/academy-haproxy-tls/haproxy.cfg' '/srv/academy-haproxy-tls/haproxy.bootstrap.cfg'
sudo install -m 0644 '/srv/academy-haproxy-tls/haproxy.tls.cfg' '/srv/academy-haproxy-tls/haproxy.cfg.next'
sudo mv '/srv/academy-haproxy-tls/haproxy.cfg.next' '/srv/academy-haproxy-tls/haproxy.cfg'
sudo systemctl reload academy-haproxy.service
sudo systemctl --no-pager status academy-haproxy.service
پیکربندی معتبر است، master بهآرامی reload شده و سرویس اختصاصی فعال میماند.
قدم بعدی
اعتماد TLS و انتخاب بکاند را مستقل بررسی کنید
فرمان اول را بیرون فرانتاند و بدون resolve یا k اجرا کنید؛ اعتبارسنجی معمول CA باید برای app.example.com موفق شود. probe محلی همان نام را روی listener مشخص بررسی میکند. هدر جعلی را فقط با لاگ یا endpoint خصوصی کنترلشدهٔ اپلیکیشن بسنجید: Forwarded نباید باقی بماند، X-Forwarded-For باید IP اتصال مستقیم و scheme برابر https باشد. endpoint دیباگ را عمومی نکنید.
در بازهٔ نگهداری، یکی از بکاندها را از readiness خارج کنید؛ پس از دو check ناموفق، درخواستهای تکراری باید به نود سالم برسند. سپس آمادگی را برگردانید و دو check موفق را ببینید. یک پاسخ ۲۰۰ بهتنهایی شاهد failover نیست؛ نشانگر پاسخ و وضعیت نشست را هم بررسی کنید.
Bash روی VM اختصاصی؛ مورد بیرونی در متن مشخص است
set -eu
curl --fail --show-error 'https://app.example.com/'
curl --fail --show-error --resolve 'app.example.com:443:192.0.2.10' \
-H 'Forwarded: for=198.51.100.99;proto=http' \
-H 'X-Forwarded-For: 198.51.100.99' 'https://app.example.com/'
openssl s_client -connect '192.0.2.10:443' -servername 'app.example.com' \
-verify_hostname 'app.example.com' -verify_return_error -CApath /etc/ssl/certs </dev/null
CA و نام دامنه بدون دورزدن معتبرند؛ شاهد خصوصی بکاند، هدر بازنویسیشده و مسیر نود سالم را نشان میدهد.
قدم بعدی
deploy hook تمدید را با دسترسی root بسازید
فایل Python بعدی فقط deploy hook است و صدور انجام نمیدهد. lineage صریح، قفل همزمانی، زنجیره و کلید و کاندیدای پیکربندی را بررسی میکند؛ سپس PEM قبلی را نگه میدارد و اتمیک تعویض میکند. خطای صدور یا اعتبارسنجی گواهی سالم را جایگزین نمیکند. شکست reload موجب بازگرداندن PEM قبلی و تلاش مجدد معتبر میشود، اما دستور همچنان با خطا خارج میشود. شکست خود بازیابی نیز باید به اپراتور برسد؛ این منطق تضمین بازیابی در هر خرابی نیست.
Bash روی VM اختصاصی؛ مورد بیرونی در متن مشخص است
set -eu
test ! -e '/srv/academy-haproxy-tls/deploy.py'
test ! -L '/srv/academy-haproxy-tls/deploy.py'
sudo install -m 0700 /dev/null '/srv/academy-haproxy-tls/deploy.py'
sudo nano '/srv/academy-haproxy-tls/deploy.py'
فایل hook فقط برای root قابل ویرایش و اجراست؛ محتوای کامل در پنل بعد است.
قدم بعدی
جایگذاری اتمیک پس از اعتبارسنجی
فایل کامل است. خطا در journal سرویس اختصاصی ثبت میشود؛ کلید را لاگ نکنید. ممکن است Certbot lineage را تمدید کرده باشد ولی hook شکست بخورد؛ پس از رفع علت، خود hook را با lineage درست دوباره اجرا کنید، نه اینکه صدور اجباری تازه سفارش دهید.
فایل روی VM اختصاصی
#!/usr/bin/python3
"""Root-only deploy hook; never print private material."""
import fcntl
import os
from pathlib import Path
import subprocess
import tempfile
ROOT = Path("/srv/academy-haproxy-tls")
DOMAIN = "app.example.com"
LINEAGE = ROOT / "acme/config/live" / DOMAIN
PEM = ROOT / "certs" / (DOMAIN + ".pem")
CONFIG = ROOT / "haproxy.cfg"
CAFILE = "/etc/ssl/certs/ca-certificates.crt"
def run(*args):
return subprocess.run(args, check=True, capture_output=True).stdout
def main():
if os.geteuid() != 0:
raise SystemExit("root required")
if Path(os.environ.get("RENEWED_LINEAGE", "")).resolve() != LINEAGE.resolve():
raise SystemExit("unexpected lineage; no changes")
os.umask(0o077)
with (ROOT / "deploy.lock").open("a") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
cert, chain, key = [LINEAGE / n for n in ("cert.pem", "chain.pem", "privkey.pem")]
run("openssl", "x509", "-in", str(cert), "-checkend", "86400", "-noout")
run("openssl", "verify", "-CAfile", CAFILE, "-untrusted", str(chain),
"-purpose", "sslserver", "-verify_hostname", DOMAIN, str(cert))
public = run("openssl", "x509", "-in", str(cert), "-pubkey", "-noout")
if public != run("openssl", "pkey", "-in", str(key), "-pubout"):
raise SystemExit("certificate/key mismatch; no changes")
if not PEM.is_file() or PEM.is_symlink():
raise SystemExit("initial reviewed PEM must already exist")
old = PEM.read_bytes()
candidate = None
changed = False
try:
fd, name = tempfile.mkstemp(prefix=".candidate-", suffix=".pem", dir=PEM.parent)
candidate = Path(name)
with os.fdopen(fd, "wb") as output:
output.write((LINEAGE / "fullchain.pem").read_bytes() + b"\n" + key.read_bytes())
output.flush()
os.fsync(output.fileno())
# Replace only the one exact crt path in a private validation config.
config = CONFIG.read_text()
if config.count("crt " + str(PEM)) != 1:
raise SystemExit("unexpected crt binding; no changes")
with tempfile.TemporaryDirectory(prefix="validate-", dir=ROOT) as work:
check = Path(work) / "haproxy.cfg"
check.write_text(config.replace("crt " + str(PEM), "crt " + str(candidate)))
run("/usr/sbin/haproxy", "-c", "-f", str(check))
backup = PEM.with_suffix(".previous.pem")
fd, name = tempfile.mkstemp(prefix=".backup-", dir=PEM.parent)
with os.fdopen(fd, "wb") as output:
output.write(old)
output.flush()
os.fsync(output.fileno())
os.replace(name, backup)
os.replace(candidate, PEM)
changed = True
run("/usr/sbin/haproxy", "-c", "-f", str(CONFIG))
run("/usr/bin/systemctl", "reload", "academy-haproxy.service")
except BaseException:
if changed:
fd, name = tempfile.mkstemp(prefix=".rollback-", dir=PEM.parent)
with os.fdopen(fd, "wb") as output:
output.write(old)
output.flush()
os.fsync(output.fileno())
os.replace(name, PEM)
run("/usr/sbin/haproxy", "-c", "-f", str(CONFIG))
run("/usr/bin/systemctl", "reload", "academy-haproxy.service")
raise
finally:
if candidate is not None:
candidate.unlink(missing_ok=True)
print("validated PEM deployed; verify the public certificate separately")
if __name__ == "__main__":
main()
فایل کامپایل میشود، فقط lineage اصلی اختصاصی را میپذیرد و PEM قبلی را محفوظ نگه میدارد.
قدم بعدی
deploy را بسنجید و رفتار dry-run را جدا بدانید
ابتدا همان گواهی معتبر فعلی را با hook دوباره deploy کنید؛ اعتبارسنجی و reload واقعی بدون سفارش گواهی جدید انجام میشود. سپس dry-run تمدید را اجرا کنید. dry-run معمولاً hook را اجرا نمیکند؛ run-deploy-hooks آن را با گواهی فعال اصلی اجرا میکند، نه گواهی نامعتبر staging. این شبیهسازی تمدید واقعی محسوب نمیشود. پس از آن دوباره TLS عمومی را بسنجید.
Bash روی VM اختصاصی؛ مورد بیرونی در متن مشخص است
set -eu
sudo python3 -m py_compile '/srv/academy-haproxy-tls/deploy.py'
sudo env RENEWED_LINEAGE='/srv/academy-haproxy-tls/acme/config/live/app.example.com' '/srv/academy-haproxy-tls/deploy.py'
sudo certbot renew --dry-run --run-deploy-hooks --cert-name 'app.example.com' \
--config-dir '/srv/academy-haproxy-tls/acme/config' --work-dir '/srv/academy-haproxy-tls/acme/work' \
--logs-dir '/srv/academy-haproxy-tls/acme/logs' --deploy-hook '/srv/academy-haproxy-tls/deploy.py'
curl --fail --show-error 'https://app.example.com/'
گواهی معتبر فعلی reload میشود؛ dry-run موفق hook را صریح اجرا میکند و گواهی عمومی معتبر میماند.
قدم بعدی
یک زمانبندی اختصاصی تمدید بسازید
برای این lineage یک زمانبندی کافی است. timer توزیع فقط روی همین VM خالی غیرفعال شد؛ روی میزبان مشترک timer گواهیهای دیگر را متوقف نکنید. سرویس اختصاصی با root و مسیرهای config، work و log خودش اجرا میشود و نیازی به بازکردن مجوز کلید ندارد.
Bash روی VM اختصاصی؛ مورد بیرونی در متن مشخص است
set -eu
test ! -e /etc/systemd/system/academy-certbot.service
test ! -L /etc/systemd/system/academy-certbot.service
sudo install -m 0644 /dev/null /etc/systemd/system/academy-certbot.service
test ! -e /etc/systemd/system/academy-certbot.timer
test ! -L /etc/systemd/system/academy-certbot.timer
sudo install -m 0644 /dev/null /etc/systemd/system/academy-certbot.timer
sudo nano /etc/systemd/system/academy-certbot.service /etc/systemd/system/academy-certbot.timer
دو فایل تازه آمادهاند؛ فرمان و زمانبندی دقیق در دو پنل بعد میآید.
قدم بعدی
سرویس اختصاصی Certbot
این unit را کامل ذخیره کنید.
فایل روی VM اختصاصی
[Unit]
Description=Renew the Academy HAProxy certificate
After=network-online.target academy-acme-webroot.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --config-dir /srv/academy-haproxy-tls/acme/config --work-dir /srv/academy-haproxy-tls/acme/work --logs-dir /srv/academy-haproxy-tls/acme/logs --cert-name app.example.com --deploy-hook /srv/academy-haproxy-tls/deploy.py --quiet
unit فقط lineage اختصاصی همین آموزش را هدف میگیرد.
قدم بعدی
timer دوبار در روز با تأخیر تصادفی
این unit را کامل ذخیره کنید.
فایل روی VM اختصاصی
[Unit]
Description=Check the Academy certificate twice daily
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
unit فقط lineage اختصاصی همین آموزش را هدف میگیرد.