新服务器一键安装nginx+https自动续期脚本

0 阅读3分钟

1,先上图

sequenceDiagram
    participant U as 你
    participant S as 部署脚本
    participant A as acme.sh
    participant D as 阿里云DNS
    participant L as Let's Encrypt
    participant N as Nginx

    U->>S: 执行一键部署脚本
    S->>S: 安装 nginx / acme.sh / 基础依赖
    S->>A: 调用 acme.sh 申请 novamorning.com 和 *.novamorning.com
    A->>L: 发起证书签发请求
    L-->>A: 返回 DNS-01 验证要求

    A->>D: 用 Ali_Key / Ali_Secret 调用阿里云 DNS API
    D-->>A: 新增 _acme-challenge TXT 记录

    L->>D: 查询 _acme-challenge TXT
    D-->>L: 返回验证值

    alt 验证通过
        L-->>A: 证书签发成功
        A->>N: 安装证书并配置自动续期
        S->>N: 写入 Nginx 配置
        S->>N: reload nginx
    else 验证失败
        L-->>A: 签发失败
        A-->>S: 返回错误
    end

    Note over N: novamorning.com / www 静态前端
    Note over N: api/admin-api 反向代理后端
    Note over N: admin.novamorning.com 静态后台前端

2,序言

  • 这份脚本是基于阿里云 DNS API
  • 原理是:acme.sh 自动调用阿里云 DNS API,给 Let's Encrypt 做 DNS-01 验证
  • 非阿里云 DNS 不是不能用,而是要换成对应服务商的 DNS 插件和密钥变量
  • 一句话:证书申请工具是通用的,DNS 自动验证插件是按服务商区分的。

3,配置前的准备工作

3.1,配置 accessKeyId 和 accessKeySecret

  • 这块用主账号的accessKeyId 和 accessKeySecret也行,不过主账号只能有两个accessKey
  • 所以建议申请RAM用户,给它赋予 'AliyunDNSFullAccess' 权限策略。
sudo tee /root/.acme_ali_dns.env >/dev/null <<'EOF'
export Ali_Key='你的AccessKeyId'
export Ali_Secret='你的AccessKeySecret'
EOF
sudo chmod 600 /root/.acme_ali_dns.env

3.2 域名到IP的解析配置好

image.png

4,脚本内容

#!/usr/bin/env bash
set -Eeuo pipefail

# ========================
# 基础配置
# ========================
ROOT_DOMAIN="novamorning.com"
CERT_MAIN_DOMAIN="novamorning.com"
CERT_WILDCARD_DOMAIN="*.novamorning.com"

C_WEB_DOMAIN="novamorning.com"
C_WEB_DOMAIN_WWW="www.novamorning.com"
C_API_DOMAIN="api.novamorning.com"
ADMIN_WEB_DOMAIN="admin.novamorning.com"
ADMIN_API_DOMAIN="admin-api.novamorning.com"

# ===== 前后端目录与端口,按你的实际情况修改 =====
C_WEB_ROOT="/software/novamorning/front/novamorning-web-front/dist_prod"
ADMIN_WEB_ROOT="/software/novamorning/front/novamorning-management-front/dist_prod"
C_API_UPSTREAM="127.0.0.1:8080"
ADMIN_API_UPSTREAM="127.0.0.1:8081"

NGINX_CONF="/etc/nginx/conf.d/novamorning.conf"
ACME_HOME="/root/.acme.sh"
CERT_DIR="/etc/nginx/ssl/novamorning"
ALI_ENV_FILE="/root/.acme_ali_dns.env"
NGINX_LOG_DIR="/var/log/nginx"

# ========================
# 日志函数
# ========================
ts() { date '+%Y-%m-%d %H:%M:%S'; }
log() { echo "[$(ts)] $*"; }

trap 'log "ERROR: 第 ${LINENO} 行执行失败,退出码=$?"' ERR

require_root() {
  if [[ "${EUID}" -ne 0 ]]; then
    echo "请使用 root 或 sudo 执行"
    exit 1
  fi
}

check_ali_credentials() {
  if [[ ! -f "${ALI_ENV_FILE}" ]]; then
    cat <<EOF
未找到阿里云 DNS 凭据文件:${ALI_ENV_FILE}

请先创建:
cat > ${ALI_ENV_FILE} <<'EOT'
export Ali_Key='你的AccessKeyId'
export Ali_Secret='你的AccessKeySecret'
EOT

然后执行:
chmod 600 ${ALI_ENV_FILE}
EOF
    exit 1
  fi

  # shellcheck disable=SC1090
  source "${ALI_ENV_FILE}"

  if [[ -z "${Ali_Key:-}" || -z "${Ali_Secret:-}" ]]; then
    echo "Ali_Key 或 Ali_Secret 为空,请检查 ${ALI_ENV_FILE}"
    exit 1
  fi
}

check_paths() {
  if [[ ! -d "${C_WEB_ROOT}" ]]; then
    log "警告:C端前端目录不存在:${C_WEB_ROOT}"
  fi

  if [[ ! -d "${ADMIN_WEB_ROOT}" ]]; then
    log "警告:后台前端目录不存在:${ADMIN_WEB_ROOT}"
  fi
}

install_base() {
  log "安装基础依赖..."
  apt update
  apt install -y nginx curl socat cron openssl ca-certificates

  systemctl enable cron
  systemctl start cron

  systemctl enable nginx
  systemctl start nginx
}

setup_firewall() {
  if command -v ufw >/dev/null 2>&1; then
    log "开放 80/443 端口..."
    ufw allow 80/tcp || true
    ufw allow 443/tcp || true
  fi
}

install_acme_sh() {
  if [[ ! -d "${ACME_HOME}" ]]; then
    log "安装 acme.sh..."
    curl https://get.acme.sh | sh -s email=admin@${ROOT_DOMAIN}
  else
    log "acme.sh 已安装,执行升级..."
    "${ACME_HOME}/acme.sh" --upgrade || true
  fi

  "${ACME_HOME}/acme.sh" --set-default-ca --server letsencrypt
  "${ACME_HOME}/acme.sh" --upgrade --auto-upgrade
}

issue_cert() {
  log "签发/更新泛域名证书..."
  export Ali_Key
  export Ali_Secret

  "${ACME_HOME}/acme.sh" --issue \
    --dns dns_ali \
    -d "${CERT_MAIN_DOMAIN}" \
    -d "${CERT_WILDCARD_DOMAIN}" \
    --keylength ec-256 \
    --server letsencrypt
}

install_cert() {
  log "安装证书到 ${CERT_DIR} ..."
  mkdir -p "${CERT_DIR}"

  "${ACME_HOME}/acme.sh" --install-cert -d "${CERT_MAIN_DOMAIN}" --ecc \
    --key-file       "${CERT_DIR}/privkey.pem" \
    --fullchain-file "${CERT_DIR}/fullchain.pem" \
    --reloadcmd      "systemctl reload nginx"
}

write_nginx_conf() {
  log "写入 Nginx 配置..."
  mkdir -p "${NGINX_LOG_DIR}"

  cat > "${NGINX_CONF}" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${C_WEB_DOMAIN} ${C_WEB_DOMAIN_WWW} ${C_API_DOMAIN} ${ADMIN_WEB_DOMAIN} ${ADMIN_API_DOMAIN};

    location / {
        return 301 https://\$host\$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${C_WEB_DOMAIN} ${C_WEB_DOMAIN_WWW};

    ssl_certificate ${CERT_DIR}/fullchain.pem;
    ssl_certificate_key ${CERT_DIR}/privkey.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    client_max_body_size 50m;

    gzip on;
    gzip_min_length 1k;
    gzip_buffers 16 8k;
    gzip_comp_level 6;
    gzip_vary on;
    gzip_proxied any;
    gzip_http_version 1.1;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/xml+rss
        application/rss+xml
        image/svg+xml
        font/ttf
        font/otf
        application/vnd.ms-fontobject;

    root ${C_WEB_ROOT};
    index index.html index.htm;

    access_log ${NGINX_LOG_DIR}/novamorning-front.access.log;
    error_log  ${NGINX_LOG_DIR}/novamorning-front.error.log;

    location / {
        try_files \$uri \$uri/ /index.html;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${C_API_DOMAIN};

    ssl_certificate ${CERT_DIR}/fullchain.pem;
    ssl_certificate_key ${CERT_DIR}/privkey.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    client_max_body_size 100m;

    access_log ${NGINX_LOG_DIR}/novamorning-api.access.log;
    error_log  ${NGINX_LOG_DIR}/novamorning-api.error.log;

    location / {
        proxy_pass http://${C_API_UPSTREAM};
        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 "upgrade";
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${ADMIN_WEB_DOMAIN};

    ssl_certificate ${CERT_DIR}/fullchain.pem;
    ssl_certificate_key ${CERT_DIR}/privkey.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    client_max_body_size 50m;

    gzip on;
    gzip_min_length 1k;
    gzip_buffers 16 8k;
    gzip_comp_level 6;
    gzip_vary on;
    gzip_proxied any;
    gzip_http_version 1.1;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/xml+rss
        application/rss+xml
        image/svg+xml
        font/ttf
        font/otf
        application/vnd.ms-fontobject;

    root ${ADMIN_WEB_ROOT};
    index index.html index.htm;

    access_log ${NGINX_LOG_DIR}/novamorning-admin-front.access.log;
    error_log  ${NGINX_LOG_DIR}/novamorning-admin-front.error.log;

    location / {
        try_files \$uri \$uri/ /index.html;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${ADMIN_API_DOMAIN};

    ssl_certificate ${CERT_DIR}/fullchain.pem;
    ssl_certificate_key ${CERT_DIR}/privkey.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    client_max_body_size 100m;

    access_log ${NGINX_LOG_DIR}/novamorning-admin-api.access.log;
    error_log  ${NGINX_LOG_DIR}/novamorning-admin-api.error.log;

    location / {
        proxy_pass http://${ADMIN_API_UPSTREAM};
        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 "upgrade";
    }
}
EOF
}

check_nginx() {
  log "检查并重载 Nginx ..."
  nginx -t
  systemctl reload nginx
  systemctl enable nginx
}

check_renew() {
  log "执行 acme.sh 自动续期演练..."
  "${ACME_HOME}/acme.sh" --cron --home "${ACME_HOME}"
}

summary() {
  log "========================================"
  log "部署完成"
  log "主站:        https://${C_WEB_DOMAIN}"
  log "主站(www):   https://${C_WEB_DOMAIN_WWW}"
  log "前台 API:    https://${C_API_DOMAIN}"
  log "后台前端:    https://${ADMIN_WEB_DOMAIN}"
  log "后台 API:    https://${ADMIN_API_DOMAIN}"
  log "证书目录:    ${CERT_DIR}"
  log "Nginx配置:   ${NGINX_CONF}"
  log "阿里云凭据:  ${ALI_ENV_FILE}"
}

main() {
  require_root
  check_ali_credentials
  check_paths
  install_base
  setup_firewall
  install_acme_sh
  issue_cert
  install_cert
  write_nginx_conf
  check_nginx
  check_renew
  summary
}

main "$@"

最后执行脚本

chmod +x setup_novamorning_gateway.sh
sudo bash setup_novamorning_gateway.sh