# OpenSSL 自签名证书：SAN、localhost 与信任安装

> OpenSSL 3.x 一行命令生成带 SAN 的自签名证书和 localhost 开发证书，并导入 macOS、Windows、Linux 信任库，附浏览器仍报警的排查表。

Source: https://ssl.sh/zh/cookbook/self-signed/
Last updated: 2026-11-10

自签名证书是用自己的私钥给自己签名、不经过 CA 的证书，OpenSSL 3.x 用一条 `openssl req -x509` 就能连私钥一起生成。它的加密和正规证书没区别，但任何客户端在你手动安装之前都不信任它，所以适合本地开发、内网设备、测试环境，不适合对外网站。输出示例来自 OpenSSL 3.6.1。

## 一条命令生成带 SAN 的自签名证书

`req -x509` 跳过 CSR 直接出证书，`-newkey` 同时生成私钥。

```bash
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -noenc \
  -keyout example.com.key -out example.com.crt \
  -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:*.example.com"
openssl x509 -in example.com.crt -noout -subject -dates -ext subjectAltName
```

```text
subject=CN=example.com
notBefore=Nov  9 18:17:00 2026 GMT
notAfter=Nov  9 18:17:00 2027 GMT
X509v3 Subject Alternative Name: 
    DNS:example.com, DNS:*.example.com
```

客户端只认 SAN，只写了 CN 的证书在所有现代浏览器里都会报域名不匹配。`-addext` 需要 OpenSSL 1.1.1 以上。不想敲命令可以用[自签名证书生成工具](https://ssl.sh/zh/tools/self-signed-certificate-generator/)，在浏览器本地生成同样的文件。

## 生成 ECDSA 自签名证书

把密钥类型换成 P-256 曲线的 `ec`，证书更小、握手更快。

```bash
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc -days 365 \
  -keyout example.com.key -out example.com.crt \
  -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com,IP:203.0.113.10"
openssl x509 -in example.com.crt -noout -text | grep -E "Signature Algorithm|Public-Key|IP Address" | head -3
```

```text
        Signature Algorithm: ecdsa-with-SHA256
                Public-Key: (256 bit)
                DNS:example.com, DNS:www.example.com, IP Address:203.0.113.10
```

所有现代客户端都支持 ECDSA P-256。`-newkey ed25519` 也能生成 Ed25519 自签证书，但大多数浏览器不接受 Ed25519 的 TLS 证书，只适合内部 mTLS。

## 用已有私钥或 CSR 自签

想沿用私钥就在 `req -x509` 里用 `-key`；手头已经有 CSR 就用 `x509 -req`。

```bash
# 已有私钥
openssl req -x509 -key example.com.key -days 365 -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com" -out example.com.crt
# 已有 CSR（把 CSR 里的 SAN 复制进证书）
openssl x509 -req -in example.com.csr -key example.com.key -days 365 \
  -copy_extensions copy -out example.com.crt
```

```text
Certificate request self-signature ok
subject=CN=example.com
```

`x509 -req` 默认丢弃 CSR 里的扩展，必须加 `-copy_extensions copy`（OpenSSL 3.0 起）或用 `-extfile` 另行指定。自签证书“没有 SAN”多半就是这个原因。

## 设置有效期

`-days` 决定有效期，不写的话 `req -x509` 只给 30 天。

```bash
openssl req -x509 -newkey rsa:2048 -noenc -keyout n.key -out n.crt -subj "/CN=example.com"
openssl x509 -in n.crt -noout -dates
```

```text
notBefore=Nov  9 18:17:00 2026 GMT
notAfter=Dec  9 18:17:00 2026 GMT
```

很多测试环境“一个月后突然坏了”就是这个默认值导致的。较新的 3.x 还支持用 `-not_before`、`-not_after` 写绝对日期（用 `openssl req -help` 确认）。有效期别超过 825 天，否则苹果设备即使手动信任也会拒绝。

## 生成 localhost 开发证书

明确标成非 CA 的服务器证书，同时写上域名和回环地址。

```bash
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc -days 825 \
  -keyout localhost.key -out localhost.crt -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=serverAuth"
openssl x509 -in localhost.crt -noout -ext basicConstraints,extendedKeyUsage,subjectAltName
```

```text
X509v3 Subject Alternative Name: 
    DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Extended Key Usage: 
    TLS Web Server Authentication
```

默认配置下 `req -x509` 会加 `CA:TRUE`，上面显式写的 `basicConstraints` 会覆盖它。苹果要求 TLS 服务器证书带 `serverAuth` EKU。RSA 私钥把 keyUsage 写成 `critical,digitalSignature,keyEncipherment`。前端开发用 Vite、webpack-dev-server 时，把这两个文件路径填到 https 配置即可。

## 用 -noenc 代替 -nodes

OpenSSL 3.0 把“不加密私钥”的参数从 `-nodes` 改名为 `-noenc`。

```bash
openssl req -help 2>&1 | grep -E "noenc|nodes"
```

```text
 -noenc                Don't encrypt private keys
 -nodes                Don't encrypt private keys; deprecated
```

3.x 两个都能用，老脚本不用改。新脚本写 `-noenc`；OpenSSL 1.1.1 只认 `-nodes`。两个都不写时 OpenSSL 会要求设置私钥密码，Nginx 每次启动都会卡在输密码。

## 用 s_server 和 curl 测试

起一个临时 TLS 服务，用 `--cacert` 连接，确认证书和私钥能正常配合。

```bash
openssl s_server -accept 8443 -cert localhost.crt -key localhost.key -www &
curl https://localhost:8443/ -o /dev/null
curl --cacert localhost.crt https://localhost:8443/ -o /dev/null -w "%{http_code}\n"
echo | openssl s_client -connect localhost:8443 -servername localhost 2>&1 | grep "Verify return"
```

```text
curl: (60) SSL certificate problem: unable to get local issuer certificate
200
Verify return code: 18 (self-signed certificate)
```

不带 `--cacert` 时报 60 号错误是预期结果，详见 [curl 60 报错](https://ssl.sh/zh/errors/curl-60-ssl-certificate-problem/)。验证码 18 表示证书链终点是一张自己签自己、且不在信任库里的证书。测试完用 `kill %1` 关掉服务。

## 在 macOS 上信任证书

把证书作为受信任根加入系统钥匙串，Safari 和 Chrome 都使用 macOS 的信任设置。

```bash
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain localhost.crt
```

```text
（成功时没有输出，系统可能要求输入密码或指纹）
```

以后不用了可以在“钥匙串访问”里删除，或执行 `sudo security remove-trusted-cert -d localhost.crt`。Firefox 有自己的证书库，需要在“设置 → 隐私与安全 → 证书 → 查看证书 → 证书颁发机构”里单独导入。

## 在 Windows 上信任证书

用管理员权限导入“本地计算机 → 受信任的根证书颁发机构”。

```bash
certutil -addstore -f Root localhost.crt
# 或者 PowerShell
Import-Certificate -FilePath .\localhost.crt -CertStoreLocation Cert:\LocalMachine\Root
```

```text
Root "Trusted Root Certification Authorities"
Signature matches Public Key
Certificate "localhost" added to store.
CertUtil: -addstore command completed successfully.
```

Edge、Chrome，以及 360、QQ 浏览器这类 Chromium 内核浏览器都读 Windows 证书库。也可以双击 `.crt` 文件选“安装证书”，手动选择“受信任的根证书颁发机构”。`certmgr.msc`（当前用户）和 `certlm.msc`（本地计算机）可以查看和删除。

## 在 Ubuntu、Debian、CentOS/RHEL 上信任证书

把 PEM 文件放进系统信任目录，再重建证书包。

```bash
# Ubuntu / Debian：文件名必须以 .crt 结尾
sudo cp localhost.crt /usr/local/share/ca-certificates/localhost.crt
sudo update-ca-certificates
# CentOS / Rocky / Alibaba Cloud Linux / Fedora
sudo cp localhost.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
```

```text
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
```

这样 curl、wget、Go 程序和多数命令行工具都会信任它。Java 用自己的 `cacerts`，Node.js 默认用内置列表，需要设置 `NODE_EXTRA_CA_CERTS`；Linux 版 Chrome 读 `~/.pki/nssdb` 里的 NSS 数据库。

## 排查浏览器为什么还报警

信任只是检查项之一，每种失败对应不同的报错。

| 现象 | 原因 | 处理 |
|---|---|---|
| `NET::ERR_CERT_AUTHORITY_INVALID` | 没导入这个浏览器使用的证书库 | 按上文导入，Firefox 单独导入 |
| `NET::ERR_CERT_COMMON_NAME_INVALID` | 访问的域名或 IP 不在 SAN 里 | 重新生成，把所有域名和 IP 写进 `subjectAltName` |
| `MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY` | 证书带 `CA:TRUE` | 加 `basicConstraints=critical,CA:FALSE` |
| 只在 Mac 或 iPhone 上被拒 | 有效期超过 825 天，或缺 `serverAuth` EKU | 缩短 `-days`，加 `extendedKeyUsage=serverAuth` |
| 电脑正常、手机报错 | 手机要单独安装证书，iOS 还要在设置里开启完全信任 | 安装描述文件并开启信任 |

Firefox 的自签名报错见 [MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT](https://ssl.sh/zh/errors/mozilla-pkix-error-self-signed-cert/)。HSTS 预加载的域名（例如 `.dev` 下的所有域名）遇到证书错误时浏览器不允许点“继续访问”。微信内置浏览器、小程序等场景基本无法让用户手动信任自签证书，这类场景只能用公网证书。

## 团队使用改用私有 CA

多台机器、多名开发者都要信任时，建一个根证书，用它给各个服务签证书。

```bash
# 只需一次：在每台设备上信任 ca.crt，然后按需签发服务证书
openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 90 -extfile server.ext -out app.crt
openssl verify -CAfile ca.crt app.crt
```

```text
Certificate request self-signature ok
subject=CN=example.com
app.crt: OK
```

有了私有 CA，每台设备只装一次根证书，服务证书可以随便轮换，不用每次重新信任。完整做法（含中间 CA、名称约束）见[私有 CA](https://ssl.sh/zh/cookbook/private-ca/)。
