sshpiper 在 Kubernetes 上的應用

2023-07-16 21:00:44

sshpiper 在 Kubernetes 上的應用

介紹

GitHub Repo

一個反向代理目標伺服器的 proxy,使用者端想請求某個 ssh 伺服器,直接請求的是 sshpiper 服務,再經由 sshpiper 服務轉發到對應的 ssh 伺服器,相當於一箇中間人。

一開始並不理解這種元件的用處,但實際用了之後感覺還是蠻有意思的。

設想有這樣一種場景,你有多個 ssh 伺服器,你可能要不停切換 ssh 伺服器,這過程中有可能使用不同的 ssh 祕鑰來連線。而且如果是想從公司外網連進來,ssh 伺服器的埠要對外網開放,會有很大的安全隱患。

ssh 可以通過密碼、祕鑰兩種方式鑑權連線,密碼方式相對簡單,我也主要是使用祕鑰模式連線的,這裡主要介紹祕鑰連線。

原理

完成整個連線過程,需要有兩套祕鑰(即兩套公鑰私鑰),我們這裡分別稱為 PublicKey_X,PrivateKey_X,PublicKey_Y,PrivateKey_Y,其中:

  • PrivateKey_X 由使用者端持有
  • PublicKey_X 由 sshpiper 持有(在 k8s 中由叢集儲存)
  • PrivateKey_Y 由 sshpiper 持有(在 k8s 中由叢集儲存)
  • PublicKey_Y 由 ssh 伺服器持有 (寫入 .ssh/authorized_keys 中)

使用者端持 PrivateKey_X ssh 請求 sshpiper,sshpiper 使用 PublicKey_X 進行校驗,校驗之後 sshpiper 持 PrivateKey_Y 請求伺服器,伺服器持 PublicKey_Y 進行校驗。

此處提及的 ssh 金鑰,公鑰均為 ssh-rsa XXXXXX 形式,私鑰均為 pem 形式,即類似:-----BEGIN PRIVATE KEY----- 開頭,-----END PRIVATE KEY----- 結尾

優點(個人總結)

  • ssh 伺服器均可以不暴露外網埠,所有外網請求由 sshpiper 代理即可。
  • 可以使用同一套 PrivateKey_XPublicKey_X 登入多臺伺服器。
  • 可以利用 PublicKey_X, PrivateKey_X 更好地控制 ssh 存取伺服器的許可權。

Kubernetes 上的應用

目標:執行一個可以通過 sshpiper 存取的 pod。

安裝

在 kubernetes Pod 中使用 sshpiper,先按照此處檔案在叢集中安裝和部署 sshpiper 服務:

https://github.com/tg123/sshpiper/tree/master/plugin/kubernetes

我使用了手動安裝,共兩步:

  1. 安裝 CRD
  2. 啟動 sshpiper 服務

啟動 sshpiper 服務時,使用如下 yaml 設定:

# sshpiper service
---
apiVersion: v1
kind: Service
metadata:
  name: sshpiper
spec:
  selector:
    app: sshpiper
  ports:
    - protocol: TCP
      port: 2222
      targetPort: 2222
      nodePort: 30022
  type: NodePort
---
apiVersion: v1
data:
  server_key: | # 此設定暫時沒發現用處,直接使用官方提供的樣例中的值即可
    LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNCWUhWV01lNzVDZ3Rzdm5rOWlTekJFU3hSdjdMb3U3K0tVbndmb3VnNzcxZ0FBQUpEQnArS0d3YWZpCmhnQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQllIVldNZTc1Q2d0c3ZuazlpU3pCRVN4UnY3TG91NytLVW53Zm91Zzc3MWcKQUFBRUJKSDU3eTFaRTUxbVo2a2VsWUR0eDQ1ajBhZGdsUk5CY0pZOE94YTY4TEJWZ2RWWXg3dmtLQzJ5K2VUMkpMTUVSTApGRy9zdWk3djRwU2ZCK2k2RHZ2V0FBQUFEV0p2YkdsaGJrQjFZblZ1ZEhVPQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K
kind: Secret
metadata:
  name: sshpiper-server-key
type: Opaque
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sshpiper-deployment
  labels:
    app: sshpiper
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sshpiper
  template:
    metadata:
      labels:
        app: sshpiper
    spec:
      serviceAccountName: sshpiper-account
      containers:
      - name: sshpiper
        imagePullPolicy: IfNotPresent
        image: farmer1992/sshpiperd:latest
        ports:
        - containerPort: 2222
        env:
        - name: PLUGIN
          value: "kubernetes"
        - name: SSHPIPERD_SERVER_KEY
          value: "/serverkey/ssh_host_ed25519_key"
        - name: SSHPIPERD_LOG_LEVEL
          value: "trace"
        volumeMounts:
        - name: sshpiper-server-key
          mountPath: "/serverkey/"
          readOnly: true          
      volumes:
      - name: sshpiper-server-key
        secret:
          secretName: sshpiper-server-key
          items:
          - key: server_key
            path: ssh_host_ed25519_key
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sshpiper-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get"]
- apiGroups: ["sshpiper.com"]
  resources: ["pipes"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-sshpiper
subjects:
- kind: ServiceAccount
  name: sshpiper-account
roleRef:
  kind: Role
  name: sshpiper-reader
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sshpiper-account
  1. 官方樣例這裡 Service 使用了預設的 ClusterIP 型別,但如果要通過外網存取,需要使用 LoadBalancer 或 NodePort 型別。我使用了 NodePort 型別,並對映給了 30022 埠
  2. 官方樣例設定了一個 Secret sshpiper-server-key, 但並沒有說明其用處。不過這個似乎對使用 sshpiper 沒有影響,直接填充官方給的預設值即可。預設值在 ReadMe 中沒有給出,可以看上面給的 github 路徑下的 sample.yaml 檔案

啟動成功後如下圖:

映象準備

由於需要存取 ssh 服務,所有業務 Pod 的基礎映象需要執行 ssh 服務並暴露 ssh 介面。還需要將 PublicKey_Y 寫入目標容器的 .ssh/authorized_keys 檔案中。

這裡我用官方使用的 lscr.io/linuxserver/openssh-server:latest 映象,它提供了 PUBLIC_KEY 環境變數,在啟動時自動將其中設定的 public_key 寫入 .ssh/authorized_keys 檔案中。

除此之外,亦可以通過設定 ConfigMap,將 public_key 掛載進容器的路徑下。

啟動容器

apiVersion: apps/v1
kind: Deployment
metadata:
  name: host-publickey
spec:
  replicas: 3
  selector:
    matchLabels:
      app: host-publickey
  template:
    metadata:
      labels:
        app: host-publickey
    spec:
      containers:
      - name: host-publickey
        image: lscr.io/linuxserver/openssh-server:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 2222
        env:
        - name: USER_NAME
          value: "user"
        - name: PUBLIC_KEY
          value: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDgMF4AKRaRf3V2+6T7rluYW37t5TwuQDcdT966jKhKNHBkLHuT/YhBuWkpHuGR3Wh3S3zGAZ73vZ8zJHXsOPmBakkxPa9lqSHMj7Y0mN/0XvpcIHIdphzKUiEIP65N6OG2ZtYaZYti8wDNs1rW+V2Vx5IlOcT8IiNQ5FNvOozS9w=="
---
apiVersion: v1
kind: Service
metadata:
  name: host-publickey
spec:
  selector:
    app: host-publickey
  ports:
    - protocol: TCP
      port: 2222
  1. USER_NAME 欄位會為你自動建立對應的使用者。
  2. PUBLIC_KEY 欄位填寫的是 ssh 公鑰明文,非 pem 形式。這裡填入的就是上面所說的 PublicKey_Y

建立 pipe

pipe 就是第一步安裝中安裝的 CRD。它負責定義 from 和 to 的相關資訊,以及儲存前面提到的 PublicKey_XPrivateKey_Y

apiVersion: v1
data:
  ssh-privatekey: |
    LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDWGdJQkFBS0JnUURnTUY0QUtSYVJmM1YyKzZUN3JsdVlXMzd0NVR3dVFEY2RUOTY2aktoS05IQmtMSHVUCi9ZaEJ1V2twSHVHUjNXaDNTM3pHQVo3M3ZaOHpKSFhzT1BtQmFra3hQYTlscVNITWo3WTBtTi8wWHZwY0lISWQKcGh6S1VpRUlQNjVONk9HMlp0WWFaWXRpOHdETnMxclcrVjJWeDVJbE9jVDhJaU5RNUZOdk9velM5d0lEQVFBQgpBb0dCQU5ZcG5rS3MvYUEwa0hQL1pOWUE5QU1SdEtseHlSR3R5bmkzNmQ5dnF2eG9KODJxS010dzhROUlIY3RvCmNyZXpPSzV0Y0Y1L0FldE1PNTdSZjgwUGlGaHNvenowWnJkU2dzNXNZa2N4aS9CQTNBa05UNXh4aVo0STQxOEoKRStVemZnVDFOT0Y5bnNzbWoxQWVnNlJ1d3RYbVJkWElRUm1wcEtVVjBNcENERGJoQWtFQStiTk0wQzhxaHFpeQpyVWg0VFpSUWhGc0FyRTMzL1NoVzNobU5pdUd0Qjc3QnVXSFBWVDlnaDFpV0Mwby9CV0FSZUlSR05WbUdhZGtTClUxZCswdk8wdVFKQkFPWFlUT1dLZEd6MWZKVGJNWDdnNUVsRmV0NVpEV3hwZFEzWWpiUEQzRDg2cDNaN1JrVHMKQ1RQdDd1VjVzZXpZZWJKK1B5SnI5WVEzOXBybm02S0xUUzhDUUhVZEpYL1hQMmpkSXNDblp0VnNKTCtQTnllWgpnaUNZbFBXaW9vSnJDbzdCWjNjZGF2TWV3SlY2ZFJWaWcyQndDSUd2K0lYNU1WUGYzZnA4NVJ6bjlQRUNRUURiClhvU1dHSDFpZVRLOGlEQkhYckhEMVJLZUlQU1U0bG9jS3ZHai8yMjQwMng5d3M2Z2ZYK1RGcWFLVW9vaytiKzkKUW8xVGR5TFBYUEo3aWs2YTVzVjFBa0VBN1FPc1lPZ0FXeFZzbCttdFBva3VkS3FUa2lYaDNVNFp5Si8zWDZ5YgpwVjVkQWtLQlRMdjVwMFdEQlcyVDB5UmpaMGNDME01ckkyMHI2ejdnWktCeFJnPT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0=
kind: Secret
metadata:
  name: host-publickey-key
type: kubernetes.io/ssh-auth
---
apiVersion: sshpiper.com/v1beta1
kind: Pipe
metadata:
  name: pipe-publickey
  annotations:
    privatekey_field_name: ssh-privatekey # this is optional, default is ssh-privatekey
spec:
  from:
  - username: "test"
    authorized_keys_data: "c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFERThzZnBiOXkweVNRMTRsaWpQNnc5QWg2UEF6SC9hdGdXVDB5c3NZL29aSGJONDlwUHk4OWt1NC9ndmUwWEZOcE5HMGN2aThOQ3J5aDdNNDBZWnN1KzlXY1BpR2RXRnVuVG4xMGhWWmVQaGhTQk5WUVByMU16dy9MNHpTb3U2amozdWh4aHI1a3pNdi9pbWY1WFFHT2U5WEVKaTBoK29lbVlPUkxybUNvKzhWUFkvb29SL2tIY3J5L3ZuVVdoek1GYzNXMC9Pck80Q2ZvUlBnc1VabGREVnZmU0toUnlQQllISkJhaHUza0xLWC9VRk9ZRnRzd2lZTWtMWHpwY0JjSmJnUDE0RHFaNGIxZEhmZHp5MGZCYThyTVJFWEI5NHErNlhnV1cvRDlKbUQzaURQd2pRengySVRTZXRCSUlhYjlvYWkzRWd0TTdDQk13ZE5tdk5mQXQgNzYwNzUyNTgwQHFxLmNvbQo="
  to:
    host: 10-244-3-30.default.pod.cluster.local:2222
    username: "user"
    private_key_secret:
      name: host-publickey-key
    ignore_hostkey: true

Piper 中所填寫的 privateKey,

  1. Secret 中定義即為 PrivateKey_Y,經過 base64 編碼,用於填寫在 to 中,作為 sshpiper 與目標伺服器之間的校驗。
  2. Pipe.spec.from.username 是 ssh 請求 sshpiper 的 username,如: ssh username@sshpiper-ip -p 30022 -i private_key.pem。這裡亦可以填寫正則匹配,需要設定 Pipe.spec.from.username_regex_match: true
  3. Pipe.spec.from.authorized_keys_data 儲存 PublicKey_Xbase64 編碼,用於使用者端發起請求時,sshpiper 會取的這個公鑰與使用者的 private_key.pem 進行驗證
  4. Pipe.spec.to.host 是 k8s 叢集中任何能路由到 Pod 的方式,可以直接是 Pod 的 ip,也可以是叢集內部支援的域名,可以被叢集 DNS 解析,也可以是 Pod 的 Service 對應的域名或路由。參考:https://kubernetes.io/zh-cn/docs/concepts/services-networking/dns-pod-service/
  5. Pipe.spec.to.username 為登入到目標伺服器上所用的使用者名稱,這個使用者名稱需要存在於目標伺服器
  6. Pipe.spec.to.private_key_secret 指定了上面定義的 Secret,用於 sshpiper 與目標伺服器之間的校驗。

注意:

  • 如果 Pipe.spec.to.host 填寫的是 Service 路由,那麼每次 ssh 時,進入的 Pod 可能時 Service 管理下的任何一個 Pod。
  • 此處所填公鑰私鑰,均為其 base64 編碼形式。

上面我使用 pod-ip-addres.namespace.pod.cluster-domain.example 方式作為 to.host 。建立 pipe 之後,通過命令 ssh test@sshpiperIP -p 30022 -i ~/.ssh/id_rsa

  • 如果 sshpiper Service 為 NodePort 型別,這裡 sshpiperIP 即為 nodeIP,port 即為 nodePort
  • 如果真實的生產環境中,這裡肯定要用上 LoadBalance,或者 nginx、ingress 等元件,以及設定域名解析等方式來代替明文 NodeIP。

應用場景和業務流程

  1. 有長期執行的 Pod,而且使用者可能頻繁進入 Pod 內部進行偵錯和開發工作。
  2. 此時使用者相當於 client。
  3. 一般要搭配 ssh 管理模組使用。
    1. 建立 ssh 金鑰,將私鑰返回給使用者,作為 client 的 PrivateKey_X,提醒使用者妥善保管
    2. ssh 模組儲存公鑰,即 PublicKey_X,但不儲存 PrivateKey_X,私鑰一旦丟失,金鑰將無法正常使用。
    3. 建立 Pod 時,列出已有 ssh 金鑰,使用者可選擇自己手中已持有的金鑰,也可以新建立一對 ssh 金鑰。
    4. 點選確認,系統後臺再生成一對 ssh 金鑰,作為 PublicKey_YPrivateKey_Y
    5. PublicKey_Y 繫結入 Pod authorized_keys 中,同時使用 PublicKey_YPublicKey_X 建立 Pipe 資源
  4. 建立成功後,使用者可使用所持 PrivateKey_X 成功登入 Pod 中。