はじめに
Cilium を用いた TLS セッション終端を Dockerコンテナで試したので手順を紹介します。
Cilium ドキュメントに次のようにありますが
Cilium は、Docker や Kubernetes などの Linux コンテナ管理プラットフォームを使用してデプロイされたアプリケーション サービス間のネットワーク接続を透過的に保護するためのオープン ソース ソフトウェアです。
ドキュメントを読み進めると、Kubernetes の手順はありますが、Docker の手順は見当たりません。古いドキュメント(Cilium v1.9)に Docker の手順がありますが、Key-Valueストアの consul が非推奨となったため etcd に変更すると、その手順では動作しませんでした。
モチベーション
Cilium の基盤となっているのは、eBPF と呼ばれる新しい Linux カーネル テクノロジです。これにより、強力なセキュリティの可視性と制御ロジックを動的に挿入できます。eBPF は Linux カーネル内で実行されるため、アプリケーションコードやコンテナー構成を変更することなくセキュリティ ポリシーを適用および更新できます。
次のブログ記事によると
次のようなネットワーク層の機能はビジネスロジックから切り離したいです。
- パケットやリクエストのフィルタリング
- セキュア通信
- シンプルなネットワーキング(Calico 等の代替)
- 負荷分散、経路制御
- 帯域幅管理
- モニタリングとトラブル要因解析
そのためにこれまでは、共有ライブラリなどでコードを分離したり、プロキシやサイドカーでプロセスを分離するといったアプローチがとられてきましたが、eBPF テクノロジを用いることでこれらの機能をカーネル空間へ移動することで(次図)
これまでプロキシによって実行されていた機能の多くをオフロードすることができ、オーバーヘッドと複雑さを軽減できる可能性があるとのこと。
本稿では、異なるノードに配置された Dockerコンテナ app を Cilium を用いてオーバーレイネットワークで接続し、一方に TLS終端を設定し動作を確認します。
Node1 のコンテナ app1
と Node2 のコンテナ app2
を接続
検証環境
Node1 と Node2 は同一構成としました。
$ uname -a
Linux debian11 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64 GNU/Linux
$ docker --version
Docker version 23.0.5, build bc4487a
$ docker compose exec -it cilium cilium version
Client: 1.13.2 8cb94c70 2023-04-17T23:19:21+02:00 go version go1.19.8 linux/amd64
Daemon: 1.13.2 8cb94c70 2023-04-17T23:19:21+02:00 go version go1.19.8 linux/amd64
Cilium のシステム動作要件は、次を参照ください。
Cilium コンポーネント概観
Docker と Cilium は、プラグインcilium-docker
(Docker ネットワークドライバープラグイン)を介してやり取りします。
docker CLI で Dockerネットワークを作成すると dockerd
は、cilium-docker
プラグインを介して cilium-agent
にネットワークの作成を要求します。ネットワーク作成後、そのネットワークでコンテナを開始すると dockerd
は、cilium-docker
プラグインを介してcilium-agent
に IPアドレスの割り当てを要求します。コンテナ間の通信に何らかのポリシーを適用したいときは、cilium CLI でcilium-agent
へポリシーを適用します。
-
Cilium エージェント(
cilium-agent
)は、クラスター内の各ノードで実行されるデーモンです。ネットワーキングやサービス負荷分散、ネットワーク ポリシー、および可視性と監視の構成を API を介して受け入れ、ネットワークアクセスを制御するための eBPF プログラムを管理します。 -
Cilium CLI(
cilium
)は、Cilium エージェントと共にインストールされるコマンドラインツールです。同じノードで実行されている Cilium エージェントの API と対話しネットワークポリシーを構成したりエージェントの状態を調べることができます。 - Cilium オペレータは、クラスター全体に対して論理的に 1 回処理する必要があるタスクを管理します。
-
プラグイン(
cilium-cni
、cilium-docker
) は、Kubernetes または Docker によって呼び出されます。ノードの Cilium エージェントの API と対話して必要なデータパス構成をトリガーし、ネットワーク、負荷分散、およびネットワーク ポリシーを構成します。 - データストアは、Ciliumエージェント間で状態を伝達するために使用します。
ここでの Cilium CLI は、別途 Kubernetes クラスター上に Cilium をインストール/管理するための CLI ツール cilium
と同じ呼称ですが別物なので混同しないようにしてください。
データストア
Cilium では、エージェント間で状態を伝達するためのデータストアが必要です。次のデータストアがサポートされています。
- Kubernetes CRD
- Key-Valueストア(KVストア)
ここでは KVストアを用います。これまで Cilium は、KVストアとして consul
と etcd
をサポートしてきましたが、リリース 1.11 で consul
は非推奨となりました。
consul
を用いると Ciliumエージェントは、実行時ログに次の deprecated
警告を出力しますが、まだ動作するようです。
Support for Consul as a kvstore backend has been deprecated due to lack of maintainers. If you are interested in helping to maintain Consul support in Cilium, please reach out on GitHub or the official Cilium slack
他方 etcd
を用いると Ciliumエージェントは、開始数分後にステータスが "KVStore: Failure Err: quorum check failed"
となりエラー終了します。これは、Ciliumエージェントが定期的に etcd のハートビート・キー(cilium/.heartbeat)の更新を監視しハートビートキーが時間内に更新されない場合、クォーラムチェックに失敗したとみなされるためです。
調べた限りでは、現時点で設定だけでエラー終了を回避することはできず、Cilium オペレータを常駐させハートビート・キーを定期的に更新する必要がありました。
Cilium 実行
Docker-Compose を用いて Node1 と Node2 のそれぞれで Ciliumコンテナを開始します。
$ export NODE1_ADDR=<your-node1-ip-address>
$ export NODE2_ADDR=<your-node2-ip-address>
$ docker compose up -d
Node2 の cilium-agent
は、Node1 の KVストア etcd
を参照します。cilium CLI は、Cilium エージェントに同梱されています。
Node1 の docker-compose.yml の例
version: '3'
services:
cilium:
container_name: cilium
image: quay.io/cilium/cilium:v1.13.3
command: >
cilium-agent
--ipv4-node="${NODE1_ADDR}"
--enable-ipv6=false
--kvstore=etcd
--kvstore-opt=etcd.address=127.0.0.1:2379
--tunnel=vxlan
--cluster-name=local
--cluster-id=0
volumes:
- /var/run/cilium:/var/run/cilium
- /sys/fs/bpf:/sys/fs/bpf
- /var/run/docker/netns:/var/run/docker/netns:rshared
- /var/run/netns:/var/run/netns:rshared
- /boot:/boot/
- /lib/modules:/lib/modules/
- /usr/lib/modules:/usr/lib/modules/
network_mode: "host"
cap_add:
- "NET_ADMIN"
privileged: true
depends_on:
- kvstore
plugin:
container_name: plugin
image: quay.io/cilium/docker-plugin:v1.13.3
command: cilium-docker
volumes:
- /var/run/cilium:/var/run/cilium
- /run/docker/plugins:/run/docker/plugins
- /var/run/docker.sock:/var/run/docker.sock
network_mode: "host"
cap_add:
- "NET_ADMIN"
privileged: true
depends_on:
- cilium
kvstore:
container_name: kvstore
image: quay.io/coreos/etcd:v3.5.8
command: etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379
network_mode: "host"
operator:
container_name: operator
image: quay.io/cilium/operator-generic:v1.13.3
command: >
cilium-operator-generic
--enable-ipv6=false
--kvstore=etcd
--kvstore-opt=etcd.address=127.0.0.1:2379
--enable-k8s=false
--cluster-pool-ipv4-cidr=10.0.0.0/16
network_mode: "host"
depends_on:
- kvstore
Node2 の docker-compose.yml の例
version: '3'
services:
cilium:
container_name: cilium
image: quay.io/cilium/cilium:v1.13.3
command: >
cilium-agent
--ipv4-node="${NODE2_ADDR}"
--enable-ipv6=false
--kvstore=etcd
--kvstore-opt=etcd.address="${NODE1_ADDR}":2379
--tunnel=vxlan
--cluster-name=local
--cluster-id=0
--certificates-directory=/opt/cilium/certs
volumes:
- /var/run/cilium:/var/run/cilium
- /sys/fs/bpf:/sys/fs/bpf
- /var/run/docker/netns:/var/run/docker/netns:rshared
- /var/run/netns:/var/run/netns:rshared
- ./tls:/opt/cilium/certs/my-tls/
- ./policy:/opt/cilium/policy/
network_mode: "host"
cap_add:
- "NET_ADMIN"
privileged: true
plugin:
container_name: plugin
image: quay.io/cilium/docker-plugin:v1.13.3
command: cilium-docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/cilium:/var/run/cilium
- /run/docker/plugins:/run/docker/plugins
network_mode: "host"
cap_add:
- "NET_ADMIN"
privileged: true
depends_on:
- cilium
Node1 と Node2 を同一クラスターのメンバーとするために cilium-agent
の起動引数 --cluster-name
と --cluster-id
に同じ値を指定します。
コンテナ cilium
に接続し cilium CLI でステータスを確認
$ docker exec -it cilium cilium status
KVStore: Ok etcd: 1/1 connected, lease-ID=694d886b8f79e705, lock lease-ID=694d886b8f79e707, has-quorum=true: 127.0.0.1:2379 - 3.5.8 (Leader)
Kubernetes: Disabled
Host firewall: Disabled
CNI Chaining: none
CNI Config file: CNI configuration file management disabled
Cilium: Ok 1.13.3 (v1.13.3-36cb0eed)
NodeMonitor: Listening for events on 1 CPUs with 64x4096 of shared memory
Cilium health daemon: Ok
IPAM: IPv4: 2/65534 allocated from 10.13.0.0/16,
IPv6 BIG TCP: Disabled
BandwidthManager: Disabled
Host Routing: Legacy
Masquerading: IPTables [IPv4: Enabled, IPv6: Disabled]
Controller Status: 21/21 healthy
Proxy Status: OK, ip 10.13.50.180, 0 redirects active on ports 10000-20000
Global Identity Range: min 256, max 65535
Hubble: Disabled
Encryption: Disabled
Cluster health: 2/2 reachable (2023-05-30T07:31:08Z)
Node1 と Node2 が正常に認識されるまで数分ほど待ちます。正常に認識されると Cluster health
が 2/2 reachable
になります。
クラスターノードに割り当てられた CIDR を確認
$ docker exec -it cilium cilium node ls
Name IPv4 Address Endpoint CIDR IPv6 Address Endpoint CIDR
local/node1 192.168.250.154 10.13.0.0/16
local/node2 192.168.251.138 10.234.0.0/16
アプリケーション コンテナ app 実行
Node1 と Node2 でそれぞれコンテナ app1
と app2
を開始しします。
Node1
Dockerネットワーク cilium-net
作成。ドライバーに cilium
を指定していることに注意
$ docker network create --driver cilium --ipam-driver cilium cilium-net
コンテナ app1
を開始しシェル接続
$ docker run -it --rm --name app1 -l name=app1 --net cilium-net alpine/curl sh
ここでオプション--net
に cilium-net
を指定していることに注意してください。プラグイン cilium-docker を介して IPアドレスが払い出されます。
コンテナ app1
の IPv4アドレスを確認
/ # ip a
...
45: cilium0@if46: ...
inet 10.13.165.153/32 scope global cilium0
...
Cilium エンドポイントにコンテナapp1
が追加されたことを確認
$ docker exec -it cilium cilium endpoint list
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
ENFORCEMENT ENFORCEMENT
31 Disabled Disabled 2532 container:name=app1 10.13.165.153 ready
...
Node2
Dockerネットワーク cilium-net
を作成
$ docker network create --driver cilium --ipam-driver cilium cilium-net
コンテナ app2
を開始しシェル接続
$ docker run -it --rm --name app2 -l name=app2 --net cilium-net busybox sh
コンテナ app2
の IPv4アドレスを確認
/ # ip a
...
35: cilium0@if36: ...
inet 10.234.96.18/32 scope global cilium0
...
Cilium エンドポイントにコンテナapp2
が追加されたことを確認
$ docker exec -it cilium cilium endpoint list
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
ENFORCEMENT ENFORCEMENT
3948 Disabled Disabled 15836 container:name=app2 10.234.96.18 ready
...
疎通確認
Node1 のコンテナ app1
から Node2 のコンテナ app2
への疎通を確認します。
コンテナ app2
でポート 9999
をリッスン
/ # nc -l -p 9999
コンテナ app1
から app2
へメッセージ送信
/ # echo hoge | nc 10.234.96.18:9999
コンテナ app2
でメッセージを受信
/ # nc -l -p 9999
hoge
TLS終端
Cilium ネットワークポリシーを適用し Node2 のコンテナ app2
に TLS 終端を設定します。
Node2
サーバ秘密鍵とサーバ証明書、Cilium ネットワークポリシーファイルを準備します。サーバ証明書とサーバ秘密鍵の作成手順は割愛します。
フォルダ構成
.
├── docker-compose.yml
├── profile
│ └── profile.json # Cilium ネットワークポリシーファイル
└── tls
├── tls.crt # サーバ証明書
└── tls.key # サーバ秘密鍵
Cilium ネットワークポリシーファイルを作成
[{
"labels": [{"key": "name", "value": "tls-termination"}],
"endpointSelector": {"matchLabels":{"name":"app2"}},
"ingress": [{
"fromEndpoints": [
{"matchLabels":{"name":"app1"}}
],
"toPorts": [{
"ports": [{"port": "443", "protocol": "TCP"}],
"terminatingTLS": {
"secret": {
"name": "my-tls"
},
"certificate": "tls.crt",
"privateKey": "tls.key"
},
"rules": {
"http": [{}]
}
}]
}]
}]
ラベル name=app2
のエンドポイントに適用するポリシーです。ラベル name=app1
からの ingress ポート443 をTLS終端します。
terminatingTLS.secret.name
は、サーバ証明書を配置するフォルダパス(コンテナ内)の一部となります。この場合、cilium-agent の起動引数に --certificates-directory=/opt/cilium/certs
を指定しているので次の場所にサーバ証明書と秘密鍵を配置する必要があります。
- /opt/cilium/certs/my-tls/tls.crt
- /opt/cilium/certs/my-tls/tls.key
CA証明書ファイル ca.crt
(デフォルト)が存在する場合やオプション terminatingTLS.trustedCA
を指定すると mTLS になるようです。
Cilium ネットワークポリシーを適用
$ docker exec -it cilium cilium policy import /opt/cilium/policy/policy.json
ポリシーが適用されたことを確認
$ docker exec -it cilium cilium endpoint list
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
ENFORCEMENT ENFORCEMENT
3948 Enabled Disabled 15836 container:name=app2 10.234.96.18 ready
...
POLICY (ingress)
が Enabled
であれば ok.
ポリシーの詳細を取得
$ docker exec -it cilium cilium policy get name=tls-termination
全てのポリシーを削除
$ docker exec -it cilium cilium policy delete --all
動作確認
コンテナ app2
でポート 443
をリッスン
/ # nc -l -p 443
コンテナ app1
から app2
へ HTTPS リクエストを送信
/ # curl --insecure https://10.234.96.18/
コンテナ app2
でリクエストを受信
/ # nc -l -p 443
GET / HTTP/1.1
host: 10.234.96.18
user-agent: curl/7.80.0
accept: */*
x-forwarded-proto: https
x-request-id: 1395028f-32ed-4887-aa8c-0defa89bd236
x-envoy-expected-rq-timeout-ms: 3600000
TLS終端され平文で受信できました。