はじめに
セキュリティ上問題となるプログラミングエラーの一つに、誤って機密データをログに記録してしまうというものがあります 1。データ構造やロジックが複雑になるほど、レビューでもこの種のエラーは見落としやすくなります。Kubernetes でも何度かこの問題が起きており、Go Flow Levee というツールを使った静的解析による対策が導入されています。
本記事では、Go 言語用の静的解析ツール「Go Flow Levee」と、Kubernetes での導入事例についてご紹介します。
Go Flow Levee とは
Go Flow Levee は、Go 言語用の静的解析ツールです。機密データなどの「ソース」が「サニタイザー」を通さずにログ関数などの「シンク」に到達していないかチェックできます。「ソース」をコピーした変数も「テイント」が付与されて伝搬が分析されることで、同様に「シンク」への到達を検出できます。2
認証情報などの機密データが誤ってログに記録されるようなケースを検出できるほか、ユーザからの入力に対してのテイントチェック(Taint Checking) としての使い方も可能です。
Go Flow Levee の基本的な使い方
Go Flow Levee の Quickstart のドキュメント をベースに、基本的な使い方をご紹介します。
検査するコード
以下の例 (quickstart.go) では、Authentication
構造体が Password
というフィールドに機密データを保持しています。このコードでは log.Printf
で、この構造体をログ出力しているため、機密データをログに記録してしまう問題があります。
package quickstart
import "log"
type Authentication struct {
Username string
Password string // 機密データ
}
func authenticate(auth Authentication) (*AuthenticationResponse, error) {
response, err := makeAuthenticationRequest(auth)
if err != nil {
// 機密データがロギングされている
log.Printf("unable to make authenticated request: incorrect authentication? %v\n", auth)
return nil, err
}
return response, nil
}
検査の設定
この例では、コードの検査に以下の 2 つの情報を設定します。この情報でテイント伝搬(taint propagation)の分析が行われ、機密データが指定した関数に渡されていないかチェックされます。
- どの型が機密データを含むか (ソース)
- 例では
Authentication
構造体のPassword
フィールド
- 例では
- 機密データをそのまま渡されてはいけない関数 (シンク)
- 例では
log.Printf
関数
- 例では
ソースの設定
機密データを含む型を表すソース(Source
) の指定には、2 種類の方法があります。
- 機密データを含む各フィールドに任意のタグを指定する
- コードの変更が必要だが、タグで一括に指定できる
- 機密データを含むフィールド名、パッケージ名、型名を記述する
- コードの変更はないが、個別に指定が必要
タグで指定する
タグで指定する場合、まず機密データを表す任意のタグを定義し、設定ファイルに記載します。以下の例では、datapolicy
のキーに、secret
という値が設定されていたときに機密データとして扱われます。
FieldTags:
- Key: datapolicy
Value: secret
機密データのフィールドにこのタグを設定します。
type Authentication struct {
Username string
Password string `datapolicy:"secret"`
}
フィールド名等で記述する
機密データを含むフィールド名、パッケージ名、型名を記述することもできます。外部パッケージなどタグを付与できない場合は、この方法を利用します。PackageRE
, TypeRE
, FieldRE
を使って、正規表現を用いることもできます。
Sources:
- Package: github.com/google/go-flow-levee/guides/quickstart
Type: Authentication
Field: Password
この方法ではコードの変更は必要ありませんが、フィールド名などが変わった場合に忘れずこの設定も更新する必要があります。
シンクの設定
機密データが渡されてはいけない関数をシンク (Sink) として設定します。以下の例では log
パッケージの Printf
を対象としています。PackageRE
、MethodRE
を使うと正規表現を用いることもできます。
Sinks:
- Package: log
Method: Printf
検査を実行する
まずは go-flow-levee (levee
バイナリ)をインストールします。
go get github.com/google/go-flow-levee/cmd/levee
前述のサンプルコードが含まれる github.com/google/go-flow-levee/guides/quickstart を準備しておきます。
git clone git@github.com:google/go-flow-levee.git
cd go-flow-levee/guides/quickstart
go vet
に levee
バイナリを指定して実行します。設定は analyzer_configuration.yaml を参照してください。
go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./...
問題箇所が検出されました。
# github.com/google/go-flow-levee/guides/quickstart
./quickstart.go:14:13: a source has reached a sink
source: ./quickstart.go:11:19
テイント伝搬の確認
quickstart.go を以下のように編集します。Password
フィールドの情報を一度 data
変数に入れて、間接的に機密情報をログ出力するようにします。
- log.Printf("unable to make authenticated request: incorrect authentication? %v", auth)
+ data := auth.Password
+ log.Printf("unable to make authenticated request: incorrect authentication? %v", data)
間接的な参照でも、正しく問題箇所が検出されました。
$ go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./...
# github.com/google/go-flow-levee/guides/quickstart
./quickstart.go:15:13: a source has reached a sink
source: ./quickstart.go:11:19
以下のように、data
変数の代入を Username
フィールドにすると、機密データがログに出力されなくなります。
- data := auth.Password
+ data := auth.Username
問題がなくなったため、結果が表示されなくなりました。
$ go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./...
# 何も表示されない
Kubernetes での導入事例
Kubernetes での導入は KEP-1933: Defend Against Logging Secrets via Static Analysis という KEP で提案されました。現在は、PR 時のチェック (prow) の Job の一つ (pull-kubernetes-verify-govet-levee) として実行されています。
Kubernetes の設定
Go Flow Levee の設定ファイルは kubernetes/kubernetes の hack/testdata/levee/levee-config.yaml に置かれています。以下のような設定が記述されています。
-
ソースは主に
datapolicy
というフィールドタグで指定されている- このタグは KEP-1753: Kubernetes system components logs sanitization で導入されたもの (この KEP 自体は deprecated)
- 設定例: Secret.Data、TokenReviewSpec.Token、ExtenderTLSConfig.KeyData
- 個別にソースを設定されているものもある
-
シンクはロガーのパッケージを正規表現
k?log
で対象としている
チェックを試してみる
levee のチェックを試すため、Kubernetes の pkg/kubelet/client/kubelet_client.goに、以下のログを追加してみます。ログに記録している KubeletClientConfig 構造体は BearerToken フィールドが機密情報を含みます。
diff --git a/pkg/kubelet/client/kubelet_client.go b/pkg/kubelet/client/kubelet_client.go
index 20a4eb9df8b..d90eb82c7cc 100644
--- a/pkg/kubelet/client/kubelet_client.go
+++ b/pkg/kubelet/client/kubelet_client.go
@@ -30,6 +30,7 @@ import (
"k8s.io/apiserver/pkg/server/egressselector"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport"
+ "k8s.io/klog/v2"
nodeutil "k8s.io/kubernetes/pkg/util/node"
)
@@ -173,6 +174,7 @@ type NodeConnectionInfoGetter struct {
// NewNodeConnectionInfoGetter creates a new NodeConnectionInfoGetter.
func NewNodeConnectionInfoGetter(nodes NodeGetter, config KubeletClientConfig) (ConnectionInfoGetter, error) {
+ klog.V(3).Infof("This log line leaks sensitive data: %v", config)
transport, err := MakeTransport(&config)
if err != nil {
return nil, err
levee の実行スクリプト hack/verify-govet-levee.sh を使って Go Flow Levee を実行します。今回追加した、問題のあるログが検出されました。
$ hack/verify-govet-levee.sh
# ... 省略
# k8s.io/kubernetes/pkg/kubelet/client
pkg/kubelet/client/kubelet_client.go:177:17: a source has reached a sink
source: pkg/kubelet/client/kubelet_client.go:176:52
make: *** [vet] Error 1
おわりに
誤って機密データをログに記録してしまう問題は、CWE-532: Insertion of Sensitive Information into Log File として共通脆弱性タイプ一覧にも分類されています。コードベースが大きくなるほど、こういった問題がないことを確認するのは難しくなります。不安があれば、一度 Go Flow Levee を試してみてはいかがでしょうか。
-
共通脆弱性タイプ一覧 CWE では、この問題を CWE-532: Insertion of Sensitive Information into Log File として分類しています。 ↩
-
Taint propagation analysis (テイント伝搬解析) と呼ばれる手法です。「テイント (taint)」「シンク (sink)」などの用語もこの分野の専門用語になります。 ↩