Aurora MySQLでIAM認証する手順と検証メモ

Aurora MySQLでIAM認証を検証したので、備忘録を兼ねて諸々メモ。

### AuroraのIAM認証とは？
DBユーザーに、個別のパスワード入力の代わりにIAMクレデンシャルで認証する機能。
DBごと、ユーザーごとのパスワード管理が不要になる。
[スロットルのリスク](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html#UsingWithRDS.IAMDBAuth.Limitations)があるためアプリケーションの認証には不向きだが、運用に伴う人による認証については一考の価値がある。

### Aurora側での設定
- クラスターでIAM認証を有効化しておく。マネジメントコンソールでいうと、**インスタンス設定**（≠クラスター設定）の以下が該当設定。
![スクリーンショット 2021-07-20 午前0.51.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/ad3f0a32-c5d6-740a-c6ce-3fb12a043816.png)
- データベースで、IAM認証を有効にしたDBユーザーを作成する。IAM認証は、あくまでこのDBユーザーに対して、AWSプロファイルのIAMクレデンシャルでログインする行為であって、DBユーザーがないと機能しない点に注意。

```sql:DBユーザーの作成
CREATE USER ssotest IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';
```

### 踏み台インスタンスでの設定
- DB接続用のmysqlクライアントを事前にインストールしておく。
    - 実は微妙にハマリポイント。Amazon Linux 2で普通に`sudo yum install mysql`とすると、MariaDB互換クライアントがインストールされる（Amazon Linux 1だとMySQL純正クライアントが入る）のだが、**MariaDB互換クライアントを使う場合、[公式ドキュメントに記載のオプション](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.AWSCLI.html)ではログインできない**（後述の通り、別のオプションを使えば行けるが、そこに辿り着くまで大分かかった）。
    - MySQL純正クライアントをインストールしてもよいが、アークテクチャーを選んでローカルインストールなど若干面倒くさい手順が必要になるので、ここでは一行で済むMariaDB互換をインストールする。
- IAM認証時は、TLS接続設定に関わらず[AuroraのCA証明書が必要](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.SSL.html)になるので、ホームディレクトリあたりにwgetしておく。
    - 以前は`rds-ca-2019-root.pem`だったが、リージョンごとの差異を吸収する`global-bundle.pem`に変わったらしい。

> （2023/4/21追記）　以前は5年ごとにCA証明書の更新が必要だったが、[2023年のアップデート](https://aws.amazon.com/jp/about-aws/whats-new/2023/01/amazon-rds-new-ssl-tls-certificates-certificate-controls/)で、長期間利用可能な認証局がサポートされた。現時点では、Aurora/MySQLでは従来の5年もの（rds-ca-2019）の他、40年もの（rds-ca-rsa2048-g1）が利用可能。詳細は[こちら](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.SSL.html#:~:text=%E4%BD%BF%E7%94%A8%E5%8F%AF%E8%83%BD%E3%81%A7%E3%81%99%E3%80%82-,%E8%AA%8D%E8%A8%BC%E5%B1%80,-%E8%AA%8D%E8%A8%BC%E5%B1%80%20(CA))を参照。

```bash:CA証明書の保存
% wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
```

### 権限付与の方針決定
- DBへのアクセスは一般に、パブリックアクセスは勿論、任意の端末からの接続を禁止するために、VPC内の特定のEC2インスタンス（踏み台）からのアクセスにセキュリティグループで縛ることが多いと思う。そうでないパターンも当然あり得るが、ここでは一応、踏み台パターンを前提とする。
- IAM認証には接続トークンの生成が必要になるが、踏み台からのIAM認証パターンは以下の二種類が考えられる。違いは、**「誰の権限で・どこでトークンを生成するか」**の観点。
    - 踏み台のインスタンスプロファイル（ロール）にIAM認証を許可するポリシーを付与。接続トークンは踏み台内で生成して環境変数に格納。
    - 踏み台には権限を付与せず、それ以外のIAMロールにIAM認証を許可するポリシーを付与。接続トークンは別端末で生成してコピペ。
- 具体的には、以下のようなポリシーをどこのロールに割り当てるかの話になる。

```json:IAM認証ポリシー例
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rds-db:connect"
            ],
            "Resource": [
                "arn:aws:rds-db:ap-northeast-1:123456789012:dbuser:test-cluster/ssotest"
            ],
            "Condition": {
                "ForAnyValue:StringLike": {
                    "aws:PrincipalOrgPaths": "o-oXXXXXXXXX/r-YYYY/ou-ZZZZZZZZZZZZZ/*"
                }
            }
        }
    ]
}
```
- プロコンはざっくり以下のようなイメージ。今回は踏み台の多系統化を避けて「別のIAMロール案」とする。具体的にはAWS SSOを使い、アクセス権限セットに`rds-db:connect`を付与する。
 
| | インスタンスプロファイル案 | 別のIAMロールに付与する案 |
| :--- | :--- | :--- |
| 利点 | 認証操作がシンプル | 踏み台は一系統でよい | 
| 欠点 | 権限ごとに踏み台を分岐する必要がある | トークンを持ち込む必要がありひと手間かかる |

### 接続

- まずは認証トークン取得。前述の方針に従い、今回はAWS SSOでログイン後、踏み台の外でトークンを取得する（踏み台でトークン生成コマンドを叩くと、踏み台のインスタンスプロファイルの権限内で生成され、今回だとどこにも接続できないことになるため）。

```bash:認証トークン取得
% aws sso login --profile ssotest-profile
% aws rds generate-db-auth-token \
> --hostname hoge \
> --region ap-northeast-1 \
> --port 3306 \
> --username ssotest \
> --profile ssotest-profile
```

- トークンが生成されたら、クリップボードにコピーする。本来は環境変数に格納するのが楽なのだが、今回は踏み台に持ち込む必要上、コピペで対応する。
- 踏み台にSSM Sessions Manager等でログイン後、DB接続を実行する。ここではMariaDB互換クライアントのパターンを使用するが、参考までにMySQL純正クライアントのパターンも併記しておく。

```bash:DB接続（MariaDB互換クライアントのパターン）
% TOKEN='（先程取得したトークンの文字列をそのままコピペ）'
% mysql -h <Auroraエンドポイント>.ap-northeast-1.rds.amazonaws.com \
> --port=3306 \
> --user=ssotest \
> --password=$TOKEN \
> --ssl-ca=~/global-bundle.pem \
> --enable-cleartext-plugin
```

> （2023/4/21追記） Amazon Linux 2023がリリースされたこともあり、久しぶりに試したところ、MariaDB互換クライアントでのオプションが `--default-auth=mysql_clear_password` から `--enable-cleartext-plugin` （MySQL互換クライアントと同じ）に変わっていた。 


```bash:DB接続（MySQL互換クライアントのパターン）
% mysql -h <Auroraエンドポイント>.ap-northeast-1.rds.amazonaws.com \
> --port=3306 \
> --user=ssotest \
> --password=$TOKEN \
> --ssl-ca=~/global-bundle.pem \
> --enable-cleartext-plugin
```

無事繋がったら完了。
なかなかMySQLのプロンプトが出ないな？という場合は、セキュリティグループを見直してみることをお薦めする。ちゃんと3306が許可されてるかとか、Aurora側でEC2側セキュリティグループをIngressに含めているか、など。
Access Deniedの際は以下のようなエラーが即時返ってくるので、そうでない場合はまずネットワークを疑う。

```bash:AccessDeniedな場合の接続エラー
ERROR 1045 (28000): Access denied for user 'ssotest'@'XXX.XXX.XXX.XXX' (using password: YES)
```

### 小ネタ
- トークンは15分だけ有効。
    - 一度生成したらいつでもどこでも再利用できるわけではなく、一定期間で切れるようになっている。
- 別アカウントで取得したトークンは使えない。
    - IAMポリシーがどうあれ、アカウントをまたいでトークンを再利用することはできない。例えば、開発アカウントでたまたま`rds-db:connect`権限を持っていてトークンを生成できたとしても、それを使って本番アカウントのDBにログインすることができないようになっている。
- 「インスタンスプロファイル案」を選択して踏み台からトークンを生成する場合、当たり前だが、RDSのAPIエンドポイントへの経路が必要になる。
    - 踏み台のEgressをガチガチに縛っている場合は、VPCエンドポイントが必要になるかも。
- AdministratorAccessなど、`iam:*`を保持している権限だと、ポリシー内容に拘わらず全部接続できてしまう。
    - 検証する際は注意しておかないと、想定した挙動にならなくて時間を無駄にする。
- AWS SSOでも使える。
    - 今は改修されているが、以前は公式ドキュメントに「DBユーザー名はロール名と同じにせよ」という誤記があり、ロール名にアカウントごとのランダム文字列が付記されるAWS SSOでは運用上成立しないように見えていたが、原文は"Make sure the specified database user name is the same as a resource in the IAM policy for IAM database access."なので単なる誤訳だったらしい。
- Resourceの部分はワイルドカードを使える。つまり、

```json:Resource句（個別指定）
            "Resource": [
                "arn:aws:rds-db:ap-northeast-1:123456789012:dbuser:test-cluster/ssotest"
            ],
```

は、要件に応じて、以下のようにも書き変えることができる。

```json:Resource句（ワイルドカード）
            "Resource": [
                "arn:aws:rds-db:*:*:dbuser:*/ssotest"
            ],
```

ただし、このままだといささか範囲が広すぎるので、せめてOrganizationsで縛るくらいはしておくこと。要件によっては、対象アカウントを本番とそれ以外に分けたり、対象クラスターを分けたりとかもあるかも知れない。

```json:Condition句（OU縛り）
            "Condition": {
                "ForAnyValue:StringLike": {
                    "aws:PrincipalOrgPaths": "o-oXXXXXXXXX/r-YYYY/ou-ZZZZZZZZZZZZZ/*"
                }
            }

```