AWSのIAMユーザー管理簿を毎週自動でメール配信する

## はじめに
どこの企業でもセキュリティ対策のためのガイドラインやチェックリストなどがあると思うが、例えばこんな要件があるとする。

> アカウントやユーザーの発行/変更/削除の記録を管理簿にて管理すること。

AWSではロールやグループを割り当てたIAMユーザーが発行される。
監査やコンプライアンスの観点からIAMユーザー管理の証跡を残すことは重要だ。[^1]

今回はこのIAMユーザーの管理簿作成を自動化してみる。

[^1]: 「[AWS Config](https://aws.amazon.com/jp/config/)で一発だろ」というツッコミはごもっともだが、非エンジニアが各種チェック作業を行う場合を考えると、ローカルファイルとして置いておくのが一番楽だと思われる。誰でも扱いやすい形式で情報を扱うことが、社内全体幸福最大化につながる。


### ざっくり言うと
- IAM認証情報レポートを毎週一回メールで自動配信するようにした[^attach]
    - CloudWatch/Lambda/SES/S3を組み合わせた
    - 社内監査やコンプライアンスチェックのためにファイルを残すことが可能
    - 具体的には下記のようなメールが送られてくる

<img width="1140" alt="スクリーンショット 2019-08-20 11.41.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/757520db-5670-dcc0-1e50-9fc65cfce697.png">

[^attach]: 応用すればコストレポートやS3バケット内のファイルの自動配信なども可能。

### 対象とする人
- 堅めの企業でAWSのセキュリティを見ている人
- 特に社内のコンプライアンスチェックや監査目的で**AWSのIAMユーザーのリスト**を作成する必要がある人

### やること
- SESでメールアドレスを認証
- Lambda関数実行用のIAMロールの作成
- Lambda関数作成

### やらないこと
- 各種アラートやチェックの自動化
    - アクセスキーやパスワードが全然ローテーションされていないぞ！などの警告は他のサービスで実現可能なので、ここではやらない。
- Slackなどのチャットサービスへの通知
    - 会社でSlack使えない場合があるため

※ここで並べたものは「やらなくていい」ではなくて「ほんとはやった方がいい」ものである。

### IAM認証情報レポート
[AWS アカウントの認証情報レポートの取得 - AWS Identity and Access Management](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_getting-report.html#getting-credential-reports-api)

IAM認証情報レポートとはアカウントのすべてのユーザーと、ユーザーの各種認証情報 (パスワード、アクセスキー、MFA デバイスなど) のステータスがまとめられたものである。


ファイル自体はcsvファイルとして生成される。csvファイルには下記の情報が含まれる。[^3]

[^3]: アクセスキーと証明書が複数ある場合はその分だけ列が増える。


列名|説明
---|---
ユーザー|ユーザ名
arn|ユーザーの Amazon リソースネーム（ARN）
user_creation_time|ユーザーが作成された日時 (ISO 8601 日付/時刻形式)
password_enabled|ユーザーがパスワードを持っているかどうか
password_last_used|AWS アカウントのルートユーザー または IAM ユーザーのパスワードを使用して最後に AWS ウェブサイトにサインインした日時
password_last_changed|ユーザーのパスワードが最後に設定された日時
password_next_rotation|新しいパスワードを設定するようユーザーに求める日時
mfa_active|ユーザーに対して多要素認証 (MFA) デバイスが有効かどうか
access_key_1_active|ユーザーがアクセスキーがACTIVEかどうか
access_key_1_last_rotated|ユーザーのアクセスキーが作成または最後に変更された日時
access_key_1_last_used_date|AWS API リクエストの署名にユーザーのアクセスキーが直近に使用されたときの日付と時刻
access_key_1_last_used_region|アクセスキーが直近に使用された AWS リージョン
access_key_1_last_used_service|アクセスキーを使用して最も最近アクセスされた AWS サービス
cert_1_active|ユーザーのX.509 署名証明書がACTIVEかどうか
cert_1_last_rotated|ユーザーの署名証明書が作成または最後に変更された日時


IAM認証情報レポートはマネジメントコンソールの下記の画面からダウンロード可能である。
![Screenshot_2019-08-15 IAM Management Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/989f9e13-a938-7321-2fb4-588404301e94.png)

ただし、これを定期的に行うのは人間のやる作業ではないため自動化する。

## SESでメールアドレスを認証する

単純なメール通知ならSNSで十分だが、今回はcsvファイルを添付したいのでSESを利用して送信用＆受信用のメールアドレスを登録する必要がある。

詳細な手順は省略するが

- 左上の`Verify a New Email Address`ボタンからメールアドレスを登録
- 受信したメールに記載されたリンクへアクセスする

これでメールアドレスの認証は完了する。Statusが[verified]になればOK。

![Screenshot_2019-08-15 SES Management Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/f708aaf8-3dab-dfe9-302b-cca41988274f.png)

SESは対応しているリージョンが少ないので注意が必要である。2019年時点では下記の3リージョンのみ利用可能。

- us-east-1(バージニア北部)
- us-west-2（オレゴン）
- eu-west-1（アイルランド）

## IAMロールを作成する

Lambda関数に与えるIAMロールを事前に用意しておく。
IAMのコンソール画面の「このロールを使用するサービス」からLambda関数を選択。

![Screenshot_2019-08-15 IAM Management Console(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/cdc67d1a-72c1-a954-cee4-bd879ab16666.png)



今回追加で必要な権限はこの３つ。

- AmazonSESFullAccess
- AmazonS3FullAccess
- IAMReadOnlyAccess


![Screenshot_2019-08-15 IAM Management Console(3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/520f6edf-67ac-04ca-4a1c-228bade011b9.png)

信頼されたエンティティとしてLambdaが選択されているのを確認したら`ロールの作成`を行う。

## Lambda関数を設定

とりあえず関数名とランタイムを指定して`関数の作成`をクリック。ロールはあとで指定する。

<img width="1437" alt="スクリーンショット 2019-08-20 11.20.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/6c382fc0-2cf6-4f2a-217d-6252aafdc5f6.png">

ここでは下記の手順でLambda関数を作成する。

- ①Lambda関数コードを入力
- ②環境変数の入力
- ③実行ロールの設定
- ④CloudWatchEventの設定

### ① Lambda関数コードを入力
下記のコードをインラインエディタに貼り付ける。


```Python:send_iam_report
import boto3
import re
import time
import datetime
import os

from botocore.exceptions import ClientError
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication


AWS_S3_BUCKET_NAME = os.environ["AWS_S3_BUCKET_NAME"]
ACCOUNT_ID = os.environ["ACCOUNT_ID"]
SENDER = os.environ["SENDER"]
RECIPIENT = os.environ["RECIPIENT"]

AWS_REGION = "us-east-1"

SUBJECT = "IAMユーザーWeeklyReport"

# The character encoding for the email.
CHARSET = "utf-8"
# The email body for recipients with non-HTML email clients.
BODY_TEXT = "IAMユーザー管理リスト\n作成日時：{}".format(datetime.datetime.today())
# The HTML body of the email.
BODY_HTML = """\
<html>
<head></head>
<body>
<p>今週のIAMユーザー管理リスト</p>
<p>アカウントID：{}</p>
<p>作成日時：{}</p>
<p>ファイル格納先：{}</p>
</body>
</html>
""".format(ACCOUNT_ID, datetime.datetime.today(), AWS_S3_BUCKET_NAME)


def lambda_handler(event, context):
    # 認証情報レポートを生成
    iam = boto3.client('iam')
    response = iam.generate_credential_report()
    print(response)
    # レポート作成が完了するまで待機
    while(response['State'] != 'COMPLETE'):
        time.sleep(1)
        continue
    result = iam.get_credential_report()
    print(result)

    # 取得日時からファイル名を生成
    pattern = r"[0-9]{4}-[0-9]{2}-[0-9]{2}"
    date = re.search(pattern, str(result['GeneratedTime'])).group(0)
    fname = "{}_{}_iam_credential_report.csv".format(date.replace("-", ""), ACCOUNT_ID)
    ATTACHMENT = "/tmp/"+fname

    # S3へ保存
    s3 = boto3.resource('s3')
    bucket = s3.Bucket(AWS_S3_BUCKET_NAME)
    bucket.put_object(Key='iam/'+fname, Body=result["Content"])

    # tmpへ一時保存
    with open('/tmp/'+fname, "wb") as f:
        f.write(result["Content"])

    # メール配信
    # Create a new SES resource and specify a region.
    ses = boto3.client('ses', region_name=AWS_REGION)

    # Create a multipart/mixed parent container.
    msg = MIMEMultipart('mixed')

    # Add subject, from and to lines.
    msg['Subject'] = SUBJECT
    msg['From'] = SENDER
    msg['To'] = RECIPIENT

    # Create a multipart/alternative child container.
    msg_body = MIMEMultipart('alternative')

    # Encode the text and HTML content and set the character encoding. This step is
    # necessary if you're sending a message with characters outside the ASCII range.
    textpart = MIMEText(BODY_TEXT.encode(CHARSET), 'plain', CHARSET)
    htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET)

    # Add the text and HTML parts to the child container.
    msg_body.attach(textpart)
    msg_body.attach(htmlpart)

    # Define the attachment part and encode it using MIMEApplication.
    att = MIMEApplication(result["Content"])

    # Add a header to tell the email client to treat this part as an attachment,
    # and to give the attachment a name.
    att.add_header('Content-Disposition', 'attachment', filename=os.path.basename(ATTACHMENT))

    # Attach the multipart/alternative child container to the multipart/mixed
    # parent container.
    msg.attach(msg_body)

    # Add the attachment to the parent container.
    msg.attach(att)

    try:
        # Provide the contents of the email.
        response = ses.send_raw_email(
            Source=SENDER,
            Destinations=[
                RECIPIENT
            ],
            RawMessage={
                'Data': msg.as_string(),
            },
            # ConfigurationSetName=CONFIGURATION_SET
        )
    # Display an error if something goes wrong.
    except ClientError as e:
        print(e.response)
        print(e.response['Error']['Message'])
    else:
        print("Email sent! Message ID:"),
    return (response['ResponseMetadata']['RequestId'])
```

やっていることは大雑把に言うと下記の通り。

- iam_credential_reportの生成、取得
- 取得したファイルをS3へ保存
- SESによりファイルを添付したメールを送信

### ② 環境変数を入力
この関数ではメールアドレスやアカウントIDなどは環境変数で管理している。Lambda関数内に直接書き込んでも動作するが、コードを直接変更するのはトラブルの元なので環境変数として外部に切り出す。
こうしておけば別のアカウントに移植する際にいちいちコードを書き換える必要がない。CloudFormationでLambdaをデプロイする場合などはコードをzipファイルにまとめる際などは非常に便利。

環境変数の設定は下記のようにコンソール上から行うことができる。[^2]

[^2]: CLIなどで登録することも可能。

![Screenshot_2019-08-15 Lambda Management Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/5baa91a5-b221-4b5e-7896-77f3a169aeba.png)

### ③ 実行ロールの設定
`既存のロールを使用する`を選択し、先ほど作成したロールを割り当てる。


### ④CloudWatchEventの設定

毎週一回レポートを作成する場合はLambda関数の`トリガーを追加`をクリックして下記の通り設定すればOK。cronによるスケジュール設定はUTCで行う必要があるので注意。

![Screenshot_2019-08-15 CloudWatch Management Console(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319677/ba7d3b92-5939-0aa3-708d-a21c8bd0a84d.png)

参考にそのほかセキュリティ周りの作業を自動化する場合に使いそうな設定パターンをまとめておく。

時間帯|Cron式|利用例|ルール名
---|---|---|---
毎日0:00(JST)|`0 15 * * ? *`|S3のデータ整形、ログ収集など|daily_task
毎週月曜日の0:00（JST） | `0 15 ? * SUN *`|1週間のセキュリティレポート作成など|weekly_task
毎月1日の0:00（JST）  | `0 15 1 * ? *`|月間のコスト請求レポート作成など|monthly_task
平日の夜22:00(JST)|`0 13 ? * MON-FRI *`|インスタンスの設定ミス、落とし忘れなどのチェック|closing_task


その他ポイントとしては

- 必要に応じてメールの件名や文面を編集する
- `AWS_REGION`はSESを設定した時のリージョンを指定する
- `tmp`ディレクトリに一時的に添付ファイルを保存→メール送信
- IAMユーザーが多いと処理に時間がかかるので時間は適当に設定する

など。

## おわりに

とりあえずこれ動かしておけば毎週必ずレポートが送られてくる。
「アカウントの管理しっかりやってるよね？」と言われたときにすぐに出せるようにしておくと安心。

## 参考
[Lambda・SESを使って、添付ファイル付きメールを送信する – サーバーワークスエンジニアブログ](http://blog.serverworks.co.jp/tech/2018/12/16/ses_lambda_attachment/)
[E メールの高度なパーソナライズ - Amazon Simple Email Service](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/send-personalized-email-advanced.html)
[AWS SDKs を使用して raw E メールを送信する - Amazon Simple Email Service](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/examples-send-raw-using-sdk.html)