SSL证书配置踩坑记一次搞定HTTPS部署难题

诸葛雨婷 工具 阅读 1,478
赞 32 收藏
二维码
手机扫码查看
反馈

线上HTTPS部署踩坑记

昨天帮客户部署一个新项目到生产环境,本以为很简单的事情,结果SSL证书这里折腾了我整整一天。本来想用Let’s Encrypt免费证书,结果各种问题层出不穷。

SSL证书配置踩坑记一次搞定HTTPS部署难题

一开始我按照平时的做法,直接用acme.sh申请证书,命令都挺熟悉了:

curl https://get.acme.sh | sh
~/.acme.sh/acme.sh --issue -d example.com --nginx

结果这里就踩第一个坑了。客户服务器的nginx配置比较特殊,用了多个location块,acme.sh自动检测的时候找不到正确的webroot路径,一直报错。折腾了半天发现原来是要手动指定webroot目录。

后来试了下手动DNS验证的方式:

~/.acme.sh/acme.sh --issue --dns -d example.com --yes-I-know-dns-manual-mode-enough-go-ahead-please

执行完命令会返回一个TXT记录值,需要去域名管理后台添加。这里我犯了个低级错误,复制粘贴的时候把前缀的下划线给漏掉了,导致验证失败。DNS记录更新一般需要几分钟生效,等了大概10分钟才开始验证。

证书安装过程的意外

证书申请下来之后,配置nginx的时候又遇到了问题。客户的服务器nginx版本比较老,不支持http2,配置文件里加上ssl_protocols那几行就会报语法错误。折腾半天才发现原来是nginx编译的时候没加–with-http_v2_module模块。

最后我的nginx配置是这样的:

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;
    
    ssl_certificate /path/to/certificate.crt;
    ssl_certificate_key /path/to/private.key;
    
    # SSL安全配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # HSTS头设置
    add_header Strict-Transport-Security "max-age=31536000" always;
    
    location / {
        proxy_pass http://backend;
        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;
    }
}

这里有几个需要注意的地方,SSL证书路径一定要确保权限正确,私钥文件应该只有root用户能读取。另外HSTS头设置是个好习惯,但要注意max-age设得太大会影响调试。

中间件配置的坑

部署完之后测试发现一个问题,某些接口返回的响应头里有Mixed Content警告。检查了一下原来是应用代码里的某些重定向还是用了HTTP协议。这种情况在前后端分离的项目里特别容易出现,特别是API请求的baseURL配置。

我在前端代码里加了一个全局的axios拦截器来处理这个问题:

import axios from 'axios';

// 创建axios实例
const apiClient = axios.create({
  baseURL: process.env.NODE_ENV === 'production' 
    ? 'https://api.example.com' 
    : 'http://localhost:3000',
  timeout: 10000,
});

// 请求拦截器
apiClient.interceptors.request.use(
  config => {
    // 确保生产环境下都是HTTPS
    if (process.env.NODE_ENV === 'production') {
      if (config.url && config.url.startsWith('http://')) {
        config.url = config.url.replace('http://', 'https://');
      }
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

export default apiClient;

还有一个细节,WebSocket连接也要改成wss协议,不然浏览器一样会报Mixed Content错误:

const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const socket = new WebSocket(${wsProtocol}//${window.location.host}/ws);

证书续期自动化

Let’s Encrypt证书只有90天有效期,所以必须设置自动续期。acme.sh自带续期功能,但为了保险起见我还是写了个crontab任务:

# 每天凌晨2点检查证书是否需要续期
0 2 * * * ~/.acme.sh/acme.sh --cron --home ~/.acme.sh > /var/log/acme_renew.log 2>&1

# 每次续期成功后重启nginx
0 2 * * * ~/.acme.sh/acme.sh --renew-all --ecc && nginx -s reload

这里有个坑需要注意,续期完成后必须reload nginx配置,否则新证书不会生效。我开始漏掉了nginx reload步骤,结果每次续期后还要手动重启nginx。

为了确保续期脚本正常工作,我还写了个监控脚本定期检查证书剩余有效期:

#!/bin/bash
CERT_FILE="/path/to/certificate.crt"
DAYS_LEFT=$(openssl x509 -in $CERT_FILE -noout -days_until_expiry)

if [ $DAYS_LEFT -lt 30 ]; then
    echo "Warning: Certificate expires in $DAYS_LEFT days" | mail -s "SSL Certificate Alert" admin@example.com
fi

客户端兼容性问题

部署完成后的第二天,有用户反映在某些老旧设备上打不开网站。查了半天发现问题出在SSL协议版本上。虽然TLS 1.0和TLS 1.1已经不安全了,但有些旧的Android设备和Windows系统还需要支持。

为了兼顾安全性又能照顾到老设备,我把配置稍微调整了一下:

ssl_protocols TLSv1.2 TLSv1.3;  # 移除了对老协议的支持
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

# 对于需要支持老设备的情况,可能需要添加更多cipher
ssl_ciphers HIGH:!aNULL:!MD5:!kEDH:!CAMELLIA;

不过我还是坚持移除了对TLS 1.0和1.1的支持,毕竟安全更重要。让那些还在用Win XP和Android 4.4以下系统的用户升级一下系统吧,这年代还用这些老古董确实有点危险。

监控告警不能少

这次折腾让我意识到SSL证书监控的重要性。之前都是靠用户反馈才发现证书过期的问题,太被动了。现在我在Prometheus里加了证书过期监控:

groups:
- name: ssl_certificates
  rules:
  - alert: SSLCertificateExpiringSoon
    expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7  # 7天内过期
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "SSL certificate expiring soon"
      description: "{{ $labels.instance }} SSL certificate expires in less than 7 days"

配合AlertManager可以实现邮件和短信通知,这样证书快过期的时候就能提前收到告警了。

整个过程下来,虽然踩了不少坑,但也算是积累了更多实战经验。下次再处理类似问题应该会更快一些。其实大部分问题都是因为对SSL/TLS协议理解不够深入,以后得多花点时间研究底层原理了。

以上是我这次SSL证书部署的踩坑记录,如果你也遇到过类似的问题或者有更好的解决方案,欢迎在评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论