编程那点事 编程那点事编程那点事

Apache多域名SSL证书配置实战指南

图片

一个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握手时,浏览器会告诉服务器"我要访问哪个域名",服务器根据这个信息选择对应的证书。

工作流程是这样的:

  1. 浏览器访问 https://aa.com

  2. 还没建立加密连接前,浏览器先发一条明文消息:"我要访问 aa.com"

  3. Apache收到后,从配置里找到 aa.com 对应的证书

  4. 用这个证书完成SSL握手

  5. 建立加密通道,开始传输数据

你的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的原理就很简单。核心就是:

  1. 每个域名一套VirtualHost配置(HTTP和HTTPS各一个)

  2. ServerName写对(SNI靠这个识别域名)

  3. 证书路径别搞混(每个域名指向自己的证书)

  4. 日志分开记录(方便以后排查问题)

我朋友配置好之后,两个品牌网站都能正常用HTTPS访问了,浏览器也不报安全警告。服务器还是那台,成本没增加,问题解决了。

如果你也在做多站点项目,遇到SSL证书的问题,可以试试这个方法。有问题欢迎留言讨论。

编程那点事 更专业 更方便

登录

找回密码

注册