本連載では、エンタープライズシステムでコンテナ/Kubernetesを活用した業務システムを開発/運用するエンジニアに向けて、知っておくべきKubernetesセキュリティの基礎知識、Microsoftが提供するパブリッククラウド「Azure」を使ったクラウドでのKubernetesセキュリティ対策のポイント、気を付けておきたい注意点などの実践的なノウハウを紹介します。

これまで、システムを悪意ある攻撃者から保護するために、システムアーキテクチャを俯瞰的にとらえてこれに対応した脅威モデルを想定すべきという考え方に基づき、ID、ホスト、ネットワーク、データにまたがった多層防御 (defense-in-depth) の実装により安全性を確保して保護をする、というコンテナセキュリティの全体像に触れてきました。

今回は、この多層防御の考え方にもとづいて、その一要素であるアプリケーションセキュリティについての対策を取り上げます。

開発者が知っておくべきコンテナアプリケーションのセキュリティ

コンテナはアプリケーションの可搬性の高さから、今や多くの開発者にとって欠かせない技術となっています。しかしながら、もしコンテナ上で実行されるアプリケーションやライブラリに脆弱性があったり、マルウェアなどの悪意のあるコンテンツが含まれていたりする場合どうなるでしょうか。

攻撃者がアプリケーションの脆弱性を見つけ出し、それを悪用することで遠隔から任意のコード実行することを「リモートコード実行(RCE: Remote Code Execution)」と呼びます。

例えば、Kubernetesクラスタで動くPodのアプリケーションに対してフロントアプリの脆弱性を突いてRCEを実行すると、攻撃者は同一ホスト上で動作するバックエンドアプリに不正に侵入し、保護すべきデータベース上にある機密情報へのアクセスを試みるでしょう。



NIST Special Publication (SP) 800-125では、仮想化を「ほかのソフトウェアが実行されるソフトウェアおよび/またはハードウェアのシミュレーション」と定義しています。例えばクラウドの仮想マシンは、ハードウェア仮想化を利用して1台の物理サーバ上で多数のOSインスタンスを実行します。

一方のコンテナは、cgroups/namespaceなどを使ってOSカーネルを仮想的に分離して複数のアプリケーションでOSを共用します。

両者の違いの1つに、アプリケーションの分離レベルがあります。フロントエンド/バックエンドなどの異なる役割を持つシステムを考えてみます。仮想マシンの場合、フロントエンド/バックエンドそれぞれを異なる仮想マシンで実行するアーキテクチャを採用することが多いでしょう。

対するコンテナの場合、Kubernetesなどのコンテナオーケストレータを使って複数のアプリケーションを同一ホストで実行し、コンピューティングリソースの最適化を行います。つまり、コンテナはハイパーバイザによって明確に分離される仮想マシンより強いセキュリティ境界を持たせることができません。



コンテナブレイクアウトとは、コンテナからホストマシン上のリソース(ファイルシステム、プロセス、ネットワークインタフェースなど)に不正にアクセスすることです。コンテナという分離された環境から脱獄する行為で、悪意あるプログラムの実行により同一ホスト上で動くほかのアプリケーションのセキュリティが侵害される恐れがあります。

例えば、2019年にはコンテナランタイムruncの脆弱性である「CVE-2019-5736」が公表されました。悪意のあるコンテナはこの脆弱性を悪用することでruncバイナリのコンテンツを書き換え、コンテナのホストシステム上で任意のコマンドを実行できます。

また、2022年2月4日「CVE-2022-0492」として、Linuxカーネルにおける特権昇格の脆弱性が公表されました。これはcgroupsの論理バグでこの脆弱性を悪用されると、権限を持たないプロセスが特権昇格して権限を獲得できます。

コンテナアプリケーションを開発するには、従来の仮想マシンで動くアプリケーションセキュリティ対策に加えて、コンテナアプリケーションならではの対策が必要です。ここでは、アプリケーション開発者が気を付けておくべき次の3つの観点と対策方法を説明します。

コンテナアプリケーションの脆弱性を減らす

シークレットを適切に管理する

コンテナアプリケーションに不要な権限を与えない



コンテナアプリケーションの脆弱性を減らす

セキュリティ侵害を防ぐには、まずアプリケーションの弱点である攻撃対象領域(アタックサーフェス)を減らすことが重要です。

安全なイメージの作成

よくベースイメージとして利用されるLinuxディストリビューションはカーネルを除く基本的な設定ファイルやパッケージ、ビルドツールなどが一通り含まれます。これらの中には本番環境には必要のないファイルが多数含まれています。

この不要なファイルを削除し、アプリケーションの実行に必要な最小限のファイルのみを含んだコンテナイメージのことをdistrolessイメージと呼びます。

例えば、Goで開発したアプリケーションの場合、実行可能なバイナリを作成するにはGoコンパイラが必要ですが、バイナリを実行するコンテナは、Goコンパイラは不要です。マルチステージビルドに分割することで、最初のステージでコンパイルを実行してバイナリを作成し、次のステージではバイナリのみを含む軽量なイメージを作成します。この本番環境用のイメージのベースイメージにはdistrolessイメージである「gcr.io/distroless/static」を使います。

dockerfile

FROM golang:1.17 as build-env

WORKDIR /go/src/app

COPY *.go .

RUN go mod init

RUN go get -d -v ./...

RUN go vet -v

RUN go test -v

RUN CGO_ENABLED=0 go build -o /go/bin/app

FROM gcr.io/distroless/static

COPY --from=build-env /go/bin/app /

CMD ["/app"]

参考: Dockerfile

なお、パブリックリポジトリからダウンロードしたイメージはマルウェアや悪意のあるコードが含まれている可能性があります。そのため必ず信頼できるイメージを使用しましょう。

例えば、Azureが提供するコンテナレジストリサービスであるAzure Container Registryを使うとイメージに署名をつけることができます。イメージの利用者は、該当のイメージが作成者本人によって署名され改変されていないことが確認できます。

$ docker push myregistry.azurecr.io/myimage:v1

...

The push refers to repository [myregistry.azurecr.io/myimage]

ee83fc5847cb: Pushed

v1: digest: sha256:aca41a608e5eb015f1ec6755f490f3be26b48010b178e78c00eac21ffbe246f1 size: 524

Signing and pushing trust metadata

You are about to create a new root signing key passphrase. This passphrase

will be used to protect the most sensitive key in your signing system. Please

choose a long, complex passphrase and be careful to keep the password and the

key file itself secure and backed up. It is highly recommended that you use a

password manager to generate the passphrase and keep it safe. There will be no

way to recover this key. You can find the key in your config directory.

Enter passphrase for new root key with ID 4c6c56a:

Repeat passphrase for new root key with ID 4c6c56a:

Enter passphrase for new repository key with ID bcd6d98:

Repeat passphrase for new repository key with ID bcd6d98:

Finished initializing "myregistry.azurecr.io/myimage"

Successfully signed myregistry.azurecr.io/myimage:v1

詳細な手順は、公式ドキュメントAzure Container Registry におけるコンテンツの信頼を参考にしてください。



ライブラリ、ミドルウエアの脆弱性対策

これはコンテナアプリケーションに限った話ではありませんが、IPAが発表した情報セキュリティ10大脅威 2022によると、脆弱性対策情報の公開に伴う悪用が多く発生していることがわかります。

例えば、2021年12月9日にApache Software Foundation は ログ出力ライブラリ「Apache Log4j」の脆弱性CVE-2021-44228を公表しました。この脆弱性は「Log4Shell」とも呼ばれ、Log4j に存在する JNDI(Java Naming and Directory Interface) Lookup機能に含まれる問題により攻撃者がリモートから任意のコードを実行できる可能性があります。この脆弱性公表の翌日には実証コードが公開されこれを悪用した攻撃が多数確認されました。

また、この脆弱性はApache Log4jを利用している「Apache Struts2」や「Apache Solr」などJavaで実装されているOSSにも影響がありました。

アプリケーション開発者が自ら実装したコードのみならず、使用するライブラリ/フレームワークやミドルウェアにも脆弱性が含まれないようにバージョンや依存関係を把握して常に最新の状態に保っておくことが重要です。

ソースコードをGitHubで管理している場合は、Dependency GraphやDependabotを活用するとよいでしょう。GitHubのリポジトリの[Settings]-[Code security and analysis]から次のサービスを有効にします。

Dependency Graph: 依存関係を可視化

Dependabot Alerts: 脆弱性を検知しアラートを送信

Dependabot Security Updates: セキュリティアップデート

Dependency Graphは、リポジトリ内の依存関係を可視化できる機能です。GitHubのdependency graphをクリックすると、依存関係やCVEの情報などの詳細を確認できます。



Dependabotは、GitHubが提供するBotサービスです。リポジトリ内の依存関係をチェックし、もし脆弱性があればアラートの通知をしたり自動でPull Requestを発行したりする機能を提供します。例えば、JavaアプリケーションでMavenを利用している場合、pom.xmlでLog4shellの脆弱性を含む「log4j-core-2.14.0」を指定したとします。この状態でGitHubにコードをプッシュします。

...

org.apache.logging.log4j

log4j-core

2.14.0

GitHubの[Security]-[Dependabot alerts]を確認すると、次のようなアラートが自動で表示されているのが分かります。Log4jのバージョンが古く脆弱性が含まれるためリモートコードが実行される可能性がある旨が警告されています。



さらにこの脆弱性を修正するためのPull Requestが自動で作成されます。開発者はこの内容を確認して速やかに対策できます。



このDependabotはパッケージ管理システムの設定ファイルをチェックし、アプリケーションで脆弱性が含まれるバージョンのライブラリが使用されていないかをGitHub Advisory Databaseに従ってチェックします。

本稿執筆時点(2022年5月)でDependabotが脆弱性と依存関係を検出できるパッケージ管理システムは以下のとおりです。

なお、このDependabotは、GitHubのプライベートリポジトリでも無料で利用できます。GitHub Enterprise Serverを利用している場合は、GitHub Connectを設定することで、Dependabotを利用できます。

このような便利な機能をうまく使って、アプリケーションに脆弱性を組み込まないよう心がけてください。ただし、GitHubが提供するセキュリティ機能はすべての脆弱性を検出するとは限らないため注意してください。また、Gradleなどサポート対象外のパッケージ管理システムもあります。

参考: Improving the developer experience for Dependabot alerts



シークレットを適切に管理する

データベース接続文字列

外部APIを呼び出すための認証キー

データの暗号鍵

各種証明書

これらの情報が外部に漏洩することで、データベースへの不正アクセスによる情報流出やAPIの不正利用、サーバの乗っ取りなどビジネスに大きな損失が生じるリスクがあります。

例えば、フロントアプリがバックエンドのデータベースサーバにアクセスするときに使用するシークレットをDockerfileのCOPY命令を使ってコンテナに追加し、その後RUN命令で削除するDockerfileを作成したとします。このやり方であればファイルを削除しているので、一見安全そうにも思えます。

FROM gcr.io/distroless/static

COPY secret /root/bad-secret

RUN rm -f /root/bad-secret

しかし、DockerイメージはDockerfileの命令ごとにレイヤごとにファイルシステムの差分を計算し、イメージレイヤを作成します。そのためdocker saveコマンドでイメージをアーカイブとして出力することで削除したはずのファイルが参照できてしまうため注意してください。

docker build -t bad-image .

docker save bad-image | tar xv

tar xvf xxxxxxxxxxxx/layer.tar

cat root/bad-secret

なお、DockerfileのCOPY命令とADD命令はどちらもローカルマシンからコンテナにファイルを追加する命令ですが、ADD命令はリモートのファイルがダウンロードできるという違いがあります。そのため、ADD命令ではインターネット上から悪意のあるファイルがコンテナイメージに追加されるリスクがあります。いずれにせよ、コンテナイメージ内でシークレットを管理するのではなく、外部から参照できるような実装にしておくことが重要です。

Kubernetesでコンテナアプリケーションを動かす場合、機密情報はSecretsリソースを使って管理します。KubernetesのSecretsリソースは以下の組込み型を提供します。

例えば、ユーザー名とパスワードを設定したSecretsリソースを作成するには次のマニュフェストを作成します。その際、「username」と「password」は元の値をBase64エンコードした値を設定します。

apiVersion: v1

kind: Secret

type: Opaque

data:

username: YWRtaW4=

password: MWYyZDFlMmU2N2Rm

PodからSecretsリソースをファイルとして利用するには、次のマニュフェストを作成します。ファイルのパーミッションはデフォルトで「0644」になります。「.spec.volumes[].secret.defaultMode」でSecretボリューム全体のデフォルトモードを指定し、必要に応じてキー単位で上書きもできます。

apiVersion: v1

kind: Pod

...

spec:

containers:

- name: mypod

image: xxxx

volumeMounts:

- name: foo

mountPath: "/etc/foo"

readOnly: true

volumes:

- name: foo

secret:

secretName: mysecret

defaultMode: 0400

また、PodでSecretsを環境変数として利用するときは、次のようなマニュフェストを作成します。

apiVersion: v1

kind: Pod

metadata:

name: secret-env-pod

spec:

containers:

- name: mycontainer

image: redis

env:

- name: SECRET_USERNAME

valueFrom:

secretKeyRef:

name: mysecret

key: username

- name: SECRET_PASSWORD

valueFrom:

secretKeyRef:

name: mysecret

key: password

restartPolicy: Never

ここで1つ注意しておきたいのは、Secretsマニュフェストで指定した「username」と「password」は一見意味のない値に見えますが、これは元の値をBase64エンコードしただけであるため、簡単にデータを読み出すことができるという点です。

AzureにはAzure Key Vault という秘匿情報を扱うためのマネージドサービスがあります。Key Vault に格納した秘匿情報は暗号化して保存され、秘匿情報に対するアクセス許可を細やかに設定できます。格納された秘匿情報は Azure によって業界標準のアルゴリズムとキーの長さを使用して自動的に保護されます。そのため、保管時の暗号化について開発者が意識する必要はありません。

AKSはシークレット ストアCSI ドライバと呼ばれるしくみを通じ、PodからAzure Key Vault上の秘匿情報にアクセスできます。

Azure Kubernetes ServiceとKey Vaultの詳細については、第6回の「Kubernetes クラスタの保護」を参照してください。



コンテナアプリケーションに不要な権限を与えない

第3回〜第5回の「コンテナ技術の基礎(1)」「コンテナ技術の基礎(2)」「コンテナ技術の基礎(3)」でも紹介しましたが、KubernetesのコンテナではUser namespaceが分離されていません。

ホストのrootはコンテナのrootと違うものではないということです。これでは困るので主にCapabilityを使ってコンテナのrootは本来持っている権限は制限されていますが、設定次第で緩和もできます。CapabilityはKubernetesマニフェストのspec.containers.securityContext.capabilitiesで指定します。以降、断りがない限り、紹介する機能はマニフェストで指定します。

Kubernetes Hardening Guidanceでは特権からの保護の観点で以下が推奨されています。

非rootユーザーで動作するコンテナを使用する

以下の方法で特権コンテナのデプロイを防止する

rootでの実行やrootへの昇格を許可するコンテナを拒否する

AppArmor, seccompなどを利用して特権操作を拒否する

hostPID, hostIPC, hostNetwork, hostPathの許可のような脱獄に使われる機能を拒否する

非rootユーザーで動作するコンテナを使用する

非rootユーザーで動作するコンテナを使用する場合はDockerfileでUSERを明示的に指定します。USERを指定しない場合は実行ユーザーはrootになります。この設定をしなくても後述する「runAsUser」を使えば実行ユーザーを指定できます。

しかし、米国家安全保障局(NSA:National Security Agency)や米国土安全保障省サイバーセキュリティ・インフラストラクチャセキュリティ庁(CISA)から公開された、Kubernetes環境に対する脅威やリスクを最小限に抑えるための対策を解説したセキュリティガイダンス「Kubernetes Hardening Guidance」によると、より確実にするために両方指定することが推奨されています。

rootでの実行やrootへの昇格を許可するコンテナを拒否する

rootでのコンテナの実行やrootへの昇格を拒否するように設定する場合はSecurityContextを利用します。SecurityContextにはContainer単位で指定する項目と、Pod単位で指定する項目があります。

Container/Podの両方に設定されている場合はContainerの値が優先されます。

「rootでの実行やrootへの昇格を許可するコンテナを拒否する」という意味で関連する各項目と推奨される設定は次の通りです。

これらの設定が漏れてクラスタ内に特権を持ったPodがデプロイされるのを検知し防止できます。こちらについては、Azure Policyを使ったSecurityContextの設定の検査の話として、次回の記事で紹介します。

apiVersion: v1

kind: Pod

metadata:

name: security-context-demo

spec:

securityContext:

runAsUser: 1000

runAsGroup: 3000

fsGroup: 2000

containers:

- name: sec-ctx-demo

# ...

securityContext:

allowPrivilegeEscalation: false

# ...

AppArmorによる保護

MAC(Mandantory Access Control、強制アクセス制限)を使った保護を実現するのがAppArmorです。AKSではAppArmorがデフォルトで有効になっており、「/proc」や「/sys」へのアクセスが制限されています。デフォルトのProfileで制限されている内容はこちらで確認できます。

seccompによる保護

コンテナのプロセスから呼び出せるシステムコールを制限することで特権からの保護できるのがseccompです。Dockerではseccompはデフォルトで有効になっていますが、Kubernetesではデフォルトでは有効になっていません。有効にする場合はここでもSecurityContextを利用します。

ホストのネームスペース共有の無効化

hostPID, hostIPC, hostNetwork, hostPathはそれぞれ以下のような効果がある設定です。デフォルトで無効になっています。通常の業務アプリケーションを動かす場合にこれらを有効にする必要は多くはありませんが、クラスタの監視やログ収集やデバッグ等を行いたい場面などは必要に応じて有効化しましょう。



コンテナイメージの脆弱性スキャン

アプリケーションに脆弱性が含まれないようにするのを開発者にゆだねるのではなく、自動的にスキャンを行うしくみを整備するのがよいでしょう。



ローカル環境でのスキャン

デスクトップで実行できるスキャナーを使用すると、個々の開発者は開発マシン上のローカルイメージをスキャンして問題を見つけることができ、ソースコードリポジトリにプッシュする前に問題を修正できます。

ビルド環境でのスキャン

ビルドパイプラインの中でイメージスキャンを組み込んでください。もしスキャンで深刻な脆弱性が明らかになった場合は、ビルドを失敗させてデプロイされないように制御できます。

コンテナレジストリでのスキャン

コンテナレジストリ上に格納されたイメージをスキャンします。例えば、Azureの場合、Microsoft Defender for Containersを使うとレジストリにプッシュしたイメージをスキャンできるだけでなく、過去30日以内にプルされたイメージもスキャンされます。

CIS Benchmarksは、米国のCIS(Center For Internet Security)が発行しているシステムを安全に構成するための構成基準およびベストプラクティスが記載されたガイドラインであり、下記のサイトで公開されています。

CIS Benchmarks

https://www.cisecurity.org/cis-benchmarks/

CIS(Center for Internet Security)は政府機関と、企業、学術機関などが協力しセキュリティ標準化に取り組む目的で2000年に設立された団体の略称です。

本稿では、すべてを紹介しきれなかった脆弱性・対策も含めて、セキュリティベストプラクティスに則ったチェックができる代表的なスキャナーを紹介します。

dockle

dockleはGoodwith社が中心となっている開発しているOSSのプロダクトです。dockleを利用するとイメージがベストプラクティスに沿って実装されているかを検査できます。

CISDockerBenchmark_v1.3.1の4章にContainer Images and Build File Configurationが定められており、DockleでCIS Benchmarkにもとづくチェック項目はこのガイドラインに対応します。

dockleによって検査できるのは下記の項目です。

Dockle独自のチェック項目は以下の通りです。

trivy

trivyはAqua Security社が中心となっている開発しているOSSのプロダクトです。

trivyを使うと脆弱性データベースを元にOSやプログラム言語のパッケージの脆弱性を検査できます。

2022-03-29時点で対応するのは以下です。

○まとめ

コンテナアプリケーションの観点では、アプリケーションの脆弱性を減らすための工夫やシークレットの管理、適切なアクセス権限を設定することでコンテナブレイクアウトを防ぐための方法について説明しました。さらに脆弱性チェックをCI/CDパイプラインに組み込み自動化することで、ベストプラクティスに則ったアプリケーション開発ができるようになります。

コンテナアプリケーションの脅威を正しく知り、開発の早い段階で適切な対応を取ることで、セキュリティ事故を未然に防ぐことができます。

○NRIデジタル株式会社 湯川勇太(ゆかわ ゆうた)

前職で大手家電量販店のECモールの立ち上げとECサイトの刷新プロジェクトを経験。NRI入社後は製造業の商品検索システムを担当した。現在は大手物流企業向けの基幹システムの方式設計や技術検証、トラブル対応を行っている。

○Microsoft 阿佐 志保 (あさ しほ)

金融系シンクタンク で 都市銀⾏情報系基盤システム統廃合、証券バックオフィスシステム共通 ミドルウエア 開発などの金融案件を経験。出産で離職後 Linux やオープンソースソフトウエアなどを独学で勉強し Docker/Kubernetesに関する書籍を執筆。現在は日本マイクロソフト株式会社で、お客様向けにAzureの技術支援を行っています。