
简介
公司的 K8s 集群都在内网,在家想连上去看个 Pod 状态、查个日志,要么开跳板机要么搞 VPN。之前一直用 NPS 做端口转发凑合用,但每个服务都要单独配一条隧道,端口多了管理起来很累。
干脆在 K8s 里搭个 WireGuard,连上 VPN 就能直接访问集群网络,跟在内网一样。
选型:wg-easy
WireGuard 本身配置不复杂,但管理客户端有点烦——要手动生成密钥对、写配置文件、分发给同事。wg-easy 是一个带 Web UI 的 WireGuard 管理工具,直接在浏览器里创建/删除客户端,还能下载配置文件或二维码,省事很多。
镜像地址:ghcr.io/wg-easy/wg-easy
网络拓扑
先看看我的架构:
手机/笔记本
↓ WireGuard 加密连接
NPS 服务器 (公网IP:1038/udp)
↓ UDP 隧道穿透
wg-easy Pod (1038/udp)
↓ wg0 解包 → SNAT
K8s 集群网络
因为集群没有公网 IP,我用了 NPS(一个内网穿透工具)把外网的 UDP 流量打到 K8s 节点的 wg-easy pod。
K8s 部署文件
Namespace
apiVersion: v1
kind: Namespace
metadata:
name: wg-easy
PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wg-easy-storage-claim
namespace: wg-easy
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: wg-easy
namespace: wg-easy
spec:
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: wg-easy
spec:
securityContext:
sysctls:
- name: net.ipv4.ip_forward
value: "1"
- name: net.ipv4.conf.all.src_valid_mark
value: "1"
containers:
- name: wg-easy
env:
- name: WG_HOST
value: vpn.example.com
- name: WG_PORT
value: "1038"
- name: INIT_ENABLED
value: "true"
- name: INIT_IPV4_CIDR
value: 10.8.0.0/24
- name: INIT_USERNAME
value: admin
- name: INIT_PASSWORD
value: 你的密码
- name: INIT_HOST
value: 你的NPS公网IP
- name: INIT_PORT
value: "1038"
- name: DISABLE_IPV6
value: "true"
image: ghcr.io/wg-easy/wg-easy:15.3.0
ports:
- containerPort: 51820
name: wg
protocol: UDP
- containerPort: 51821
name: http
protocol: TCP
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_MODULE
volumeMounts:
- mountPath: /etc/wireguard
name: config
volumes:
- name: config
persistentVolumeClaim:
claimName: wg-easy-storage-claim
Service(HTTP Web UI)
apiVersion: v1
kind: Service
metadata:
name: wg-easy-http
namespace: wg-easy
spec:
ports:
- name: http
port: 51821
protocol: TCP
targetPort: http
selector:
app.kubernetes.io/name: wg-easy
type: ClusterIP
再配个 Ingress 就能通过域名访问 Web UI 了。
踩坑记录
坑1:INIT_ENABLED 必须开
我最开始只配了一堆 INIT_* 变量,以为它们会自动生效。结果发现 wg0.conf 里压根没按我写的来。
去翻文档才发现——INIT_ENABLED=true 不写,所有 INIT_ 变量都是摆设。
坑2:Pod 内 IPv6 被禁
容器起来后 Web UI 能访问,但点进去就报 500:
Unable to access interface: No such device
wg show 看不到 wg0 接口。手动 wg-quick up wg0 发现:
ip -6 address add fdcc:ad94:bacf:61a4::cafe:1/112 dev wg0
RTNETLINK answers: Permission denied
集群的 Pod 里 IPv6 默认是禁用的(disable_ipv6=1),但 wg-easy 生成的 wg0.conf 默认带了 IPv6 地址,加不上去导致整个 wg-quick up 失败。
解决方法:加环境变量 DISABLE_IPV6=true,让 wg-easy 只生成 IPv4 配置。
坑3:缺少 sysctl
WireGuard 需要内核开启 IP 转发,不然客户端连上也没法转发流量到集群网络。在 Pod spec 里加:
spec:
securityContext:
sysctls:
- name: net.ipv4.ip_forward
value: "1"
- name: net.ipv4.conf.all.src_valid_mark
value: "1"
网络连通原理
客户端连上 VPN 后(分配 IP 10.8.0.2),想去访问集群里的某个 Pod(比如 10.4.x.x),流量是这样的:
- 客户端查路由表,
10.4.0.0/16走 WG 隧道 - 加密后发给 NPS 公网 IP
- NPS 转发到 K8s wg-easy Pod 的
1038/udp - 解密后从
wg0接口出来 - 内核做 SNAT(MASQUERADE),源 IP 变成 Pod 自己的 IP
- 请求到达目标 Pod,正常回包
关键就是 wg-easy Pod 里的 iptables 规则(自动生成的):
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
这行把所有从 VPN 出来的流量 SNAT 成 Pod 自己的 IP,这样在 K8s 网络里就跟普通 Pod 通信一样,不需要额外的路由配置。
客户端配置
在 wg-easy Web UI 创建客户端后,下载的配置文件大概这样:
[Interface]
PrivateKey = xxxx
Address = 10.8.0.2/32
DNS = 1.1.1.1
MTU = 1420
[Peer]
PublicKey = xxxx
PresharedKey = xxxx
AllowedIPs = 你的集群网段, 10.8.0.0/16
Endpoint = 你的NPS公网IP:1038
PersistentKeepalive = 25
注意几点:
- AllowedIPs 要包含集群网段,不然客户端不会把访问集群的流量送进 VPN 隧道
- PersistentKeepalive 建议设成 25,UDP 穿透长时间空闲会断,保活包能维持连接
- Endpoint 配的是 NPS 的外网地址和端口
验证连接
服务端看连接状态:
kubectl exec -n wg-easy <pod> -- wg show
连上了会显示:
peer: xxxx
endpoint: 客户端公网IP:端口
allowed ips: 10.8.0.2/32
latest handshake: 1 minute ago
transfer: 1.23 KiB received, 456 B sent
没连上就只看到 endpoint: (none) 和 latest handshake: (never)。
客户端连上后,直接在终端 ping 一个集群内的 Pod IP 试试:
ping 10.4.x.x
通了就说明大功告成。
欢迎关注我的博客 www.bboy.app
Have Fun
