IAMによるAWS権限管理運用ベストプラクティス (2)

よく訓練されたアップル信者、都元です。前回(昨日)はAWSのクレデンシャルとプリンシパルを整理し、「開発運用スタッフ」が利用するクレデンシャルについてプラクティスを整理しました。今回はAWS上で稼働する「システム」が利用するクレデンシャルについてのプラクティスを整理しましょう。

システムが利用するクレデンシャル

システムが利用するとはどういうことかといいますと、要するに「ユーザがアップロードしたファイルをS3に保存する」だとか「S3バケットに保存されたファイル一覧を取得して表示する」だとか、そういう操作をするシステムを作ることです。このようなシステムでは、APIキーを利用しますね。

AWSのAPIキーには、これもまた大きく分けて2種類があります。

  • long lived credentials (永続キー)
  • short lived session credentials (一時キー)

皆さんが一般的に目にしているAPIキーは、永続キーになります。「アクセスキーID」と「シークレットアクセスキー」の組み合わせですね。こちらは、明示的に無効化の操作をしなければ、発行後無期限で利用可能です。

一方後者は、「アクセスキーID」と「シークレットアクセスキー」の他に「セッショントークン」という3つの要素の組で構成されます。さらに、一時キーには有効期限 *1があります。つまり、有効期限が切れたら再発行が必要です。

一時キーの発行・再発行等の処理は、普段は見えないプログラム内で自動的に行われるため、多くの場合、開発運用スタッフが目にすることはありません。

プラクティス4 : APIキーをシステム内にハードコーディングしない

システムがAPIキーを手に入れるには、何通りもの手段があります。それぞれ例示していきたいと思います。ここから出てくるコードはJavaです。

BasicAWSCredentials

AWS Java SDKによる、最もよくあるAPI利用例はこんなかんじです。

AWSCredentials cred = new BasicAWSCredentials("accesskey", "secretkey");
AmazonS3 s3 = new AmazonS3Client(cred);
// ...

ほとんどの資料で、このような説明がなされます。しかしここで使っているキーは永続キーです。このようなコードは当然git等にコミットして管理されるため、権限の共有範囲を管理できなくなってしまいます。

PropertiesCredentials

下記のように、クラスパス内から読み込んだとしても、同様にコミット管理されることになり、あまり適切ではありません。

AWSCredentials cred = new PropertiesCredentials(getClass().getResourceAsStream("/cred.properties"));
AmazonS3 s3 = new AmazonS3Client(cred);
// ...

AWSCredentialsProvider

ところで、AWS Java SDKの各クライアントのコンストラクタとして、ここまでは下記の(1)を利用していました。が、これとは別に(2)というコンストラクタもあります。

public AmazonS3Client(AWSCredentials awsCredentials) … (1)
public AmazonS3Client(AWSCredentialsProvider credentialsProvider) … (2)

前者は「クレデンシャルそのもの」を渡すパターン、そして後者は「必要に応じてクレデンシャルを取得する手段」を渡すパターンです。ベタであまり意味は無い *2例ですが、StaticCredentialsProvidergetCredentialsで、コンストラクタから受け取ったクレデンシャルをそのまま返すAWSCredentialsProviderの実装です。

AWSCredentials cred = new BasicAWSCredentials("accesskey", "secretkey");
AWSCredentialsProvider cp = new StaticCredentialsProvider(cred);
assert cp.getCredentials() == cred;
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

ClasspathPropertiesFileCredentialsProvider

AWSCredentialsProviderの実装は他にもあります。例えばClasspathPropertiesFileCredentialsProviderは、前述の「クレデンシャルをクラスパス内から読み込む」ような実装です。引数を省略すると、AwsCredentials.propertiesを読み込むようになっています。

AWSCredentialsProvider cp = new ClasspathPropertiesFileCredentialsProvider();
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

PropertiesCredentials再び

コード内に書くパターンよりももう少しマシな例として、下記のような書き方を見てみましょう。

AWSCredentials cred = new PropertiesCredentials(new File("/path/to/cred.properites"));
AmazonS3 s3 = new AmazonS3Client(cred);
// ...

この書き方では、クレデンシャルをアプリケーションの外に出すことはできますが、これでもデプロイするサーバのファイルシステムにはクレデンシャルを書き込むことになります。つまり、運用担当者は自由に読み書きができるはずですので、共有範囲のコントロールはできません。

EnvironmentVariableCredentialsProvider

APIキーを環境変数から読み込む仕組みです。

// set AWS_ACCESS_KEY_ID and AWS_ACCESS_KEY env
AWSCredentialsProvider cp = new EnvironmentVariableCredentialsProvider();
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

これもコード内からクレデンシャルを追い出すことができますが、どこかで環境変数を設定するコマンドを書かなければなりません。

SystemPropertiesCredentialsProvider

Javaにはシステムプロパティという仕組みがありまして、起動時のオプションでアプリケーションに値を引き渡せます。

// -Daws.accessKeyId=accesskey -Daws.secretKey=secretKey
AWSCredentialsProvider cp = new SystemPropertiesCredentialsProvider();
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

まぁこれも、どこかにキーを生書きする必要があるでしょう。

InstanceProfileCredentialsProvider

さて、ここまで色々紹介して来ましたが、「APIキーをシステム内にハードコーディングしない」というプラクティスは何れも達成できていませんね。そろそろ対応策を提示したいと思います。

// apply IAM role
AWSCredentialsProvider cp = new InstanceProfileCredentialsProvider();
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

まず、このコードは「IAM Roleを適用したEC2インスタンス」でしか上手く動きません。IAM Roleの適用についてはIAM roles for EC2 instancesを使ってみるを参照してください。

EC2インスタンスには、EC2 Metadata Serviceという機能があります。EC2内から169.254.169.254に対してHTTPリクエストを行うことにより、インスタンスIDやAMIのIDをはじめ、様々な情報を得ることができます。このInstanceProfileCredentialsProviderは内部的に、このEC2 Metadata Serviceを利用して一時キーを取得しています。従って、EC2以外(例えば開発マシン)でこの実装を使っても、一時キーが得られないわけです。

さて、永続キーはBasicAWSCredentialsを使いましたが、一時キーにはセッショントークンも含まれますので、別の実装BasicSessionCredentialsを使います。

AWSCredentials cred = new BasicSessionCredentials("accessKey", "secretKey", "sessionToken");

InstanceProfileCredentialsProviderの内部では、EC2 Metadata Serviceから得たキーをこのように処理しています。

さて、非常に長くなりましたが「APIキーをシステム内にハードコーディングしない」というプラクティスはIAM Roleを使って実現します。この仕組みを使えば、システムの「AWSアカウント間ポータビリティ」も柔軟に確保できます。素晴らしい。

AWSCredentialsProviderChain

ただ、InstanceProfileCredentialsProviderを利用する場合、一つ欠点があります。そう、ローカル開発環境で動かなくなるんですね。そこで、AWSCredentialsProviderChainを使うと良いでしょう。複数のAWSCredentialsProviderを内包し、コレでダメだったら次コレ、と Chain of responsibility してくれる実装です。

ローカル開発環境では、環境変数なりシステムプロパティなりに、自分の持っているIAMのクレデンシャルを突っ込んでおけば上手く動作するってわけです。

AWSCredentialsProvider cp = new AWSCredentialsProviderChain(
		new EnvironmentVariableCredentialsProvider(),
		new SystemPropertiesCredentialsProvider(),
		new InstanceProfileCredentialsProvider()
		);
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

って毎回書くのめんどくさいじゃないですか。そういう実装があります。

DefaultAWSCredentialsProviderChain

AWSCredentialsProvider cp = new DefaultAWSCredentialsProviderChain();
AmazonS3 s3 = new AmazonS3Client(cp);
// ...

って毎回書くのもそこそこめんどくさいじゃないですか。じゃあコレです。

AmazonS3 s3 = new AmazonS3Client();
// ...

!?

そうです。デフォルトの設定が最も適切で、最もお勧めなんですね。長旅でしたがコレが答えです。

プラクティス5 : IAM Roleが使えない場合でもせめて一時キーを使う (ちょっとやり過ぎ感アリ)

ただ、このプラクティスの例外として考えられるのは、AWS外の例えばオンプレミスサーバ上で動くシステムからAWS APIを叩きたい場合、これは適切に権限を絞ったIAMユーザを作成し、そのユーザに対する永続キーを利用するしかありません。

しかし、そんな中でもセキュリティに気を遣う場合(これは正直ちょっとやり過ぎ感は否めないですが)、こんな実装もあります。

STSSessionCredentialsProvider

永続キーから一時キーを得るAWSCredentialsProviderです。一時キーを得るためには、AWS Security Token Service(STS) *3におけるGetSessionTokenというアクションを使います。

まぁ、ホント単純に、一時キーを発行してくれるだけのアクションです。この仕組みを使って、永続キーはGetSessionTokenアクションの実行専用とし、実用的なAPIを叩くのは一時キーだけにする、という方針で、多少セキュアになるかもなぁ、という。

AWSCredentialsProvider cp = new ClasspathPropertiesFileCredentialsProvider();
AWSSecurityTokenService sts = new AWSSecurityTokenServiceClient(cp);
GetSessionTokenResult sessionToken = sts.getSessionToken();
Credentials stsCredentials = sessionToken.getCredentials();

AWSCredentials cred = new BasicSessionCredentials(
		stsCredentials.getAccessKeyId(),
		stsCredentials.getSecretAccessKey(),
		stsCredentials.getSessionToken());
// 1時間後に無効に…

というようなことを中でやってくれるのがSTSSessionCredentialsProviderです。

AWSCredentialsProvider cp = new STSSessionCredentialsProvider(new ClasspathPropertiesFileCredentialsProvider());
AmazonS3 s3 = new AmazonS3Client(cp);
// ...
// 一回得たキーが有効期限切れを起こしても、自動更新してくれます。

あーんまり強烈な効果は感じられない実装ですが。

まとめ

一言。要するに、システムは永続キーを使っちゃダメです。最低でも、永続キーをハードコーディングしないようにシステムを作りましょう。

次回はー。どうしようかな。もう少し一時キーを掘り下げるか、クロスアカウントアクセスについて掘り下げるか、「エンドユーザが持つモバイルアプリ等」が利用するクレデンシャルについて触れるか…。まぁ、その辺りだと思います(場当たり)。

脚注

  1. デフォルトで12時間、発行時の指定により15分〜36時間までコントロールできる
  2. そのため、StaticCredentialsProviderinternalパッケージに属しています。
  3. IAMの補助プロダクト的な位置づけですね。EC2に対するAutoScalingやELBのような…。