AWS アカウント管理 API のセキュアな運用方法を考える

:::note info
本記事で触れるのは AWS アカウントに関連する API 操作であり、マルチアカウントの運用手法そのものには触れません。
:::

## はじめに
AWS Account Management (AWS アカウント管理) の API を使用すると、AWS アカウントの属性情報を変更できます。具体的には以下のような AWS アカウントの管理タスクを実行することができます。

* 主要連絡先の確認、更新
* 代替連絡先 (請求/オペレーション/セキュリティ) の確認、更新
* リージョンのオプトイン/オプトアウト、設定状況の確認
* プライマリ (ルートアカウント) メールアドレスの確認、更新

https://docs.aws.amazon.com/accounts/latest/reference/API_Operations.html

ルートアカウントのメールアドレスに関連する操作は 2024 年 6 月のアップデートでサポートされた機能です。

https://aws.amazon.com/jp/about-aws/whats-new/2024/06/manage-member-account-root-email-addresses-aws-organization/

これらの API は AWS Organizations で管理するメンバーアカウントの情報を一元管理できるため、非常に便利です。一方でこれまでルートアカウントでのみ可能だった操作が管理アカウントから操作できてしまうことに対して、何となく不安を覚える方もいらっしゃるかもしれません。

本記事では、これらの操作を API で行うことのメリットやできるだけ安全に運用するための方法について考察します。

## API で操作できることのメリット

### IAM や SCPs による細やかなアクセス制御
IAM ポリシーに設定可能なアクション、リソース、条件キーの一覧は以下に記載されています。これらを組み合わせることで細やかなアクセス制御が可能です。

https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsaccountmanagement.html

例えば条件キー `account:EmailTargetDomain` を使用すると、ルートアカウントのメールアドレス変更時、条件に一致したドメインのメールアドレスへの変更のみを許可できます。これにより意図しないドメインへのメールアドレス変更を防ぐことができます。ルートアカウントで直接変更を行う場合はこのような制御を強制することができません。

```policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowWriteAccountEmail",
            "Effect": "Allow",
            "Action": [
                "account:AcceptPrimaryEmailUpdate",
                "account:StartPrimaryEmailUpdate"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "account:EmailTargetDomain": "example.com"
                }
            }
        }
    ]
}
```

### ルートアカウントにログインする必要がない
そもそもルートアカウントには極力ログインしたくありません。以下のようなサービスコントロールポリシー (SCPs) でルートアカウントの操作自体を禁止しているケースもあるかと思います。

```policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalARN": "arn:aws:iam::*:root"
        }
      }
    }
  ]
}
```

そのようなケースではルートアカウントの操作をするために一度 SCP を外したり、SCP の適用を除外している OU にアカウントを一時的に移動するなどの運用が考えられますが、そのような作業自体オペレーションミスなどのリスクを孕んでいます。

また誤って存在しないメールアドレスでアカウントを作成してしまった場合、パスワードリセットによるサインインとアドレス変更が実施できません。API 操作による変更であればこのケースでも対応できます。

### 大規模な変更
API 操作により作業を自動化/半自動化でき、大規模な変更管理が行いやすくなります。AWS アカウントの属性情報を変更するという機会は頻繁には発生しませんが、会社名変更やそれに伴うドメイン変更等で一括の対応が必要なケースもあります。

## 安全な運用を考える

### 委任管理者アカウントの設定
AWS Account Management は AWS Organizations との信頼されたアクセスを設定し、委任管理者アカウントを指定できます。

https://docs.aws.amazon.com/accounts/latest/reference/using-orgs.html

「管理アカウントはそのアカウントでしかできないタスクのみのために使用する」という [プラクティス](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_best-practices_mgmt-acct.html) に準拠するため、委任管理者アカウントを設定することをおすすめします。

委任管理者アカウントで作業することのデメリットとしては、コンソールでの操作はできず、CLI/SDK 経由での操作に限られることです。連絡先情報やルートアカウントのアドレス変更の UI は AWS Organization コンソール内に存在するため、管理アカウントでなければその UI にアクセスできないためです。

AWS Account Management の信頼されたアクセスが有効になっていない場合は、コンソールまたは CLI/SDK から有効化します。

https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-account.html

CLI のコマンドは以下です。

```shell-session
$ aws organizations enable-aws-service-access \ 
    --service-principal account.amazonaws.com
```

コンソールから実施する場合は AWS Organization のサービスから有効化できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/be0e56ce-5332-f179-12a9-f931cf61477e.png)

有効化する際に、「次のボックスに「有効化」と入力して、追加のセットアップタスクを実行せずに信頼されたアクセスを有効にすることを確認します (推奨されません)。」というダイアログが表示されますが、AWS Account Manamenet の場合は、専用の UI や API は用意されていないため AWS Organization から直接有効化して問題ありません。

:::note info
信頼されたアクセスの有効化と無効化の操作は AWS Organizations ではなく、連携する各サービスのコンソールまたは CLI/SDK から実行することが推奨されています。各サービス側で有効化することにより信頼されたアクセスの設定だけではなく、サービスで必要なリソース作成等の初期化が行われるためです。
:::


委任管理者アカウントの設定はコンソールから実施できないため、CLI/SDK 経由で実行します。

```shell-session
$ aws organizations register-delegated-administrator \
    --account-id 123456789012 \
    --service-principal account.amazonaws.com
```
https://docs.aws.amazon.com/accounts/latest/reference/using-orgs-delegated-admin.html


### IAM ポリシーの検討
#### AWS 管理ポリシーの使用
2024 年 9 月時点で以下の2つの AWS 管理ポリシーが用意されています

* [AWSAccountManagementReadOnlyAccess](https://docs.aws.amazon.com/ja_jp/aws-managed-policy/latest/reference/AWSAccountManagementReadOnlyAccess.html)
* [AWSAccountManagementFullAccess](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSAccountManagementFullAccess.html)

いずれもその名のとおり読み取り専用アクセスとフルアクセスを提供するポリシーなので、細やかな制御を行いたい場合はユーザー管理のカスタムポリシーが適しています。

#### カスタムポリシーの使用
以下の例では条件キー `account:AccountResourceOrgPaths` により、特定の OU に含まれる AWS アカウントの主要/代替連絡先の取得および更新、ルートアカウントメールアドレスの更新を許可しています。また `account:EmailTargetDomain` により `example.com` ドメインへのメールアドレス変更を許可しています。


```policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAWriteAccountContactInformation",
            "Effect": "Allow",
            "Action": [
                "account:DeleteAlternateContact",
                "account:GetAlternateContact",
                "account:GetContactInformation",
                "account:GetPrimaryEmail",
                "account:PutAlternateContact",
                "account:PutContactInformation",
            ],
            "Resource": "*",
            "Condition": {
                "ForAnyValue:StringLike": {
                    "account:AccountResourceOrgPaths": [
                        "o-xxxxxxxxxx/r-xxxx/ou-xxxx-xxxxxxxx/*"
                    ]
                }
            }
        },
        {
            "Sid": "AllowWriteAccountEmail",
            "Effect": "Allow",
            "Action": [
                "account:AcceptPrimaryEmailUpdate",
                "account:StartPrimaryEmailUpdate"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "account:EmailTargetDomain": "example.com"
                },
                "ForAnyValue:StringLike": {
                    "account:AccountResourceOrgPaths": [
                        "o-xxxxxxxxxx/r-xxxx/ou-xxxx-xxxxxxxx/*"           
                    ]
                }
            }
        }
    ]
}
```

許可されていない OU の情報を取得しようとしたり、 `example.com` に一致しないドメインのメールアドレスに変更したりしようとすると以下のようなエラーとなります。(見やすさ優先でログは改行しています)

```shell-session
$ aws account get-primary-email --account-id 1111111111111

An error occurred (AccessDeniedException) when calling the StartPrimaryEmailUpdate operation: 
User: arn:aws:sts::{ManagementAccountId}:assumed-role/AWSReservedSSO_UpdateAccountInfo_xxxx/user@example.com
is not authorized to perform: account:GetPrimaryEmail on resource: 
arn:aws:account::{ManagementAccountId}:account/o-{OrganizationId}/1111111111111
```

OU ではなく、AWS アカウント ID で明示的に制限したい場合は Resources 句で組織内のメンバーアカウント ID を羅列することもできます。

```json
{
    "Resource": [
        "arn:aws:account::{ManagementAccountId}:account/o-{OrganizationId}/{AccountId}",
    ],
}
```


### 2つの操作モード
AWS Account Management の API はスタンドアロンと Organizations の 2 種類の操作モードが存在します。権限管理を行う際は、これらの違いについても意識しておく必要があります。

https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-api-modes-of-operation.html

* **Standalone context**
    * 自アカウントの属性情報へアクセスまたは変更する際に使用されます
    * リクエストに AccountId パラメータを含めない場合、このモードが適用されます
* **Organizations context** 
    * 組織内の異なるアカウントの属性情報へアクセスまたは変更する際に使用されます
    * リクエストに AccountId パラメータを含めるとこのモードが適用されます
    * 操作は管理アカウントまたは委任管理者アカウントからのみ実行できます

一例として以下のルートアカウントのメールアドレスに関連する操作は Organizations コンテキストでのみ動作しますが、それ以外の操作はスタンドアロンコンテキストでも動作します。

* `GetPrimaryEmail`
* `StartPrimaryEmailUpdate`
* `AcceptPrimaryEmailUpdate`

```shell
# リクエストにアカウント ID (`--acount-id`)を含めない場合は自アカウントの属性にアクセスする 
# レスポンスはダミーデータです
$ aws account get-contact-information
{
    "ContactInformation": {
        "AddressLine1": "Kanda 3-5-2",
        "AddressLine2": "Fictional Building 8F",
        "City": "Chiyoda-ku",
        "CompanyName": "Example Corp.",
        "CountryCode": "JP",
        "FullName": "Taro Yamada",
        "PhoneNumber": "+8130-1234-5678",
        "PostalCode": "101-0047",
        "StateOrRegion": "Tokyo"
    }
}
        
# ルートアカウントのメールアドレスに関連する操作は Standalone Context では実行できない
# AWS CLI の場合、メールアドレスに関連する操作は --account-id が必須の引数になっている
$ aws account get-primary-email

usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:

  aws help
  aws <command> help
  aws <command> <subcommand> help

aws: error: the following arguments are required: --account-id
```

また Resources 句で IAM ポリシーアクションの対象アカウントを絞りたい場合、Standalone と Organizetions では ARN の指定方法が異なります。

```shell
# Standalone context
arn:aws:account::{AccountId}:account

# Organizations context
arn:aws:account::{ManagementAccountId}:account/o-{OrganizationId}/{AccountId}
```


### モニタリング
AWS Account Management の API 操作は CloudTrail によるログ記録や EventBrdige によるモニタリングがサポートされています。

https://docs.aws.amazon.com/accounts/latest/reference/monitoring.html

EventBridge については、本記事記載時点ではリージョンを有効化および無効化するイベントおよび CloudTrailのAPI コールを介したイベントのみサポートされています。

> Only events that are specific to enabling and disabling Regions and API calls via CloudTrail are currently available for Account Management.

## 参考: その他 CLI での操作例
### ルートアカウントのメールアドレス変更
```shell
# メールアドレスの確認 (変更前)
$ aws account get-primary-email --account-id 111111111111
{
    "PrimaryEmail": "before@example.com"
}

# メールアドレスの変更プロセスの開始
$ aws account start-primary-email-update --account-id 111111111111 \
    --primary-email after@example.com
{
    "Status": "PENDING"
}

# メールアドレス変更プロセスの承認
# 変更後のメールアドレスに送信されたワンタイムパスワードを引数に指定する必要がある
$ aws account accept-primary-email-update --account-id 111111111111 \
  --primary-email after@example.com
  --otp 123456
{
    "Status": "ACCEPTED"
}

# メールアドレスの確認 (変更後)
$ aws account get-primary-email --account-id 111111111111
{
    "PrimaryEmail": "after@example.com"
}
```

以上です。
参考になれば幸いです。