Logo
“使用 mTLS 保护自建服务”的封面

使用 mTLS 保护自建服务

Avatar

Skyone

科技爱好者

飞牛OS 路径穿越漏洞敲响警钟:仅靠强密码无法防御软件漏洞。

本文详解如何用 mTLS(双向 TLS 认证)保护自建 NAS、Web 服务、API 等,包含反向代理配置(以 Traefik 为例)、OpenSSL 生成证书、浏览器/App 导入证书的指南。

引子:强密码==安全?

相信各位都有所耳闻,最近,国产NAS系统飞牛OS爆出了一个高危漏洞:

2026年初,多位用户和安全社区相继披露:飞牛OS的某些版本存在严重的路径穿越(Path Traversal)0day漏洞。攻击者无需任何身份验证,只需构造特定的URL请求,就能直接读取NAS设备上的任意文件——从系统配置文件、用户存储的照片视频、私钥证书,到家庭相册、财务表格、个人隐私文档……全部暴露在黑客眼前。简单来说,你的NAS瞬间变成了一个“无需密码的公开网盘”,黑客想看什么就能看什么,想下载什么就能下载什么。

这个漏洞影响范围极广,尤其是那些将飞牛NAS通过公网端口、官方内网穿透(fn connect)等方式暴露到互联网上的用户,几乎处于完全裸奔状态。社区反馈显示,大量设备在漏洞曝光前已被批量扫描和利用,甚至出现数据被窃取、设备被植入后门、沦为僵尸网络的情况。飞牛官方随后通过静默推送修复补丁(例如1.1.15及更高版本)试图阻断攻击链,但早期缺乏及时公告、漏洞细节披露迟缓,也引发了不少用户对厂商安全响应态度的质疑。

这件事给我们敲响了警钟:仅仅依赖“强密码 + 登录认证”并不一定安全

为了方便访问,自建服务(NAS、个人云盘、家庭服务器、内部管理后台等)大多跑在公网上~~(当然你自建VPN也可以)~~。软件本身再怎么加固账号密码、再怎么限制弱口令、再怎么开启两步验证,一旦程序逻辑出现哪怕一个低级但致命的漏洞(如路径穿越、命令注入、认证绕过),攻击者就能完全绕过表层的“密码防线”,直接拿到最高权限的数据访问能力。飞牛OS的这次事件,正是最典型的“认证再强也挡不住代码漏洞”的案例。

密码可以改,密钥可以重置,但如果攻击者已经提前把你的所有文件都拷贝走,一切防护都成了马后炮。

那么,面对这类“软件自身漏洞导致的越权访问”,我们还能做些什么?有没有一种机制,能在应用层认证之外再加一道更强硬、更底层的防护墙,让即便软件本身出现严重漏洞,黑客也很难真正拿到有效数据?

答案就是本文要重点探讨的:mTLS(Mutual TLS,双向证书认证)

mTLS 简介

Mutual TLS(简称 mTLS,双向 TLS 认证),也叫双向证书认证,是 TLS 协议的一种扩展认证模式。

默认情况下,TLS 协议只通过 X.509 证书向客户端证明服务器的身份,而客户端到服务器的身份验证则交给应用层处理(比如用户名/密码、Token 等)。不过 TLS 本身也支持通过客户端侧的 X.509 证书来实现客户端身份认证。只不过因为需要提前为每个客户端分发证书、管理证书生命周期,而且对普通终端用户来说体验较差(不像输入密码那么简单),所以在面向消费者的互联网应用中几乎从不使用这种方式。

因此,**mTLS(Mutual TLS)更多出现在企业到企业(B2B)**场景中:客户端数量有限、类型相对统一(大多是程序、脚本、微服务、内部工具等)、运维负担可控,同时对安全性的要求远高于消费级环境。在这类场景下,mTLS 能让通信双方在 TLS 握手阶段就完成双向强身份验证——服务器验证客户端证书,客户端也验证服务器证书——从而建立起真正的“双方互信”通道。

——来自维基百科 - Mutual authentication

简单一句话总结就是:

mTLS = 标准 TLS(服务器证书认证) + 强制要求客户端也提供并验证有效的 X.509 证书

其工作原理如图所示:

mTLS 是如何工作的

这种机制把身份验证从“应用层”下沉到了“传输层加密协议”本身,极大降低了应用代码出现漏洞时被绕过的风险。正如飞牛OS事件所暴露的:即使应用层认证逻辑被路径穿越或命令注入击穿,只要连接本身没有通过 mTLS 建立,黑客就能直接发起请求;反之,如果连接必须持有有效客户端证书才能建立,攻击者即便知道了 URL 和漏洞,没有提供正确的客户端证书,连 TLS 握手都完不成,何谈漏洞利用?

本文不会过多纠结于 mTLS 的密码学细节或理论模型,而是聚焦于最实操的部分:

  • 如何便捷的为自建服务配置 mTLS
  • 如何生成、管理、签发客户端证书(我写了个简单的一键式脚本)
  • 以 Immich 和 BitWarden 为例看看套上 mTLS 后应用是否能正常运行

为自建服务配置 mTLS

生成自签名根证书

在 mTLS 环境中,我们需要一个根证书(Root CA) 来签发服务器证书和客户端证书。这个根证书是整个信任链的起点,自签名即可(因为我们自己控制信任,不需要公网 CA)。

这一步非常简单,并且只需要执行一次,使用 OpenSSL 生成自签名的根 CA 证书和私钥:

openssl req -new -x509 -nodes -sha256 -days 3650 -newkey rsa:4096 \
  -keyout ca.key -out ca.crt \
  -subj "/C=US/O=OrganizationName/CN=CommonName"

解释一下参数:

  • -nodesno DES 的缩写,不对私钥进行加密(不设置密码)
  • -days 3650:CA证书有效期设为 3650 天
  • -subj:这里直接指定证书的主题,只是用来标识。可以选的字段有:
    • C=:国家(Country)
    • ST=:省/州(State/Province)
    • L=:城市(Locality)
    • O=:组织(Organization)
    • CN=:通用名称(Common Name)这个必须有,并且Chrome选择证书时只会显示这一项

执行后,你会得到两个文件:

  • ca.key:根 CA 的私钥
  • ca.crt:根 CA 的公钥证书

这两个文件务必好好保存,以后签子证书要用到。

配置反向代理

这里主要以 Traefik 为例,如果你使用 Nginx,参考 Nginx - Module ngx_http_ssl_module。如果你对Traefik不了解,可以看看我的另一篇文章:使用 Traefik 作为 Docker 的反向代理

首先,需要在动态配置文件中定义 mTLS 策略,创建或编辑 /etc/traefik/dynamic.yml(或你使用的动态配置文件),添加如下键值对:

tls:
  options:
    # 自定义一个名字,例如 mtls
    mtls:
      clientAuth:
        caFiles:
          # 前面生成的根 CA 证书路径(容器内路径)
          - /etc/traefik/ca/ca.crt
        # 表示强制要求客户端提供证书且必须验证通过
        clientAuthType: RequireAndVerifyClientCert

然后在 entrypointrouter 上启用该 mTLS 策略即可,Traefik 的配置非常灵活,你可以选择在 entrypoint 上启用 mTLS,这样其下的全部 router 都会应用此规则。相反,如果你只希望部分域名启用 mTLS,只需在其对应的 router 下配置 mTLS。

  1. 全局强制所有 HTTPS 流量都走 mTLS

    entryPoints:
      websecure:
        address: ":443"
        http:
          tls:
            # mtls 是签名 tls 配置的名称
            # file 是动态配置文件的名称
            options: mtls@file
    
  2. 只对特定服务启用 mTLS

    http:
      routers:
        jellyfin-admin:
          rule: "Host(`admin.mydomain.com`)"
          service: jellyfin
          entryPoints:
            - websecure
          tls:
            # 只在此 router 启用 mTLS
            options: mtls@file
        nextcloud:
          rule: "Host(`cloud.mydomain.com`)"
          service: nextcloud
          entryPoints:
            - websecure
          tls:
            # 同样启用
            options: mtls@file
    

这样服务端的配置就完成啦~

生成客户端子证书

客户端证书(也叫子证书、Leaf Client Cert)是由我们刚刚生成的根 CA(ca.crt / ca.key) 签发的,用于证明客户端的身份。它与根 CA 的关系是信任链: 根 CA 是信任锚点 → 签发客户端证书 → Traefik 只信任由这个根 CA 签发的证书 → 实现 mTLS 双向强认证。

考虑到生成证书的命令参数过多,非常麻烦,我写了一个脚本 make-client.sh,建议直接放到存放 ca.crt 和 ca.key 的目录下运行(例如 ~/ca/),修改脚本开头的几个变量,每次为一个新设备运行一次即可。

#!/usr/bin/env bash

organization_name="Skyone"       # 你的组织/个人的标识
client_name="Windows 10 laptop"  # 设备/用途的描述性名称
client_name_file="win10laptop"   # 保存的文件名(简短且无空格)

set -e

# 创建客户端私钥(RSA 4096)
openssl genrsa -out client.key 4096

# 创建客户端的证书请求 (CSR)
openssl req -new -sha256 \
  -key client.key -out client.csr \
  -subj "/C=US/O=$organization_name/CN=$organization_name $client_name mTLS Client"

# 用 CA 给客户端证书签名, 730 天(约 2 年)
openssl x509 -req -sha256 -days 730 \
  -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out client.crt

# 导出 PKCS#12/.p12 证书 (不带证书链)
# 便于 Windows / macOS / iOS / Android 导入
openssl pkcs12 -export \
  -inkey client.key -in client.crt \
  -out "client-$client_name_file.p12" -name "$client_name_file"

rm client.csr client.key client.crt

运行时会提示你输入导出密码,这是导入到系统密钥链/浏览器时用的密码,建议设置一个临时好记的密码,导入完设备后就可以删除 .p12 文件了。

导入客户端证书

我们刚刚通过 make-client.sh 生成了 PKCS#12 格式的客户端证书文件(.p12),里面同时包含了私钥和证书本身。现在需要将这个 .p12 文件安全拷贝到目标设备上,然后导入系统/浏览器的证书存储中。导入成功后,访问 mTLS 保护的服务时,浏览器或客户端工具就会自动(或手动选择)使用这个证书完成双向认证。

Windows 设备

  1. .p12 文件拷贝到 Windows 电脑(U 盘、局域网共享、加密邮件等方式,完成后立即删除源文件)。
  2. 双击 .p12 文件,系统会启动证书导入向导。
  3. 在“证书存储位置”页面,选择 当前用户下一步
  4. 输入导入时设置的导出密码(脚本运行时提示输入的那个密码)。
  5. 存储位置选择 个人(Personal / My) → 下一步完成
  6. 导入成功后,可通过 certmgr.msc(Win + R 输入)打开证书管理器,在“个人 → 证书”下看到刚导入的证书(名称通常是你设置的 $organization_name $client_name mTLS Client)。

macOS 设备

  1. 双击 .p12 文件,系统会自动打开“钥匙串访问”应用。
  2. 输入导出密码,钥匙串会提示选择存储位置(推荐“登录”钥匙串)。
  3. 导入后,在钥匙串访问中搜索证书名称,可在“我的证书”分类下找到。

iOS / iPadOS

  1. 进入 设置 → 通用 → VPN 与设备管理 → 已下载的描述文件(或直接搜索“证书”)。
  2. 安装描述文件,输入导出密码。
  3. 安装完成后,证书会出现在 设置 → 通用 → 关于本机 → 证书信任设置(部分版本)或直接在 Safari 使用时自动弹出选择。

Android 设备

不同厂商 ROM 路径略有差异(小米/华为/One UI 等可能名字不同),以Pixel手机为例:

  1. 进入 设置→安全与隐私→更多安全和隐私设置 → 加密与凭据(或直接搜索“证书”)。
  2. 选择“安装证书”→ 找到 .p12 文件,输入导出密码。
  3. 安装完成后,证书会出现在“用户证书”列表中。

使用浏览器测试

访问被 mTLS 保护的站点,浏览器会弹出“选择证书”,直接选择刚导入的证书(通常显示你设置的 CN 名称,如 “Skyone Windows 10 laptop mTLS Client”),点击确定。

通过认证后,一切操作都不会有影响。因为 mTLS 是在 TLS 握手时实现的,而 HTTP 协议位于 TLS 内,应用程序根本感受不到 mTLS 的存在,后端程序不需要任何修改。

浏览器选择客户端证书

一些特定应用的说明

启用 mTLS 后,并非所有客户端和服务都会“自动适配”——这取决于应用本身如何处理 TLS 连接和客户端证书。以下我遇见过的常见自建服务场景的实际表现与应对方式,供参考。

浏览器访问的 Web 应用

几乎所有基于浏览器的 Web 应用(包括但不限于我测试过的: Jellyfin、Immich、Nextcloud、Home Assistant、Photoprism、FileBrowser、Portainer、Traefik Dashboard、Vaultwarden、Bitwarden Web 等) 都会照常工作,就像 mTLS 不存在一样,服务端不需要任何配置。

现代浏览器(Chromium based、Firefox、Safari)在 TLS 握手阶段都能自动处理客户端证书。

诸如 Bitwarden 的浏览器插件也是如此,因为它们本身也就是一个 Web 页面。

原生 App

移动端 App 的证书处理能力差异很大,主要取决于其底层网络库:

  • 基于 Android 原生 HttpURLConnectionOkHttp 的 App 都能自动调用 Android 系统级证书,无需额外配置。
  • 自带独立 HTTP 客户端的 App(尤其是基于 Flutter、React Native 的应用)不走系统 TLS 证书存储,需要 App 开发者显式支持证书导入。例如基于 Flutter 开发的 Immich 在 App 的“设置” → “高级” 里有 “SSL 客户端证书”的导入选项。
  • 基于 Web 的套壳 App 实际上大多使用的是 WebView,和浏览器访问一样。

本文作者

Skyone

发布于

2026年3月11日

许可协议

CC BY-NC-SA 4.0

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!


隐私政策

Copyright © Skyone 2026