mkcert 本地开发 CA 与 HTTPS 证书工具手册
本地 CA 如何进入开发机
mkcert 为开发机创建一套本地 CA,把根证书装进选定的信任库,再签发包含正确 SAN 的开发证书。浏览器、Java、Node 和容器读取的信任库可能不同,完成签发后仍要分别验证实际客户端。mkcert 不替 Nginx、Caddy、应用框架或容器配置 HTTPS,也不适合给生产和最终用户设备发证书。
Linux 上,Ubuntu/Debian 使用 sudo apt-get update && sudo apt-get install mkcert libnss3-tools openssl curl。若发行版没有 mkcert 包,从官方发布页选择与 uname -m 对应的二进制;RHEL 系的 NSS 工具包为 nss-tools。下载后先执行 mkdir -p "$HOME/.local/bin",再用 install -m 0755 已核对的二进制 "$HOME/.local/bin/mkcert" 安装,并把该目录加入 PATH。macOS 和 Windows 的安装方式见官方 README。不要把未经来源核对的下载文件直接提权执行。
command -v mkcert
mkcert -version
mkcert -help
mkcert -CAROOT项目官方 README 是安装方式、支持的 root store 和参数顺序的权威入口。发行节奏与操作系统信任策略并不相同,升级时应在隔离开发机重放安装、签发和卸载,而不是只看 mkcert -version 能运行。
install 会改变哪些本地状态
第一次执行 mkcert -install 时,mkcert 会在 CAROOT 创建本地根证书和私钥,并尝试把根证书安装进所选信任库。下面在 Linux 普通用户终端先建立独占实验目录,再固定本次唯一的 CAROOT;安装系统根时 mkcert 可能调用 sudo,只在获准修改这台开发机信任库时执行:
MKCERT_ORIGINAL_DIR="$PWD"
mkdir -p "$HOME/.local/share"
MKCERT_LAB="$(mktemp -d "$HOME/.local/share/mkcert-lab.XXXXXX")"
export CAROOT="$MKCERT_LAB/ca"
mkdir -m 700 "$CAROOT" "$MKCERT_LAB/certs"
cd "$MKCERT_LAB"
TRUST_STORES=system,nss mkcert -install
mkcert -CAROOT预期 mkcert -CAROOT 返回本次 $MKCERT_LAB/ca,安装输出分别说明 system 与 NSS 是否成功;若提示缺少 certutil,不能把 NSS 记为已安装,先补工具再对同一 CAROOT 重试。rootCA.pem 是公开根证书,可以在明确授权的开发环境中用于建立信任;rootCA-key.pem 是 CA 私钥,拿到它的人可以签发任何被这台机器信任的名称。后者不能进入 Git、镜像、备份制品、工单、聊天软件或共享网盘。
mkcert 可以识别系统、NSS 和 Java 三类 store。上面的实验只选择 system 与 NSS;退出时必须仍以同一 CAROOT 和 TRUST_STORES=system,nss 卸载。如果还需要 Java,应在首次安装前把 java 加进安装和卸载所用的同一列表,而不是在实验中途换一张根。
如果包含 java,JAVA_HOME 会影响 mkcert 找到的 Java 信任库。IDE 自带运行时、Gradle daemon、容器和 CI 仍可能使用另一套 JDK;“mkcert 已显示安装 Java store”不能证明所有 Java 进程都会信任。Java 侧应使用 keytool 对真实运行时复核 alias 和指纹。
CAROOT 决定 mkcert 存放和查找哪一张 CA,不会把已有根自动迁移到新目录。为另一个项目建立独立 CA 时,应在另一次实验开始前新建工作根并选择目录,分别记录根指纹和安装过的 store;不要顺着上面的 -install 再切换目录执行第二次安装,否则结尾只卸载当前根会遗漏前一张。多个 CAROOT 提高隔离性,也增加轮换和卸载责任,不能把同一个 CA 私钥复制给所有人。
名称列表决定证书身份
mkcert 的位置参数就是证书 SAN。DNS 名称、IPv4、IPv6 和通配符会按各自类型写入证书。项目常用的一组名称可以这样生成:
TRUST_STORES=system,nss mkcert \
-cert-file certs/dev.local.test.pem \
-key-file certs/dev.local.test-key.pem \
localhost 127.0.0.1 ::1 dev.local.test-cert-file、-key-file、-p12-file 等选项必须出现在名称列表之前。localhost 与 dev.local.test 是 DNS SAN,127.0.0.1 与 ::1 是 IP SAN;给证书增加 DNS 名称不会让 IP 访问自动匹配。应用实际使用哪个 URL,就必须验证那个主机标识是否在 SAN 中。
用 OpenSSL 检查签发后的 SAN、指纹与有效期:
openssl x509 -in certs/dev.local.test.pem -noout \
-subject -issuer -fingerprint -sha256 \
-ext subjectAltName -dates在同一终端启动只监听本机的测试服务,并保存子进程 PID;服务输出写入本次独占目录,不需要修改 hosts:
cp "$(mkcert -CAROOT)/rootCA.pem" certs/dev-root.pem
chmod 0644 certs/dev-root.pem
openssl s_server -accept 127.0.0.1:8443 -www \
-cert certs/dev.local.test.pem -key certs/dev.local.test-key.pem \
> "$MKCERT_LAB/s_server.log" 2>&1 &
MKCERT_SERVER_PID=$!仍在当前终端执行 curl -q --noproxy '*' --retry 5 --retry-connrefused --retry-delay 1 --fail-with-body --cacert certs/dev-root.pem https://127.0.0.1:8443/,应得到测试页面;若连接被拒绝,先查看 s_server.log,不要把未启动的服务当作证书失败。需要 DNS 名称时,在原 curl 命令加 --resolve dev.local.test:8443:127.0.0.1,并请求 https://dev.local.test:8443/。证书与私钥接入真实服务后,再按 OpenSSL 严格握手同时验证 SNI、链和主机名。mkcert 只负责生成文件;如果服务仍发送旧证书,检查服务配置、挂载、reload 或负载均衡节点。
用反例区分 SAN 与信任问题
一台已经信任本地 CA 的开发机访问 dev.local.test 成功,并不能证明证书适合 api.local.test。先保持同一根和同一服务,只改请求主机名;若得到 hostname mismatch,说明 CA 信任成立,缺的是 SAN。重新签发包含目标名称的叶子证书即可,不需要重建根 CA。
再把 rootCA.pem 作为显式信任锚交给一个没有执行过 mkcert -install 的隔离容器,验证应当成功;去掉该显式 CA 后应当失败。这个反例证明容器自己的 CA store 与宿主机信任相互独立。
docker run --rm --network host \
-v "$PWD/certs/dev-root.pem:/work/dev-root.pem:ro" \
curlimages/curl:8.21.0 \
-q --noproxy '*' --fail-with-body --cacert /work/dev-root.pem \
https://127.0.0.1:8443/这条命令针对原生 Linux Docker Engine:host 网络让容器访问同一主机的回环监听,URL 中的 127.0.0.1 也在前面的 IP SAN 中。Docker Desktop 需另行启用其支持的 host networking,不能直接照搬默认 bridge 地址。去掉 --cacert 重跑应报 curl 60;恢复后成功。客户端只挂载公开根,叶子私钥留在服务端,CA 私钥始终留在 CAROOT。网络语义见 Docker host 网络。
Node、浏览器、Java 和 Docker 不共享一个世界
mkcert 官方说明支持系统、NSS 和在 JAVA_HOME 可见时的 Java store,但运行时行为仍要按实际版本验证。Node 可以通过 NODE_EXTRA_CA_CERTS 追加 PEM 根证书,这个变量只在进程启动时读取;应用如果在 TLS 请求里显式提供 ca,默认和额外 CA 都不会继续参与。
NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" node --input-type=module - <<'JS'
import https from 'node:https';
const request = https.get('https://127.0.0.1:8443/', response => {
console.log(`status=${response.statusCode}`);
response.resume();
response.on('end', () => {
if (response.statusCode !== 200) process.exitCode = 1;
});
});
request.setTimeout(3000, () => request.destroy(new Error('request timeout')));
request.on('error', error => { console.error(error.message); process.exitCode = 1; });
JS支持系统 CA 的 Node 发行线还可以使用 --use-system-ca 或 NODE_USE_SYSTEM_CA=1,但团队不能把新运行时能力反推到旧版本。NODE_TLS_REJECT_UNAUTHORIZED=0 会关闭验证,不是 mkcert 接入方式。Node CLI 文档说明了额外 CA 的读取时机和显式 ca 的覆盖行为。
Docker 拉取 registry 时是 Docker host 或 daemon 在验证;容器内应用访问 HTTPS 时,读取的是镜像内部 CA store;构建阶段还可能由 BuildKit 独立发起网络请求。三条路径需要分别验证。只在宿主机运行 mkcert -install,不能保证容器和远程 daemon 自动信任。
移动设备也必须安装公开的 rootCA.pem,并遵守系统对用户根证书和应用网络策略的限制。只应对受控测试设备操作,任务完成后删除配置文件和信任;不要把个人开发 CA 当成移动团队的长期共享基础设施。
项目应该提交什么
仓库可以提交测试域名、生成命令、服务端证书路径约定、.gitignore 和验证脚本。下面这些真实文件必须留在本机或受控 secret store:
.local/mkcert-ca/
certs/*.pem
certs/*.key
certs/*.p12多人项目更稳妥的做法通常是每位开发者拥有自己的本地 CA,仓库只规定叶子证书需要覆盖的名称。共享测试环境若需要统一身份,应进入内部 CA 或短生命周期签发服务,而不是复制某位开发者的 rootCA-key.pem。
这条边界也影响故障半径。个人 CA 私钥泄漏时,只需撤销少量终端的信任;共享 CA 私钥泄漏时,每台信任它的设备都必须清理、重建和重新签发。所谓“方便大家不用安装”常常只是把一次操作换成更大的撤销成本。
卸载要对准原根并验证信任已撤销
保留前面的本机测试服务运行,在同一终端确认仍指向安装时的根,并记录公开指纹。用不带 --cacert 的新 curl 进程验证当前系统信任:它必须先成功,后面的卸载反例才有意义。环境中的 CA 覆盖变量一并排除,避免把显式信任误认成系统信任:
test "$CAROOT" = "$MKCERT_LAB/ca"
mkcert -CAROOT
openssl x509 -in "$CAROOT/rootCA.pem" -noout -fingerprint -sha256
env -u CURL_CA_BUNDLE -u SSL_CERT_FILE -u SSL_CERT_DIR \
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--output /dev/null https://127.0.0.1:8443/若这里不能成功,先检查 system store 是否真的安装、当前 curl 使用哪套 CA,不要拿后续失败证明卸载有效。基线通过后,对同一 CAROOT 和安装时选定的 store 执行:
TRUST_STORES=system,nss mkcert -uninstall
curl_rc=0
env -u CURL_CA_BUNDLE -u SSL_CERT_FILE -u SSL_CERT_DIR \
curl -q --noproxy '*' --silent --show-error \
--output /dev/null https://127.0.0.1:8443/ || curl_rc=$?
printf 'curl_rc=%s\n' "$curl_rc"
test "$curl_rc" -eq 60预期 curl 报证书链不受信且退出码为 60;连接拒绝、超时或 HTTP 错误都不能算卸载通过。NSS 浏览器需关闭旧进程后重新打开并按原根指纹检查;如果安装时还选择了 Java,也应到当时的 JAVA_HOME 对应 store 核对。若任何 store 仍信任旧根,先用对应管理工具按指纹清理,不要删除 CAROOT 后再猜。mkcert 的卸载实现不会删除 CA 文件;显式 --cacert 或 NODE_EXTRA_CA_CERTS 仍能让新客户端信任它,因此这一步只检查系统信任变化。
确认信任撤销后,在同一终端结束前面启动的测试服务,移除 Node 环境变量、服务挂载或容器中的显式 CA,再清理本次实验文件。下面先核对变量和子进程,kill 后用 wait 回收;只删除本次创建的固定文件,目录里若出现其他内容,rmdir 会失败并留下现场供核查,不递归清空未知目录:
MKCERT_STOP_OK=
if test "$CAROOT" = "$MKCERT_LAB/ca" &&
test "$PWD" = "$MKCERT_LAB" &&
ps -p "$MKCERT_SERVER_PID" -o args= | grep -F 'openssl s_server -accept 127.0.0.1:8443' >/dev/null &&
kill "$MKCERT_SERVER_PID"; then
wait "$MKCERT_SERVER_PID" 2>/dev/null || true
MKCERT_STOP_OK=1
else
printf '服务身份或停止结果未确认,暂不清理证书文件\n' >&2
fikill 或进程检查失败时先核对 PID 和端口,不要继续删除服务仍在使用的私钥。确认服务已停止后执行:
test "$MKCERT_STOP_OK" = 1 &&
test "$CAROOT" = "$MKCERT_LAB/ca" &&
test "$PWD" = "$MKCERT_LAB" &&
cd "$MKCERT_ORIGINAL_DIR" &&
rm -- "$MKCERT_LAB/certs/dev.local.test.pem" \
"$MKCERT_LAB/certs/dev.local.test-key.pem" "$MKCERT_LAB/certs/dev-root.pem" &&
rmdir -- "$MKCERT_LAB/certs" &&
rm -- "$CAROOT/rootCA.pem" "$CAROOT/rootCA-key.pem" &&
rmdir -- "$CAROOT" &&
rm -- "$MKCERT_LAB/s_server.log" &&
rmdir -- "$MKCERT_LAB"若最初使用的并非本篇独占目录,不要套用这组文件删除命令,应按实际 CAROOT 和制品归属分别处理。
如果 rootCA-key.pem 曾离开受控终端,删掉本地文件不等于恢复。应撤销所有设备对该根的信任,重建 CA,重新签发叶子证书,并检查仓库历史、镜像层、CI 缓存和备份制品。CA 私钥的风险来自仍然存在的信任关系,而不是文件是否还躺在原目录。
