在本地Ubuntu上申请好证书,再把证书放到云服务器上用,这活儿听起来简单,但真的操作过的人都知道,坑全藏在细节里。我自己踩过几次之后,把整个流程彻底捋了一遍,从申请方式选择、证书文件迁移、nginx配置,到续期自动化,完整记录下来,希望能帮到有同样需求的人。不管你是家里有台Ubuntu机器、想给云上的站点配上HTTPS,还是本来就在本地管理域名和证书、只是想把服务迁到云上,这篇文章都值得看完。
1. 整体思路:为什么证书能在本地申请、云端使用
很多朋友看到“在本地Ubuntu申请的证书放到云服务上”这个需求,第一反应是:证书不是跟着域名和服务器走的吗?换台机器还能用?这里要先把SSL证书的底层逻辑讲透,不然后面操作起来心里没底。
1.1 证书的本质是一对密钥,不是“绑定在某台机器上”
SSL证书,尤其是像Let's Encrypt签发的证书,本质上是一个“公钥 + 身份信息 + CA签名”的文件组合,配套的还有一个私钥文件。公钥和私钥是成对出现的,整个HTTPS握手过程中,服务器向客户端出示证书,然后用自己的私钥完成一次签名验证,证明“我确实持有这个证书对应的私钥”。
换句话说,证书并不绑定某台服务器的IP或主机名,它绑定的是“域名 + 私钥”。只要域名没变、私钥没丢,把证书文件和私钥文件整体搬到另一台机器上,这台机器就能继续用同一个HTTPS身份。这就好比身份证上的信息写的是你本人,你把身份证和本人一起搬到另一个城市,身份证依然有效,并不需要在新的城市重新办一张。
好多人迁移证书失败,恰恰是因为只拷贝了证书文件、忘带私钥,或者把私钥丢了,然后又去重新申请。其实只要原来的私钥还在,本地申请好的证书完全可以平移使用。
1.2 为什么会有“本地申请、云端部署”这种需求
我见的比较多的场景有三种。
第一种:域名解析在家里的公网IP上,或者本地专门跑了一套证书管理服务。家里那台Ubuntu机器本身就是ACME客户端的主控端,申请和续期都在它上面跑,云服务器只是后来加的前端节点,需要共享同一份证书。
第二种:本地没有对公网开放的80端口,或者不想把端口暴露出去,于是用DNS验证方式在本地申请。证书申请下来后,再手动部署到云端。这种情况下,证书的“出生地”在本地,用户实际访问的却可能是云端,就不得不做迁移。
第三种:企业内部有自己的CA体系,或者用的是自签名证书,签发工具只在某台内网Ubuntu机器上装了,云端服务器是单独买的机器,两边没有统一的证书管理平台,最省事的方式就是从本地导出再导入。
了解这些场景后你会发现,这个流程并不是纯粹的“闲得没事折腾”,它背后对应着真实的网络拓扑和证书管理现状。
1.3 为什么不能“复制粘贴”了事
很多人以为把证书文件scp到云端、填进nginx就能跑,结果经常遇到“浏览器显示证书链不完整”“iOS设备直接不信任”“curl报错”等现象。原因在于:部署证书不是只丢一个cert.pem过去那么简单。
证书文件本身有好几个:服务器证书、中间证书、完整证书链、私钥。这四者的关系我后面会详细拆。部署时要搞清楚哪个文件该放在哪个配置项里,私钥权限不能太开放,证书目录的属主和权限也要修正。而且,Let's Encrypt证书默认有效期只有90天,本地机器上的自动续期不会自动带动云端,需不需要做同步、怎么做同步,这些都是“复制粘贴”解决不了的。
2. 申请证书前的准备与验证方式选择
在动手申请之前,先想清楚一个问题:你的域名到底用哪种方式验证所有权?这一步直接决定了你能不能顺利在本地申请到证书。
2.1 环境准备:安装certbot并确认域名解析
我习惯用certbot,它是Let's Encrypt官方推荐的ACME客户端。在本地Ubuntu上安装很简单:
sudo apt update sudo apt install certbot装完之后,先确认域名能解析到你预期的地址。这一步很关键,很多人申请失败,不是certbot的问题,而是域名解析还没生效。
dig +short example.com如果你用的是DNS验证,域名解析指向哪里其实不一定重要,因为验证过程靠的是DNS记录。但如果你打算用HTTP验证,那域名解析必须能到达一台能访问到80端口的机器。申请之前把这些都确认好,后面能省掉大量排查时间。
2.2 三种验证方式怎么选
ACME验证方式主要有三种,每种都有各自的适用场景。
第一种是standalone方式。certbot会临时起一个监听80端口的服务,ACME服务器会通过你的域名访问这个端口来验证。这台机器必须公网可达、80端口没被占用。命令大概是:
sudo certbot certonly --standalone -d example.com --email you@example.com --agree-tos --no-eff-email第二种是webroot方式。certbot会在你的网站目录里放一个临时验证文件,ACME服务器通过HTTP访问这个文件完成验证。这个适合nginx或apache已经在跑的场景,但同样要求80端口能公网访问。
第三种是DNS验证方式。certbot通过ACME的DNS-01 challenge,让你在域名的DNS记录里添加一条TXT记录,证明你控制这个域名。我比较推荐它作为“本地申请”的首选,因为即使本地服务不暴露到公网、没有80端口,只要你能改动DNS解析记录,就能申请成功。手动方式是这样:
sudo certbot certonly --manual --preferred-challenges dns -d example.comcertbot会要求你登录DNS服务商,创建一条指定的TXT记录,然后回车继续。缺点是如果DNS服务商不支持API,每次续期都要手动改,比较麻烦。好在现在主流DNS服务商基本都有API,配合certbot的DNS插件,可以实现全自动验证。
我补一句实践上的经验:如果不是必须要签名到代码、或者申请的是通配符证书,直接用DNS验证最省心。通配符证书因为域名前缀不确定,目前只能用DNS验证,这也是很多人在本地申请带*.example.com证书的唯一选择。
2.3 生成的四个文件分别是什么
申请成功后,certbot会在/etc/letsencrypt/live/你的域名/目录下生成四个关键文件:
privkey.pem:私钥,绝不能泄露。cert.pem:服务器证书本身。chain.pem:中间证书,也就是CA用来给服务器证书签名的那个证书。fullchain.pem:服务器证书 + 中间证书的合并体。
用一张表说清楚:
| 文件 | 内容 | 部署时放哪里 |
|---|---|---|
| privkey.pem | 私钥 | ssl_certificate_key |
| cert.pem | 服务器证书 | 一般不用 |
| chain.pem | 中间证书 | 配合cert.pem某些场景用 |
| fullchain.pem | 完整证书链 | ssl_certificate |
强烈建议配置nginx或apache时用fullchain.pem,而不是cert.pem。如果你只填cert.pem,多数现代客户端会因为缺少中间证书而判定证书链不完整,直接报错。
3. 把证书从本地迁移到云服务并配置nginx
这一节是整个流程的核心,我会把每一步命令和原理都写清楚,照着做基本不会出问题。
3.1 在本地打包并传输证书
先进入证书目录,打包时需要带上完整证书链和私钥。我不建议打包整个/etc/letsencrypt,因为里面还有账户信息、配置备份等和云端部署无关的内容,带过去反而容易把目录结构搞乱。
cd /etc/letsencrypt/live/example.com sudo tar czf ~/example.com-certs.tar.gz fullchain.pem privkey.pem然后通过scp把压缩包传到云服务器:
scp ~/example.com-certs.tar.gz user@your-cloud-server:~/这里有个安全提示:privkey.pem是私钥,传输全程必须走加密通道。scp本身走的是SSH,加密没问题。但要特别提醒,别图方便把证书压缩包扔到公开的网盘、代码仓库或者聊天工具里,私钥一旦泄露,你整个域名的HTTPS保护就形同虚设了。
3.2 云上解压并修正权限
在云服务器上执行:
sudo mkdir -p /etc/letsencrypt/live/example.com sudo tar xzf ~/example.com-certs.tar.gz -C /etc/letsencrypt/live/example.com然后必须修正权限。因为证书是从本地打包带过来的,默认属主和权限可能不适合云上的nginx进程读取,尤其是privkey.pem,如果权限是644,意味着任何用户都能读私钥,这是非常危险的。
sudo chown -R root:root /etc/letsencrypt/live/example.com sudo chmod 700 /etc/letsencrypt/live sudo chmod 755 /etc/letsencrypt/live/example.com sudo chmod 644 /etc/letsencrypt/live/example.com/fullchain.pem sudo chmod 600 /etc/letsencrypt/live/example.com/privkey.pem目录权限为什么要这样设?/etc/letsencrypt/live设置为700,普通用户进不去,相当于把证书目录锁起来;证书文件本身设置为644,nginx master进程以root启动后,子进程切换成nginx用户后仍能读取普通文件;私钥设置成600,只有root能读。这样既保证nginx能加载,又尽量缩小了私钥暴露面。
3.3 完整nginx配置示例与生效检查
编辑nginx站点配置,比如/etc/nginx/sites-available/example.com:
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 80; server_name example.com; return 301 https://$host$request_uri; }写完后先测试配置,再重载:
sudo nginx -t sudo systemctl reload nginxnginx -t会检查配置语法和证书路径能不能正常读取,这一步输出syntax is ok后再reload。不要直接restart nginx,生产环境下restart会短暂断开所有连接,reload是平滑重载,更稳妥。
验证是否真正生效,我通常不看浏览器,而是用命令直接检查:
curl -v https://example.com 2>&1 | grep -E "subject:|issuer:"或者用openssl手动检查证书链是否完整:
openssl s_client -connect example.com:443 -servername example.com -showcerts看到证书链里包含服务器证书和中间证书、并且最终信任,才算部署成功。
4. 常见问题与排查技巧实录
这一节是我实际工作中踩坑最多的部分,每个问题都值得单独拿出来讲。
4.1 浏览器提示“证书不受信任”但curl又能用?
有一种很诡异的情况:你用curl访问一切正常,但手机浏览器或者一些老设备直接弹出“证书不受信任”。原因通常是证书链不完整。
curl对证书链的要求相对宽松,但浏览器更严格。如果nginx配的ssl_certificate只指向cert.pem,很多客户端拿不到中间证书,就会认为证书由未知CA签发。解决办法就是把配置改成fullchain.pem。我见过不少人在这上面卡了一天,最后发现只是把文件填错了。
还有一种情况:你把证书从一个CA迁移过来,中间证书名字、路径都和本地不一样,结果云端只复制了cert.pem过去。记住,迁移时最好直接打包fullchain.pem,少走弯路。
4.2 nginx替换证书不生效
“我把新证书传上去了,nginx也reload了,怎么浏览器还是显示旧证书?”这是高频问题。多数原因有三个:
第一,你修改了证书文件,但nginx没有真正重新加载。systemctl reload nginx有时候会因为语法检查失败而没有实际重载,先跑nginx -t确认。
第二,浏览器端有OCSP缓存和会话缓存。新证书部署后,浏览器会缓存旧证书信息。建议用隐身窗口或者换一个网络环境测试,也可以用curl -v直接看服务器返回的证书,这是最真实的。
第三,云服务器厂商自带的Web防火墙、负载均衡或者CDN节点缓存了旧证书。如果你是在阿里云ECS上用SLB/负载均衡做HTTPS,证书要在负载均衡控制台里替换,而不是只在后端ECS上改nginx。这个点特别值得注意,很多人改了后端机器没改SLB,自然不生效。
4.3 云端续期失效
Let's Encrypt证书有效期90天,certbot默认会配置定时任务自动续期,在本地机器上没问题。但你把证书迁到云上后,云端并没有对应的certbot账户和自动续期任务,所以90天后云端证书就过期了。
解决思路是:不要试图在云端单独申请一份新证书,而是让本地继续作为证书签发方,定时把最新证书同步到云端。具体做法我放在下一节详细展开。
4.4 frp内网穿透场景下的证书到底放哪
顺便讲一个大家容易混淆的场景:如果你用frp这类内网穿透工具,把本地Ubuntu的服务暴露到公网,流量路径是“用户 -> 云服务器 -> 隧道 -> 本地服务”。这时很多人误以为证书应该放在云服务器上,其实要分情况。
如果frp的请求是TCP穿透,TLS终结发生在本地服务上,那么证书必须放在本地。云服务器只是当一个流量转发器,它根本不需要证书。如果你在云服务器上配了nginx做域名转发和HTTPS终结,那证书才需要放到云上,同时本地服务要改成HTTP回源。
判断标准很简单:TLS会话在哪里终止,证书就在哪里。不要一股脑把证书都放云上,反而可能导致本地服务不能正确启用HTTPS。
5. 续期自动化:让本地证书自动同步到云端
最后这部分是升华,也是我发现真正能帮大家省心的地方。证书部署不是一锤子买卖,90天续期是常态,手工操作迟早翻车。
5.1 certbot renew 与 deploy-hook
certbot的续期命令是:
sudo certbot renew它只会续期到期时间不足30天的证书。如果续期成功,会触发--deploy-hook指定的脚本。这个hook非常适合做证书同步,因为只有证书真正更新了才会执行,平时不会白白跑一遍同步逻辑。
我推荐在本地写一个同步脚本/usr/local/bin/sync_certs_to_cloud.sh:
#!/bin/bash rsync -avz -e ssh /etc/letsencrypt/live/example.com/ user@cloud-server:/etc/letsencrypt/live/example.com/ ssh user@cloud-server "chown -R root:root /etc/letsencrypt/live/example.com && chmod 600 /etc/letsencrypt/live/example.com/privkey.pem && systemctl reload nginx"然后用sudo certbot renew --deploy-hook /usr/local/bin/sync_certs_to_cloud.sh测试一次。如果本地和云端的续期状态一致,会自动跳过。
5.2 配置定时任务
certbot安装时默认会写一个systemd timer,你也可以自己加cron,确保每天尝试一次续期:
sudo crontab -e加入一行:
30 2 * * * certbot renew --quiet --deploy-hook /usr/local/bin/sync_certs_to_cloud.sh每天凌晨2点半跑一次,只有到期前30天才会真正续期,平时开销几乎为零。证书更新后自动同步到云端,并reload nginx,整个流程就全自动了。
需要注意:renew --quiet在没有需要续期的证书时不会输出任何信息,日志排查时想看细节,可以单独加--deploy-hook后面的日志输出,比如重定向到文件。
5.3 云上要不要也装certbot做双保险
我个人的建议是:既然选择了本地申请再上云,就维持单一签发源,不要让云端也去签一份证书。两个certbot同时运行,各自维护一套证书文件,迟早有一天会混淆。把云端当成一个“消费证书”的角色,本地只负责签发和同步,权责清晰,排查问题也容易。
当然,如果你担心本地机器长期关机或者登录失效,可以在云端留一个手动恢复包,并存一份私钥备份到加密的离线存储里。但日常自动续期和同步,靠本地就够了。
这里再分享一个小技巧:证书文件不要直接放在普通用户目录里,我习惯在云上维护一份/etc/letsencrypt/live/example.com,并且把整个证书目录纳入备份。这样即使云服务器重装,也能快速恢复到原状,不用再折腾证书迁移流程。
我自己在实操中最深的体会是:证书迁移本身不难,难的是想清楚“谁签发、谁部署、谁续期”这三个角色。本地Ubuntu负责签发和续期,云服务器负责部署和提供HTTPS服务,中间的同步交给脚本自动完成。只要这个模型建立起来,后续基本不会再为证书头疼。如果只是单次迁移,注意带全完整证书链、保护好私钥、正确配置nginx,也就足够了。