背景

在 Kubernetes 集群中,很多时候服务没有对外暴露的网络入口,排查问题或日常运维都需要进入集群内部。通常的做法是:

  1. 在集群内部署一个 SSH 服务作为跳板机
  2. 通过 SSH 反向隧道将跳板机暴露到外部可访问的服务器上
  3. 开发者通过隧道连接跳板机,再访问集群内部服务

本文介绍如何在 K8s 中用一个 Pod 两个容器实现 SSH 跳板机 + autossh 自动隧道,实现稳定的内网穿透。

架构

用户 --> 跳板机服务器:6022 --> autossh 隧道 --> sshd 容器:22 --> K8s 内部网络

整个方案由两个容器组成,共享同一个 Pod 网络:

  • sshdhermsi/alpine-sshd 镜像,提供 SSH 登录服务,作为进入集群的入口
  • autosshjnovack/autossh 镜像,建立到外部服务器的反向隧道,保持连接稳定

两个容器通过 127.0.0.1:22 通信,无需额外的 Service。

前置准备

SSH 密钥

创建用于隧道认证的密钥对:

1ssh-keygen -t rsa -b 4096 -f ~/.ssh/autossh_id_rsa -N ""

将公钥添加到跳板机服务器的 ~/.ssh/authorized_keys

1ssh-copy-id -i ~/.ssh/autossh_id_rsa.pub user@10.0.0.1

容器镜像

  • sshd: hermsi/alpine-sshd — 轻量级 Alpine SSH 服务,支持通过环境变量创建用户
  • autossh: jnovack/autossh — 封装了 autossh 的 Docker 镜像,通过环境变量配置隧道

完整 YAML

将以下内容保存为 ssh-tunnel.yaml

 1---
 2apiVersion: v1
 3kind: Namespace
 4metadata:
 5  name: devops-prd
 6---
 7apiVersion: v1
 8kind: Secret
 9metadata:
10  name: ssh-key
11  namespace: devops-prd
12type: Opaque
13data:
14  # SSH 私钥(base64 编码),用于 autossh 连接跳板机服务器
15  # 生成: base64 -w0 ~/.ssh/autossh_id_rsa
16  autossh_id_rsa: <BASE64_ENCODED_PRIVATE_KEY>
17  # SSH 公钥,用于 xu 用户的密钥认证登录
18  # 生成: base64 -w0 ~/.ssh/autossh_id_rsa.pub
19  autossh_id_rsa.pub: <BASE64_ENCODED_PUBLIC_KEY>
20---
21apiVersion: apps/v1
22kind: Deployment
23metadata:
24  name: ssh-tunnel
25  namespace: devops-prd
26  labels:
27    app: ssh-tunnel
28spec:
29  replicas: 1
30  selector:
31    matchLabels:
32      app: ssh-tunnel
33  template:
34    metadata:
35      labels:
36        app: ssh-tunnel
37    spec:
38      restartPolicy: Always
39      volumes:
40        - name: ssh-key
41          secret:
42            secretName: ssh-key
43            defaultMode: 0400
44      containers:
45        # ---- 容器1: SSH 跳板机 ----
46        - name: sshd
47          image: hermsi/alpine-sshd
48          imagePullPolicy: IfNotPresent
49          ports:
50            - containerPort: 22
51          env:
52            - name: ROOT_LOGIN_UNLOCKED
53              value: "false"
54            - name: SSH_USERS
55              value: "xu:1000:1000"
56          volumeMounts:
57            - name: ssh-key
58              subPath: autossh_id_rsa.pub
59              mountPath: /conf.d/authorized_keys/xu
60              readOnly: true
61
62        # ---- 容器2: autossh 隧道 ----
63        - name: autossh
64          image: jnovack/autossh
65          imagePullPolicy: IfNotPresent
66          env:
67            - name: SSH_REMOTE_USER
68              value: "user"
69            - name: SSH_REMOTE_HOST
70              value: "10.0.0.1"
71            - name: SSH_REMOTE_PORT
72              value: "22"
73            - name: SSH_MODE
74              value: "-R"
75            - name: SSH_TUNNEL_PORT
76              value: "6022"
77            - name: SSH_TARGET_HOST
78              value: "127.0.0.1"
79            - name: SSH_TARGET_PORT
80              value: "22"
81            - name: SSH_BIND_IP
82              value: "127.0.0.1"
83            - name: SSH_KEY_FILE
84              value: "/etc/ssh-key/autossh_id_rsa"
85            - name: SSH_OPTIONS
86              value: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
87          volumeMounts:
88            - name: ssh-key
89              mountPath: /etc/ssh-key
90              readOnly: true

部署步骤

1. 准备密钥

将 SSH 密钥对用 base64 编码后填入 Secret:

1# 私钥给 autossh 用
2base64 -w0 ~/.ssh/autossh_id_rsa
3
4# 公钥给 sshd 做用户认证
5base64 -w0 ~/.ssh/autossh_id_rsa.pub

2. 部署

1kubectl apply -f ssh-tunnel.yaml

3. 验证

部署后,通过跳板机服务器连接:

1ssh xu@10.0.0.1 -p 6022 -i ~/.ssh/autossh_id_rsa

登录成功后,可以在跳板机容器内访问集群内部服务:

1# 访问集群内的其他 Pod
2curl http://some-service.devops-prd.svc.cluster.local:8080
3
4# 或者通过 kubectl 进入其他容器
5kubectl exec -it some-pod -- /bin/sh

关键问题与解决方案

问题 1:SSH 密钥路径错误

jnovack/autossh 镜像默认查找密钥路径为 /id_rsa,需要通过 SSH_KEY_FILE 环境变量显式指定:

1- name: SSH_KEY_FILE
2  value: "/etc/ssh-key/autossh_id_rsa"

问题 2:主机密钥检查失败

首次连接时,远程服务器的主机密钥不在 known_hosts 中会导致连接失败。通过 SSH_OPTIONS 跳过检查:

1- name: SSH_OPTIONS
2  value: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"

问题 3:公钥文件挂载为目录

hermsi/alpine-sshd 要求每个用户的公钥文件放在 /conf.d/authorized_keys/<用户名>。直接用 volume 挂载会变成目录,需要用 subPath 挂载为文件:

1volumeMounts:
2  - name: ssh-key
3    subPath: autossh_id_rsa.pub
4    mountPath: /conf.d/authorized_keys/xu
5    readOnly: true

为什么用双容器而不是单容器

将 sshd 和 autossh 放在两个容器中有以下好处:

维度 双容器 单容器
关注点分离 sshd 和 autossh 各司其职 需要进程管理,脚本复杂
镜像维护 各自使用专注的镜像 需要自定义镜像或安装依赖
更新灵活 单独更新任一容器 整体更新,耦合度高
可观测性 各自有独立日志 日志混在一起

两个容器在同一个 Pod 中共享网络命名空间,127.0.0.1:22 就能访问 sshd,和单容器体验一致。

环境变量说明

autossh 环境变量

变量 说明 示例
SSH_REMOTE_USER 跳板机服务器用户名 user
SSH_REMOTE_HOST 跳板机服务器地址 10.0.0.1
SSH_MODE -R 为反向隧道 -R
SSH_TUNNEL_PORT 跳板机监听端口 6022
SSH_TARGET_HOST 本地目标地址(sshd 容器) 127.0.0.1
SSH_TARGET_PORT 本地目标端口 22
SSH_KEY_FILE 私钥路径 /etc/ssh-key/autossh_id_rsa
SSH_OPTIONS 额外的 SSH 参数 -o StrictHostKeyChecking=no

sshd 环境变量

变量 说明 示例
ROOT_LOGIN_UNLOCKED 是否允许 root 登录 false
SSH_USERS 创建附加用户(用户名:UID:GID) xu:1000:1000

安全建议

  1. 不要硬编码密钥:示例中的 Secret 为了方便展示使用了 base64,生产环境建议用 External Secrets Operator 或 Vault 管理
  2. 限制跳板机访问:在跳板机服务器上通过 iptables 或安全组限制 6022 端口的来源 IP
  3. 定期轮换密钥:autossh 的 SSH 密钥应定期更换
  4. 审计日志:启用 sshd 的详细日志,并接入日志收集系统

总结

通过一个 Pod 两个容器的方式,可以在 Kubernetes 集群中快速部署 SSH 跳板机 + autossh 反向隧道,实现稳定的内网穿透。这个方案的好处是:

  • 一个 YAML 文件搞定,不需要额外配置
  • 双容器分离关注点,维护简单
  • 利用 Pod 共享网络,通信零开销
  • autossh 自动重连,隧道稳定可靠

参考