
一个Apache容器跑多个域名,如何给每个域名配置独立的SSL证书?
写在前面
上个月帮朋友处理了一个棘手问题:他的公司运营两个品牌网站,后台用的是同一套CMS系统。为了节省服务器成本,两个域名都指向同一个Apache容器。问题来了——每个域名都申请了独立的SSL证书,但他不知道怎么在Apache里配置。
折腾了一下午,终于搞定。今天分享一下具体操作,希望能帮到遇到类似问题的朋友。
你可能遇到的场景
先说说什么时候会碰到这个需求:
场景一:一套系统多个品牌 公司有主品牌和子品牌,共用一套电商系统,但域名不同。比如主站用 aa.com,子站用 bb.com。
场景二:SaaS多租户平台 你开发了一套SaaS系统,每个客户绑定自己的独立域名,但后端代码都是同一份。
场景三:国际化业务 中国站用 aa.cn,美国站用 aa.com,内容管理系统是同一套,只是前端展示语言不同。
我朋友的情况就是第一种。两个域名都要HTTPS,但不能共用证书(会有安全警告),所以必须给每个域名配独立证书。
核心技术:SNI到底是什么
先说说以前的困境
在SNI技术出现之前,一个IP地址的443端口只能绑定一个SSL证书。如果你有多个域名,要么花钱买多域名证书(贵),要么多开几个IP(麻烦)。
SNI解决了什么问题
SNI(Server Name Indication) 简单说就是在SSL握手时,浏览器会告诉服务器"我要访问哪个域名",服务器根据这个信息选择对应的证书。
工作流程是这样的:
浏览器访问 https://aa.com
还没建立加密连接前,浏览器先发一条明文消息:"我要访问 aa.com"
Apache收到后,从配置里找到 aa.com 对应的证书
用这个证书完成SSL握手
建立加密通道,开始传输数据
你的Apache支持SNI吗
查一下版本就知道了:
# 进入Apache容器docker exec -it your-apache-container bash# 查看版本httpd -v如果显示的版本号是:
Apache 2.4.x:完全支持,放心用
Apache 2.2.12 到 2.2.x:支持,但要确保OpenSSL版本在0.9.8f以上
Apache 2.2.12以下:不支持,建议升级
现在Docker官方镜像基本都是2.4版本,一般不用担心。
证书文件长什么样
不同渠道申请的证书,文件格式可能不一样。我见过最常见的两种:
免费证书(Let's Encrypt)
下载下来是两个 .pem 文件:
your-domain/
├── fullchain.pem # 完整证书链
└── privkey.pem # 私钥
配置时这么写:
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
商业证书(阿里云、腾讯云等)
下载Apache版本,一般是三个文件:
your-domain/
├── domain.com.key # 私钥
├── domain.com_public.crt # 网站证书
└── domain.com_chain.crt # 中间证书
配置时这么写:
SSLCertificateFile /path/to/domain.com_public.crt
SSLCertificateKeyFile /path/to/domain.com.key
SSLCertificateChainFile /path/to/domain.com_chain.crt
我朋友用的就是阿里云证书,是第二种格式。
这里有个坑要注意:私钥文件(.key)千万别泄露!一旦被别人拿到,他可以伪装成你的网站。所以文件权限一定要设成600。
开始配置(以两个域名为例)
假设现在有两个域名:
aa.com(主站)
bb.com(子站)
它们都指向同一个目录:/www/wwwroot/php_cms/public
第一步:整理证书文件
把证书按域名分开放:
# 创建目录mkdir -p /www/server/panel/vhost/cert/aa.com
mkdir -p /www/server/panel/vhost/cert/bb.com
# 上传证书文件后,设置权限chmod 600 /www/server/panel/vhost/cert/aa.com/*.key
chmod 644 /www/server/panel/vhost/cert/aa.com/*.crt
chmod 600 /www/server/panel/vhost/cert/bb.com/*.key
chmod 644 /www/server/panel/vhost/cert/bb.com/*.crt
最终结构像这样:
/www/server/panel/vhost/cert/
├── aa.com/
│ ├── aa.com.key
│ ├── aa.com_public.crt
│ └── aa.com_chain.crt
└── bb.com/
├── bb.com.key
├── bb.com_public.crt
└── bb.com_chain.crt
第二步:配置aa.com
每个域名需要配两个 VirtualHost:
一个监听80端口(HTTP),负责跳转到HTTPS
一个监听443端口(HTTPS),配置SSL证书
HTTP配置(80端口)
<VirtualHost *:80>
ServerName aa.com
ServerAlias www.aa.com
DocumentRoot "/www/wwwroot/php_cms/public"
# 记录日志
ErrorLog "/www/wwwlogs/aa.com-error.log"
CustomLog "/www/wwwlogs/aa.com-access.log" combined
# 强制跳转HTTPS
RewriteEngine on
RewriteCond %{SERVER_PORT} !^443$
RewriteRule ^(.*)$ https://%{SERVER_NAME}$1 [L,R=301]
# 禁止访问敏感文件
<FilesMatch "\.(env|git|svn|htaccess)">
Require all denied
</FilesMatch>
# 目录权限
<Directory "/www/wwwroot/php_cms/public">
AllowOverride All
Require all granted
Options FollowSymLinks
</Directory>
</VirtualHost>
HTTPS配置(443端口)
<VirtualHost *:443>
ServerName aa.com
ServerAlias www.aa.com
DocumentRoot "/www/wwwroot/php_cms/public"
# 日志文件
ErrorLog "/www/wwwlogs/aa.com-error.log"
CustomLog "/www/wwwlogs/aa.com-access.log" combined
# 开启SSL
SSLEngine on
SSLCertificateFile /www/server/panel/vhost/cert/aa.com/aa.com_public.crt
SSLCertificateKeyFile /www/server/panel/vhost/cert/aa.com/aa.com.key
SSLCertificateChainFile /www/server/panel/vhost/cert/aa.com/aa.com_chain.crt
# 只用安全的加密协议
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on
# 禁止访问敏感文件
<FilesMatch "\.(env|git|svn|htaccess)">
Require all denied
</FilesMatch>
# 目录权限
<Directory "/www/wwwroot/php_cms/public">
AllowOverride All
Require all granted
Options FollowSymLinks
</Directory>
</VirtualHost>
第三步:配置bb.com
bb.com的配置和aa.com几乎一样,就是把域名和证书路径换一下:
HTTP配置
<VirtualHost *:80>
ServerName bb.com
ServerAlias www.bb.com
DocumentRoot "/www/wwwroot/php_cms/public"
ErrorLog "/www/wwwlogs/bb.com-error.log"
CustomLog "/www/wwwlogs/bb.com-access.log" combined
# 跳转HTTPS
RewriteEngine on
RewriteCond %{SERVER_PORT} !^443$
RewriteRule ^(.*)$ https://%{SERVER_NAME}$1 [L,R=301]
<FilesMatch "\.(env|git|svn|htaccess)">
Require all denied
</FilesMatch>
<Directory "/www/wwwroot/php_cms/public">
AllowOverride All
Require all granted
Options FollowSymLinks
</Directory>
</VirtualHost>
HTTPS配置
<VirtualHost *:443>
ServerName bb.com
ServerAlias www.bb.com
DocumentRoot "/www/wwwroot/php_cms/public"
ErrorLog "/www/wwwlogs/bb.com-error.log"
CustomLog "/www/wwwlogs/bb.com-access.log" combined
# SSL证书(注意路径是bb.com的)
SSLEngine on
SSLCertificateFile /www/server/panel/vhost/cert/bb.com/bb.com_public.crt
SSLCertificateKeyFile /www/server/panel/vhost/cert/bb.com/bb.com.key
SSLCertificateChainFile /www/server/panel/vhost/cert/bb.com/bb.com_chain.crt
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on
<FilesMatch "\.(env|git|svn|htaccess)">
Require all denied
</FilesMatch>
<Directory "/www/wwwroot/php_cms/public">
AllowOverride All
Require all granted
Options FollowSymLinks
</Directory>
</VirtualHost>
第四步:检查配置并重启
配置写好后,先测试一下语法有没有错:
# 测试配置apachectl configtest
# 如果显示 Syntax OK,就可以重启了重启Apache:
# Docker环境docker restart your-apache-container
# 或者不重启容器,只重载配置docker exec your-apache-container apachectl graceful
查看VirtualHost是不是都配置好了:
apachectl -S# 应该能看到:# *:443 aa.com# *:443 bb.com验证是否生效
浏览器验证
打开浏览器,分别访问:
http://aa.com(应该自动跳转到https://aa.com)
http://bb.com(应该自动跳转到https://bb.com)
点击地址栏的小锁图标,查看证书信息:
aa.com 显示的证书应该是颁发给 aa.com 的
bb.com 显示的证书应该是颁发给 bb.com 的
如果两个都显示正确,说明SNI生效了。
命令行验证
用OpenSSL工具测试更准确:
# 测试aa.comopenssl s_client -connect aa.com:443 -servername aa.com < /dev/null 2>/dev/null | openssl x509 -noout -subject# 输出应该包含:subject=CN = aa.com# 测试bb.comopenssl s_client -connect bb.com:443 -servername bb.com < /dev/null 2>/dev/null | openssl x509 -noout -subject# 输出应该包含:subject=CN = bb.com踩过的坑和解决办法
坑1:访问任何域名都显示同一个证书
原因:ServerName写错了,或者Apache选错了默认VirtualHost
解决办法:
# 检查配置加载顺序apachectl -S# 第一个加载的会成为默认VirtualHost# 建议在配置文件开头加一个默认配置:<VirtualHost *:443> ServerName _default_
SSLEngine on
# 使用一个默认证书</VirtualHost>坑2:证书和私钥不匹配
症状:Apache启动失败,报错 "Certificate and private key do not match"
验证方法:
# 查看证书的modulusopenssl x509 -noout -modulus -in aa.com_public.crt | openssl md5
# 查看私钥的modulusopenssl rsa -noout -modulus -in aa.com.key | openssl md5
# 两个MD5值应该一模一样如果不一样,说明证书和私钥不是一对,重新下载正确的文件。
坑3:HTTP没有自动跳转HTTPS
原因1:mod_rewrite模块没启用
# Debian/Ubuntua2enmod rewrite
systemctl restart apache2
# 或者检查是否已启用apachectl -M | grep rewrite
原因2:AllowOverride设置不对
确保 <Directory> 配置里有:
AllowOverride All
坑4:Docker环境证书找不到
如果用Docker运行Apache,证书路径需要映射到容器内:
# docker-compose.ymlversion: '3'services: apache: image: httpd:2.4 volumes: - ./certs:/www/server/panel/vhost/cert
- ./config:/usr/local/apache2/conf/extra
- ./www:/www/wwwroot
ports: - "80:80" - "443:443"证书更新后记得重启容器:
docker restart apache-container
性能优化小技巧
配置好基本功能后,还可以做些优化:
开启HTTP/2
HTTP/2能让网站加载更快,配置很简单:
# 在<VirtualHost *:443>里加一行
Protocols h2 http/1.1
前提是Apache版本要2.4.17以上,并且启用了mod_http2模块。
启用OCSP Stapling
减少浏览器验证证书的时间:
# 在Apache主配置文件里加上
SSLUseStapling on
SSLStaplingCache "shmcb:/var/run/ocsp(128000)"
开启HSTS
强制浏览器以后只用HTTPS访问:
# 在<VirtualHost *:443>里加上
Header always set Strict-Transport-Security "max-age=31536000"
写在最后
给多个域名配置独立SSL证书,看起来复杂,其实理解了SNI的原理就很简单。核心就是:
每个域名一套VirtualHost配置(HTTP和HTTPS各一个)
ServerName写对(SNI靠这个识别域名)
证书路径别搞混(每个域名指向自己的证书)
日志分开记录(方便以后排查问题)
我朋友配置好之后,两个品牌网站都能正常用HTTPS访问了,浏览器也不报安全警告。服务器还是那台,成本没增加,问题解决了。
如果你也在做多站点项目,遇到SSL证书的问题,可以试试这个方法。有问题欢迎留言讨论。