mkcert 本地开发 CA 与 HTTPS 证书工具手册
本地 HTTPS 的难点不在签出一张证书
自签叶子证书很容易生成,麻烦的是每个浏览器都弹告警、Java 和 Node 又各自拒绝,团队最后把关闭校验写进脚本。mkcert 的价值是为开发机创建一套本地 CA,把它装进选定的信任库,再用这套 CA 签发包含正确 SAN 的开发证书。它不会替 Nginx、Caddy、应用框架或容器配置 HTTPS,也不适合给生产和最终用户设备发证书。
先确认安装来源和实际二进制。macOS 常用 Homebrew,Windows 可用 Chocolatey 或 Scoop,Linux 既可以使用发行版包,也可以使用项目发布的二进制;Firefox/NSS 支持在部分系统上还需要 certutil。团队脚本不要下载一个不校验来源的可执行文件后直接提权运行。
command -v mkcert
mkcert -version
mkcert -help
mkcert -CAROOT项目官方 README 是安装方式、支持的 root store 和参数顺序的权威入口。发行节奏与操作系统信任策略并不相同,升级时应在隔离开发机重放安装、签发和卸载,而不是只看 mkcert -version 能运行。
install 会改变哪些本地状态
第一次执行 mkcert -install 时,mkcert 会在应用数据目录创建本地根证书和私钥,并尝试把根证书安装进可识别的信任库:
mkcert -install
mkcert -CAROOTmkcert -CAROOT 打印的目录至少需要区分两个文件。rootCA.pem 是公开根证书,可以在明确授权的开发环境中用于建立信任;rootCA-key.pem 是 CA 私钥,拿到它的人可以签发任何被这台机器信任的名称。后者不能进入 Git、镜像、备份制品、工单、聊天软件或共享网盘。
mkcert 可以识别系统、NSS 和 Java 三类 store。需要限制安装范围时,先为当前命令设置 TRUST_STORES,而不是让工具扫描到什么就改什么:
TRUST_STORES=system,nss mkcert -install如果包含 java,JAVA_HOME 会影响 mkcert 找到的 Java 信任库。IDE 自带运行时、Gradle daemon、容器和 CI 仍可能使用另一套 JDK;“mkcert 已显示安装 Java store”不能证明所有 Java 进程都会信任。Java 侧应使用 keytool 对真实运行时复核 alias 和指纹。
为不同项目或安全域维护独立 CA 时,可以在执行前设置 CAROOT。这改变的是 mkcert 存放和查找 CA 的目录,不是把已有私钥自动迁移过去:
export CAROOT="$PWD/.local/mkcert-ca"
mkcert -CAROOT
mkcert -install这个目录仍必须被版本控制忽略,并限制文件权限。多个 CAROOT 提高隔离性,也增加轮换和卸载责任;团队需要记录哪一个开发环境信任了哪一个根,而不是把同一个 CA 私钥复制给所有人。
名称列表决定证书身份
mkcert 的位置参数就是证书 SAN。DNS 名称、IPv4、IPv6 和通配符会按各自类型写入证书。项目常用的一组名称可以这样生成:
mkdir -p certs
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 读取结果,但不应在 mkcert 文中复制整套 TLS 排障教程:
openssl x509 -in certs/dev.local.test.pem -noout \
-subject -issuer -fingerprint -sha256 \
-ext subjectAltName -dates把证书和私钥接入服务后,再按 OpenSSL 严格握手同时验证 SNI、链和主机名。mkcert 只负责生成文件;如果服务仍发送旧证书,修复点在服务配置、挂载、reload 或负载均衡节点,不在 mkcert -install。
用反例区分 SAN 与信任问题
一台已经信任本地 CA 的开发机访问 dev.local.test 成功,并不能证明证书适合 api.local.test。先保持同一根和同一服务,只改请求主机名;若得到 hostname mismatch,说明 CA 信任成立,缺的是 SAN。重新签发包含目标名称的叶子证书即可,不需要重建根 CA。
再把 rootCA.pem 作为显式信任锚交给一个没有执行过 mkcert -install 的隔离容器,验证应当成功;去掉该显式 CA 后应当失败。这个反例证明容器自己的 CA store 与宿主机信任相互独立。
docker run --rm \
-v "$PWD/certs:/work:ro" \
curlimages/curl:latest \
--cacert /work/dev-root.pem \
https://host.docker.internal:8443/示例中的镜像标签在团队仓库里应锁定到批准的版本或摘要;这里的重点是挂载公开 CA,而不是把 CA 私钥烘焙进镜像。Linux 容器访问宿主机的地址还取决于运行时网络配置,连接失败要先与证书失败分开。
Node、浏览器、Java 和 Docker 不共享一个世界
mkcert 官方说明支持系统、NSS 和在 JAVA_HOME 可见时的 Java store,但运行时行为仍要按实际版本验证。Node 可以通过 NODE_EXTRA_CA_CERTS 追加 PEM 根证书,这个变量只在进程启动时读取;应用如果在 TLS 请求里显式提供 ca,默认和额外 CA 都不会继续参与。
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
node scripts/check-local-https.mjs支持系统 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 私钥泄漏时,每台信任它的设备都必须清理、重建和重新签发。所谓“方便大家不用安装”常常只是把一次操作换成更大的撤销成本。
卸载要先撤销信任,再删除文件
卸载顺序不能反过来。先让 mkcert 使用原来的 CAROOT 撤销它安装的信任,再保存公开指纹并删除 CA 目录:
mkcert -CAROOT
mkcert -uninstall如果先删除 rootCA.pem 和 CAROOT,工具可能失去定位旧根的材料,系统、浏览器或 Java store 里的信任却仍然存在。之后还要删除叶子证书和私钥,移除服务挂载、Node 环境变量、容器层与启动配置,最后卸载二进制。
清理后的验证要启动全新的浏览器和运行时进程。对原来的测试服务执行严格握手,应从成功变为明确的不受信错误;如果仍成功,说明系统、NSS、Java、Node 或应用显式 CA 中至少还有一个副本。此时用对应 store 的管理工具按指纹删除,不能靠重复执行 mkcert -uninstall 猜测。
如果 rootCA-key.pem 曾离开受控终端,删掉本地文件不等于恢复。应撤销所有设备对该根的信任,重建 CA,重新签发叶子证书,并检查仓库历史、镜像层、CI 缓存和备份制品。CA 私钥的风险来自仍然存在的信任关系,而不是文件是否还躺在原目录。
