TPDN Blog
2021-02-01

VPSとパブリックなネットワークを使用し、セキュアなK3sクラスタを構築する

CNIにKiloを使用し、ノード間通信を暗号化したK3sクラスタを作成する

KEY POINTS
  • K3s のデフォルト設定は、ノード間通信を暗号化していない
  • Kiloを用いることで、Wireguardによる暗号化がなされたメッシュネットワークを手軽に使用することができる
  • 実際に激安VPSを6台使用し、セキュアなK3s クラスタの構築を行った

2022-08 追記

  • 現在では単に暗号化したいだけなら(ネットワーク周りに特別なこだわりがなく、暗号化できればなんでもよいなら・NAT超えが必要ないなら)CalicoでWireguard使う方が圧倒的に楽です。
  • もっというなら、K3sにこだわりがない場合はK0sを使用した方が圧倒的に楽です。

K3sは低いスペックでも動作可能な 軽量Kubernetesである。

K8sと比べて圧倒的に低いスペックで動作可能という性質を生かし、激安VPSやクラウドの無料枠を魔活用したクラスタ構築に使われる機会もあり、複数の事例が公開されている。そしてそれらではパブリックなネットワークを直接使っているケースも多い。

しかしながら私が見る限り、多くの資料(特に日本語圏の資料、Qから始まる緑色の某サイトとか)ではセキュリティ的な面で好ましくない設定でクラスタを構築しているように見える。まあ詳細を書いてないだけで実際はちゃんとやってるのかもしれないが・・・。

そこでノード間通信を暗号化して最低限のセキュリティを確保したK3sクラスタを実際に構築し、手順(およびAnsible Playbook)を公開する。

なお、ここに書かれている手法はあくまでも現時点(2021-02-01)に動作するものでしかない。 Kubernetes界隈は非常に動きが激しく、K3sも例外ではない。 もし変更があった場合は読み替える必要がある。


k3sクラスタの構築にわざわざパブリックなネットワークを使用する、具体的なユースケース

私が思うところではこんな感じだろうか。

サーバーエンジニアの趣味

  • BlackFridayセールで安いVPSをいろんな会社から買ってクラスタ作成
  • 仮想LAN機能を持たない激安VPS会社のVPSを複数台買ってクラスタ作成
  • クラウド事業者の永久無料枠を最大限活用
    • GCPとOCIの無料枠を併用すれば、それだけで3ノード確保できる
  • VultrのIPv6専用インスタンスを活用するため、他のVPSと組み合わせる

大量のサーバーを組み合わせて自分専用の実験環境を作ることは、あらゆるサーバーエンジニア共通の夢だと思う。たぶん。

業務

  • IoT
    • コントロールプレーンをVPSに設置し、ワーカーノード(の一部)をRasPiに設置し、IoT機器に組み込む
    • まさにk3sの想定している用法そのまんま(だと思う)
  • モバイルゲームのeSports大会
    • 会場の小型PCにゲームサーバー用ワーカーノードを設置
    • 重そうなもの・複雑な機能を持ったもの・ライセンス周りの制約があるミドルウェアなどは会場外部のノードに設置し、軽そうなものは小型PCを使う
    • helmやkubectl applyによるデプロイを前提として作られたモバイルゲームの場合、eSports向け展開がやりやすくなるかもしれない

もちろんパブリックなネットワークにこだわる必要性は一切ない。 実際には以下の方法でプライベートなネットワークを使用できることが多いだろう。

  • IoT用閉域網SIMカードを使う
  • インテリジェントな業務用ネットワーク機器を使う
  • 力業で物理的に回線を引き込む

だが手近にあるパブリックなネットワークを使用できると何かと便利ではある。 何らかの制約(物理的な場所、ネットワーク環境、契約、時間、予算など)がある場合に柔軟な対応ができるかもしれない。

パブリックなネットワークを使用するにあたり、なぜデフォルト設定が好ましくないのか?

非常に単純である。デフォルト設定ではノード間通信の暗号化がされていないのだ。

k3sのデフォルト設定

特殊な設定を一切行っていないk3sでは、CNIとしてバックエンドにVXLANを使用したFlannelが使用される。

https://rancher.com/docs/k3s/latest/en/installation/network-options/

VXLANは単にL2フレームをUDPでラップしただけのもので暗号化は一切行われておらず、簡単に中身を読むことができる。

まあ、Podレベルで実装を工夫して通信の暗号化をしていることもあるかもしれない。 とはいえ基本的にはデフォルト設定をパブリックなネットワークで使うべきではない。

パブリックなネットワークを使ったk3sのノード間通信を暗号化し、最低限セキュアにする方法

自分の思いつく物として、以下の手法がある

  • k3sの標準機能を使い、暗号化に対応したFlannelバックエンドを使用する
    • IPsecや、Wireguardが使用できる
  • CNIを暗号化に対応してかつ扱いやすい別のものに変更する
    • 例えばCalicoやKiloはWireguardによる暗号化に対応している
  • 手動でVPN構築を行う
    • k3sに依存せず、応用性は最も高い
    • 手動での設定は面倒

以下の理由から、ここではCNIをKiloに変更する手法を採用している

  • 単に私がマルチk3sクラスター実験環境を組みたいと思っているから
    • そもそもKiloはマルチKubernetesクラスター環境を実現することを目的に作られている
  • 自力構築が面倒なフルメッシュVPNをお手軽に組める
    • 信頼度の低い激安VPS愛好家にとって、フルメッシュネットワークを手軽に組めることは魅力的
  • 導入実績がそこそこあり、わかりやすい資料もある
  • (当時存在した手段の中では)インストールが簡単
  • 高い柔軟性
    • 経路を自由自在に制御して複雑なメッシュネットワークが組める
      • フルメッシュ以外のネットワークも組める
    • 元々Kiloはマルチクラウドクラスターを作るためのもの
      • なので暗号化のためだけに導入するというのは、オーバーキルな感じがある
    • wireguardは手段の1つ

実際にVPSを複数台使った構築例

いくつか注意点がある。

  • KubernetesユーザーやLinuxユーザーなら常識的に考えてわかるだろうと思うものは省略している
  • ここのコマンド例がどんな環境でも動くことは一切保証できない
    • あくまでも私の使った激安VPSで動いたというだけである
    • VPS・コンピューティングインスタンス(例えばEC2)によって初期状態は異なる
  • この例ではコマンドを1つ1つ手動で実行しているが、実際にはAnsibleを使うべきだろう
    • 大量のマシンに手動でSSHなんてたまったものではない。
    • 実際に自分がk3sクラスター(serverノード1台、agentノード5台)を構築した際にはAnsibleを使用している

k3sのセットアップを行う前に、全てのノードで実行するもの

UFWの設定

ここではUFWを使用して最低限の設定を行う。

# Wireguardのために、UDP 51820を開放する
# 台数分だけ各サーバーで実行
ufw allow from <ノード1のIPアドレス> to any port 51820 proto udp
ufw allow from <ノード2のIPアドレス>  to any port 51820 proto udp
ufw allow from <ノード3のIPアドレス>  to any port 51820 proto udp
ufw allow from <ノード4のIPアドレス>  to any port 51820 proto udp
ufw allow from <ノード5のIPアドレス>  to any port 51820 proto udp
ufw allow from <ノード6のIPアドレス>  to any port 51820 proto udp
# ----------

# k3sのための設定を入れる
ufw allow in on kube-bridge from 10.42.0.0/16

# sshにレートリミットをかける
ufw limit ssh

# ufw有効化
ufw enable

Wireguardのインストール

Ubuntuの場合、導入は非常に簡単である。

apt install wireguard

k3s serverノード(コントロールプレーン)の作成

token作成以外は、serverノードのrootユーザーで実行する必要がある。

token作成

まずはtokenを作成する。要は適当な長さのランダム文字列を生成すればよい。 やり方は何でもよい。自分の場合はこんな感じで作成した。

tpdn@tpdn-VirtualBox:~$ pwgen -1 50
eb6eezeigh1Icheiloh6Iaqu9tah9diko4ooPh4zierae6Cae2

インストール

https://get.k3s.io から入手できるインストールスクリプトを使用する。

curl -sfL https://get.k3s.io | \
k3s_TOKEN="tokenをここに入れる" \
sh - server --flannel-backend=none

このコマンド1つでsystemdへの設定投入、カーネルモジュールの有効化などk3sを動かすのに必要な物一式が入る。

オプションは好きに設定して良いが、CNIをKiloに入れ替えるので--flannel-backend=noneが必要である。

kubeconfigを入手する

serverノードの/etc/rancher/k3s/k3s.yamlをscpやsftpで手元にコピーすればよい。

しかしながらサーバーのアドレスが以下のようになっている。

server: https://127.0.0.1:6443

これだとserverノードの外部から接続できないので、適当なエディタでIPアドレスを書き換えて保存する。

server: https://<your server public ip>:6443

ここまでやればkubectlコマンドが使えるようになる。

KUBECONFIG=k3s.yaml kubectl get nodes

k3s agentノード(ワーカー)の作成

各agentノードのrootユーザーで実行する必要がある。

kubeconfigをagentノードにコピー

「kubeconfigを入手する」で作成したk3s.yamlをagentノードの/etc/rancher/k3s/k3s.yamlにアップロードする。

普通にk3sを構築するのであれば不要だが、Kiloを使うために必要な作業である。

https://github.com/squat/kilo/blob/b6461181460c99f89514c4df4cf61d92c87920be/manifests/kilo-k3s.yaml#L168-L172

インストール

curl -sfL https://get.k3s.io | \
k3s_URL="https://<your server public ip>:6443" \
k3s_TOKEN="tokenをここに入れる" \
sh - agent

Kiloのインストール

https://github.com/squat/kilo/blob/master/manifests/kilo-k3s.yaml をダウンロードし、kubectl applyする。

kubectl apply -f /tmp/kilo-k3s.yaml

フルメッシュネットワークを組みたい場合

https://kilo.squat.ai/docs/topology#full-mesh

manifestのDaemonSetのオプションに

--mesh-granularity=full

を付けてkubectl applyすればよい。

ここまで一通りやるとどのようになるか

こんな感じのk3sクラスターが完成する。以下は実際にserver1台、agent5台のクラスタを構築した例である。(エンドポイントはダミーに置き換えている)

(ansible-venv) tpdn@tpdn-VirtualBox:~/k3s-kilo-playbook-example$ KUBECONFIG=files/k3s.yaml kubectl get nodes
NAME           STATUS   ROLES                  AGE   VERSION
agent-node-3   Ready    <none>                 25h   v1.20.2+k3s1
agent-node-1   Ready    <none>                 25h   v1.20.2+k3s1
agent-node-2   Ready    <none>                 25h   v1.20.2+k3s1
agent-node-4   Ready    <none>                 25h   v1.20.2+k3s1
server-node    Ready    control-plane,master   25h   v1.20.2+k3s1
agent-node-5   Ready    <none>                 25h   v1.20.2+k3s1
root@server-node:~# wg
interface: kilo0
  public key: 0kKfArnPxNBblekTLPHm1BMhzrAbVcTqbh9EudDa51k=
  private key: (hidden)
  listening port: 51820

peer: Jddm35Ig1PsX2RwI3i2bYvDzSAZ3kVjmqJexbnY4Dp8=
  endpoint: agent-node-1.example.com:51820
  allowed ips: 10.42.4.0/24, 10.4.0.3/32
  latest handshake: 1 minute, 9 seconds ago
  transfer: 319.90 KiB received, 480.49 KiB sent

peer: N7GH4CO4BNmwcInVDC3TNWbV7/gd3UuriDHgV75LqVR=
  endpoint: agent-node-2.example.com:51820
  allowed ips: 10.42.1.0/24, 10.4.0.5/32
  latest handshake: 2 hours, 47 minutes, 46 seconds ago
  transfer: 1.35 MiB received, 83.46 KiB sent

peer: RrCuDQhtxPr20tbR3qPmVvWFvbG9QrME5vRFWAL+0nh=
  endpoint: agent-node-3.example.com:51820
  allowed ips: 10.42.5.0/24, 10.4.0.1/32

peer: e1dsyWfUyz2PnCwKF7s2NJM5RoSP1s8b2cCM78VuguU=
  endpoint: agent-node-4.example.com:51820
  allowed ips: 10.42.3.0/24, 10.4.0.2/32

peer: TxZuEMbIXsKy07jhf3LfaSjTSEvSqEw1q67QD3uGIKx=
  endpoint: agent-node-5.example.com:51820
  allowed ips: 10.42.2.0/24, 10.4.0.4/32

Ansible Playbook

手動でコマンド打つなんてやっていられない、こんなコマンド列見たくないので早くソース見せろという人は以下を参照してほしい。

https://github.com/tpdn/k3s-kilo-playbook-example

参考資料

https://jbhannah.net/articles/k3s-wireguard-kilo で書かれていることをもとにしている。やっていることはほとんど同じである。

Change Log
  • 2022-08-07: 現在はもっと楽な方法があるということを追記。あいまいな表現を変更。