RailsのActiveJobログで秘匿情報をマスクする方法

2023/12/27に公開

背景

私が開発に携わっているプロダクトでは、Rails アプリケーションのログを Datadog に収集していました。また、ログとして収集する際に、ユーザーに関する情報(ユーザー名、メールアドレス等)にはマスキングを行っていました。

マスキングの処理としては、以下のコードのように一般的な手法で対応していました。

config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += %i[email]

発生した問題

Datadog のログにメールアドレスが表示されていることに気づきました。具体的には、ActionMailer を実行するための ActiveJob のログにメールアドレスが表示されていました。
これは ActiionMailer の with メソッドにメールアドレスをそのまま渡していることが原因でした。

# 例
HogeMailer
  .with(email: 'hoge@example.com')
  .account_invitation
  .deliver_later

この問題を解決するためのアプローチとして、メールアドレスをそのまま渡すのではなく、メールアドレスを保持するモデルのインスタンスを渡せば、ログには Global ID の情報のみが表示されます。

# 例
HogeMailer
  .with(user: User.first)
  .account_invitation
  .deliver_later

しかし、多くの ActionMailer がメールアドレスをパラメータとして受け取るよう実装されており、変更が大変でしたので、ActiveJob のログをマスクする方針で対応しました。

やったこと

ActionJob のログが定義されている箇所を調べた結果、ActiveJob::LogSubscriber#format で定義されていることがわかりました。

https://github.com/rails/rails/blob/5b62994778d197b26185472bbe94b6086760789d/activejob/lib/active_job/log_subscriber.rb#L154-L165

そこで、このメソッドにモンキーパッチを適用することにしました。
具体的には、arg が Hash の場合に実行される arg.transform_values { |value| format(value) } の前に、arg にマスク処理を実行するようにしました。
また、マスク処理にはログをマスキングする際に呼び出される ActiveSupport::ParameterFilterクラスを使用しました。
ActiveSupport::ParameterFilter を使用した例は以下のようになります。

 f = ActiveSupport::ParameterFilter.new(%i[email])
 f.filter({
   email: 'hoge@example.com',
   foo_email: 'foo@example.com',
   bar: {
     email_baz: 'baz@example.com'
   }
 })
 # => {:email=>"[FILTERED]",
 #     :foo_email=>"[FILTERED]",
 #     :bar=>{:email_baz=>"[FILTERED]"}}

そして、実装したコードは以下の通りです。
この実装により、ActiveJob のログも期待通りマスクされるようになりました。

config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += %i[email]

+ ActiveSupport.on_load(:active_job) do
+   module ActiveJob
+     class LogSubscriber
+       private

+       def format(arg)
+         case arg
+         when Hash
+           # arg にマスクする処理を実行するようにした。
+           mask_parameter(arg).transform_values { |value| format(value) }
+         when Array
+           arg.map { |value| format(value) }
+         when GlobalID::Identification
+           begin
+             arg.to_global_id
+           rescue StandardError
+             arg
+           end
+         else
+           arg
+         end
+       end

+       def mask_parameter(args)
+         parameter_filter = ActiveSupport::ParameterFilter.new(%i[email])
+         parameter_filter.filter(args)
+       end
+     end
+   end
+ end

参考

https://www.writesoftwarewell.com/how-parameter-filtering-works-in-rails/

https://railsguides.jp/engines.html#利用可能なフック

しくみのテックブログ

Discussion