diff --git a/modules/app_stack/ec2.tf b/modules/app_stack/ec2.tf index c8514ba..d2f126b 100644 --- a/modules/app_stack/ec2.tf +++ b/modules/app_stack/ec2.tf @@ -56,12 +56,17 @@ resource "aws_instance" "api_server" { } locals { - nginx_script_b64 = base64encode(templatefile("${path.module}/scripts/nginx_setup.sh.tftpl", { + nginx_install_script_b64 = base64encode(templatefile("${path.module}/scripts/nginx_install.sh.tftpl", { domain_name = var.domain_name email = var.cert_email conf_file_name = var.nginx_conf_name })) + nginx_conf_script_b64 = base64encode(templatefile("${path.module}/scripts/nginx_conf.sh.tftpl", { + domain_name = var.domain_name + conf_file_name = var.nginx_conf_name + })) + alloy_config = templatefile("${path.module}/../../config/side-infra/config.alloy.tftpl", { loki_ip = data.aws_instance.monitoring_server.private_ip }) @@ -75,8 +80,13 @@ locals { alloy_version = var.alloy_version })) - nginx_ssm_params = jsonencode({ - commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_script_b64} | base64 -d | sudo bash"] + nginx_install_ssm_params = jsonencode({ + commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_install_script_b64} | base64 -d | sudo bash"] + executionTimeout = ["3600"] + }) + + nginx_conf_ssm_params = jsonencode({ + commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_conf_script_b64} | base64 -d | sudo bash"] executionTimeout = ["3600"] }) @@ -86,12 +96,13 @@ locals { }) } -# [리소스 1] Nginx 설정 변경 감지 및 실행 -resource "null_resource" "update_nginx" { +# [리소스 1] Nginx 설치 및 인증서 확보 +# 설치가 이미 끝나 있으면 스크립트가 즉시 종료하므로 평상시에는 사실상 실행되지 않습니다. +resource "null_resource" "install_nginx" { depends_on = [aws_instance.api_server] triggers = { - script_hash = sha256(templatefile("${path.module}/scripts/nginx_setup.sh.tftpl", { + script_hash = sha256(templatefile("${path.module}/scripts/nginx_install.sh.tftpl", { domain_name = var.domain_name email = var.cert_email conf_file_name = var.nginx_conf_name @@ -103,10 +114,100 @@ resource "null_resource" "update_nginx" { command = <<-EOT set -euo pipefail INSTANCE_ID='${aws_instance.api_server.id}' + + # 인스턴스가 교체된 직후에는 SSM 에이전트가 아직 등록되지 않아 + # send-command 가 InvalidInstanceId 로 즉시 실패합니다. 등록될 때까지 기다립니다. + PING_STATUS="" + SSM_ATTEMPTS=0 + while [ "$SSM_ATTEMPTS" -lt 60 ]; do + PING_STATUS=$(aws ssm describe-instance-information \ + --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ + --query "InstanceInformationList[0].PingStatus" \ + --output text 2>/dev/null || echo "None") + if [ "$PING_STATUS" = "Online" ]; then + break + fi + SSM_ATTEMPTS=$((SSM_ATTEMPTS + 1)) + sleep 10 + done + if [ "$PING_STATUS" != "Online" ]; then + echo "SSM agent not registered within 600s (last status: $PING_STATUS)" >&2 + exit 1 + fi + + COMMAND_ID=$(aws ssm send-command \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters '${local.nginx_install_ssm_params}' \ + --output text \ + --query "Command.CommandId") + ATTEMPTS=0 + while [ "$ATTEMPTS" -lt 360 ]; do + STATUS=$(aws ssm get-command-invocation \ + --command-id "$COMMAND_ID" \ + --instance-id "$INSTANCE_ID" \ + --query "Status" --output text 2>/dev/null || echo "Pending") + case "$STATUS" in + Success) exit 0 ;; + Failed|Cancelled|TimedOut|Undeliverable) + echo "SSM command $STATUS" >&2 + aws ssm get-command-invocation \ + --command-id "$COMMAND_ID" \ + --instance-id "$INSTANCE_ID" \ + --query "StandardErrorContent" --output text >&2 + exit 1 ;; + esac + ATTEMPTS=$((ATTEMPTS + 1)) + sleep 10 + done + echo "SSM command timed out after 3600s" >&2 + exit 1 + EOT + } +} + +# [리소스 2] Nginx 설정 변경 감지 및 실행 +# 설정 템플릿이 바뀌면 이 리소스만 재생성되어, 설치 작업 없이 conf 재작성과 reload 만 수행합니다. +resource "null_resource" "update_nginx_conf" { + depends_on = [null_resource.install_nginx] + + triggers = { + script_hash = sha256(templatefile("${path.module}/scripts/nginx_conf.sh.tftpl", { + domain_name = var.domain_name + conf_file_name = var.nginx_conf_name + })) + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + INSTANCE_ID='${aws_instance.api_server.id}' + + # 인스턴스가 교체된 직후에는 SSM 에이전트가 아직 등록되지 않아 + # send-command 가 InvalidInstanceId 로 즉시 실패합니다. 등록될 때까지 기다립니다. + PING_STATUS="" + SSM_ATTEMPTS=0 + while [ "$SSM_ATTEMPTS" -lt 60 ]; do + PING_STATUS=$(aws ssm describe-instance-information \ + --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ + --query "InstanceInformationList[0].PingStatus" \ + --output text 2>/dev/null || echo "None") + if [ "$PING_STATUS" = "Online" ]; then + break + fi + SSM_ATTEMPTS=$((SSM_ATTEMPTS + 1)) + sleep 10 + done + if [ "$PING_STATUS" != "Online" ]; then + echo "SSM agent not registered within 600s (last status: $PING_STATUS)" >&2 + exit 1 + fi + COMMAND_ID=$(aws ssm send-command \ --instance-ids "$INSTANCE_ID" \ --document-name "AWS-RunShellScript" \ - --parameters '${local.nginx_ssm_params}' \ + --parameters '${local.nginx_conf_ssm_params}' \ --output text \ --query "Command.CommandId") ATTEMPTS=0 @@ -134,7 +235,7 @@ resource "null_resource" "update_nginx" { } } -# [리소스 2] Side Infra 설정 변경 감지 및 실행 +# [리소스 3] Side Infra 설정 변경 감지 및 실행 resource "null_resource" "update_side_infra" { depends_on = [aws_instance.api_server] diff --git a/modules/app_stack/scripts/nginx_conf.sh.tftpl b/modules/app_stack/scripts/nginx_conf.sh.tftpl new file mode 100644 index 0000000..f624103 --- /dev/null +++ b/modules/app_stack/scripts/nginx_conf.sh.tftpl @@ -0,0 +1,155 @@ +#!/bin/bash + +set -e + +# --- variables setting --- +DOMAIN="${domain_name}" +CONF_NAME="${conf_file_name}" +CONF_PATH="/etc/nginx/sites-available/$CONF_NAME" +UPSTREAM_CONF="/etc/nginx/conf.d/upstream.conf" +CERTBOT_WEBROOT="/var/www/certbot" +CERT_FULLCHAIN="/etc/letsencrypt/live/$DOMAIN/fullchain.pem" +CERT_PRIVKEY="/etc/letsencrypt/live/$DOMAIN/privkey.pem" + +echo "Start Nginx Config Update for $DOMAIN with config file: $CONF_NAME" + +# 인증서 확보는 nginx_install.sh 의 책임이다. +# 인증서가 없으면 443 블록 때문에 nginx -t 가 실패하므로 설정을 쓰기 전에 중단한다. +if [ ! -f "$CERT_FULLCHAIN" ] || [ ! -f "$CERT_PRIVKEY" ]; then + echo "Certificate not found for $DOMAIN. Run the nginx install script first." >&2 + exit 1 +fi + +write_service_conf() { + cat < $CONF_PATH +map \$http_upgrade \$connection_upgrade { + default upgrade; + '' ''; +} + +# 1차 차단: 도메인과 일치하지 않는 요청 차단 (IP 직접 접근, 알 수 없는 Host 헤더) +# 응답 없이 연결을 즉시 종료하여 봇이 서버 존재를 인식하지 못하게 함 +server { + listen 80 default_server; + server_name _; + return 444; +} + +server { + listen 443 ssl default_server; + server_name _; + + ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; + + return 444; +} + +server { + listen 80; + server_name $DOMAIN; + + location ^~ /.well-known/acme-challenge/ { + root $CERTBOT_WEBROOT; + default_type "text/plain"; + try_files \$uri =404; + } + + location / { + return 301 https://\$host\$request_uri; + } +} + +server { + listen 443 ssl; + server_name $DOMAIN; + + ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; + + client_max_body_size 10M; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_stapling on; + ssl_stapling_verify on; + + # 2차 차단: 취약점 탐색용 정적 파일 확장자 요청 차단 + # (\$|[/?]) 로 확장자 뒤에 /path 또는 ?query 가 붙는 우회 패턴도 차단 + location ~* \.(php|asp|aspx|jsp|cgi|sql|bak|backup|config|ini|log|sh|xml|txt|html|htm)(\$|[/?]) { + return 444; + } + + # .env, .env.production, .git 등 dotfile 변형 차단 + location ~* (^|/)\.(env|git) { + return 444; + } + + # 3차 차단: 내부 전용 API 는 VPC 내부에서 app 포트로 직접 호출하므로 외부 노출을 막는다 + # DB EC2 의 백업 실패 알림은 8080/9080 직접 경로를 쓰므로 이 차단에 영향받지 않는다 + location ^~ /internal { + return 444; + } + + location / { + proxy_pass http://app_backend; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \$connection_upgrade; + } +} +EOF +} + +# Create upstream config file only on first provisioning (initial active slot: blue on port 8080) +# Blue-Green 배포 시 이 파일만 교체하고 nginx -s reload 로 트래픽 전환 +# 이미 존재하면 덮어쓰지 않음 — 재적용 시 현재 active 슬롯 유지 +if [ ! -f "$UPSTREAM_CONF" ]; then + cat < $UPSTREAM_CONF +upstream app_backend { + server 127.0.0.1:8080; +} +UPSTREAM_EOF +fi + +# 적용에 실패하면 되돌릴 수 있도록 기존 설정을 백업해 둔다 +BACKUP_PATH="" +if [ -f "$CONF_PATH" ]; then + BACKUP_PATH="$CONF_PATH.bak" + cp "$CONF_PATH" "$BACKUP_PATH" +fi + +write_service_conf + +ln -sf $CONF_PATH /etc/nginx/sites-enabled/$CONF_NAME +rm -f /etc/nginx/sites-enabled/default + +# 문법 검증에 실패하면 백업으로 되돌린다. +# 검증 전에 파일을 이미 덮어썼기 때문에, 되돌리지 않으면 다음 reload 때 깨진 설정이 반영된다. +if ! nginx -t; then + echo "Nginx config test failed. Rolling back." >&2 + if [ -n "$BACKUP_PATH" ]; then + mv "$BACKUP_PATH" "$CONF_PATH" + else + # 되돌릴 기존 설정이 없는 첫 적용이라면, 방금 만든 설정과 링크를 제거한다. + # 그대로 두면 default 링크까지 지워진 상태라 다음 restart 때 nginx 가 기동하지 못한다. + rm -f /etc/nginx/sites-enabled/$CONF_NAME "$CONF_PATH" + fi + nginx -t || echo "Rollback verification also failed." >&2 + exit 1 +fi + +systemctl reload nginx || systemctl restart nginx + +if [ -n "$BACKUP_PATH" ]; then + rm -f "$BACKUP_PATH" +fi + +echo "Nginx config update complete!" diff --git a/modules/app_stack/scripts/nginx_install.sh.tftpl b/modules/app_stack/scripts/nginx_install.sh.tftpl new file mode 100644 index 0000000..6e3d51e --- /dev/null +++ b/modules/app_stack/scripts/nginx_install.sh.tftpl @@ -0,0 +1,109 @@ +#!/bin/bash + +set -e + +# --- variables setting --- +DOMAIN="${domain_name}" +EMAIL="${email}" +CONF_NAME="${conf_file_name}" +CRON_NAME=$(printf '%s' "$CONF_NAME" | tr -c 'A-Za-z0-9_-' '-') +CERTBOT_WEBROOT="/var/www/certbot" +CERT_FULLCHAIN="/etc/letsencrypt/live/$DOMAIN/fullchain.pem" +CERT_PRIVKEY="/etc/letsencrypt/live/$DOMAIN/privkey.pem" + +if [ -z "$CRON_NAME" ]; then + CRON_NAME="nginx" +fi + +CRON_PATH="/etc/cron.d/certbot-$CRON_NAME" + +echo "Start Nginx Install for $DOMAIN" + +# 설치 확인: 아래가 모두 갖춰져 있으면 새로 설치할 것이 없으므로 즉시 종료한다. +# 설정 파일 갱신은 nginx_conf.sh 가 담당하므로, 이 스크립트는 평상시 실행되지 않는다. +# 덕분에 conf 한 줄을 고칠 때 apt/pip/certbot 같은 외부 네트워크 작업이 돌지 않는다. +if command -v nginx >/dev/null 2>&1 \ + && [ -x /usr/bin/certbot ] \ + && [ -f "$CERT_FULLCHAIN" ] \ + && [ -f "$CERT_PRIVKEY" ] \ + && [ -f "$CRON_PATH" ]; then + echo "Nginx installation already complete. Skipping." + exit 0 +fi + +reload_nginx() { + nginx -t + systemctl reload nginx || systemctl restart nginx +} + +# ACME HTTP-01 챌린지 전용 임시 설정. +# 인증서가 없는 동안에는 443 블록을 쓸 수 없으므로(ssl_certificate 경로가 없어 nginx -t 실패) +# 발급 전까지 80 포트만 열어둔다. 발급이 끝나면 nginx_conf.sh 가 서비스 설정으로 덮어쓴다. +write_http_challenge_conf() { + cat < /etc/nginx/sites-available/$CONF_NAME +# 1차 차단: 도메인과 일치하지 않는 요청 차단 (IP 직접 접근, 알 수 없는 Host 헤더) +# 응답 없이 연결을 즉시 종료하여 봇이 서버 존재를 인식하지 못하게 함 +server { + listen 80 default_server; + server_name _; + return 444; +} + +server { + listen 80; + server_name $DOMAIN; + + location ^~ /.well-known/acme-challenge/ { + root $CERTBOT_WEBROOT; + default_type "text/plain"; + try_files \$uri =404; + } + + location / { + return 301 https://\$host\$request_uri; + } +} +EOF +} + +# 1. Install necessary packages for Nginx and Certbot +apt-get update +apt-get install -y nginx python3 python3-venv libaugeas0 + +# 2. Install Certbot (using pip) +python3 -m venv /opt/certbot/ +/opt/certbot/bin/pip install --upgrade pip +/opt/certbot/bin/pip install certbot certbot-nginx +ln -sf /opt/certbot/bin/certbot /usr/bin/certbot + +# 3. Prepare Nginx webroot for HTTP-01 challenge +mkdir -p "$CERTBOT_WEBROOT" + +# 4. 인증서가 아직 없으면 챌린지 설정으로 80 포트를 열어 발급을 준비한다 +if [ ! -f "$CERT_FULLCHAIN" ] || [ ! -f "$CERT_PRIVKEY" ]; then + write_http_challenge_conf + ln -sf /etc/nginx/sites-available/$CONF_NAME /etc/nginx/sites-enabled/$CONF_NAME + rm -f /etc/nginx/sites-enabled/default + reload_nginx +fi + +# 5. Issue or renew SSL certificate (Non-interactive mode) +certbot certonly --webroot \ + --webroot-path "$CERTBOT_WEBROOT" \ + --non-interactive \ + --agree-tos \ + --keep-until-expiring \ + --email "$EMAIL" \ + -d "$DOMAIN" + +echo "Certificate is ready." + +# 6. Register auto-renewal cron job +cat < $CRON_PATH +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +0 0,12 * * * root /opt/certbot/bin/python -c 'import random,time; time.sleep(random.random() * 3600)' && /usr/bin/certbot renew --webroot --webroot-path $CERTBOT_WEBROOT --quiet --deploy-hook "/usr/bin/systemctl reload nginx" +EOF + +echo "Nginx install complete!" diff --git a/modules/app_stack/scripts/nginx_setup.sh.tftpl b/modules/app_stack/scripts/nginx_setup.sh.tftpl deleted file mode 100644 index 6bd8808..0000000 --- a/modules/app_stack/scripts/nginx_setup.sh.tftpl +++ /dev/null @@ -1,202 +0,0 @@ -#!/bin/bash - -set -e - -# --- variables setting --- -DOMAIN="${domain_name}" -EMAIL="${email}" -CONF_NAME="${conf_file_name}" -CRON_NAME=$(printf '%s' "$CONF_NAME" | tr -c 'A-Za-z0-9_-' '-') -UPSTREAM_CONF="/etc/nginx/conf.d/upstream.conf" -CERTBOT_WEBROOT="/var/www/certbot" -CERT_FULLCHAIN="/etc/letsencrypt/live/$DOMAIN/fullchain.pem" -CERT_PRIVKEY="/etc/letsencrypt/live/$DOMAIN/privkey.pem" - -echo "Start Nginx Setup for $DOMAIN with config file: $CONF_NAME" - -if [ -z "$CRON_NAME" ]; then - CRON_NAME="nginx" -fi - -reload_nginx() { - nginx -t - systemctl reload nginx || systemctl restart nginx -} - -write_http_challenge_conf() { - cat < /etc/nginx/sites-available/$CONF_NAME -# 1차 차단: 도메인과 일치하지 않는 요청 차단 (IP 직접 접근, 알 수 없는 Host 헤더) -# 응답 없이 연결을 즉시 종료하여 봇이 서버 존재를 인식하지 못하게 함 -server { - listen 80 default_server; - server_name _; - return 444; -} - -server { - listen 80; - server_name $DOMAIN; - - location ^~ /.well-known/acme-challenge/ { - root $CERTBOT_WEBROOT; - default_type "text/plain"; - try_files \$uri =404; - } - - location / { - return 301 https://\$host\$request_uri; - } -} -EOF -} - -write_full_nginx_conf() { - cat < /etc/nginx/sites-available/$CONF_NAME -map \$http_upgrade \$connection_upgrade { - default upgrade; - '' ''; -} - -# 1차 차단: 도메인과 일치하지 않는 요청 차단 (IP 직접 접근, 알 수 없는 Host 헤더) -# 응답 없이 연결을 즉시 종료하여 봇이 서버 존재를 인식하지 못하게 함 -server { - listen 80 default_server; - server_name _; - return 444; -} - -server { - listen 443 ssl default_server; - server_name _; - - ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; - - return 444; -} - -server { - listen 80; - server_name $DOMAIN; - - location ^~ /.well-known/acme-challenge/ { - root $CERTBOT_WEBROOT; - default_type "text/plain"; - try_files \$uri =404; - } - - location / { - return 301 https://\$host\$request_uri; - } -} - -server { - listen 443 ssl; - server_name $DOMAIN; - - ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; - - client_max_body_size 10M; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - ssl_stapling on; - ssl_stapling_verify on; - - # 2차 차단: 취약점 탐색용 정적 파일 확장자 요청 차단 - # ($|[/?]) 로 확장자 뒤에 /path 또는 ?query 가 붙는 우회 패턴도 차단 - location ~* \.(php|asp|aspx|jsp|cgi|sql|bak|backup|config|ini|log|sh|xml|txt|html|htm)($|[/?]) { - return 444; - } - - # .env, .env.production, .git 등 dotfile 변형 차단 - location ~* (^|/)\.(env|git) { - return 444; - } - - location / { - proxy_pass http://app_backend; - proxy_http_version 1.1; - proxy_set_header Host \$host; - proxy_set_header X-Real-IP \$remote_addr; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto \$scheme; - proxy_set_header Upgrade \$http_upgrade; - proxy_set_header Connection \$connection_upgrade; - } -} -EOF -} - -# 1. Install necessary packages for Nginx and Certbot -apt-get update -apt-get install -y nginx python3 python3-venv libaugeas0 - -# 2. Install Certbot (using pip) -python3 -m venv /opt/certbot/ -/opt/certbot/bin/pip install --upgrade pip -/opt/certbot/bin/pip install certbot certbot-nginx -ln -sf /opt/certbot/bin/certbot /usr/bin/certbot - -# 3. Prepare Nginx webroot for HTTP-01 challenge -mkdir -p "$CERTBOT_WEBROOT" - -# Create upstream config file only on first provisioning (initial active slot: blue on port 8080) -# Blue-Green 배포 시 이 파일만 교체하고 nginx -s reload 로 트래픽 전환 -# 이미 존재하면 덮어쓰지 않음 — 재프로비저닝 시 현재 active 슬롯 유지 -if [ ! -f "$UPSTREAM_CONF" ]; then - cat < $UPSTREAM_CONF -upstream app_backend { - server 127.0.0.1:8080; -} -UPSTREAM_EOF -fi - -CERT_EXISTS=false -if [ -f "$CERT_FULLCHAIN" ] && [ -f "$CERT_PRIVKEY" ]; then - CERT_EXISTS=true - write_full_nginx_conf -else - write_http_challenge_conf -fi - -ln -sf /etc/nginx/sites-available/$CONF_NAME /etc/nginx/sites-enabled/$CONF_NAME -rm -f /etc/nginx/sites-enabled/default -reload_nginx - -# 4. Issue or renew SSL certificate (Non-interactive mode) -certbot certonly --webroot \ - --webroot-path "$CERTBOT_WEBROOT" \ - --non-interactive \ - --agree-tos \ - --keep-until-expiring \ - --email "$EMAIL" \ - -d "$DOMAIN" - -echo "Certificate obtained successfully." - -# 5. Create Nginx configuration file -if [ "$CERT_EXISTS" != "true" ]; then - write_full_nginx_conf - - # 6. Create symbolic link and remove default configuration - ln -sf /etc/nginx/sites-available/$CONF_NAME /etc/nginx/sites-enabled/$CONF_NAME - rm -f /etc/nginx/sites-enabled/default -fi - -# 7. Register auto-renewal cron job -cat < /etc/cron.d/certbot-$CRON_NAME -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - -0 0,12 * * * root /opt/certbot/bin/python -c 'import random,time; time.sleep(random.random() * 3600)' && /usr/bin/certbot renew --webroot --webroot-path $CERTBOT_WEBROOT --quiet --deploy-hook "/usr/bin/systemctl reload nginx" -EOF - -# 8. Nginx restart -reload_nginx - -echo "Nginx setup complete!"