最近のWEBアプリケーションは、クラウド環境上にLinuxコンテナクラスタとしてデプロイすることが多くなってきました。 Linuxコンテナクラスタを管理するツールとして、Kubernetesが大変人気があります。 Kubernetesを使うと、設定ファイルの記述通りにコンテナクラスタが生成され、その状態が維持されるので、WEBアプリケーションの管理が容易になります。 その一方で、このような環境では数多くのコンテナが動的に生成、破棄されるので、セキュリティのモニタリングやインシデント検知が非常に難しくなってしまいます。
オープンソースのセキュリティ監視ツールであるFalcoは、Kubernetesとシームレスに統合できるため、クラウド上のWEBアプリケーションのセキュリティモニタリングに非常に適しています。 FalcoはLinuxカーネルのシステムコールを監視し、あらかじめ定義したルールに基づき、不正なアクティビティを検知します。 Falcoの監視と通知は、リアルタイムで行われるため、セキュリティインシデントに対する迅速な対応が可能となり、攻撃や侵入の検出に役立ちます。
今回、Falcoの仕組みやデプロイ方法、どのようなアクティビティが検出可能かなどについて調査を行いましたので、このブログで紹介したいと思います。
FalcoはLinux OS向けのランタイムセキュリティツールです。 システム上での異常なアクティビティや潜在的なセキュリティリスクをリアルタイムで検知し、警告するように設計されています。 その中核は、カーネル内でのシステムコール監視モジュールと、監視ルールに基づいて異常なイベントを検知するエージェント、さらに外部への通知を行うFalcosidekickエージェントからなっています。
以下がその仕組みと特徴です。
Falcoの監視ルールは開発元が用意したものをそのまま利用しても良いですし、組織のセキュリティポリシーに合わせてカスタマイズすることも可能です。 最新のFalcoではシステムコールのモニタリング機能に加え、プラグインを介してより多くのデータソースからセキュリティイベントを取り込むことも可能です。
Falcoでは以下に示したようなイベントを検出できるので、セキュリティリスクの早期検知に役立ちます。
Falcoの主要コンポーネントは、以下の通りです。
Kubernetes環境では、Falco podはdaemonsetとしてすべてのノード上にデプロイされます。 Falco podは、Linuxカーネルモジュール又はebpfのモジュールローダ、Falcoエージェント、監視ルールの自動アップデーターなどの複数コンテナで構成されます。 モジュールローダは、リポジトリからコンパイル済みのLinuxカーネルモジュール、ebpfモジュール等を自動的にダウンロードし、カーネルにロードします。 リポジトリに適切なバージョンのモジュールが存在しない場合には、コンテナ内でソースからモジュールをコンパイルしカーネルにロードします。
FalcoではLinuxカーネルモジュールまたはebpfモジュールをカーネルにロードし、システムコールの監視を行いますが、カーネルにモジュールをロードする行為自体がセキュリティリスクとみなされることもあるため、クラウド環境などではモジュールのロード自体が許されていない場合があります。 したがって現実的には、インフラ環境によって推奨されるモジュールを利用することになると思います。
Falcoのブログには、環境ごとの推奨モジュール、インストール方法がまとめられています。 本ブログの執筆時点では、AWS EKSではカーネルモジュールが推奨され、GKEではebpfモジュールが推奨されるようです。
Falcoプロジェクトでは、FalcoをKubernetes環境上にデプロイする方法として、kustomization.yamlによる方法とHelm Chartによる方法を用意しているようです。 本稿ではHelm Chartによるデプロイ方法について説明します。
HelmはKubernetes用のアプリケーションをパッケージ化し、簡単にデプロイおよび管理するためのツールです。次のような手順でHelmをセットアップし、falcoをデプロイすることができます。
Helmのインストール
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
Helm Chartリポジトリの追加
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
Falcoのデプロイ
helm install falco falcosecurity/falco -f myvalues.yaml --namespace falco --create-namespace
ここでのオプションの内容は以下の通りです。
Helm Chartのデフォルト設定は、次のレポジトリの values.yaml に定義されています。このデフォルト設定をオーバーライドしたい場合に、myvalues.yamlを利用します。
myvalues.yamlの内容は、例えば以下のように記述します。
driver:
loader:
enabled: true
falcosidekick:
enabled: true
webui:
enabled: true
redis:
storageClass: "local-storage"
service:
type: "NodePort"
上記の設定では、次のようなカスタマイズを行っています。
Falcoが正しくデプロイできたかどうかは、helm list コマンド、kubectl get pods コマンドなどで確認できます。
helm list コマンドでは、Chartのデプロイ状況が確認できます。
# helm list -n falco
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
falco falco 1 2023-11-20 17:49:41.962917835 +0900 JST deployed falco-3.7.1 0.36.0
kubectl get pods コマンドでは、Kubernetes上にデプロイされた各podの状態が確認できます。 次の例では、falco podがv131-v136の各ノードで一つずつデプロイされていること、falco-falcosidekick、falco-falcosidekick-ui podは全体二つずつ、falco-falcosidekick-ui-redis podは全体で一つだけデプロイされていること等が分かります。
# kubectl get pods -n falco -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
falco-4dfjc 2/2 Running 0 9m46s 10.244.4.204 v135
falco-659kn 2/2 Running 0 9m46s 10.244.1.195 v132
falco-7scxs 2/2 Running 0 9m46s 10.244.5.206 v136
falco-falcosidekick-749d77d5c7-bsp5f 1/1 Running 0 9m46s 10.244.5.204 v136
falco-falcosidekick-749d77d5c7-lzdwd 1/1 Running 0 9m46s 10.244.3.252 v134
falco-falcosidekick-ui-6885c6cffd-k4bt8 1/1 Running 2 (9m44s ago) 9m46s 10.244.5.205 v136
falco-falcosidekick-ui-6885c6cffd-zhrbn 1/1 Running 2 (9m45s ago) 9m46s 10.244.4.203 v135
falco-falcosidekick-ui-redis-0 1/1 Running 0 9m46s 10.244.3.253 v134
falco-h9s99 2/2 Running 0 9m46s 10.244.0.211 v131
falco-n25zx 2/2 Running 0 9m46s 10.244.2.194 v133
falco-zshln 2/2 Running 0 9m46s 10.244.3.254 v134
Falcoには次の図のような簡易的なWeb UIが容易されています。
このページ右上の EXPORT ボタンを押すと、json形式のログファイルをダウンロードすることもでき、非常に便利です。
Web UIにはNodePort、Port Forward、ingressなどを通してアクセスすることができます。 ここでは、NodePortを通してのアクセスと、Port Forwardを利用する方法についてまとめます。
kubectl get svc でKubernetesのサービスの確認が可能です。 次の例では、falco-falcosidekick-uiにクラスターIP:10.109.212.176、Port:2802がアサインされ、さらにNodePort:30282で外部からアクセス可能であることが分かります。
# kubectl get svc -n falco
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
falco-falcosidekick ClusterIP 10.109.145.202 2801/TCP 2m17s
falco-falcosidekick-ui NodePort 10.109.212.176 2802:30282/TCP 2m17s
falco-falcosidekick-ui-redis ClusterIP 10.97.33.253 6379/TCP 2m17s
WEBブラウザを開きKubernetesのNodeのひとつで30282番ポート(例えばhttp://10.0.0.131:30282)にアクセスすることで、Web UIを開くことが可能です。
NodeのIPアドレスは kubectl get nodes -o wide で確認可能です。
root@v130:~# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
v131 Ready control-plane 54d v1.28.2 10.0.0.131 Debian GNU/Linux 12 (bookworm) 6.4.11-64kvmg01 containerd://1.7.6
v132 Ready control-plane 54d v1.28.2 10.0.0.132 Debian GNU/Linux 12 (bookworm) 6.4.11-64kvmg01 containerd://1.7.6
v133 Ready control-plane 54d v1.28.2 10.0.0.133 Debian GNU/Linux 12 (bookworm) 6.4.11-64kvmg01 containerd://1.7.6
v134 Ready 54d v1.28.2 10.0.0.134 Debian GNU/Linux 12 (bookworm) 6.4.11-64kvmg01 containerd://1.7.6
v135 Ready 54d v1.28.2 10.0.0.135 Debian GNU/Linux 12 (bookworm) 6.4.11-64kvmg01 containerd://1.7.6
v136 Ready 54d v1.28.2 10.0.0.136 Debian GNU/Linux 12 (bookworm) 6.4.11-64kvmg01 containerd://1.7.6
まず、次のコマンドでPort Forwardをセットアップします。WEBブラウザを開き、kubectl port-forward を実行しているホストの1234番ポート(例えばhttp://127.0.0.1:1234)にアクセスすれば、Web UIを開くことが可能です。
kubectl port-forward svc/falco-falcosidekick-ui -n falco --address 0.0.0.0 1234:2802
Falcoのリポジトリには、デフォルトのルールセットが用意されており、ユーザーはそれらのルールを必要に応じて有効化したり、修正したり、独自のルールを追加したりすることができます。 デフォルトルールセットを全く使わずに、独自のルールのみでFalcoを運用することも可能です。
このブログ記事の執筆時点で、Falcoプロジェクトでは88個のデフォルトルールセットが用意されており、重要度に応じて次の4つのファイルに分けられています。
Helmのコマンドラインで特に何も指定しない場合は、falco_rules.yaml のみがロードされます。 falco_rules.yaml には次の25個のルールが定義されています。
Falcoの公式サイトには、各ファイルのルールの内容、デフォルトで有効かどうかなどについて、わかりやすくまとめられたページが用意されています。
falco_rules.yaml の他に、falco-incubating_rules.yaml falco-sandbox_rules.yaml をロードしたい場合には、helmコマンド実行時に明示的に指定する必要があります。
helm install falco falcosecurity/falco \
--set "falcoctl.config.artifact.install.refs={falco-rules:2,falco-incubating-rules:2,falco-sandbox-rules:2}" \
--set "falcoctl.config.artifact.follow.refs={falco-rules:2,falco-incubating-rules:2,falco-sandbox-rules:2}" \
--set "falco.rules_file={/etc/falco/k8s_audit_rules.yaml,/etc/falco/rules.d,/etc/falco/falco_rules.yaml,/etc/falco/falco-incubating_rules.yaml,/etc/falco/falco-sandbox_rules.yaml}"
あるいは、上記のようにコマンドラインに引数として指定する代わりに、myvalues.yamlで次のような設定を行うと、helmコマンドのオプションを簡略化することができます。
falcoctl:
config:
artifact:
install:
refs:
- falco-rules:2
- falco-incubating-rules:2
- falco-sandbox-rules:2
follow:
refs:
- falco-rules:2
- falco-incubating-rules:2
- falco-sandbox-rules:2
falco:
rules_file:
- /etc/falco/k8s_audit_rules.yaml
- /etc/falco/rules.d
- /etc/falco/falco_rules.yaml
- /etc/falco/falco-incubating_rules.yaml
- /etc/falco/falco-sandbox_rules.yaml
その場合、helmのコマンドラインは次のようになります。
helm install falco falcosecurity/falco -f myvalues.yaml --namespace falco --create-namespace
Falcoプロジェクトで用意されているデフォルトルールセットには、すべてMITRE ATT&CK のタグ付けがされていて、既知の攻撃パターンを幅広く網羅していると言えます。しかしながらその逆に、未知の攻撃パターンに対しては必ずしも万全とは言えないかもしれません。
未知の攻撃パターンを検知するには、ホワイトリスト形式のカスタムルールを作成すると良いかもしれません。 現在、ホワイトリスト形式のカスタムルールの検討も行っていますが、それについては別の機会にまとめたいと思います。
Falcoルールファイルは、List、Macro、Ruleの三種類の要素からなっています。
以下は、Disallowed SSH Connection Non Standard Port というルールの例です。
- macro: outbound
condition: >
(((evt.type = connect and evt.dir=<) or
(evt.type in (sendto,sendmsg) and evt.dir=< and
fd.l4proto != tcp and fd.connected=false and fd.name_changed=true)) and
(fd.typechar = 4 or fd.typechar = 6) and
(fd.ip != "0.0.0.0" and fd.net != "127.0.0.0/8" and not fd.snet in (rfc_1918_addresses)) and
(evt.rawres >= 0 or evt.res = EINPROGRESS))
- list: ssh_non_standard_ports
items: [80, 8080, 88, 443, 8443, 53, 4444]
- macro: ssh_non_standard_ports_network
condition: (fd.sport in (ssh_non_standard_ports))
- rule: Disallowed SSH Connection Non Standard Port
desc: >
Detect any new outbound SSH connection from the host or container using a non-standard port. This rule holds the potential
to detect a family of reverse shells that cause the victim machine to connect back out over SSH, with STDIN piped from
the SSH connection to a shell's STDIN, and STDOUT of the shell piped back over SSH. Such an attack can be launched against
any app that is vulnerable to command injection. The upstream rule only covers a limited selection of non-standard ports.
We suggest adding more ports, potentially incorporating ranges based on your environment's knowledge and custom SSH port
configurations. This rule can complement the "Redirect STDOUT/STDIN to Network Connection in Container" or
"Disallowed SSH Connection" rule.
condition: >
outbound
and proc.exe endswith ssh
and fd.l4proto=tcp
and ssh_non_standard_ports_network
output: >-
Disallowed SSH Connection (connection=%fd.name lport=%fd.lport rport=%fd.rport
fd_type=%fd.type fd_proto=fd.l4proto evt_type=%evt.type user=%user.name user_uid=%user.uid
user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname
command=%proc.cmdline terminal=%proc.tty exe_flags=%evt.arg.flags %container.info)
priority: NOTICE
tags: [maturity_stable, host, container, network, process, mitre_execution, T1059]
Falcoをhelmでデプロイした場合には、デフォルトのルールセットは定期的にアップデートされます。 定期アップデートはFalco pod内の falcoctl-artifact-follow コンテナにより行われます。
If you install the Helm chart, at least version 3.0.0 with: helm install falco Falco, by default, will load the latest rules file that is compatible with your Falco version and keep it up to date automatically via falcoctl. These are published on GitHub Packages.
シンプルなnginx podを起動し、次の3つの操作を実行し、falcoで検知可能かどうか確認しました。
Falcoでの検知結果はWeb UIで確認し、そこからダウンロードしたjsonログファイルの該当部分を記載しました。
nginx podの起動
kubectl apply -f simple-pod.yaml
simple-pod.yamlの内容
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
# kubectl exec -it nginx -- /bin/bash
root@nginx:/#
{
"uuid": "4b1a818b-7cd0-4985-ba95-a6f2fa897f4e",
"output": "09:10:51.667430058: Notice A shell was spawned in a container with an attached terminal ...",
"priority": "Notice",
"rule": "Terminal shell in container",
"time": "2023-11-27T09:10:51.667430058Z",
"source": "syscall",
"output_fields": {
"container.id": "e6f8a1e1dd97",
"container.image.repository": "docker.io/library/nginx",
"container.image.tag": "latest",
"container.name": "nginx",
"evt.arg.flags": "EXE_WRITABLE",
"evt.time": 1701076251667430100,
"evt.type": "execve",
"k8s.ns.name": "default",
"k8s.pod.name": "nginx",
"proc.cmdline": "bash",
"proc.exepath": "/usr/bin/bash",
"proc.name": "bash",
"proc.pname": "runc",
"proc.tty": 34817,
"user.loginuid": -1,
"user.name": "root",
"user.uid": 0
},
"hostname": "v136",
"tags": [
"T1059",
"container",
"maturity_stable",
"mitre_execution",
"shell"
]
}
"Terminal shell in container"ルールにマッチしてインシデントが検知されていることが分かります。 このルールは、コンテナ内でシェルプロセスが実行されたことを検知します。
apt install vim
vi test
{
"uuid": "4485356c-4899-4b52-acc7-75308454a04c",
"output": "09:14:04.692611115: Critical Executing binary not part of base image ... ",
"priority": "Critical",
"rule": "Drop and execute new binary in container",
"time": "2023-11-27T09:14:04.692611115Z",
"source": "syscall",
"output_fields": {
"container.id": "e6f8a1e1dd97",
"container.image.repository": "docker.io/library/nginx",
"container.image.tag": "latest",
"container.name": "nginx",
"container.start_ts": 1701069045261889000,
"evt.arg.flags": "EXE_WRITABLE|EXE_UPPER_LAYER",
"evt.time": 1701076444692611000,
"evt.type": "execve",
"k8s.ns.name": "default",
"k8s.pod.name": "nginx",
"proc.aname[2]": "containerd-shim",
"proc.cmdline": "vi test",
"proc.cwd": "/",
"proc.exe": "vi",
"proc.exe_ino.ctime": 1701076429805272800,
"proc.exe_ino.ctime_duration_proc_start": 14886864198,
"proc.exe_ino.mtime": 1683195884000000000,
"proc.exepath": "/usr/bin/vim.basic",
"proc.name": "vi",
"proc.pname": "bash",
"proc.sname": "bash",
"proc.tty": 34816,
"user.loginuid": -1,
"user.name": "root",
"user.uid": 0
},
"hostname": "v136",
"tags": [
"PCI_DSS_11.5.1",
"TA0003",
"container",
"maturity_stable",
"mitre_persistence",
"process"
]
}
"Drop and execute new binary in container"ルールにマッチしてインシデントが検知されていることが分かります。 このルールはoverlayfsの最上位層に新規に作成されたファイルの実行を検知します。 なぜなら最上位層のファイルはもともとコンテナのベースイメージに存在していたものではなく、あとから侵入者が作成したものである可能性を排除できないからです。 このルールにより、侵入者がクラッキングツールをダウンロードし実行した場合などに、それを逃さずに検知することができます。
root@nginx:/# ssh example.com -p 443
kex_exchange_identification: read: Connection reset by peer
Connection reset by 93.184.216.34 port 443
{
"uuid": "303636ae-93cd-4474-b796-3d87aa28c089",
"output": "03:29:15.493403233: Notice Disallowed SSH Connection ... ",
"priority": "Notice",
"rule": "Disallowed SSH Connection Non Standard Port",
"time": "2023-11-28T03:29:15.493403233Z",
"source": "syscall",
"output_fields": {
"container.id": "e6f8a1e1dd97",
"container.image.repository": "docker.io/library/nginx",
"container.image.tag": "latest",
"container.name": "nginx",
"evt.arg.flags": null,
"evt.time": 1701142155493403100,
"evt.type": "connect",
"fd.lport": 36820,
"fd.name": "10.244.5.218:36820->93.184.216.34:443",
"fd.rport": 443,
"fd.type": "ipv4",
"k8s.ns.name": "default",
"k8s.pod.name": "nginx",
"proc.cmdline": "ssh example.com -p 443",
"proc.exepath": "/usr/bin/ssh",
"proc.name": "ssh",
"proc.pname": "bash",
"proc.tty": 34816,
"user.loginuid": -1,
"user.name": "root",
"user.uid": 0
},
"hostname": "v136",
"tags": [
"T1059",
"container",
"host",
"maturity_stable",
"mitre_execution",
"network",
"process"
]
}
"Disallowed SSH Connection Non Standard Port"ルールにマッチしてインシデントが検知されていることが分かります。 このルールでは、ポート番号"80, 8080, 88, 443, 8443, 53, 4444"へのSSH接続を検知します。 これらのポートはHTTPやHTTPS、DNSなどで使われるポートなので経路上のファイアーウォールなどで塞がれることが少なく、22番ポートでの外部通信が禁止されている場合などに、SSHの代替ポートとして利用することが可能だからです。
Falcoのデフォルトルールセットは、既知の攻撃パターンを幅広く網羅していて非常に強力です。 しかしながら、未知の攻撃パターンに対しては必ずしも万全とは言えないかもしれません。
例えば、Falcoのインシデント検知の例で示したケースのうち次の二つについても、抜け穴が考えられます。
この例では、overlayfsの最上位層に新規に作成されたファイルの実行を検知しました。 しかし、コンテナにはtmpfsがマウントされていることも多く、そこに置かれたファイルの実行は検知しません。 この例のnginxコンテナの場合、/devにtmpfsがマウントされていました。 そこで、viを/devにコピーし/dev/viを実行してみましたが、Falcoで検知することはできませんでした。
root@nginx:/# cp /usr/bin/vi /dev/
root@nginx:/# /dev/vi test
この例では、ポート番号"80, 8080, 88, 443, 8443, 53, 4444"へのSSH接続を検知していました。 この場合ポートを限定しているため、その他の任意のポート番号で外部接続が可能であった場合に、それを利用したSSH接続を検知することができません。
この様に、Falcoのデフォルトルールセットで検知できないエッジケースは他に数多く存在すると予想されます。 未知の攻撃パターンを検知するには、ホワイトリスト形式のカスタムルールを作成すると良いかもしれません。 その試みについては別の機会にまとめたいと思います。
Falcoの概要、デプロイ方法、インシデント検出の例などについてまとめました。 Falcoは、Kubernetes上ではHelm Chart等により簡単にデプロイ可能です。 また、Falcoのレポジトリには、デフォルトのルールセットが用意されています。 ユーザーはこれをそのまま利用したり、各ルールを状況に応じて有効・無効を切り替えて使うことができます。 Falcoのデフォルトルールセットは強力ではありますが、デフォルトルールセットの問題点に記載しているような未知のセキュリティリスクを検知できない場合があります。 次回以降の記事でホワイトリスト形式のカスタムルールの書き方、適用方法などについてまとめたいと思います。
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。
合わせて読みたい
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。