TPDN Blog
2021-02-01

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

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

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

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

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

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

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

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

あと、この記事ではいわゆる「K3s語」を使っている。 要は、masterノードではなくserverノード、workerノードではなくagentノードということである。


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

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

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

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

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

ロマンに理由や合理性は不要。

業務

  • IoT
    • serverノードをVPSに設置し、agentノードをSBC(例えばRasPi)に設置し、SBCをIoT機器に組み込む
    • まさにK3sの想定している用法そのまんまである
  • モバイルゲームのeSports大会
    • 会場の小型PCにゲームサーバー用agentノードを設置し、agentの管理は会場外のserverノードを使用する
    • helmやkubectlによるデプロイを前提として作られたモバイルゲームの場合、eSports向け展開がやりやすくなるかもしれない
    • K3s serverノードを会場の外に置けると、会場常駐エンジニアの数を減らせる(リモート対応できる)

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

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

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

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

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

K3sのデフォルト設定

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

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

VXLANは単にL2フレームをUDPでラップしただけのもので暗号化は一切行われておらず、簡単に中身を読むことができる。 もし興味があればWiresharkで見ると良い。素人でも直感でヤバさを理解できるだろう。

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

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

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

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

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

  • 単に私がマルチK3sクラスター実験環境を組みたいと思っているから
    • そもそもKiloはマルチKubernetesクラスター環境を実現することを目的に作られている
  • 自力構築が面倒なフルメッシュVPNをお手軽に組める
    • 信頼度の低い激安VPS愛好家にとって、フルメッシュネットワークを手軽に組めることは魅力的である
  • 導入実績がそこそこあり、わかりやすい資料もある

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

いくつか注意点がある。

  • KubernetesユーザーやLinuxユーザーなら常識的に考えてわかるだろうと思うものは省略している
  • ここのコマンド例がどんな環境でも動くことは一切保証できない
    • あくまでも私の使った激安VPSで動いたというだけである
    • SBC・VPS・コンピューティングインスタンス(例えばEC2)によって初期状態は異なる
  • この例ではコマンドを1つ1つ手動で実行しているが、実際にはAnsibleを使うべきだろう
    • 大量のマシンに手動でSSHなんてたまったものではない。
    • 実際に自分がK3sクラスター(server1台、agent5台)を構築した際には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
# Ansibleなどの構成管理ツールを使うなら、limitはやめといたほうがいいかもしれない

# ufw有効化
ufw enable

Wireguardのインストール

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

apt install wireguard

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を入手する

サーバーノードの/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

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 で書かれていることをもとにしている。やっていることはほとんど同じである。