この記事は、 DeNA Advent Calendar 2021 の17日目の記事です。
こんにちは、 @karupanerura です。 普段はAWSやGCPを使って仕事をしています。
前作 に引き続き、AWS IAM Userのアクセスキーの定期的なローテーションを自動化したので、今回はそれについて書きます。
IAM Userの使いどころ
IAM Userの利用にはアクセスキー(Access Key IDとAccess Key Secretの組)やコンソールログイン用のパスワードなどのクレデンシャルが必要です。
当然これらが漏洩すればそれを不正に利用され得るリスクがあるので、可能であればなるべくIAM Roleを利用するべきです。 IAM Roleならクレデンシャルの管理が不要で、アプリケーションやサービスに対してそのIAM Roleを通して任意の権限を与えることができるため、管理も非常に簡単です。
しかし、人生はそう甘くはなく、IAM Userを使う必要がある場面が時々存在します。
2つ例を挙げましょう。
まず1つは、AWSコンソールなどを通してAWSリソースを人が操作する場面です。
一般的に、rootアカウントで常用するリスクは非常に高いので利用を避けるべきです。しかし、人間はAWSのサービスではないためIAM Roleを直接使うことはできません。 代わりに、多くの場合はIAM Userを払い出して人々はそのIAM UserとしてAWSリソースを操作ことになることでしょう。 場合によってはマルチアカウント構成にして、踏み台用のアカウントにIAM Userを作り、目的のアカウントのIAM RoleにAssume Roleして利用することもあるかと思います。(DeNAでは全社的にこの方法を採用しています。)
もう1つは、AWS外部のCI/CDシステムなどからプログラムがAWSリソースを操作する場面です。
こちらも同様に、AWS外部のシステムは一般にAWSサービスではないためIAM Roleを利用することができません。 代わりに、多くの場合はIAM Userを払い出して、プログラムがそのIAM UserとしてAWSリソースを操作することになることでしょう。
いずれも、IAM Userを避けられない場面です。
クレデンシャルの管理
前述のとおりIAM Userを利用する上ではクレデンシャルの管理は避けられません。
個人利用のIAM Userのクレデンシャルは一般のログイン用クレデンシャルと同様に個人が善良に管理する前提を置くことができるでしょう。 しかし、システム利用のIAM Userのアクセスキーはそのシステムを管理するすべての人々の目に触れることになるため、漏洩リスクが比較的高いと言えます。
たとえば、悪意を持った退職者がシステム用のアクセスキーを持ち出して、不正に利用されてしまったとします。 もし、そのアクセスキーを非常に長い期間でそのまま利用し続けていたとしたら、不正利用を誰が行ったのかを絞り込むのが難しくなってしまうでしょう。 ただでさえ漏洩リスクが比較的高いのに、万が一の漏洩時にその対応が難しい状況は好ましくありません。
それを避けるためにはアクセスキーを長い期間使い回さずに、定期的にローテーションすることが肝要です。
アクセスキーを定期的にローテーションすることによって、それぞれのアクセスキーを利用できる期間が限られるため、先述した漏洩時のリスクを低減することができます。 不正に持ち出されたアクセスキーが利用できる期間が限られることでそれが不正に利用されるリスクが減らせるのはもちろんのこと、誰が不正に利用した可能性があるのかを絞り込むことも比較的容易になります。
アクセスキーのローテーション
アクセスキーをその利用元のシステムを無停止のままローテーションするには、以下のようなシーケンスをたどることになります。 (具体的には公式ドキュメントの アクセスキーの更新 にある説明が参考になります。)
- 初期状態: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり) - 新しいアクセスキーの作成: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:なし) - 新しいアクセスキーに置換開始: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり) - 新しいアクセスキーに置換完了: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:なし), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり) - 古いアクセスキーを無効化: アクセスキー(ID:
AKIEXAMPLE1/Status:Deactive/利用:なし), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり) - 古いアクセスキーを削除: アクセスキー(ID:
AKIEXAMPLE2/Status:Active/利用:あり)
新しいアクセスキーを作成した後、新しいアクセスキーを利用できるようにアクセスキーの利用元が持つアクセスキーを置換する必要があります。(詳細な例は後述のCircle CIにおけるContextに関連する説明のなかで紹介します。) 置換したアクセスキーは即座にそれだけが使われるとは限らず、すでに実行中の処理は古いアクセスキーを利用して動いているため、その状態の差異を考慮する必要があります。 そのため、置換開始/置換完了のフェーズを意識する必要があります。
また、ローテーションという目的から、新しいアクセスキーを発行したら必ず古いアクセスキーは無効化する必要があります。 しかし、もしまだ利用が続いているアクセスキーを不用意に無効化してしまうと、それを利用しているシステムが動かなくなってしまいます。
さらに、AWSの制約でIAM User当たりで持てるアクセスキーの上限が2つまでと決まっています。 無効化したアクセスキーを無限に保持しておくことはできないため、このように無効化したあと削除するというシーケンスとなります。 無効化したアクセスキーは復元が可能ですが、削除すると復元はできなくなるため、不用意に削除すると復旧に時間を要すことになります。
このように、アクセスキーのローテーションは大変面倒でかつ繊細な作業です。 人間がこれを行う上では純粋にその手順の複雑さが高いのはもちろん、それぞれのIAM User毎にこれを行う必要があるためにヒューマンエラーの余地が大きいことが問題になります。
また、次の項で説明しますが、この作業を行う権限を持つ人は増やしにくいため、作業が属人化しがちです。 このようなストレスフルな作業が属人化してローテーションが難しいことは業務の持続可能性を保つ上で問題になります。
そこで、アクセスキーのローテーションがなんとか自動化できないかを考えます。
しかし、その自動化を考えたときに人の作業をそのままプログラムにやらせようとするとある問題に突き当たることになります。
一般的なアクセスキーのローテーション作業とその自動化の上での問題点
一般的には、管理者的なIAM User/Roleがアクセスキーの作成権限を持ち、対象のIAM Userのアクセスキーのローテーションを行うことになります。
しかし、この場合はローテーション対象のIAM Userのアクセスキーを得る手段があるため、権限昇格が実質的に可能です。
権限昇格とは、本来そのIAM User/Roleに付与された権限を超えて不当に権限を得る行為です。
たとえば、本来であればローテーション対象のIAM Userのアクセスキーのローテーション権限しか持たなかったとしても、 アクセスキーを作成することで対象のIAM Userのアクセスキーを得てそれを通じて本来そのIAM Userが持ち得なかった権限を行使することが可能です。
このように、ローテーションを行う主体となるIAM User/Roleの権限をどれだけ絞ったとしても、ローテーション対象となるIAM Userの権限に権限昇格ができてしまうことになります。 悪意を持った人間にその権限が渡った場合、権限権限によって結果的に大きな権限を持ち得ることによって、場合によってはシステムに対して影響が大きい攻撃も可能になってしまうことでしょう。
人がローテーションの主体となる場合は、ローテーション対象となるIAM Userの権限もすべて持ち合わせた管理者的なIAM User/Roleを使うことになることが多いため、 権限昇格が問題になることはないでしょう。 また、一般的にはその強い権限を行使するべき最小限の人にそれを付与するため、仮にそのなかにその権限を不当な目的で行使する人がいたとしてもその特定は比較的容易です。
しかし、自動化を考えたときにそのような強力な権限を持ったIAM User/Roleをシステムに利用させることは難しい場合が多いかと思います。
前述のとおり、システム利用のIAM Userのアクセスキーは漏洩リスクが比較的高いと言えますし、IAM Roleを使える場合はまだ良いかと思いますがその管理には気を遣う必要があるでしょう。 さらに、いずれもRCE(Remote Code Execution)やローテーション処理のコードへ悪意のある処理を混ぜる攻撃に対して、影響を受けることになり得る範囲が非常に広くなってしまいます。
このように、一般的なアクセスキーのローテーション作業をそのまま自動化しようとすると権限管理上の課題が存在します。
逆に言えば、権限昇格が出来ないアクセスキーのローテーションが可能であれば、その自動化の可能性が開けます。
権限昇格が出来ないローテーション
IAM Userがその自分自身のアクセスキーをローテーションする場合は、懸念していたような権限昇格は起き得ません。
ローテーション対象のIAM Userのアクセスキーを得る手段があるとはいえ、その対象が自分自身だけであれば使える権限は変わらないことは自明です。
自分自身のアクセスキーだけローテーションすることができる権限の具体例としては、以下のIAMポリシーのようになります。
この例ではアクセスキーをローテーションするのに必要なActionに制限しつつ、Action対象のリソースをそのIAM User自身だけに制限しています。
(適切に制約をするためにアカウントIDを記載する必要があるのでダミーとして000000000と表現しています。)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreateAccessKey",
"iam:DeleteAccessKey",
"iam:GetAccessKeyLastUsed",
"iam:ListAccessKeys",
"iam:UpdateAccessKey"
],
"Resource": "arn:aws:iam::000000000:user/${aws:username}"
}
]
}
ちなみに、必要に応じてIP制限なども付け加えると外部に持ち出しての利用ができなくなるため、よりセキュアにになります。(例によってダミーとして192.0.2.0/24で記載しています。)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreateAccessKey",
"iam:DeleteAccessKey",
"iam:GetAccessKeyLastUsed",
"iam:ListAccessKeys",
"iam:UpdateAccessKey"
],
"Resource": "arn:aws:iam::000000000:user/${aws:username}",
"Condition": {
"IpAddress": {
"aws:SourceIp": [
"192.0.2.0/24"
]
}
}
}
]
}
管理者ではなく自分自身でローテーションする事ができるので、これであれば権限昇格の問題は解決しそうです。
ローテーションの自動化
ローテーションを自動化する上で、その手順は極めて重要です。 状態に応じて適切に判断して操作を行わなければ、永久にローテーションの進行をブロックしてしまったり、逆にまだ使っているアクセスキーを停止してしまうことになりかねません。
改めて、アクセスキーのローテーションのシーケンスを再掲します。
- 初期状態: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり) - 新しいアクセスキーの作成: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:なし) - 新しいアクセスキーに置換開始: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり) - 新しいアクセスキーに置換完了: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:なし), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり) - 古いアクセスキーを無効化: アクセスキー(ID:
AKIEXAMPLE1/Status:Deactive/利用:なし), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり) - 古いアクセスキーを削除: アクセスキー(ID:
AKIEXAMPLE2/Status:Active/利用:あり)
これを実現するフローは以下の3つのステップで考えることができます:
- 新しいアクセスキーの作成と既存のアクセスキーからの置き換え
-
- 初期状態: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり)
- 初期状態: アクセスキー(ID:
-
- 新しいアクセスキーの作成: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:なし)
- 新しいアクセスキーの作成: アクセスキー(ID:
-
- 新しいアクセスキーに置換開始: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:あり), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり)
- 新しいアクセスキーに置換開始: アクセスキー(ID:
-
- 古いアクセスキーの無効化
-
- 新しいアクセスキーに置換完了: アクセスキー(ID:
AKIEXAMPLE1/Status:Active/利用:なし), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり)
- 新しいアクセスキーに置換完了: アクセスキー(ID:
-
- 古いアクセスキーを無効化: アクセスキー(ID:
AKIEXAMPLE1/Status:Deactive/利用:なし), アクセスキー(ID:AKIEXAMPLE2/Status:Active/利用:あり)
- 古いアクセスキーを無効化: アクセスキー(ID:
-
- 古いアクセスキーの削除
-
- 古いアクセスキーを削除: アクセスキー(ID:
AKIEXAMPLE2/Status:Active/利用:あり)
- 古いアクセスキーを削除: アクセスキー(ID:
-
それぞれどのように考えるとよいか説明します。
新しいアクセスキーの作成と既存のアクセスキーからの置き換え
ローテーションの手順として、最初に新しいアクセスキーの作成と既存のアクセスキーからの置き換えを行う必要があります。
これは2つの操作ですが、これらは一度に行う必要があります。
新しいアクセスキーを作成した後、それを既存のアクセスキーと置き換えなければ古いアクセスキーの利用が続いてしまいます。 また、新しいアクセスキーはそれを作成したタイミングでしか得ることができません。そのため、それをどこかで保存するくらいなら同じ手順内で置き換えてしまったほうが良いでしょう。
しかし、トランザクションのような便利なものはないので、これらはアトミックな操作として行うことはできません。 そのため、アクセスキーの作成には成功したが、既存のアクセスキーからの置き換えはできなかったという状態が起こりえます。
この問題への対策として、リトライと事前試行を行うことが重要です。
まず、リトライに関しては特に既存のアクセスキーからの置き換えを適切にリトライすることが非常に重要です。 Exponential Backoff and Jitter ( 和訳 )などを参考にリトライを行うことで障害時の影響を最小限に抑えることができるでしょう。
また、事前試行としては、アクセスキーの作成前にアクセスキーの置換に必要な権限のテストも兼ねて権限の置換を試行することで、 この事前試行からその後の実際の置換までの間で後者だけが失敗するという稀なケースを踏まない限りは成功することが期待できるでしょう。
そしてこの操作は、アクセスキーが1つしか存在せずかつその作成日時がローテーションの対象とするのに十分なほど古い場合に実行することで、適切なタイミングで実行することができます。 これらの情報は ListAccessKeys によって得ることができます。
いずれの対策も軽減策でしかありませんが、これによってほとんどの問題は回避できるかと思います。
古いアクセスキーの無効化
次に、もう使われなくなった古いアクセスキーを無効化します。
IAM Userのアクセスキーは削除する前に一度無効化をする必要があります。 無効化したアクセスキーは再度有効化することが可能なので、万が一なにか問題が起きた場合はこのフェーズから復旧させることが可能です。
難しいのがこの実行タイミングです。
使われなくなった。ということをどのように判定すれば良いでしょうか? これには GetAccessKeyLastUsed の情報が有用ですがその使い方は意外と簡単ではありません。
単純に考えると新しいアクセスキーが作成された後に、古いアクセスキーが利用されていなかった場合は使われなくなったといえそうです。 しかし、これはアクセスキーの置換に失敗しそれをリトライしている最中にアクセスキーが利用された場合は破綻します。 また、AWSリソースを操作するまえに失敗したりAWSリソースを操作する事前条件を満たさなかったりなどして、たまたま利用できていないだけかもしれません。
ニッチケースですが、この状況に陥った場合は永久に成立し得ない条件が揃うまでそれを待ち続けることになります。
ほかにも、十分な期間つかわれないことを待つということも考えられますが、こちらも同様にたまたま利用されないだけであった場合に、アクセスキーの置換に失敗していても古いアクセスキーの無効化を実行してしまいます。
いずれの場合も、アクセスキーの置換に失敗していた場合にまだ使われているはずの古いアクセスキーを無効化してしまうことになります。 これは実は重大な問題になりえるもので、なぜなら外部からAWSリソースを操作するものはCI/CDなどアプリケーションの運用にとって重要なものが多いはずで、止まるとユーザー影響こそないにせよ開発に大きな影響をもたらし得るものが多いと思います。 このようなことは避けたいはずです。
では、どうすればよいのか。
古いアクセスキーが使われなくなった。ということを判断するために、新しいアクセスキーが使われていることも同時に保証するとこれを高い確率で防ぐことができるのではないかと考えました。
これは実現できるのか疑問に思う方もいらっしゃるかと思いますが、実はGetAccessKeyLastUsedの呼び出し自体によるGetAccessKeyLastUsedの結果の更新は行われない(そうでなければ自分自身の呼び出し結果が2度目から得られることになってしまい、リトライに耐えられず実用的でない)ために、
正しく他の操作で利用されていないかを見ることができます。
新しいアクセスキーが使われていてかつ古いアクセスキーの利用が止まっている場合には、最終利用時刻がどんどん乖離していくはずです。 つまり、古いアクセスキーの最終利用時刻と新しいアクセスキーの最終利用時刻の時間が一定の閾値を超えることで、正しく新しいアクセスキーに置換できたことが確からしいといえるのではないでしょうか。
これが失敗するケースは、同一のアクセスキーの利用箇所が複数あり、かつそれらのうち片方だけが置換に失敗していた場合です。 しかし、失敗していた場合でも古いアクセスキーと新しいアクセスキーの最終利用時刻は乖離しないはずで、よほど悪い条件が重なって新しいアクセスキーだけでAPIアクセスに成功してしまったというようなことでも起きないと無効化にまでは至らないはずです。
あるいは、あまりに利用頻度が少ないものだと期待どおりの期間のうちにローテーションが完了しない場合も考えられます。 しかし、この場合もその利用が行われればローテーションが進捗するためさほど大きな問題にはならないでしょう。
さらに、DeNAではすべてのAWSアカウントで横断的にローテーションの監視も行われているので、 万が一、アクセスキーのローテーションが長期間行われていないIAM Userがあってもそれを検出できます。
つまり、これによって高い確度で使われなくなったアクセスキーだけを無効化することができ、失敗した場合のリスクも最小限に留めることができると考えられると思います。
古いアクセスキーの削除
最後に、無効化したアクセスキーを削除します。
無効化は万が一の場合のロールバックのために行っているので、無効化が行われてから一定の期間を空けてから削除する必要があります。
これは単純に最終利用時刻から一定時間が経過したら削除すると良いでしょう。 無効化されるであろうタイミングからアクセスキー系の問題を把握して対処するに十分な期間が空いており、かつ次回のローテーションに先んじて削除できていれば問題ありません。
Circle CIでの実行
これらを自動的に実行するために Circle CI を利用します。
なお、DeNAでは Circle CI Enterprise (Server) を Github Enterprise と併用しているため、一般的なCircle CIとは少々異なる部分もあることにご注意ください。
やること自体は上記で説明していますので、ここではそれをCircle CI上で実行する上でのポイントをいくつか紹介します。
Contextを利用する
Circle CIの Context という機能を利用することで、特定の範囲のGroup/Teamに属するCircle Userに限定して任意のクレデンシャルを共有することができます。 これによって、クレデンシャルへのアクセス権を持ち得る人を制限することができます。 アクセスキーをこれに入れて使うことでセキュアにIAM Userのアクセスキーを共有することが可能です。
アクセスキーのローテーションを行うProjectと、実際にその権限を使うProjectの間で同一のContextを共有して利用することで、同一のIAM Userのアクセスキーをそれぞれが利用することができます。
また、Contextは Web API を用いて操作することが可能です。 このAPIを使ってアクセスキーを新しく作成したそれに置換することができます。これを操作する権限を持ったシステム用Circle Userのパーソナル APIトークンを環境変数に持たせておくとよいでしょう。
ただし、 Context naming for CircleCI server の項にあるとおり、 Github Enterpriseにおいては、(contextがOrganization毎に存在するリソースであるにも関わらず)複数の組織間でcontextの名前を一意に設定する必要がある点に注意が必要です。
Scheduled Pipelineの制限
Scheduled Pipeline ではcronのように定期的にパイプラインを実行することができます。
しかし、
Migrate scheduled workflows to scheduled pipelines
に以下のようにあるとおり、
Scheduled Pipelineは実行ユーザー(actor)を持たないため制限付きのContext(restricted context)を利用できないという制限があります。
Cannot control the actor, so scheduled workflows can’t use restricted contexts.
今回は、Contextを操作するため、それを操作する権限を持つシステム用Circle Userのパーソナル APIトークンが必要です。 このAPIトークンを使って、そのユーザーとしてPipelineを別途実行すればこの制約を回避することができます。 (具体的な方法は こちらのフォーラム の回答を参考にしました。)
これによって、Scheduled Pipelineでこれを自動実行できるようになりました。
まとめ
アクセスキーのローテーションは面倒ですが重要な定常業務です。 自動化は難しいと思われるかと思いますが、いくつかのハードルを乗り越えればその自動化は不可能ではありません。
今回はCircle CIを使って自動化してみましたが、 Jenkins や Github Actions などを利用した自動化も考えられると思います。 ぜひ皆さんもチャレンジしてみてください。
なお、これは 前作 と併せて、改善しつつOSS化を目指したいところです。 前作もまだOSS化に至ってはいませんが、皆さんに利用してもらえるものにできるよう引き続き改善と汎用化を進めていきたいと思います。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。