# Tomcat 配置 SSL 证书：PEM 文件部署 HTTPS

> 在 Tomcat 10.1/11 上直接用 PEM 证书文件配置 HTTPS，开启 TLS 1.2/1.3、HTTP 跳转 HTTPS，续期后自动重启 Tomcat；附阿里云/腾讯云证书用法。

Source: https://ssl.sh/zh/guides/tomcat-ssl/
Last updated: 2026-11-10

Tomcat 的 `SSLHostConfig` 可以直接引用 PEM 格式的证书文件，不用转换成 JKS 或 PKCS12。做法是用 Certbot 申请 Let's Encrypt 证书，部署钩子把文件复制给 Tomcat，再在 443 端口配置 HTTPS 连接器，HTTP 自动跳转。

适用于 Tomcat 10.1、11.0（9.0 的 `SSLHostConfig` 写法相同），Linux + Java 17 及以上，Certbot 4.1 及以上。

## 前提条件

Certbot 首次签发要用 80 端口跑临时验证服务，Tomcat 需要有绑定 443 的权限。

- Tomcat 以 systemd 服务方式运行，用户为 `tomcat`，`CATALINA_BASE` 为 `/opt/tomcat`（按实际安装路径调整）。
- 已用 snap 安装 Certbot：`sudo snap install --classic certbot && sudo ln -s /snap/bin/certbot /usr/local/bin/certbot`。
- `example.com` 的 A/AAAA 记录指向服务器，例如 `203.0.113.10`；服务器在中国大陆的需已备案。
- 防火墙和安全组放行 TCP 80、443，80 端口当前没有被占用。80 端口不通时改用 [acme.sh DNS 验证](https://ssl.sh/zh/guides/acme-sh-dnspod-aliyun-dns/) 签发。

## 第一步：申请证书

80 端口空闲时，用 standalone 模式让 Certbot 临时起一个 Web 服务完成 HTTP-01 验证：

```bash
sudo certbot certonly --standalone -d example.com -d www.example.com
```

证书在 `/etc/letsencrypt/live/example.com/`：`cert.pem`（站点证书）、`chain.pem`（中间证书）、`fullchain.pem` 和 `privkey.pem`。

## 第二步：把证书复制到 Tomcat 可读的位置

`/etc/letsencrypt/live` 只有 root 能读。写一个部署钩子，每次签发和续期后复制文件并设置属主：

```bash
sudo tee /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
dest=/opt/tomcat/conf/ssl
install -d -o tomcat -g tomcat -m 700 "$dest"
install -o tomcat -g tomcat -m 644 "$RENEWED_LINEAGE/cert.pem"    "$dest/cert.pem"
install -o tomcat -g tomcat -m 644 "$RENEWED_LINEAGE/chain.pem"   "$dest/chain.pem"
install -o tomcat -g tomcat -m 600 "$RENEWED_LINEAGE/privkey.pem" "$dest/privkey.pem"
systemctl restart tomcat
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/tomcat.sh
```

## 第三步：配置 HTTPS 连接器

编辑 `/opt/tomcat/conf/server.xml`，添加 443 的 HTTPS 连接器，并把 HTTP 连接器的 `redirectPort` 指向它：

```xml
<Connector port="80" protocol="HTTP/1.1"
           connectionTimeout="20000" redirectPort="443" />

<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           SSLEnabled="true" scheme="https" secure="true" maxThreads="150">
    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
    <SSLHostConfig protocols="+TLSv1.2+TLSv1.3">
        <Certificate certificateFile="conf/ssl/cert.pem"
                     certificateKeyFile="conf/ssl/privkey.pem"
                     certificateChainFile="conf/ssl/chain.pem" />
    </SSLHostConfig>
</Connector>
```

相对路径以 `CATALINA_BASE` 为基准。`certificateChainFile` 不要填 `fullchain.pem`，它不能包含站点证书。

## 第四步：允许绑定 443 并强制 HTTPS

普通用户的 Java 进程默认不能绑定 1024 以下端口，在 systemd 里授予权限：

```bash
sudo systemctl edit tomcat
```

```ini
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
```

让所有请求跳转到 HTTPS，在 `conf/web.xml`（或应用自己的 `web.xml`）的 `</web-app>` 前加：

```xml
<security-constraint>
    <web-resource-collection>
        <web-resource-name>Entire site</web-resource-name>
        <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <user-data-constraint>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
</security-constraint>
```

现在 80 端口归 Tomcat 了，续期不能再用 standalone。改成 webroot 方式，由 ROOT 应用提供验证文件：

```bash
sudo systemctl daemon-reload && sudo systemctl restart tomcat
sudo certbot reconfigure --cert-name example.com --webroot -w /opt/tomcat/webapps/ROOT
```

## 验证

配置正确时，443 端口返回 Let's Encrypt 证书和完整证书链，HTTP 请求跳转到 HTTPS。用 [SSL 证书检查](https://ssl.sh/zh/tools/ssl-checker/) 从外部检查，或在终端运行：

```bash
curl ssl.sh/example.com
```

在服务器上确认证书链和跳转：

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9] s:'
curl -sI http://example.com | head -n 3
sudo journalctl -u tomcat -n 50 --no-pager | grep -i -E 'ssl|443'
```

## 自动续期

snap 版 Certbot 的定时器每天运行两次 `certbot renew`，4.1 及以上按 ARI 建议时间续期；续期成功后第二步的钩子复制新证书并重启 Tomcat。用演练检查验证路径：

```bash
sudo certbot renew --dry-run
```

演练不会执行部署钩子，第一次实际续期后要确认线上的到期时间。重启 Tomcat 会短暂断开连接；Tomcat 也提供了不重启重载 `SSLHostConfig` 的 JMX 操作，用法以对应版本的官方文档为准。Let's Encrypt 不再发到期提醒邮件，建议加到 [ssl.sh 监控](https://ssl.sh/zh/monitor/)（开发中）。

## 常见问题排查

- 443 端口报 **java.net.BindException: Permission denied**：第四步的权限没生效，运行 `sudo systemctl daemon-reload && sudo systemctl restart tomcat`。
- **Tomcat 启动了但连接器报私钥错误**：`tomcat` 用户读不到 `privkey.pem`，重新执行部署钩子。
- **Java、安卓客户端报 PKIX 错误**：证书链配错了，见 [PKIX path building failed](https://ssl.sh/zh/errors/java-pkix-path-building-failed/) 和 [缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)。
- **续期报连接超时**：webroot 目录没有在 `/.well-known/acme-challenge/` 下对外提供，见 [certbot 连接超时](https://ssl.sh/zh/errors/certbot-timeout-during-connect/)。
- **ERR_SSL_PROTOCOL_ERROR**：443 上是明文 HTTP 连接器或漏了 `SSLEnabled`，见 [ERR_SSL_PROTOCOL_ERROR](https://ssl.sh/zh/errors/err-ssl-protocol-error/)。
