K3s 配置 Envoy Gateway:结合 Cloudflare Tunnel 与 Tailscale 实现内外网服务隔离
✨ 笔记记录了在 K3s 中通过 Envoy Gateway 实现服务内外网隔离的运维配置过程,包括利用 Tailscale 接入 LoadBalancer 暴露私有服务、通过 Cloudflare Tunnel 暴露公开服务,并结合 cert-manager 自动签发 Let's Encrypt TLS 证书以及配合 external-dns 实现 Cloudflare DNS 记录的自动化管理。
这几天重装迁移了一台线路较普通的服务器,正好当实验环境试试 Envoy Gateway,顺便记录下大致配置过程。
在 AI Agent 大行其道的今天,这种操作方式多少显得有点“古法运维”。AI 固然能几秒钟把服务拉起来,但如果不细致审视底层的组装细节,配置次数多了时间长了,整个就容易变成“黑盒”。按需配置、理清关键组件的流量走向,这种清晰的掌控感还是不错的。(也可能是”登味儿/守旧派“,或者说单纯烧不起 Token 🤣
配置概览
目标效果:在同一个 K3s 集群内,将公开服务与私有服务从入口层进行彻底隔离:
- 私有服务:通过 Envoy Gateway 暴露为
LoadBalancer类型的 Service,配合集群内 K3s 自带的 ServiceLB(原 Klipper LoadBalancer),借助 Tailscale 节点网络进行内网访问。 - 公开服务:通过 Envoy Gateway 暴露为
ClusterIP类型的 Service,流量仅在集群内部流转,前端由部署在集群内的cloudflared容器以 Cloudflare Tunnel 方式反向代理出去。
[GatewayClass: envoy-gateway] (控制面控制器)
│
├──> [Gateway: envoy-gateway-tailscale] ──> Service类型: LoadBalancer (Tailscale 内网接入)
│
└──> [Gateway: envoy-gateway-cloudflare] ──> Service类型: ClusterIP (Cloudflare Tunnel 接入)此外,配合 cert-manager 完成 Let's Encrypt TLS 证书自动签发,并利用 external-dns 根据网关类型自动同步 DNS 记录(Tailscale 自动建 A 记录,Cloudflare Tunnel 自动建 CNAME 记录)。
K3s 配置安装
https://k3s.io/基础的系统配置和 Tailscale 安装此处略去不表。
配置文件 /etc/rancher/k3s/config.yaml
write-kubeconfig-mode: "0600"
# token: "" # 强随机字符串 (openssl rand -hex 16)
token-file: /etc/rancher/k3s/token
bind-address: "0.0.0.0"
node-ip: "<Tailscale-IP>"
node-external-ip: "<Tailscale-IP>"
flannel-external-ip: true
tls-san:
- "127.0.0.1"
- "localhost"
- "<Tailscale-IP>"
- "<HostName>"
- "<Tailscale-MagicDNS>"
- "<个人域名>"
disable:
- traefik
kube-proxy-arg:
- "proxy-mode=nftables"
# 基础的 kubelet 参数就放在这,官方建议是使用专门的 kubelet 配置文件
kubelet-arg:
- "max-pods=110"
- "image-gc-high-threshold=80"
- "image-gc-low-threshold=60"
- "container-log-max-size=10Mi"
- "container-log-max-files=3"
- "eviction-hard=memory.available<300Mi,nodefs.available<2Gi"
- "serialize-image-pulls=false"
# 需调整内核参数
protect-kernel-defaults: true
secrets-encryption: truetoken-file 生成
mkdir -p /etc/rancher/k3s
TOKEN_FILE="/etc/rancher/k3s/token"
if [[ ! -f "${TOKEN_FILE}" ]]; then
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 16 > "${TOKEN_FILE}"
else
head -c 32 /dev/urandom \
| base64 \
| tr -dc 'a-zA-Z0-9' \
| head -c 32 > "${TOKEN_FILE}"
fi
chmod 600 "${TOKEN_FILE}"
else
log "检测到已有 token,跳过生成"
fi内核参数配置
cat >/etc/sysctl.d/99-k3s-kubelet.conf <<EOF
# Kubernetes network forwarding
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# Bridge netfilter
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
# Protect kernel defaults requirements
vm.overcommit_memory = 1
kernel.panic = 10
kernel.panic_on_oops = 1
EOF
sysctl --system >/dev/null安装命令 curl -sfL https://get.k3s.io | sh -

面板用的是 Radar 项目,本机启动,CLI + Web 形式,提供 MCP Server
https://github.com/skyhook-io/radarEnvoy 网关部署
https://gateway.envoyproxy.io/直接用 Helm 安装 Envoy Gateway,可配置项:https://gateway.envoyproxy.io/docs/install/gateway-helm-api/
# 安装
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace --set config.envoyGateway.logging.format=json
# 等待 Envoy Gateway 启动就绪
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available
控制面安装好了,接下来就是 GatewayClass、Gateway、HTTPRoute 以及数据面的网络/路由发布。
创建 Gateway Class
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller创建 EnvoyProxy 配置,覆写 Envoy Gateway Controller 之后生成的 Service 名称,
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: cloudflare-proxy-config
namespace: envoy-gateway-system
spec:
provider:
type: Kubernetes
kubernetes:
envoyService:
type: ClusterIP
name: envoy-svc-fixed创建 Gateway,其中 Cloudflare Tunnel 对应引入上面的 Envoy 自定义覆写配置,Tailscale 不用
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: cloudflare-gateway
namespace: envoy-gateway-system
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
infrastructure:
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: cloudflare-proxy-config
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tailscale-gateway
namespace: envoy-gateway-system
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All- Tailscale Gateway:Service 为默认的
LoadBalancer。K3s 的 ServiceLB 检测到后,会在节点启动 Pod 进行端口转发与流量透传。 - Cloudflare Gateway:引入自定义
EnvoyProxy配置,指定 Service 为ClusterIP,仅限 K3s 内网访问。
Tailscale 链路测试
用 Kubernetes 的 httpbin 测试服务
Deployment + Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: httpbin
template:
metadata:
labels:
app: httpbin
spec:
containers:
- name: httpbin
image: kennethreitz/httpbin
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
namespace: default
spec:
selector:
app: httpbin
hostnames:
- httpbin.okhk.net
ports:
- port: 80
targetPort: 80HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: httpbin
namespace: default
spec:
parentRefs:
- name: tailscale-gateway
namespace: envoy-gateway-system
sectionName: http
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: httpbin
port: 80本地可以通过修改 hosts 或在内网 DNS 中配置域名指向 Tailscale 节点 IP,即可成功访问。

Cloudflare Tunnel 接入
上面验证了 Tailscale 的接入,下面开始接入 Cloudflare Tunnel,将其运行在 K3s 内部,直接对接 Envoy。
Namespace
apiVersion: v1
kind: Namespace
metadata:
name: cloudflareSecret
在 Cloudflare Zero Trust 处创建得到 Tunnel Token,或者调用 API 也行,不做赘述
apiVersion: v1
kind: Secret
metadata:
name: cf-tunnel-secret
namespace: cloudflare
type: Opaque
stringData:
tunnel-token: "<Tunnel Token>"Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflare
labels:
app.kubernetes.io/name: cloudflared
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: cloudflared
template:
metadata:
labels:
app.kubernetes.io/name: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:2026.9.3
imagePullPolicy: IfNotPresent
args:
- tunnel
- --output
- json
- --loglevel
- debug
- --metrics
- 0.0.0.0:2000
- --no-autoupdate
- run
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: cf-tunnel-secret
key: tunnel-token
ports:
- name: metrics
containerPort: 2000
protocol: TCP
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
livenessProbe:
httpGet:
path: /ready
port: metrics
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: metrics
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}部署完成后,Cloudflare Dashboard 显示隧道连接已建立。配置一条 Published application routes ,Service 指向 K3s 中 Envoy Gateway Service。也就是:envoy-svc-fixed.envoy-gateway-system。此时公网流量经过 Cloudflare CDN 边缘节点,由 Tunnel 穿透注入集群,再传递给 Envoy Gateway 进行路由分发。



至此 Tailscale 和 Cloudflare Tunnel 访问 K3s 服务的基本链路已经测试通过。
cert-manager 自动签发 Let's Encrypt 证书
Helm 部署
helm install \
cert-manager oci://quay.io/jetstack/charts/cert-manager \
--version v1.21.2 \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=trueCloudflare API Key 创建
登录 Cloudflare Dashboard,进入 My Profile -> API Tokens,创建 Token:
• Permissions:
◦ Zone - DNS - Edit
◦ Zone - Zone - Read
• Zone Resources:选择包含您域名的 Zone(或 Include - All zones)
将 Token 存储为 Kubernetes Secret,后续要使用 ClusterIssuer ,必须存储在 cert-manager 相同 namespace 下
kubectl create secret generic cloudflare-api-token-secret \
--namespace cert-manager \
--from-literal=api-token=<CLOUDFLARE_API_TOKEN>配置 cert-manager ClusterIssuer 开启 DNS-01 Challenge 认证
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-cloudflare-dns
spec:
acme:
# 邮箱,用于接收 LetsEncrypt 证书到期提醒
email: your-email@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-cloudflare-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token申请证书(Envoy Gateway Namespace 下)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: okhk-tls
namespace: envoy-gateway-system
spec:
secretName: okhk-tls-secret
issuerRef:
name: letsencrypt-cloudflare-dns
kind: ClusterIssuer
dnsNames:
- "*.okhk.net"在 Envoy Gateway 的 Listener 中引用挂载 TLS 证书
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: envoy-gateway
namespace: envoy-gateway-system
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: "*.okhk.net"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: okhk-tls-secret
allowedRoutes:
namespaces:
from: All
infrastructure:
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: custom-proxy-config至此 TLS 证书配置完成,后续域名只需要配置 Tailscale 的 A 记录或者 Cloudflare Tunnel 的 CNAME 记录即可完成私有服务和公开服务的接入。
external-dns 配置
为了避免手动维护 DNS 记录,通过部署 external-dns 实现 HTTPRoute 与 DNS 的自动同步:
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update
helm install external-dns external-dns/external-dns \
-namespace external-dns \
--create-namespace \
-f external-dns-values.yamldomainFilters:
- okhk.net
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
key: apiKey
name: external-dns
extraArgs:
- --cloudflare-dns-records-per-page=5000
- --cloudflare-record-comment=provisioned by external-dns
- --events
- --min-event-sync-interval=5s
- --batch-change-size=200
- --batch-change-interval=1s
- --metrics-address=:7979
- --events-emit=RecordError
interval: 1m
logFormat: json
logLevel: debug
policy: upsert
provider:
name: cloudflare
registry: txt
secretConfiguration:
data:
apiKey:
enabled: true
mountPath: /etc/secrets
sources:
- gateway-httproute
txtOwnerId: edns
txtPrefix: _externaldns.- Tailscale 路由:
external-dns默认解析 Gateway 的外部 IP 并生成 A 记录,正好契合 Tailscale LoadBalancer 的 IP 地址。 - Cloudflare Tunnel 路由:在 Gateway 资源上添加注解
external-dns.kubernetes.io/target指向 Tunnel CNAME,并在HTTPRoute上添加注解以自动开启 Cloudflare Proxied:
Gateway 中配置 CNAME 记录指向目标:external-dns.kubernetes.io/target
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
annotations:
external-dns.kubernetes.io/target: <Tunnel-ID>.cfargotunnel.com
name: cloudflare-gateway
namespace: envoy-gateway-system
spec:
gatewayClassName: eg
infrastructure:
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: cf-proxy-config
listeners:
- allowedRoutes:
namespaces:
from: All
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: All
hostname: "*.okhk.net"
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- kind: Secret
name: okhk-tls-secret
mode: Terminate
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tailscale-gateway
namespace: envoy-gateway-system
spec:
gatewayClassName: eg
listeners:
- allowedRoutes:
namespaces:
from: All
hostname: "*.okhk.net"
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- kind: Secret
name: okhk-tls-secret
mode: TerminateHTTPRoute 中配置开启 Cloudflare 代理: external-dns.kubernetes.io/cloudflare-proxied
metadata:
annotations:
external-dns.kubernetes.io/cloudflare-proxied: "true"下图中 RemoteAddr10.42.0.28 和 10.42.0.29 分别是两个 Gateway 实例的 Pod IP,可见确实是两个 Gateway 分别转发的。


完成上述架构配置后,日后新增服务只需编写对应的 HTTPRoute 并绑定到不同的 Gateway,即可无缝完成私有服务与公开服务的隔离发布。
记录的有些杂,本来想用 AI 跑一轮优化下行文结构,看了下还是算了,就先到这。