こんにちは。エンジニアのゆきちです。 普段はGoでアプリケーションを書いたりAWSでインフラ構築をしたりしてます。
HRBrainという企業の目標管理サービスを作っています。
なので、弊社では様々な企業さんの大切な情報を取り扱っています。
それにはもちろん個人情報も含まれるわけですが、そうなると僕ら管理スタッフが直接データを見ることができてはいけません。
とはいえ、こういうエラーが出たんですが...などの問い合わせに対応するためには、時によって本番のデータにアクセスして調査する必要があります。
制限も何もなければ当然丸っとそのまま見れちゃうわけですが、それができてしまってはSaaS企業としてのプライドが廃る!という所存でやっていきます。
そんな時にはどうすればいいんでしょうか?
答えは、 データを匿名化すること です。
今回、本番環境のデータベースのデータをマスキングして、調査者が見れる環境をGolangとAWSを使って構築できるようにしたのでそれを紹介していこうとおもいます。
“必要な人が、必要な情報を、必要な時だけ見れる” ように強制します。
匿名化の手法
匿名化(anonymization)とはデータセットにある個人のアイデンティティを守るためにするべき全てのことです。
二つの手法があります。
一つは、マスキング(masking)。もう一つは非特定化(de-identification)。
マスキングと非特定化はそれぞれ、データセットの別のフィールドを扱います。つまり、あるフィールドにはマスキングが使われ、あるフィールドには非特定化が使われるということが同時に行われうるのです。マスキングは名前や社会保障番号といったものを保護するのに使われます。非特定化は属性データや個人の社会経済情報といったもの、例えば年齢、自宅や職場の郵便番号、収入、子供の数、人種といったフィールドを保護するのに使われます。
調査環境の要件と要望
- リモートで全て完結させる(データをローカルに持っていかれると行動ログなどを取れなくなるため。また、シンプルに安全でないため)
- 調査用に加工したデータをアプリケーションと接続した状態でブラウザから見れるようにしたい。(デバッグ性能を格段にあげてカスタマーサポートへのお問い合わせ回答速度を上げる)
- 上に関連して、ソースコードを書き換えてデバッグできるようにしたい
- 誰がいつどの企業のデータを調査したかのログは残したい
- 複数環境を調査できるようにする(同時並行で複数企業ということ)
- 作成した環境は調査が終わったら勝手に消えて欲しい
実際の動作フロー
まずは完成形をイメージするために、環境を起動する方法と調査・デバッグの流れについて説明します。技術的なポイントなどは後述。
運用管理者と調査者が知っていなきゃいけない・気にすることは以下のことだけです。
起動方法
起動ポイントはAWS Lambda Functionです。なぜこれを実行トリガーとして採用したかというと理由は2つです。
- 前段にAPI Gatewayなどのサービスを置くことで、外部サービス例えばSlack Commandなどによる実行ができる
- Cloud Watch Event Ruleを使うことで、起動した後の終了処理をcron実行させることができる
入力情報
次にじゃあ具体的にはどういうインプット渡すの?ってところです。以下に対応しています。
- 会社名 - どの会社データを見たいか。
- 調査者(デベロッパー)のメールアドレス - 当該作成環境の調査者を制限します。
- RDSのスナップショットID - 過去のある時点の状態をリストアしたい場合に指定します。指定がなければ最新のスナップショットを使います。
- マスキング処理をスキップしたいデータベースカラム(複数) - カラム名のリスト。指定したカラムに入っている値はマスクされなくなります。
- マスキング処理をスキップしたいユーザーID(複数) - 指定したユーザーIDに関連するデータベースカラムに入っている値はマスクされなくなります。
こんな感じです。
そして、運用上 このLambdaを実行できるのは実行管理者に限られます。
調査の流れ
調査開始まで
- 調査者は上の入力情報を管理者に渡して調査環境の作成を依頼します。
- 管理者は渡された入力情報を確認します。問題がなければ環境の作成を実行します。必要に応じて、「なんでこのカラムもマスキング外すの?」みたいな議論をします。
- 環境の作成が完了したら、調査者のメールアドレスに通知が届きます。内容には、アプリケーションへの全ユーザー共通のログインパスワードとテナント管理画面へのログインパスワードが記載されています。
- しばらく時間が経つと調査環境は自動で消滅します。
調査開始後のデバッグ方法
- ブラウザからアプリケーションにログインしてデバッグします。あるユーザーの動作を確認したい場合はそのユーザーとしてログインできます。
- ブラウザからテナント管理画面にログインしてデバッグします。
- AWS System Managerの Session Managerからインスタンスにsshログインしてデバッグします。
- ソースコードを書き換えて動作の変化をみる
- ローカルデータベースにクライアント接続して加工データを直接みる
って感じです。
この時に調査者がみれる情報は、 調査対象企業のデータに限られ且つほぼ全てのデータがマスク処理された状態 となります。
また、 立ち上がった調査環境にアクセスできるのは当該の調査者に限られます。
アーキテクチャと実装
では次に技術的なポイント
アーキテクチャ図を見てもらいながら、処理の流れをできるだけ時系列に沿って説明していきます。
EC2の起動とロードバランサ
まず管理者によって実行されたLambda Functionは、EC2 Instanceを起動します。この時の初期設定はユーザーデータに記述して任せています。(golangインスコしたり、ソースコードをチェックアウトしたり)
EC2の起動実行と同時に、ALBに対して3つのことをやっています。
- ターゲットグループの作成
- 起動インスタンスのターゲット追加
- リスナールールの追加
です。
後述しますが、ローカル開発環境では、APIもテナント管理画面も、それからデータベースや他のミドルウェアもDockerを使ってコンテナ上で動かしています。
そして、今回はそれをまんま利用します。つまり一つのインスタンス上に、複数のサービスが全てコンテナとして展開されています。
上に書いたALBの設定は、グローバルアクセスするときに、どのサービスにルーティング(今回の場合はポートルーティング)すればいいかをリスナールールに設定してあげるためです。
つまり、このホストだったらこのポートにルーティングするみたいなことをやりたいす。
例)
- admin-hoge.fuga.com -> instance-A :8888
- api-hoge.fuga.com -> instance-A :9999
ALBのリスナールールを使うと、これ以外にも色々と柔軟にルーティングすることができます。
AWSのロードバランサには、3つのロードバランサの種類がありますが、上の機能はアプリケーションロードバランサだけが有しています。クラシックロードバランサでは実現できないですが、要件次第では上のことをできる必要はないので、必要に応じて選定しましょう。
さて、次に進む前に先ほどさらっと触れたEC2ユーザーデータについての詳細です。
ユーザーデータの中では何やってるのか?
- Docker, PostgresやGolangのインストール
- ソースコードをS3から取得
- ローカル開発環境をセットアップ
- 本番DBのスナップショットを復元・加工したのちダンプしてローカルのDBコンテナに復元
- 全部終わったら、Slackチャンネルに通知・調査者にメール送信
ってことをやっています。
各種コマンドのインスコやセットアップに関しては、実際は、ユーザーデータで毎回やる必要はありません。たまにpostgresのインストールに失敗する事象を確認してしまったので、むしろpackerなど使ってAMIとしてあらかじめ用意しておいた方がいいです。postgresがまじでry
次。ソースコードはzip化してS3に放り込んでいます。それを取ってきていつも自分のパソコン使ってローカル開発してる環境を構築します。大体どのプロジェクトも初回のセットアップコマンドみたいなのを用意してると思います。
うちの場合、各サービスのDockerコンテナが立ち上がり、ライブリロードツールがアプリケーションを実行し、autoreload on changeな状態がすぐに出来上がる感じになっているので、それをやります。
README.mdなどに手続き型の説明(このコマンド実行してね、次はこれ、そのあとはアレ)みたいなプロジェクトだとしても、それを同様手続き型でユーザーデータに落とし込めばいいだけです。難しいことはありません。
開発環境が整ったら、DBコンテナのデータに本番のデータを加工して持ってくるだけです。ここは話が長くなるので、後述します。
全ての準備が整ったら、Slack通知・調査者にメール送信をしてユーザーデータの仕事は終わりです。
本番のデータを加工して持ってくる
長くなる話をここでします。
やることは6つ。
- 本番環境のスナップショットからデータベース(RemoteDB)を復元
- RemoteDBのデータから対象企業以外のデータを除外
- RemoteDBのデータをマスキング
- RemoteDBのデータをローカルにダンプ
- ダンプしたデータをローカルのDBコンテナに復元
- 無用となったRemoteDBを削除
スナップショットは実行時に渡されたものがあればそれを使い、なければ最新のものを使うようにしています。これは、過去のある時点に遡ってデータを調査したいって時に有用です。
そうして、一時的なデータベースインスタンス(RemoteDB)を作成します。
調査者は調査対象の企業さんのデータだけを見れればいいわけなので、それ以外の企業のデータは調査対象から除外しておきます。
続いて、マスキング処理をかけます。ここの詳細については後述します。
あとはリモートからダンプして、ローカルにデータ入れて、リモートのDB消します。管理者や調査者が加工前の元のデータにアクセスできるタイミングはありません。
この辺の安全性確保については、合わせてSecurity GroupやIAMを適切に設定することが必要です。(セキュリティは後述)
以上です。これで調査が開始できます。
データマスキングについて
基本的に全てのデータをマスクします。見えちゃいけないデータを見えない形に落とし込みます。
例えば、うちのプロダクトは目標評価管理ですので、太郎くんが上司との面談で「僕じつは花子ちゃんのことが好きで、毎日同じ電車に乗ってるんです!デュフデュフ...」という報告をしたとしましょう。
結構やばい発言ですが上司としては記録としてしっかり残します。弊社のデータベースに保存されます。
さて。調査者が全てのデータにナマのままアクセスできたとすると、「あの会社には太郎というヤバイ変態社員がいる。やばい会社じゃねえか!」ということが露呈してしまいます。
まあそういうヤバイ情報流出(流出といっても弊社内で収まっているわけですが)を防ぐ為にも、生データ見れるのはマズイわけです。
なので「僕$$$$$ち$$$$$が$$$$$、$$$$$車$$$$$ん$$$$$フ$$$$$.」みたいにして原文を意味不明にします。(左のマスキング方法は一例です)
茶番が終わったので、マスキングの実装方法について説明します。
まず基本は、 全ての文字列型 の情報をマスキングします。
なぜなら個人情報を特定するものはほぼ文字だからです。
他にあるとしたら、画像。数字だと電話番号とか社会保険番号とかでしょうか。大抵そういった情報は数字でも文字列型として保存していそうですが。
各ユーザーのプロフィール画像などに関しては、全て共通のダミー画像にデータを挿げ替えています。
ただ、全ての文字列型データがマスキングすることはできないケースがほとんどだと思います。例えばuuidとか採用してるとそのカラムは無理ですよね。そういった例外は個別にハンドリングしてあげる必要があります。
他の例を出します。最初の1文字だけをマスクするというルールを適用するとします。そして四字熟語が入っているカラムにこれを適用するとします。
「魑***」
これ特定できちゃいますよね。正解できるクイズが出来上がりました。答えは「魑魅魍魎」です。
漢字が使用頻度の低いものであればあるほど、特定が容易になります。四字熟語だったからまあいいもののこれが姓名だとどうでしょうか?
難読人名クイズ。芸能人です、誰でしょう?
「蛭*」「能*」
これもわかる人は分かりそうです。答えは「蛭子能収」でした。
企業が特定できてるなか人名の一部がみえると結構分かりそうなものです。こういったこともケアしてあげる必要がありそうです。
困ったのは、JSON文字列をカラムに突っ込んでいる場合です。MySQLもPostgreSQLもJSON型を持ったりクエリ操作できたりと割と普通なことになってますが、うちでもそういうカラムがあります...。
JSONオブジェクトの文字列値だけをマスキングするという条件処理を入れてあげねばなりません。
GolangでJSON文字列マスキングライブラリを作りました
https://github.com/uqichi/go-json-mask
あらゆるjsonオブジェクトは map[string]interface{}
にパースが可能なので、それを利用して、再帰処理を入れつつ書いてみました。
フィールドを指定することでマスキング処理をスキップすることができるようにしています。これは前述の理由から要件次第で必要となります。
また、どういったマスキングをかけるかということも利用側が書けるようにコールバックを渡す形にしてあります。
こんな感じで、例外条件を処理しながら、最大限できるところまでマスクして、セキュアな情報閲覧を可能にしています。
勝手に消えてくれる環境
環境を終了させるLambda Functionを作っておいて、Cloud Watch Events Ruleをトリガーに設定して定期実行させるようにしています。
実行時点で、対象インスタンスをフィルタして、拾われたやつは終了させます。
タグを使って対象を絞り、起動時間からの経過を見て終了判断をしています。
安全確保のための仕組み
AWS Security Group
AWSが提供するファイアウォールです。
3つ設定していて、それぞれALB,EC2,一時作成RDS用となっています。それぞれ受け口、出口を最低限に設定します。
弊社内からアクセスできないようにするとか、適当に作ったインスタンスからのアクセスを拒否するといったことを実現します。
AWS IAM Role
EC2インスタンスにはIAM Instance Profileが設定しており、各種サービスに対して必要なリソースに必要なアクションしか行えないようになっています。
これは、本番稼働のDBへの削除とかのヤバイ実行を防ぎますので必ず設定します。
スタンドアローンなリモート環境
自分のPC端末にダンプという手順を取ってしまうと、マスキングされてるデータとはいえ危険です。
なのでリモートで完結するようにしています。
調査環境で何をやろうと本番環境に影響することはありません。
これは前提として、ローカル開発環境で依存しているS3やRedisなどが全てMock化されている恩恵であります。
調査担当者以外の人間はアクセス不可
ブラウザからデバッグする際には、環境作成時にランダム発行されたパスワードが設定されています。
これは環境起動後に調査担当者のみに通知されるものなので、管理者含め他の人間がそのデータにアクセスすることはできません。
と、ここは、SSMへのログイン権限を絞れていないと上の制限もあまり意味をなしません。IAMリソースを操作する形になります。
SSH接続中の行動ログを取れる
今回sshにSession Managerを使う方法を採用しているため、行動ログを取ることができます。
Systems Manager は AWS CloudTrail と統合されていて、AWS 内でユーザーやロール、または Systems Manager のサービスによって実行されたアクションを記録することができます。
ネットワーク超えてのデータの持ち出しなど怪しいことやってる人がいたらわかります。
これはEC2のssh key pairを使ってのsshだと実現できないことです。
調査環境放置を防ぐ
起動したインスタンスはCloudWatch Eventsが定期的にLambdaを実行して勝手に消してくれます。
インスタンスとともに、ターゲットグループ削除・リスナールールから登録解除もやっているので、環境を終了する時は、手動で削除する必要はありません。調査が終わったら放置でOK
--
はい、終わり。
いかがでしたでしょうか。
SaaS企業で働いているエンジニアのかたは、少なからずこういった企業や個人データのケアについて考える必要・機会があるかと思います。
他によりベターな方法や知見があったり、同じ悩みをもつ人がいたら、ぜひ教えてください。
長くなりましたが読んでくださってありがとうございました。