注意, 此文章转载自我的公司内网博客.
简介 最近在调研如何能够提供为用户提供容器的shell登录, 本篇博客整理了一下思路,
探讨了两种方式, 一种基于代理方式访问Kubernetes集群中的pod的功能;
另一种方案, 基于webshell但允许用户从本地登录.
并就这两种方案的安全性, 与功能使用情况进行分析.
用户需求 虽然我们属于PaaS平台, 开发者提供了代码和配置由我们部署即可, 但是出现以下问题时,
开发者可能会希望自己登录到自己的容器中运行某些指令:
开发过程中对环境的调试: 环境变量查看, 数据库试连接, 日志打印及代码运行情况验证
已经上线的服务: 开发者期望手动执行一些脚本, 使用真实的shell执行脚本更快捷方便
解决方案 - 增加Shell容器 目前的环境中, 应用上线会创建一个kubernetes的Deployment
, Service
, Ingress
这样三个服务.
由Ingress转发流量至对应的Pod.
为了避免影响已经在运行的线上服务, shell登录功能不在线上容器中进行, 而是单独启动新的Pod提供shell服务.
由于Kubernetes集群使用的不是host网络, 而是在flannel一个分配的内部IP.
那么这个Pod的启动后, Kubernetes集群外部如何访问这个服务:
增加跳板机或是代理的服务, 对外提供出口, 再由代理服务器访问Kubernetes的内部资源
使用WebShell, 由我们的平台分配一个该服务对应的域名, 再增加鉴权提高安全性
下面我会就这两种方案分别介绍调研结果.
ssh代理使用 - ProxyCommand 此部分探讨的ssh为OpenSSH, 并且需要支持ProxyCommand参数.
Service代理TCP端口 既然对外提供了代理, 那么内部如何区分每个应用呢. 此处打算采用Service代理TCP端口的功能.
整个服务如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 --- apiVersion: v1 kind: Service metadata: name: app01-ssh spec: selector: app: app01-ssh ports: - protocol: TCP port: 22 targetPort: 22 name: proxied-tcp-22
针对不同的应用, 只要是在k8s中的pod, 我们就使用telnet appXX-ssh 22
的形式来访问对应的服务.
Dockerfile以及Kubernetes配置文件见附录部分.
使用跳板机
跳板机的ssh config配置如下
1 2 3 4 Host deb User root HostName app1.ssh ProxyCommand ssh -q -W %h:%p jumpserver
1 2 3 4 5 6 7 8 9 +---------------+ +-------->+ app3.ssh | | +---------------+ +---------------+ +---------------+ |ssh jump server+-------->+ app2.ssh | +---------------+ +---------------+ | +---------------+ +-------->+ app1.ssh | +---------------+
使用代理 – HTTPProxy 这是我调研的一种方案:
OpenSSH的ProxyCommand的作用是使用子进程建立TCP连接, 而后利用管道与父进程通信,
在明白了这一点之后, 我们其实可以依靠HTTP建立连接后转而传输TCP数据包.
1 2 ssh -o "ProxyCommand=nc -X connect -x 127.0.0.1:8888 %h %p" corvo@192.168.137.23
其中127.0.0.1:8888
是本机上开放的http代理, 较为简单的代理服务器可以参考HTTP 代理原理及实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var http = require ('http' );var net = require ('net' );var url = require ('url' );function connect (cReq, cSock ) { var u = url.parse ('http://' + cReq.url ); var pSock = net.connect (u.port , u.hostname , function ( ) { cSock.write ('HTTP/1.1 200 Connection Established\r\n\r\n' ); pSock.pipe (cSock); }).on ('error' , function (e ) { cSock.end (); }); cSock.pipe (pSock); } http.createServer ().on ('connect' , connect).listen (8888 , '0.0.0.0' );
具体的线上服务, 我个人倾向使用HTTPProxy. 有两点好处:
Jump Server
的建立需要维护一个OpenSSH server, http代理只需要维护比较小范围的代码
http代理中, 可以方便的控制用户想可以到达的服务, 可以在程序中建立白名单, 安全性要比Jump Server
来的高
WebShell - 网页与本地功能 基础的WebShell WebShell部分的调研工作主要围绕gotty , 主要依靠gotty+tmux实现对机器访问的支持.
1 2 gotty -w tmux new -A -s gotty
访问http://127.0.0.1:9000
即可看到
gotty客户端 在gotty的README中, 有介绍如何从终端访问到GoTTY的服务. 推出了gotty-client
1 2 3 go get github.com/moul/gotty-client/cmd/gotty-client gotty-client http://127.0.0.1:9000/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ┌─────────────────┐ ┌──────➤│ /bin/bash │ │ └─────────────────┘ ┌──────────────┐ ┌──────────┐ │ │ │ Gotty │ ┌───────┐ ┌──➤│ Browser │───────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ │ │ │ ┌─────────────────┐ │ Bob │───┤ websockets─➤│ │─➤│ emacs /var/www │ │ │ │ ╔═ ══ ══ ══ ══ ╗ │ │ │ └─────────────────┘ │ │ │ ║ ║ │ │ │ └───────┘ └──➤║ gotty-client ║───────┘ │ │ ║ ║ │ │ ╚═ ══ ══ ══ ══ ╝ └──────────┘ │ ┌─────────────────┐ └──────➤│ tmux attach │ └─────────────────┘
基本原理是复用了GoTTY的websocket, 不过我试用下来, 发现对tmux的支持实在太差.
对于tmux的重度用户来讲, 这个几乎是完全不可用的状态, 字符的打印太离谱了.
两种方案对比 有了以上两种可选方案, 一种是基于原生ssh, 另一种方案是类似于WebShell的形式.
下面我就两种方案的难易度, 安全性, 以及维护成本方面进行讨论.
功能以及实现难度讨论
功能上:
两种方案均能实现用户连接到容器内部的要求.
我平时用原生的ssh太多了, 个人期望能够利用OpenSSH提供的各种功能.
WebSSH给人一种花里胡哨的感觉, 快捷键支持有问题, 一个Ctrl-W
就意味着页面被关闭.
实现难度上:
前者需要增加服务作为代理服务器, 另外需要做好私钥的管理
后者需要结合gotty等WebShell工具, 同样也需要做好权限控制, 本身也支持密码登录
维护与下线成本讨论 前者需要维护代理服务器, 以及本地连接时的ProxyCommand
后者可能需要维护gotty
未来可能会有gotty-client
后者维护成本较高.
安全性问题讨论
关于四层代理服务的安全性, 建议通过以下方式保证(以http代理服务器为例):
此代理服务器仅允许内部网络访问
此代理服务器增加basic auth
, 要求用户连接时给出用户名密码
此代理仅允许部分域名类似于app01-ssh
这样的ssh
服务的域名通过, 不允许访问除ssh域名之外的其他域名或ip
如果能做到以上3点, 代理服务器与WebShell的安全性基本一致.
总结 本文探讨了两种在外部连接Kubernetes的pods的方案:
一种基于ssh代理方式访问Kubernetes集群中的pod的功能;
另一种基于WebShell但允许用户从本地登录.
并就这两种方案的功能性, 实现难度与安全性进行分析.
从个人角度来看, 更倾向于使用代理方式, 毕竟原生的ssh有许多配套软件,
例如scp
, sshfs
, 也可以利用LocalForward
这些配置.
已知问题与解决方案 在我测试远程服务器的连接过程中, 出现了如下的问题:
1 2 3 ~ ❤ ssh -o "ProxyCommand=nc -X connect -x 10.202.37.229:8081 %h %p" root@172.17.0.15 Bad packet length 1349676916. ssh_dispatch_run_fatal: Connection to UNKNOWN port 65535: message authentication code incorrect
在使用wireshark
抓包之后看到, netcat
将tcp包拆分, 导致远程服务器解析到的ssh数据包不完整, 无法应答.
鉴于出现以上问题, 我决定自己写一个简易的代理工具替换掉nc, 以下为该工具的简易版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 """ 使用方式, python3: ssh -o "ProxyCommand=python3 tun.py 10.202.37.229:8081 %h %p" root@172.17.0.15 """ import sysimport socketfrom threading import Threadimport http.client as httplibfrom urllib.parse import urlparse, urlencodefrom urllib.request import urlopen, Requestfrom urllib.error import HTTPErrorclass ProxyHTTPConnection (httplib.HTTPConnection, object ): def __init__ (self, *args, **kwargs ): super (ProxyHTTPConnection, self ).__init__(*args, **kwargs) self .stop = False def connect (self ): httplib.HTTPConnection.connect(self ) self .send(b"CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self ._real_host.encode('utf8' ), self ._real_port)) response = self .response_class(self .sock, method=self ._method) (version, code, message) = response._read_status() if code != 200 : self .close() raise socket.error( "Proxy connection failed: %d %s" % (code, message.strip())) self .interpreterloop(self .sock) def interpreterloop (self, sock ): Thread(target=self .readloop, args=(sock, )).start() self .writeloop(sock) def readloop (self, sock ): while not self .stop: try : sock.send(sys.stdin.buffer.read(1 )) except socket.error as e: print (e) return def writeloop (self, sock ): while not self .stop: try : c = sock.recv(1 ) except KeyboardInterrupt: self .stop = True return if not c: break sys.stdout.buffer.write(c) sys.stdout.flush() if __name__ == '__main__' : proxy = sys.argv[1 ] remote_host = sys.argv[2 ] remote_port = sys.argv[3 ] p = ProxyHTTPConnection(proxy) p._real_host = remote_host p._real_port = int (remote_port) p.connect()
附录 Kubernetes中启用sshd服务
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 FROM python:3.7 .4 -stretchRUN apt-get update && apt-get install -y openssh-server RUN mkdir /var/run/sshd RUN echo 'root:hello' | chpasswd RUN echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config EXPOSE 22 CMD ["/usr/sbin/sshd" , "-D" ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 --- apiVersion: apps/v1 kind: Deployment metadata: name: app01-ssh spec: replicas: 1 selector: matchLabels: app: app01-ssh template: metadata: labels: app: app01-ssh spec: containers: - name: app01-ssh image: test imagePullPolicy: Never ports: - containerPort: 22 name: app01-ssh --- apiVersion: v1 kind: Service metadata: name: app01-ssh spec: selector: app: app01-ssh ports: - protocol: TCP port: 22 targetPort: 22 name: proxied-tcp-22