前言
自从开始兼职运维的工作,遇到了很多问题,当然也从解决问题的过程中学到很多知识,这不, 问题又来了,一个同事艾特我,说他的的 VPN 连接不上了。经过进一步询问,原来是 openVPN 客户端 打不开,而不是 VPN 连接不上,软件兼容性问题,很快修复了。但这也让我思考了一个问题, 如果真是 VPN 连接不上,我该怎么排查呢,这方面的知识不清楚,所以要把知道储备一下。
为什么需要OpenVPN?
先来思考一下为啥需要 OpenVPN 呢?
想象一下,公司的内部网络是一座戒备森严的城堡,里面藏着代码库(GitLab)、数据库、监控系统等各种"宝藏"(服务)。 而公共互联网,则是城堡外广阔但危险的荒野。 如果城堡没有防御,任何路人都能进来瞎逛,这显然不行。OpenVPN就是解决这个问题的完美方案,它包含两个核心部件:
- OpenVPN 服务器:城堡唯一吊桥的守卫。他的职责是只允许身份被确认的自己人上桥。
- iptables:城堡坚固的城墙和城门卫兵。他们的命令是:"除了从吊桥(VPN)来的自己人,其他从外面来的所有人都不准进!"
所以,流程就是:先通过 OpenVPN 这个"守卫"认证身份,走上"吊桥",然后 iptables 这个"城墙卫兵"才会允许你进入"城堡"内部。
OpenVPN 如何确认你的身份?------ 介绍信 vs 工牌
介绍信 和 工牌 是 OpenVPN 最常用的两种认证方式。
证书认证(介绍信)
这种方式就像古代的通关文牒。每个人(每个设备和用户)都有一封独一无二、由公司权威机构(CA) 签发的介绍信(证书)。
流程: 守卫(OpenVPN服务器)会非常仔细地检查你的介绍信上的钢印(CA签名)是否真实。只要介绍信是真的,就放行。
优点: 极难伪造,安全性极高。即使密码泄露,没有对应的介绍信也无法连接。
缺点: 管理稍微复杂,需要为每个需要连接的人生成并分发一对唯一的证书和密钥。
用户名/密码认证(工牌)
这种方式更直接,就像出示工牌并输入密码。
流程: 守卫(OpenVPN服务器)把收到的工牌号和密码,交给城堡里的一位专门的验证员(比如监测脚本) 去查档案。验证员跑去公司的花名册(比如数据库)里核对,确认无误后通知守卫放行。
优点: 非常方便,用户只需要记住密码,无需管理证书。
缺点: 密码泄露风险,安全性相对较低。
OpenVPN 如何使用?
OpenVPN 的使用可以分为两大角色: 服务器端(Server) 的部署和客户端(Client) 的连接。
- 服务器 (Server) :一个拥有公网IP的机器,运行着 OpenVPN 服务端进程,监听来自客户端的连接。它就像是公司的大门。
- 客户端 (Client) :需要接入内网的电脑或手机,上面安装了 OpenVPN 客户端软件,并持有配置文件(
.ovpn
文件)。它就像是员工的工牌。 - 配置文件和证书 :这是双方建立信任和加密的基础。
服务器端(Server)安装与配置
这是在 VPN 服务器上进行的操作。我们使用的是 Ubuntu ,这里就以 Ubuntu 为例来说。如果不知道你用的是啥系统,可以执行 cat /etc/os-release
查看。
安装 OpenVPN
使用包管理器安装:
sql
sudo apt update
sudo apt install openvpn easy-rsa
OpenVPN 证书认证的密钥和证书生成
可以把这一整套流程想象成为一个国家建立一套护照系统。
整体比喻:护照系统
- 根证书 (CA Certificate)-> 外交部 + 护照印制厂 :是整个系统信任的根基,负责签发所有护照。
- 服务器证书 (Server Certificate)-> 国家的边防检查章 :贴在国门上,用来验证这个国家(服务器)是真实的,不是假冒的。
- 客户端证书 (Client Certificate)-> 公民的个人护照 :每个公民(客户端)独一无二的身份证明。
- DH 参数 (Diffie-Hellman Parameters)-> 外交密电码本 :用于双方在公开场合安全地协商出一把只有他们知道的临时会话密钥。
- TLS 密钥 (TLS-Auth Key)-> 边防暗号 :一个预先共享的简单暗号,用于快速识别和丢弃非法闯入者(DDoS攻击、随机扫描流量)。
- CRL (Certificate Revocation List)-> 通缉令名单 :列出了所有已经被作废的护照(证书),即使护照本身看起来没问题,只要在名单上就拒绝入境。
1. 初始化证书生成环境
bash
cd easy-rsa/
./easyrsa init-pki
- 干什么用 :初始化一个全新的
pki
(Public Key Infrastructure, 公钥基础设施) 目录结构。这会创建所有需要的子目录(private
,issued
,reqs
等)来存放后续生成的证书和密钥。 - 何时使用: 只在第一次搭建 OpenVPN CA,或者想彻底重置所有证书时使用。如果已经有了一套在用的证书,千万不能执行这一步,否则会清空所有现有证书。
2. 初始化CA证书
bash
./easyrsa --batch build-ca nopass
- 干什么用 :创建最重要的根证书(ca.crt) 和根密钥(ca.key)。
nopass
表示不给根密钥加密,这样启动OpenVPN服务时就不需要输入密码,方便自动化,但安全性稍低(如果服务器被攻破,密钥直接被盗)。 - 何时使用 :同第1步,只在最初搭建时做一次。
--batch
是无人值守模式,自动使用默认值。 - 重要性 :这是所有信任的源头。
ca.key
必须被绝对安全地离线保管! 谁拥有它,谁就能签发任何证书。
3. 初始化服务器证书
ini
EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass
-
干什么用 :为OpenVPN服务器生成一个证书(
server.crt
)和私钥(server.key
)。这个证书由步骤2的CA签名。EASYRSA_CERT_EXPIRE=3650
:设置证书有效期为10年(3650天)。server
:是服务器证书的通用名(Common Name)。nopass
:同样,不对服务器密钥加密。
-
何时使用 :为你的OpenVPN服务器生成身份证明。 每个服务器都需要一个。
4. 初始化客户端证书
ini
EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full client nopass
-
干什么用 :为客户端 (用户)生成一个证书(
client.crt
)和私钥(client.key
)。这个证书同样由CA签名。client
:是第一个客户端的通用名。在实际中,你需要为每个用户执行一次此命令,并使用不同的名字(如./easyrsa build-client-full zhangsan nopass
)。
-
何时使用: 为每一个需要连接VPN的用户/设备生成一个唯一的客户端证书 。这是"一人一证"的基础。
5. 初始化 crl.pem 文件
ini
EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl
- 干什么用 :生成证书吊销列表(CRL)文件
pki/crl.pem
。如果某个员工的电脑丢了或者离职了,你可以吊销他的证书,并将其加入这个"黑名单"。 - 何时使用: 初始搭建时生成,之后每当需要吊销某个客户端证书时,都需要重新生成此文件 (使用
./easyrsa revoke client-name
然后再次gen-crl
),并更新到服务器上。
6. 生成DH参数
bash
./easyrsa gen-dh
- 干什么用 :生成迪菲-赫尔曼(Diffie-Hellman)参数文件
pki/dh.pem
。这个过程很慢(需要生成大素数),但它对于实现前向保密(Forward Secrecy) 至关重要------即使服务器私钥未来被泄露,过去的通信记录也无法被解密。 - 何时使用: 初始搭建时生成一次即可 ,除非你需要升级密钥长度(如从2048位升级到4096位)。
7. 生成TLS密钥文件
css
openvpn --genkey --secret ta.key
- 干什么用 :生成一个静态的TLS认证密钥
ta.key
。它不是一个证书,而是一个简单的共享密钥。 - 何时使用: 初始搭建时生成一次即可 。用于抵御DDoS攻击和端口扫描,非法的连接尝试在TLS层面就会被丢弃,减轻服务器负担。这是一个可选项,但强烈推荐。
8. 统一存储服务端所需文件
bash
cp ta.key pki/ca.crt pki/dh.pem pki/private/server.key pki/issued/server.crt pki/crl.pem /etc/openvpn/server/
- 干什么用 :将服务器运行所需要的所有文件集中放到OpenVPN的服务配置目录下。
- 何时使用: 每次你更新了以上任何文件(如服务器证书、CRL列表)后,都需要重新复制到服务器目录,并重启OpenVPN服务才能生效。
9. 验证服务器证书
objectivec
openssl verify -CAfile ca.crt -purpose sslserver server.crt
- 干什么用 :这是一个验证步骤 。它使用你自建的CA(
ca.crt
)去验证服务器证书(server.crt
)是否由该CA合法签发,并且其用途(sslserver
)是否正确。 - 何时使用: 在将证书部署到服务器之前,用于检查证书是否生成正确 。如果验证失败,说明证书有问题,不能使用。
这套流程构建了一个安全、可靠的身份认证基础,是OpenVPN企业级应用的基石。
编写服务器配置文件
创建一个服务器配置文件,这是大脑。
bash
vim /etc/openvpn/server.conf
一个最基础的配置示例:
bash
# 基本设置
port 1194
proto udp
dev tun
# 证书和密钥
ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/server.crt
key /etc/openvpn/server/server.key
dh /etc/openvpn/server/dh2048.pem
# 网络设置
server 10.8.0.0 255.255.255.0 # 分配给客户端的VPN内网IP段
ifconfig-pool-persist /var/log/openvpn/ipp.txt
push "redirect-gateway def1 bypass-dhcp" # 告诉客户端所有流量都走VPN
push "dhcp-option DNS 8.8.8.8" # 给客户端推送DNS服务器
# 安全和性能
keepalive 10 120
cipher AES-256-CBC
user nobody
group nogroup
persist-key
persist-tun
# 日志
status /var/log/openvpn/openvpn-status.log
log /var/log/openvpn/openvpn.log
verb 3
# 允许客户端之间互相访问
client-to-client
;mute 20
explicit-exit-notify 1
auth-user-pass-verify /etc/openvpn/checkpwd via-env
client-cert-not-required
username-as-common-name
script-security 3
reneg-sec 0
;up /etc/openvpn/update-resolv-conf
;down /etc/openvpn/update-resolv-conf
部分配置详解:
-
日志详细级别 (Verbosity Level)
verb 3
-
作用 :设置 OpenVPN 日志的详细程度。
-
解读:
0
:静默,只输出致命错误。1-4
:普通运行级别。3
是一个很好的平衡点,会记录连接、断开等重要信息,但不会产生太多噪音。5-9
:调试级别。如果遇到连接问题,可以暂时提高到5
或6
来获取更详细的日志。
-
维护 :日常运行保持
3
;排查问题时调高。
- 静默重复消息 (Mute Repeated Messages)
ini
;mute 20
- 作用 :防止相同的日志消息重复输出,刷屏。最多只连续输出 20 条同类消息。
- 解读 :行首的分号
;
表示该行是注释 ,配置未生效。如果需要抑制重复日志,可以删除分号。
- 显式退出通知 (Explicit Exit Notify)
vbnet
explicit-exit-notify 1
- 作用 :当服务器重启或停止时,主动通知客户端,让客户端能够更优雅地处理断开连接,并可能自动重连。
- 解读 :这通常在使用 UDP 协议时是必要的,因为 UDP 是无状态的。如果服务器使用的是 TCP 协议,这个选项可能就不需要了。
- 用户名/密码验证脚本 (Authentication Script)
bash
auth-user-pass-verify /etc/openvpn/checkpwd via-env
- 作用 :这是最重要的配置之一。它告诉 OpenVPN,当客户端连接时,不要只相信证书,还要执行一个外部脚本
/etc/openvpn/checkpwd
来验证客户端提供的用户名和密码。 via-env
:表示通过环境变量将用户名和密码传递给脚本(而不是通过临时文件)。脚本会接收到username
和password
这两个环境变量。- 维护:
/etc/openvpn/checkpwd
这个脚本是运维自定义的。它可能连接了公司的一个数据库来验证账号密码。 维护用户就是维护这个脚本所对接的系统。
- 不要求客户端证书 (No Client Certificate Required)
swift
client-cert-not-required
- 作用 :明确告诉 OpenVPN 服务器不要要求客户端提供证书。
- 解读 :这直接关联上一条配置。既然使用了用户名/密码认证,就不再需要每个客户端都拥有一个独一无二的证书了。这简化了客户端的部署(只需要一个通用的
.ovpn
文件),但将安全重心完全放在了用户名/密码上。
- 使用用户名作为通用名 (Username as Common Name)
csharp
username-as-common-name
- 作用 :在内部,OpenVPN 需要一个"通用名"(Common Name, CN)。当不使用客户端证书时,这个选项告诉服务器使用"用户名"来填充这个字段。
- 解读 :这对于日志记录和后续的脚本处理非常有用。在日志和连接列表中,你会看到具体的用户名(如
zhangsan
),而不是一个证书的 CN 或未知标识。
-
脚本安全级别 (Script Security Level)
script-security 3
-
作用 :允许 OpenVPN 执行外部脚本。
-
解读:
0
:不允许执行任何脚本。1
:允许执行内置脚本(如--up
,--down
)。2
:允许执行内置脚本和自定义脚本。3
: (允许via-env
传递密码) 这是为了允许像auth-user-pass-verify
这样的脚本通过环境变量接收密码所必需的级别。 不要随意设置为更高的值,存在安全风险。
- 禁用重新协商 (Disable Renegotiation)
matlab
reneg-sec 0
- 作用 :设置为
0
表示完全禁用 TLS 会话重新协商。 - 解读 :重新协商是一种安全机制,可以定期更新加密密钥。但在某些历史版本中,存在与重新协商相关的漏洞。强制禁用它是一种安全加固措施,确保连接一旦建立,其密钥材料就不会被刷新(可能被攻击)。现代版本的 OpenVPN 通常不需要这样设置,但这表明之前的运维非常注重安全。
- 更新解析器配置脚本 (DNS Scripts - 已注释)
bash
;up /etc/openvpn/update-resolv-conf
;down /etc/openvpn/update-resolv-conf
- 作用 :这些脚本用于在 VPN 连接建立(
up
)或断开(down
)时,自动更新客户端的 DNS 服务器设置。 - 解读 :行首的分号
;
表示这些配置未启用 。这意味着连接 VPN 后,客户端可能无法自动使用公司内部的 DNS 服务器来解析内部域名(如gitlab.company.com
)。用户可能需要手动配置 DNS。
启动 OpenVPN 服务并设置开机自启
perl
sudo systemctl start openvpn@server
sudo systemctl enable openvpn@server
# 检查状态
sudo systemctl status openvpn@server
第二部分:客户端(Client)连接
这是在需要连接 VPN 的用户电脑上进行的操作。
- 创建
.ovpn
配置文件: 创建一个名为client.ovpn
的文件,内容如下。
vbnet
client
dev tun
proto udp
remote your_server_ip 1194 # 将 your_server_ip 替换成你服务器的公网IP
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
tls-auth tls.key 1
auth-user-pass
cipher AES-256-CBC
verb 3
-
将
client.ovpn
文件以及前面生成的客户端证书ca.crt、client.crt、client.key、tls.key
安全地发送给用户。 -
导入配置:
- 打开 OpenVPN Connect 客户端。
- 点击 "导入" 或 "File" -> "Import",选择下载的
client.ovpn
文件。 - 配置会出现在列表中。
-
连接: * 在客户端列表中,点击 "连接"。 * 首次连接可能会询问你是否信任此连接,选择 yes。 * 如果配置正确,状态会显示为"已连接"。
再谈 via-env
前面我们提到配置文件中有auth-user-pass-verify /etc/openvpn/checkpwd via-env
,
via-env 的真实含义是: OpenVPN 主进程在调用(fork)/etc/openvpn/checkpwd 这个子进程之前,会为这个即将诞生的子进程单独设置好 username 和 password 这两个环境变量。
这个过程可以类比为:
OpenVPN 主进程(父亲)准备让一个脚本(儿子)去办事。
- 父亲拿了两张纸条,一张写著 username="zhangsan",另一张写著 password="abc123"。
- 父亲生了一个儿子(checkpwd 进程),只在交给儿子的档案袋里放进了这两张纸条。
- 儿子打开档案袋,能看到这两张纸条,并据此办事。
- 父亲自己手里没有这两张纸条,家里(系统)的其他成员(其他进程)也完全看不到这个档案袋里的内容。
所以,username 和 password 这两个环境变量:
只存在于 /etc/openvpn/checkpwd 这个脚本进程的运行生命周期内。
只对这个脚本进程可见。
在脚本执行结束后,这个进程被销毁,这两个环境变量也就随之消失了。
OpenVPN 主进程自身、系统上的其他用户、其他进程都无法直接看到它们。这是一种安全措施。
在脚本中是这么获取的:
css
username, password := os.Getenv("username"), os.Getenv("password")
OpenVPN 连接维护的核心机制: 会话重新协商
脚本中,有这样一段日志:
ini
_, _ = fmt.Fprintln(file, fmt.Errorf("[%s] Authentication Successfully for User: %q",
now, username))
然后,我去查看日志文件, 输出内容如下:
csharp
[2025-08-21 15:08:55] Authentication Successfully for User: "zhangsan"
...
[2025-08-21 16:08:55] Authentication Successfully for User: "zhangsan"
...
[2025-08-21 17:08:55] Authentication Successfully for User: "zhangsan"
可以看到,日志每隔1小时就会打印一次,这并不是有人在频繁登录,而是 OpenVPN 的 "会话重新协商"(Session Renegotiation) 过程。
配置文件中特殊配置(reneg-sec 0 失效)?
配置文件里有一行 reneg-sec 0,它的本意是禁用重新协商。但是,这个设置有一个众所周知的限制:
reneg-sec can only be used on the client side.
也就是说,在服务器端配置 reneg-sec 0 是无效的。服务器无法单方面禁用重新协商,它只能接受客户端发起的重新协商请求。如果大多数客户端没有配置 reneg-sec 0,那么重新协商就依然会发生。
配置文件中特殊配置(client-cert-not-required + auth-user-pass-verify): 配置了 client-cert-not-required(不要求客户端证书),在重新协商时,客户端没有证书可以提供来进行身份验证。 因此,OpenVPN 服务器退而求其次,重新触发了最初的认证方式------也就是调用 auth-user-pass-verify 脚本,再次验证用户名和密码。
简单总结一下这个循环:
重新协商事件发生 → 客户端无证书可用 → 服务器要求再次进行用户密码认证 → 触发 /etc/openvpn/checkpwd 脚本 → 脚本验证成功并打印日志 Authentication Successfully
为什么频率是 1 小时?
OpenVPN 的默认重新协商时间就是 3600 秒(1 小时)。这意味着任何没有显式禁用重新协商的客户端,都会每隔一小时向服务器发起一次重新协商请求,从而触发上述的整个认证流程。
最后
到这里 OpenVPN 的搭建及使用就算完成了,知识点还是比较多的,篇幅也比较长,但也只是着重介绍了 OpenVPN 服务器,还有另一块 iptables 放到之后的文章来聊。