🎃

IAMアクセスキーの自動ローテーションを実装した話

に公開

IAMアクセスキーの自動ローテーションを実装した話

はじめに

セキュリティに強い会社に最近入社したので、初めの1本目はセキュリティに関連したことでも書こうかなと思い、色々と考えた結果、直近で実装した「AWS汎用ダッシュボードの構成」について書いていこうと思います。
このダッシュボードは、「1つのダッシュボードから、複数のAWSアカウントのリソース情報を横断的に可視化したい」という要件がありましたので、それを実現するための認証方式で実装しました。
そこで実装したのが下記の構成で、踏み台用のIAMユーザの認証情報を利用し、各AWSアカウントのIAMロールをAssumeRoleする構成にしました。※本来であれば、IAMユーザではなく、IAMロールを使用することをAWSでは推奨していますが、常時見える状態にしたいという要件上、今回はIAMユーザを使うパターンで実装してます。

EC2
↓
IAMユーザ(Access Key/Secret Access Key)
↓
IAMロール
↓
情報を見たいAWSアカウント群

図式化すると、こんな感じですね。ここでは、IAMユーザのアクセスキーのローテーションも併せて実装してます。

では、上記でやった内容をより、次章以降で詳しく書いていこうと思います。

実装編

(1)IAMユーザの作成

まず、下記のポリシーを持つIAMユーザを作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAssumeTargetRole",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": [
                "arn:aws:iam::111111111111:role/CrossAccount-AAAA-TargetRole",
                "arn:aws:iam::222222222222:role/CrossAccount-AAAA-TargetRole",
                "arn:aws:iam::333333333333:role/CrossAccount-AAAA-TargetRole",
                "arn:aws:iam::444444444444:role/CrossAccount-AAAA-TargetRole"
            ]
        }
    ]
}

(2)ターゲットAWSアカウント側での認証情報の作成作業

複数のAWSアカウントに対して、手順(1)で作成したIAMユーザからのアクセスに対してReadOnlyアクセス権限を付与する必要があるので、1個1個作成するのではなく、StackSetsの使用がベストプラクティスになるので、下記のCfnテンプレートを使ってデプロイします。

AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Creates a custom role in THIS account that trusts a specific IAM principal
  from another AWS account (user or role), defined via Parameters.

Parameters:
  TrustedPrincipalArn:
    Type: String
    Description: ARN of the IAM User or IAM Role allowed to assume this role.
    Default: arn:aws:iam::999999999999:user/AAAA-user

  RoleName:
    Type: String
    Default: CrossAccount-AAAA-TargetRole
    Description: Name of the role to create in this (target) account.

  ManagedPolicyArns:
    Type: List<String>
    Default: arn:aws:iam::aws:policy/ReadOnlyAccess
    Description: >
      AWS managed or customer managed policy ARNs to attach to the role.

  MaxSessionDurationSeconds:
    Type: Number
    Default: 3600
    AllowedValues: [900, 3600, 43200]
    Description: Max session duration for the role in seconds (15m, 1h, or 12h).

Resources:
  CrossAccountRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      MaxSessionDuration: !Ref MaxSessionDurationSeconds
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: TrustSpecificPrincipal
            Effect: Allow
            Principal:
              AWS: !Ref TrustedPrincipalArn
            Action:
              - sts:AssumeRole
              - sts:TagSession
      ManagedPolicyArns: !Ref ManagedPolicyArns

(3)アクセスキーのローテーション

ここまでで、下記のリソースが作成されました。

概要 説明
source側認証設定 特定のAWSアカウントに対して、AssumeRoleする権限のみを付与したIAMユーザ
ターゲット側認証設定 特定のIAMユーザからのみ、 AssumeRoleを許可する権限

ここまで実装すれば、EC2側に認証情報を設定さえすれば、クロスアカウントアクセスによるAWS環境情報の取得ができるようになりました。
ただ、AWSのベストプラクティス的には、IAMユーザの認証情報をOS内に格納しっぱなしは、セキュリティリスクが高いと評価されてしまうので、非常によろしくないです。
そのため、プラスアルファの対応として、アクセスキーのローテーションを実装する必要があります。

現時点では、IAMユーザのアクセスキーのローテーション機能は、AWS側では実装されていないので、そこはユーザ側で実装する必要があります。
使用するAWSサービスとしては、LambdaとSecretsManagerの2つです。

(3.1) SecretsManager側の実装

「app/access-keys」という名前で、シークレットを作成し、Access Key/Secret Access Keyを格納する。

(3.2) Lambda側の実装

EventBridge scheduler+Lambdaで定期的に手順(3.1)のシークレットを定期的にローテーションする仕組みを設定する。
サンプルコードも下記に記載します。(Python3.10)

import boto3
import json
import os

IAM_USER = os.environ["IAM_USER"]
SECRET_NAME = os.environ["SECRET_NAME"]

def lambda_handler(event, context):
    iam = boto3.client("iam")
    secrets = boto3.client("secretsmanager")

    # Get current secret (for checking the current value. Not currently used)
    try:
        secret_value = secrets.get_secret_value(SecretId=SECRET_NAME)
        current_creds = json.loads(secret_value["SecretString"])
    except Exception as e:
        print(f"Failed to get current secret: {e}")
        current_creds = None

    # Get a list of existing access keys
    keys = iam.list_access_keys(UserName=IAM_USER)["AccessKeyMetadata"]
    print(f"Existing keys: {[k['AccessKeyId'] for k in keys]}")

    # Issue a new access key
    new_key = iam.create_access_key(UserName=IAM_USER)["AccessKey"]
    print(f"Created new access key: {new_key['AccessKeyId']}")

    # Update Secrets Manager values
    new_secret_string = json.dumps({
        "access_key": new_key["AccessKeyId"],
        "secret_key": new_key["SecretAccessKey"]
    })

    secrets.put_secret_value(
        SecretId=SECRET_NAME,
        SecretString=new_secret_string
    )
    print("Updated Secrets Manager with new credentials")

    # Delete the old access key (all but the new one)
    for key in keys:
        if key["AccessKeyId"] != new_key["AccessKeyId"]:
            iam.delete_access_key(
                UserName=IAM_USER,
                AccessKeyId=key["AccessKeyId"]
            )
            print(f"Deleted old access key: {key['AccessKeyId']}")

    return {"status": "success"}

上記で、IAMアクセスキーをローテーションし、Secrets Managerに更新後のデータが格納される仕組みとなってます。
あとは、EC2側の認証ファイルをCron+Scriptでちょちょっと書き換えるようにすればOKです。

最後に

はい、まあこんな感じで、IAMユーザの認証情報をOS内に埋め込む運用において、セキュリティを意識した構成で実装しましたよ。という内容の記事でした。
この記事がどなたかの参考になれば幸いです。

Discussion