はじめに
こんにちは、SREの🦊です。
今回は、Kubernetes環境における「特権モード(privileged: true)からの脱却」について記載しました!
突然ですが、皆さんはカーネルパラメータを使用したパフォーマンスチューニングを試したことはありますか?
今回は、このカーネルパラメータをEKS/Karpenter環境で拡張する方法を検証しました。
この方法は、ネット上に見つけられなかったのと個人的にかなり苦労した検証だったので、備忘としてブログに残しておきます!
本記事では、なぜ特権モードを使っていたのか、なぜ脱却したかったのか、どのようにして脱却したのかをお伝えします。
本記事では、カーネルパラメータやパフォーマンスチューニングについては記載しません。そのため、チューニングについては以下の記事が参考になりそうだったため記載しておきます。
特権モードとは
Kubernetesにおける特権モード(privileged: true)とは、コンテナにホストマシンとほぼ同等の権限を付与する設定です。
通常、コンテナはLinuxのNamespaceやcgroupsによって隔離されており、ホストシステムへのアクセスは制限されています。特権モードを有効にすると以下のような権限が付与されます。
- ホストのすべてのデバイスへのアクセス
- カーネルモジュールのロード
- ホストのネットワーク設定の変更
- すべてのLinux Capabilitiesの付与
つまり、コンテナの隔離性がほぼ無効化され、ホストOSに対して何でもできる状態になります。
なぜ特権モードを使っていたのか
TIME_WAITソケットによるポート枯渇問題
過去に実施した負荷試験で以下の事象がありました。あるマイクロサービスでトラフィックが閾値を超えた段階で、リソース状況とは関係なく5xxエラーが発生していました。
failed to connect db: execute query: dial tcp xx.xxx.xx.xx:3306: connect: cannot assign requested address
Podに入ってTIME_WAITの状態を確認すると、3万件以上のソケットが滞留していました。
$ netstat -an | grep TIME_WAIT | wc -l
32707
これがポート枯渇の原因でした。
TIME_WAITとは、TCP通信でコネクションが終了した後に一時的に保持される状態のことです。遅延したパケットの処理やコネクションの正しい終了を保証するために重要な役割を持っています。しかし、高トラフィック環境ではこのTIME_WAITソケットが大量に滞留し、ポートの枯渇を招く可能性があります。
initContainerを使用する方法と問題点
この問題に対応するため、私たちの管轄するマイクロサービスでは、カーネルパラメータのtcp_tw_reuseとsomaxconnを拡張していました。tcp_tw_reuseはTIME_WAIT状態のソケットの再利用を許可するパラメータで、somaxconnはリスニングソケットの接続待ちキューの最大長を指定するパラメータです。
ただ、ここで問題になるのが、これらのカーネルパラメータをどうやってPod内で設定するかです。
Kubernetesでは、カーネルパラメータは「safe」と「unsafe」に分類されています。net.core.somaxconnやnet.ipv4.tcp_tw_reuseはunsafeなsysctlに分類されるため、通常の方法では設定できません。
そこで、よくある実装例として、initContainerに特権モード(privileged: true)を設定する方法があります。sysctlコマンドでカーネルパラメータを拡張する方法で、管轄のサービスでも同様の実装が行われていました。
initContainers:
- name: sysctl
image: public.ecr.aws/docker/library/busybox:1.36.1
command:
- "/bin/sh"
- "-c"
- "sysctl -w net.core.somaxconn=65535; sysctl -w net.ipv4.tcp_tw_reuse=1"
securityContext:
privileged: true # これがセキュリティ的に問題
この実装自体は問題なく動いていたのですが、セキュリティ的には大きな問題がありました。
なぜ特権モードから脱却したかったのか
特権モード(privileged: true)を設定したコンテナは、ホストシステムの全機能にアクセスできてしまいます。カーネル機能への無制限アクセスが可能で、万が一そのコンテナに侵入された場合、クラスタ全体に影響が及びます。
「initContainerはPod起動時にしか動作しないので、リスクが限定的では?」という見方もあるかもしれません。確かに、常時稼働するメインコンテナに比べて攻撃可能な時間窓は短いです。しかし、initContainerの起動中に侵入された場合、特権を悪用してホストへバックドアを仕掛けたり、他のPodへ横展開したりする可能性があります。短い時間窓であっても、特権モードのリスクは無視できません。
当時、組織全体でPod Security Standards(PSS)への準拠を進めており、privileged: trueの使用はRestrictedプロファイルで明確に禁止されている設定でした。セキュリティポリシーの観点からも、この状態を放置するわけにはいきませんでした。
とはいえ、カーネルパラメータの拡張は検証コストや削除時の影響を考えると、負荷対策として必要不可欠でした。そのため、特権モードを使わずに、安全にカーネルパラメータを拡張する方法を見つける必要がありました。
特権モードを使わない方法の検証(失敗)
試行1: initContainerにcapabilitiesを追加
特権モードの代わりにLinux capabilitiesを使う方法を試しました。NET_ADMINやSYS_ADMINを追加すれば、必要な権限だけを付与できると考えました。
initContainers:
- name: sysctl
image: public.ecr.aws/docker/library/busybox:1.36.1
command:
- "/bin/sh"
- "-c"
- "sysctl -w net.core.somaxconn=65535;"
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_ADMIN
privileged: false
結果は以下のエラーになりました。
sysctl: error setting key 'net.core.somaxconn': Read-only file system
unsafeなsysctlを設定するには、capabilitiesだけでは権限が足りず、privileged: trueが必要でした。
試行2: deployment単位でsecurityContextを指定
PodのsecurityContextでsysctlを直接指定する方法を試しました。
spec:
securityContext:
sysctls:
- name: net.core.somaxconn
value: "65535"
- name: net.ipv4.tcp_tw_reuse
value: "1"
結果は以下のエラーです。
Warning SysctlForbidden kubelet forbidden sysctl: "net.core.somaxconn" not allowlisted
kubeletの設定でallowedUnsafeSysctlsを許可していないと使えませんが、EKSではkubeletの設定を直接変更できません。
EC2NodeClassのspec.kubeletでの設定も試しました。しかし、Karpenterではspec.kubelet.allowedUnsafeSysctlsがサポートされていませんでした。
Error from server: field not declared in schema
試行3: userDataでsysctl -wコマンドを実行
EC2NodeClassのuserDataでNode起動時にカーネルパラメータを設定する方法を試しました。
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: my-nodeclass
spec:
userData: |
#!/bin/bash
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
しかし、Podで確認すると設定が反映されていませんでした。
$ sysctl net.core.somaxconn
net.core.somaxconn = 4096 # 変わっていない
Node(ホストOS)のカーネルパラメータを変更しても、Podは独立したネットワーク名前空間で動作するため、設定が継承されません。
hostNetwork: trueを設定すればNodeと同じネットワーク名前空間を使えますが、セキュリティ的にアンチパターンであり、Port競合のリスクもあります。
試行4: userDataで/etc/sysctl.confに永続化
/etc/sysctl.confに設定を書き込んで永続化する方法も試しました。
userData: |
#!/bin/bash
echo "net.core.somaxconn=65535" >> /etc/sysctl.conf
echo "net.ipv4.tcp_tw_reuse=1" >> /etc/sysctl.conf
sysctl -p
これも結果は同じでした。ホストOSの設定ファイルとPod内のファイルシステムは別物なので、Podには反映されません。
解決策: EC2NodeClassを用いたallowedUnsafeSysctlsの許可
AWSサポートに問い合わせたところ、解決策が見つかりました。
具体的には、**EC2NodeClassのuserDataでAL2023のNodeConfigを使えば、kubeletのallowedUnsafeSysctlsを設定できる。**とのことでした。
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: al2023-with-sysctls
spec:
amiFamily: AL2023
userData: |
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
kubelet:
config:
allowedUnsafeSysctls:
- net.core.somaxconn
- net.ipv4.tcp_tw_reuse
spec.kubeletでの設定とは違い、userDataとしてNodeConfigを渡すことでkubeletの設定を変更する方法です。AL2023のAMIで利用可能です。
これにより、Podのspec.securityContext.sysctlsで設定したカーネルパラメータがエラーなく適用されるようになりました。
# deployment.yamlの抜粋
spec:
securityContext:
sysctls:
- name: net.core.somaxconn
value: "65535"
- name: net.ipv4.tcp_tw_reuse
value: "1"
以上の結果から、initContainerとprivileged: trueが不要になることがわかりました。
allowedUnsafeSysctlsのセキュリティリスク
特権モードは脱却できそうですが、そもそもallowedUnsafeSysctlsを使うことで新たなセキュリティリスクが生じないか、評価する必要がありました。
allowedUnsafeSysctlsのリスク
Kubernetesがsysctlを「unsafe」と分類する理由は、これらのパラメータがNamespace単位で隔離されておらず、同一Node上の他のPodに影響を与える可能性があるためです。しかし、net.core.somaxconnやnet.ipv4.tcp_tw_reuseはネットワークNamespaceに属するパラメータであり、Pod間で隔離されています。
また、allowedUnsafeSysctlsは許可リスト方式で動作するため、明示的に指定したパラメータのみ設定可能です。特権モードのように「何でもできる」状態とは異なり、影響範囲を限定できます。
結論
上記のリスクは存在するものの、セキュリティリスクは限定的であり、既存のアクセス制御で十分軽減できると判断しました。
まとめ
本記事では、特権モード(privileged: true)からの脱却を実現した際の問題と解決策を記載しました!
Kubernetesのsysctl管理やEKSの制約、KarpenterのEC2NodeClassの機能など、複数の技術要素を理解した上で解決策にたどり着きました。
技術的な学びとしては以下の点が大きかったです。
- Kubernetesにおけるカーネルパラメータのsafe/unsafeの分類と、それぞれの設定方法
- EKS環境でのkubelet設定の制約と、AL2023のNodeConfigを使った設定方法
- セキュリティと運用のトレードオフを考慮した意思決定の重要性
この記事が、同じような課題に直面している方の参考になれば幸いです。