本文の内容は、2022年11月15日にEDUARDO MÍNGUEZが投稿したブログ(https://sysdig.com/blog/how-to-secure-helm/)を元に日本語に翻訳・再構成した内容となっております。
Helmは、Kubernetesアプリケーションのデプロイに広く使われています。いくつかのコマンドで簡単に公開と利用ができ、GitOpsパイプラインに統合することもできます。しかし、Helmは十分に安全なのでしょうか?盲目的に信頼できるのでしょうか?
この記事では、Helmを使用する利点と落とし穴について説明し、安全性を確保する方法についていくつかの推奨事項を提示します。さあ、はじめましょう
なぜHelmなのか?
Helmは元々DeisLabsによって作られたオープンソースのCNCFプロジェクトです。その仕組みについて詳しく知りたい場合は、Learning Cloud Native hubのHelm 101の記事を読むことをお勧めします。Kubernetes上でアプリケーションのライフサイクルを管理するのは難しいというのが正直なところです。アプリケーションをデプロイするだけでも、少なくともデプロイが必要ですし、通常はConfigMapやSecretを変更してアプリケーションの設定を調整したり、必要に応じてCRDをデプロイしたりと、様々な作業が必要です。明らかに、一筋縄ではいきません。
環境変数を使って独自のデプロイメントファイルを作成し、実行時にenvsubstで置き換える(envsubst < deploy.yml | kubectl apply -f -)ことで「DIYテンプレートエンジン」を実現できますが、これはおそらく最適解ではありません。
Kustomizeは以前のDIYソリューションを改善したものですが、同様にいくつかの制限があります(主にテンプレート化に重点を置いており、パッケージングではありません)。Jsonnetもテンプレート化に使用できます。
Helmは完璧ではありませんが、シンプルなコマンドインターフェイス、Artifact Hubと呼ばれる9000以上のチャートが利用できるリポジトリ(そして自分のリポジトリに自分のチャートをホストする機能)、テンプレートエンジン(60以上の利用可能な関数、ほとんどがGoテンプレート言語に基づいている)を提供することでそのプロセスを簡単にしようとします。これにより、複雑なアプリケーションをパッケージ化し、特定のパラメータを与えるだけで簡単にデプロイできるようにすることができます。
例えば、レプリケーションを有効にした MySQL クラスター全体を、architecture=replication パラメータを使うだけでデプロイできます(正直に言うと、非自明な作業です)。
また、hooks(’pre-install’ などのデプロイプロセスの特定のポイントで特定のタスクを実行する)などの高度な機能もあり、ArgoCD や Flux などの GitOps ツールと統合することができます。ライブラリチャートや名前付きテンプレートを活用したり、ポストレンダリングタスクを実行することもできます(例:Kustomizeを実行する)。
Helmのセキュリティーを確保する方法
ここまで多くのことを説明してきましたが、セキュリティ面には一切注意を払っておらず、ほとんどのチャートはデフォルトでは安全ではありません。カバーしたいプロセスに応じて、取り組むべきいくつかの角度があります。 Helm チャート、つまりチャートによって作成された Kubernetes オブジェクトを使用しているだけですか、それともカスタム Helm チャートについて話しているのでしょうか?
カスタムHelmチャート
独自のHelmチャートを作成する場合、いくつかの一般的な推奨事項と、セキュリティに焦点を当てたものが適用されます。- チャートはGitリポジトリに保存する。2022年現在では当たり前のことかもしれませんが、Git を使うことでロールバックが容易になったり、変更を追跡できたりと、いくつかの利点があります。
- Helmのチャートを適切なリポジトリに保存する。チャートはHTTPで提供することもできますが、最近は何でもHTTPSですよね?
- helm lintやその他のリンターを使って、Helmチャートが適切に形成されていることを確認します。くだらないtypoのために本番環境を壊したくはないですよね?
apiVersion: v2
name: hello-world
description: A Helm chart for Kubernetes
type: application
appVersion: "0.0.1"
$ helm lint --strict
==> Linting .
[ERROR] Chart.yaml: version is required
[INFO] Chart.yaml: icon is recommended
[ERROR] templates/: validation: chart.metadata.version is required
[ERROR] : unable to load chart
validation: chart.metadata.version is required
Error: 1 chart(s) linted, 1 chart(s) failed
チャートには一貫したバージョニングを使用する(HelmはSemVer2規格に準拠しています)。再現性を高めるため、また、脆弱性が発見されたためにチャートを更新する必要があるような状況で迅速に対応するために有効です。お使いのチャートがバージョン管理されていない、あるいは「latest」を使っている場合、どちらをアップデートしますか。
チャート自体のバージョン(Chart.yamlファイルのバージョン)と、アプリケーションのバージョン(appVersion)の2種類があります。
$ helm show chart falcosecurity/falco | grep -E '^version|^appVersion'
appVersion: 0.33.0
version: 2.2.0
Changelogを保つことを忘れないでください(Falcoがやっているように)
ユースケースをカバーするために、Helmチャートのテストシナリオを作成します。helm test <RELEASE_NAME>を実行してデプロイしたチャートをテストするKubernetesオブジェクト(Helmテンプレートのようなもの)を作成することで、Helmデプロイが成功したことを検証することです。例えば、アプリケーションをデプロイしたのと同じネームスペースで動作するシンプルなポッドで、アプリケーションAPIに問い合わせを行い、正しくデプロイされているかどうかを確認するテストが可能です:
apiVersion: v1
kind: Pod
metadata: … annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ .Values.service.name }}:{{ .Values.service.port }}']
restartPolicy: Never
通常、テストは templates/tests/ フォルダに格納され、テストであることを識別するために “helm.sh/hook”: test というアノテーションをつけることが要求されます。
$ helm test hello-world
NAME: hello-world
...
Phase: Succeeded
$ kubectl get po -n hello-world
NAME READY STATUS RESTARTS AGE
hello-world-78b98b4c85-kbt58 1/1 Running 0 91s
hello-world-test 0/1 Completed 0 67s
helm package -sign で簡単にチャートに署名できます (そして helm install –verify で検証できます)。ソフトウェアコンポーネントの完全性を保証することは、ソフトウェアのサプライチェーンを確保する際に最も一般的な作業です。これは通常、デジタル署名 (ソフトウェア自体に含まれているか、それに近いもの) を検証することを意味します。Helm は PGP ベースのデジタル署名を使用して、パッケージチャートと一緒に保存される実績ファイル (.prov) に保存される実績レコードを作成します。例を見てみましょう。
$ helm package --sign --key 'Eduardo Minguez' hello-world --keyring ~/.gnupg/secring.gpg
Password for key "Eduardo Minguez (gpg key) <edu@example.com>" >
Successfully packaged chart and saved it to: /home/edu/git/my-awesome-stuff/hello-world-0.0.1.tgz
provenanceファイルは次のようになります:
$ cat hello-world-0.0.1.tgz.prov
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
...
name: hello-world
...
files: hello-world-0.0.1.tgz: sha256:b3f75d753ffdd7133765c9a26e15b1fa89784e18d9dbd8c0c51037395eeb332e
-----BEGIN PGP SIGNATURE-----
wsFcB…
-----END PGP SIGNATURE-----%
署名が一致しない場合、Helmは文句を言います:$ helm verify hello-world-0.0.1.tgz
Error: openpgp: invalid signature: hash tag doesn't match
helm install –verifyを実行すると、自動的に証明ファイルがチェックされます:
$ helm install --verify myrepo/mychart-1.2.3
あるいは、チャートをpullして検証することもできます:
$ helm pull --verify myrepo/mychart-1.2.3
公開鍵は –verify が動作するためにあらかじめ信頼されている必要があるので、どこかで公開されている必要があり、そうでない場合は失敗します:
$ helm pull --verify myrepo/mychart-1.2.3
Error: openpgp: signature made by unknown entity
$ cat security/pubkey.gpg | gpg --import --batch
$ helm pull --verify myrepo/mychart-1.2.3
Signed by:..
署名ストレージとしてRekorを使うためのsigstoreというHelmプラグインもあるので、そちらを使うとさらによいでしょう。
CI/CDパイプラインでそれまでのすべてのステップ(テスト、バージョン管理、署名、リリース)を自動化し、すべての変更についてベストプラクティスと一致することを確認し、手動で変更を行う際の潜在的な問題を回避することができます。
チャートのテストとリリースを行う GitHub actions ワークフローの作成方法については、helm/charts-repo-actions-demo を参考にするとよいでしょう。
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.4.0
with:
charts_dir: charts
config: cr.yaml
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
Kubernetesオブジェクト
テンプレート経由でKubernetesオブジェクトを作成する場合、Helmはセキュリティ対策をそのまま提供するわけではありません。自己責任で、コンテナをルートユーザーでデプロイしたり、全能力を発揮させたりといったバッドプラクティスを適用することができます(OK、やってみたいかも)。それでは、いくつかの推奨事項についてお話しましょう。ロールベースのアクセス制御(RBAC)を使用して、オブジェクトの権限を制限する(すべてにcluster-adminを使用しないこと)。例えば、falcosidekickのHelmチャートは、K8s Deploymentで使われる必要なパーミッションを最小化するために、Role、ServiceAccount、RoleBindingを作成します:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "falcosidekick.fullname" . }}
…
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "falcosidekick.fullname" . }}
…
rules:
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
…
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
…
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "falcosidekick.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "falcosidekick.fullname" . }}
…
まともなデフォルトを用意する。例えば、あなたのチャートがMySQLポッドを含んでいる場合、デフォルトのパスワードを使用しないようにしましょう。その代わり、ランダムに生成するか、ユーザーに強制的に指定させるようにしましょう。ただし、このGitHub issueやこのブログ記事で紹介されているアップグレードの対処方法など、考慮すべき点がいくつかあります。
アップグレード時に上書きをしないようにするには、以下のようにlookup関数とリソースポリシーアノテーションを使用します。
{{- if not (lookup "v1" "Secret" .Release.Namespace "hello-world") }}
apiVersion: v1
kind: Secret
metadata:
name: mysecret
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
stringData:
password: {{ randAlphaNum 24 }}
{{- end }}
攻撃対象領域を小さくする。デプロイを小さくし、必要に応じてフラグや値を使用してコンポーネントや機能を有効にします。たとえば、Falco Helm チャートはデフォルトでは falcosidekick をデプロイしませんが、簡単に有効にすることができます。
falcosidekick:
# -- Enable falcosidekick deployment.
enabled: false
これをChartで使用します:
dependencies:
- name: falcosidekick
condition: falcosidekick.enabled
Kubernetesの残りの推奨事項はすべて適用されるので(例えばCISベンチマークを含む)、Kubernetesのオブジェクト定義をスキャンしてベストプラクティスを確認してください。もしあなたの好みのツールがHelmチャートをサポートしていなくても、心配しないでください。以下のように、Helmテンプレートコマンドを使用して、常に前のステップでKubernetesオブジェクトをレンダリングすることができます。
$ helm template falco falcosecurity/falco --namespace falco --create-namespace --set driver.kind=ebpf > all.yaml
このように検証します:
$ myawesometool --verify all.yaml
Helmチャートを使う
- Helmチャート、特にサードパーティのものを盲目的に信用するのはやめましょう。幸い、これまで見てきたように、helm templateコマンドはHelmチャートが作成したKubernetesオブジェクトをレンダリングして出力するので、Kubernetesクラスターにデプロイする前に少なくともその結果をざっと見ておくことは良いプラクティスでしょう。joaquinito2051のチャートはおそらく使わない方が良いでしょう。
- 先に説明したように、helm verify を使って使用するチャートのデジタル署名を確認し、想定しているチャートを使用していることを確認しましょう。
- 未使用のリリースをアンインストールする。もしあなたがもう使っていないHelmのリリースがあれば、それをアンインストールして攻撃対象領域を減らしてください。
- 使用するHelmチャートは常にアップデートするようにしましょう(Helmバイナリやプラグインも同様です!)。現実問題として、ミスやバグは起こるので、Helmチャート自体やHelmチャートが作成するオブジェクト(例えば、脆弱性が発見されたコンテナイメージを使用する場合)の両方について、常に最新の修正を施した最新バージョンを使用することが良い考えです。これはサブチャートにも適用されます。アップグレードによって何が変わるかを確認する方法はいくつかあり、helm diff プラグインを使うこともできます:
$ helm diff --install foo --set image.tag=1.14.0 .
あるいは helm template を使ってマニフェストをレンダリングし、kubectl で diff を取る方法もあります:
$ helm template --is-upgrade --no-hooks --skip-crds foo --set image.tag=1.14.0 . | kubectl diff --server-side=false -f -
ただし、両方のアプローチを使用する場合、いくつかのコーナーケースがあり、すべてのシナリオをカバーするために両方を確認することが理想的です。詳しくはkubectlとHelmのdiffの課題の記事をご覧ください。
- Kubernetesのシークレットを暗号化して保存する。Base64はエンコードアルゴリズムであり、暗号化アルゴリズムではありませんし、その名前にもかかわらず、Kubernetesのシークレットはシークレットではありません。暗号化されたシークレットをコードと一緒に保存することを好む人もいれば、別の場所に保存することを好む人もいるため、これは複雑な議論になる話題です。helm-secretsプラグイン、Hashicorp Vault、BitnamiのSealed secrets、Mozillaのsops、DIYソリューションなど、言及に値する代替案がいくつかあります。helm-secretsをvals経由でAWS SSMで利用する簡単な例を見てみましょう。
AWS SSMのSecureStringオブジェクトを作成します:
$ aws ssm put-parameter --name mysecret --value "secret0" --type SecureString
必要なHelmパラメータを確認します。この例では、「secretdata」です:
$ cat hello-world/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
stringData:
password: {{ .Values.secretdata }}
検証します:
$ helm secrets --backend vals template hello-world -s templates/secret.yaml --set secretdata="ref+awsssm://mysecret"
---
# Source: hello-world/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
stringData:
password: secret0
最後に、チャートをレンダリングした直後に、以下のように任意のコマンドを実行することもできます。
$ helm install mychart my-chart --post-renderer my-script.sh
my-script.shスクリプトは、環境変数を適用するためのkustomizeの実行、特定のパラメータが使用されていないことの確認、データを取得するためのWebhookを呼び出すスクリプト、Windowsバッチスクリプトなど、ほとんどすべてのものにすることができます!あなたの想像力は無限大です