Carpe Diem

備忘録

ログで機密情報をマスキングする方法

背景

DBのconfigのように一部機密情報が含まれるものを環境変数(k8s Secret等)で注入することは多いです。

そしてその環境変数がちゃんと設定されているか起動時にログを吐きたいということもよくあります。

一方で

type Config struct {
        Addr     string
        Port     int
        Password string
}

この様にパスワードが含まれるstructをログに吐くと、せっかくSecret等で管理した機密情報が漏れてしまいます。

そういったケースで一部のフィールドだけマスキングする方法を紹介します。

環境

  • Go 1.21.1
  • zap v1.25.0
  • golang.org/x/exp/slog v0.0.0-20230905200255

方法

zapとslogそれぞれのやり方を紹介します。

zapの場合

zapではzapcore.ObjectMarshalerを実装することで、ログを吐く際に中身をカスタマイズすることが可能です。

以下の例ではパスワードフィールドのみマスキングするようにしています。

func (c Config) MarshalLogObject(enc zapcore.ObjectEncoder) error {
        enc.AddString("addr", c.Addr)
        enc.AddInt("port", c.Port)
        enc.AddString("password", "****") // パスワードをマスク
        return nil
}

var (
        conf    Config
        logger  *zap.Logger
)

func init() {
        envconfig.Process("myapp", &conf)
        logger, _ = zap.NewDevelopment()
}

func main() {
        defer logger.Sync()
        logger.Info("zap", zap.Object("config", conf))
}

動作結果

生成されるログは次のようになります。

2023-09-12T06:16:00.583+0900 INFO log-masking/main.go:50 zap {"config": {"addr": "localhost", "port": 8080, "password": "****"}}

期待通り機密情報のみマスキングすることができます。

この設計の良いところは

  • 既存の実装を大きく変更することなく導入出来る(ログを吐いている場所自体は修正する必要がない)
  • 一度導入(interfaceを実装)すれば良く、実装漏れしにくい
  • ホワイトリスト式なので追加したものだけ表示される
    • enc.AddString("xxx", c.Xxx)しなければ表示されないので、実装漏れで機密情報がログに残ってしまったということがない

点でしょう。

slogの場合

Go 1.21から導入された公式の構造化ログであるslogも同様なことが可能です。

slog.LogValuerを実装することで実現できます。

func (c Config) LogValue() slog.Value {
        return slog.GroupValue(
                slog.String("addr", c.Addr),
                slog.Int("port", c.Port),
                slog.String("password", "****"), // パスワードをマスク
        )
}

var (
        conf    Config
        slogger *slog.Logger
)

func init() {
        envconfig.Process("myapp", &conf)
        slogger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
}

func main() {
        slogger.Info("slog", "config", conf)
}

動作結果

生成されるログは次のようになります。

{"time":"2023-09-12T06:16:00.584025+09:00","level":"INFO","msg":"slog","config":{"addr":"localhost","port":8080,"password":"****"}}

zapと同様に期待通り機密情報のみマスキングすることができます。

その他

どんな値が入っているか見当がつくようにしたい

機密情報をマスキングしたい一方で、ちゃんと期待した値が入っているか確認したい気持ちもあります。

そんな時は次のようにハッシュ化して一部だけ表示するのが良いでしょう。

const (
        hashPrefixLength = 8
)

// hash returns sha256 hash value of input string.
// You can confirm hash value on terminal as follows:
// $ echo -n "xxx" | shasum -a 256
func hash(in string) string {
        hash := sha256.Sum256([]byte(in))
        return hex.EncodeToString(hash[:])[:hashPrefixLength] + "****"
}

ログの部分を次のように修正すると、

func (c Config) MarshalLogObject(enc zapcore.ObjectEncoder) error {
        enc.AddString("addr", c.Addr)
        enc.AddInt("port", c.Port)
        enc.AddString("password", hash(c.Password))
        return nil
}

生成されるログはこのようになります。

{
  "config": {
    "addr": "localhost",
    "port": 8080,
    "password": "89e01536****"
  }
}

mypasswordという値を期待している場合、ローカルでハッシュ化すると

$ echo -n "mypassword" | shasum -a 256
89e01536ac207279409d4de1e5253e01f4a1769e696db0d6062ca9b8f56767c8  -

となり、期待通りの値が入っていると分かります。

ハッシュ値そのままはダメ?

ちなみにハッシュ値全てを入れるのはレインボーテーブル攻撃が可能になるため推奨できません。

次のようなレインボーテーブルの確認サイトで検証すると、パスワードによっては容易に割り出せてしまいます。

ref: CrackStation - Online Password Hash Cracking - MD5, SHA1, Linux, Rainbow Tables, etc.

struct tagで管理する方法はある?

interfaceを実装せずstructのtagで管理できたら良いと思って調べてみると、次のようなライブラリもあります。

github.com

ただこの場合はログを吐く前に各オブジェクトをラップする必要があり、既存の実装を修正するのは非常に大変&今後も漏れが出てくるので導入しても管理が大変になるでしょう。

サンプルコード

今回のサンプルコードはこちらです。

github.com

まとめ

環境変数など外部から注入した値がちゃんと設定されているかログで確認したい、けど機密情報は漏洩させたくないという場合に、ログの一部をマスキングする方法を紹介しました。