ジャンプアカウント環境でIP制限、Permission Boundry付与できるようにしてみた

# はじめに
皆さん、こんにちは。ジャンプアカウント記事第３弾です。

今回はジャンプアカウント環境でIP制限やPermissionBoundary付与できるようにしましたので、その方法をご紹介します。

今回の改修内容は、スイッチロール先のIAMロールに対して、IP制限およびPermission Boundaryを付与することです。この改修により、セキュリティを強化し、アクセス権限の制御をより厳格に管理できるようにします。

以前の記事は以下２件です、合わせてお読みください！

https://qiita.com/pohd_ccoe/items/a3b135717e2d09bacd14

https://qiita.com/pohd_ccoe/items/49694a0f2429714f587e

# 構成
スイッチロール先にIP制限を付与します。そのため、指定IP以外の場合にスイッチロール時にエラーとなり、スイッチできなくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/08e2bb04-50eb-6478-8b48-9bb54ca724c2.png)


また、Permission Boundaryもスイッチロール先に付与します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/08fc37a7-7844-5ee3-7c9f-d78788003651.png)


このようにスイッチ先ロールにカスタム設定を付与することでジャンプアカウントのIAMユーザーに一律の制限を付与するのではなく、特定アカウント環境に対して制限を付与することが可能になります。

## 環境
使用している環境は以下となります。
```
$ node -v
v18.19.0

$ cdk --version
2.125.0 (build 5e3c3c6)
```

## ファイル構成
今回の改修点は以下になります。
- iam.ts
スイッチ用ロールでIP制限やPermission Boundaryを付与できるように改修
- cmn-jump-stack.ts
テンプレート作成時に指定するIPやPermission Boundary名を渡すように改修

```
CMN-JUMP
├── bin
│ └── cmn-jump.ts 　　　　　　　　　　　 # エントリーポイント
├── lib
│ ├── constants
│ │ └── vendor.ts 　　　　　　　　　　　 # パートナー毎のIAM設定
│ ├── constructs
│ │ ├── iam.ts 　　　　　　  　　　　　　# JUMPアカウントのIAM定義
│ │ └── service-managed-stacksets.ts 　# サービスマネージド型Stacksets定義
│ │ └── self-managed-stacksets.ts      # セルフマネージド型Stacksets定義
│ ├── templates
│ │ └── iam.ts 　　　　　  　　　　　　　# SwitchRole先のIAM設定（改修）
│ ├── types
│ │ ├── account.ts 　　　　　　　　　　　# AWSアカウント設定
│ │ └── common.ts 　　　　　　　　　　 　# 共通定数
│ └── cmn-jump-stack.ts 　　　　　　    # JUMPアカウントのCDKスタック定義（改修）
└── README.md
```

## リソース
### iam.ts
IP制限をしたい時は許可するIPアドレスを、Permission Boundaryを付与したい時は付与するポリシー名を、呼び出し元である`cmn-jump-stack`からもらい、設定するようにします。
IP制限は各ロール毎に設定できます。
```typescript:iam.ts
import { Aws, aws_iam as iam, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AuthType, DirectAccountId, ResellerAccountId } from '../types/common';

interface IamStackProps extends StackProps {
  vendorName: string;
  assumeRoleSourceAccountId: DirectAccountId | ResellerAccountId;
  permissionsBoundaryName?: string;
  adminIps?: string[];
  powerIps?: string[];
  readIps?: string[];
  powerIamIps?: string[];
}

export class IamStack extends Stack {
  private assumeRoleSourceAccountId: DirectAccountId | ResellerAccountId;
  private permissionsBoundaryName: string | undefined;

  constructor(scope: Construct, id: string, props: IamStackProps) {
    super(scope, id, props);

    this.assumeRoleSourceAccountId = props.assumeRoleSourceAccountId;
    this.permissionsBoundaryName = props.permissionsBoundaryName;

    // AuthType: admin
    const adminRole = new iam.Role(this, 'AdministratorAccessRole', {
      roleName: `${props.vendorName}-${AuthType.Admin}-role`,
      assumedBy: new iam.AccountPrincipal(this.assumeRoleSourceAccountId),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'),
      ],
      permissionsBoundary: this.setPermissionsBoundary('Admin'),
    });
    this.applyIpRestrictions(adminRole, props.adminIps);

    // AuthType: power
    const powerRole = new iam.Role(this, 'PowerUserAccessRole', {
      roleName: `${props.vendorName}-${AuthType.Power}-role`,
      assumedBy: new iam.AccountPrincipal(this.assumeRoleSourceAccountId),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'),
      ],
      permissionsBoundary: this.setPermissionsBoundary('Power'),
    });
    this.applyIpRestrictions(powerRole, props.powerIps);

    // AuthType: read
    const readRole = new iam.Role(this, 'ReadOnlyAccessRole', {
      roleName: `${props.vendorName}-${AuthType.Read}-role`,
      assumedBy: new iam.AccountPrincipal(this.assumeRoleSourceAccountId),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCloudShellFullAccess'),
      ],
      permissionsBoundary: this.setPermissionsBoundary('Read'),
    });
    this.applyIpRestrictions(readRole, props.readIps);

    // AuthType: power-iam
    const powerIamRole = new iam.Role(this, 'PowerUserAndIamFullAccessRole', {
      roleName: `${props.vendorName}-${AuthType.PowerAndIam}-role`,
      assumedBy: new iam.AccountPrincipal(this.assumeRoleSourceAccountId),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('IAMFullAccess'),
      ],
      permissionsBoundary: this.setPermissionsBoundary('PowerIam'),
    });
    this.applyIpRestrictions(powerIamRole, props.powerIamIps);
  }

  protected setPermissionsBoundary(
    roleName: string
  ): iam.IManagedPolicy | undefined {
    if (this.permissionsBoundaryName) {
      return iam.ManagedPolicy.fromManagedPolicyArn(
        this,
        `PermissionsBoundaryPolicy${roleName}`,
        `arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:policy/${this.permissionsBoundaryName}`
      );
    } else {
      return undefined;
    }
  }

  protected applyIpRestrictions(role: iam.Role, ips?: string[]): void {
    if (ips) {
      role.assumeRolePolicy?.addStatements(
        new iam.PolicyStatement({
          effect: iam.Effect.DENY,
          actions: ['sts:AssumeRole'],
          principals: [
            new iam.AccountPrincipal(this.assumeRoleSourceAccountId),
          ],
          conditions: {
            NotIpAddress: {
              'aws:SourceIp': ips,
            },
          },
        })
      );
    }
  }
}
```

### cmn-jump-stack.ts
テンプレート作成するiam.tsにIPアドレスリスト、Permission Boundary名を渡すようにします。
IPアドレスリスト、Permission Boundary名は`vendor.ts`で定義しています。
```typescript: cmn-jump-stack.ts
import { DefaultStackSynthesizer, Stack, StackProps, Stage } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Iam } from './constructs/iam';
import { SelfManagedStackSets } from './constructs/self-managed-stacksets';
import { ServiceManagedStackSets } from './constructs/service-managed-stacksets';
import { IamStack } from './templates/iam';
import { accounts } from './types/accounts';
import { AccountType, DirectAccountId, Vendor } from './types/common';

interface CmnJumpStackProps extends StackProps {
  vendor: Vendor;
}

export class CmnJumpStack extends Stack {
  constructor(scope: Construct, id: string, props: CmnJumpStackProps) {
    super(scope, id, props);

    const regions = ['ap-northeast-1'];

    props.vendor.stackSetsInfos.forEach((stackSetsInfo, i) => {
      const account = accounts.find((acc) => acc.id === stackSetsInfo.account)!;
      const stage = new Stage(
        this,
        `Stage-${props.vendor.name}-${account.alias}`
      );

      new IamStack(stage, 'IamStack', {
        synthesizer: new DefaultStackSynthesizer({
          generateBootstrapVersionRule: false,
        }),
        vendorName: props.vendor.name,
        assumeRoleSourceAccountId: DirectAccountId.JUMP,
        permissionsBoundaryName: stackSetsInfo.permissionsBoundaryName,
        adminIps: stackSetsInfo.adminIps,
        powerIps: stackSetsInfo.powerIps,
        readIps: stackSetsInfo.readIps,
        powerIamIps: stackSetsInfo.powerIamIps,
      });

      const templateBody = JSON.stringify(stage.synth().stacks[0].template);

      if (account.type === AccountType.DIRECT) {
        new ServiceManagedStackSets(
          this,
          `ServiceManagedStackSets${account.id}`,
          {
            stackSetsInfo: stackSetsInfo,
            accountInfo: account,
            regions: regions,
            templateBody: templateBody,
            vendorName: props.vendor.name,
          }
        );
      } else if (account.type === AccountType.RESELLER) {
        new SelfManagedStackSets(this, `SelfManagedStackSets${account.id}`, {
          stackSetsInfo: stackSetsInfo,
          accountInfo: account,
          regions: regions,
          templateBody: templateBody,
          vendorName: props.vendor.name,
        });
      }
    });

    new Iam(this, 'Iam', {
      vendorName: props.vendor.name,
      groupInfos: props.vendor.groupInfos,
    });
  }
}

```

### vendor.ts
ベンダー毎にスイッチ先でどんな権限を使用するかを設定しています。そこに新しくIP制限やPermission Boundaryの設定を追加しました。
デフォルトは制限なし、設定を追記したもののみ反映されます。

```typescript:例）vendor.ts
// read権限にIP制限をかける、かつPermission Boundaryを付与する
vendors.push({
  name: 'test',
  stackSetsInfos: [
    {
      account: DirectAccountId.AAA,
      readIps: ['192.168.1.1/24'],
      permissionsBoundaryName: 'testboundrypolicy',
    },
  ],
  groupInfos: [
    {
      users: ['test@example.com'],
      accountAuthMaps: [
        {
          account: DirectAccountId.AAA,
          authTypes: [AuthType.Read],
        },
      ],
    },
  ],
});
```

# デプロイ
以下のテストデータでデプロイしてみました。
```typescript:vendor.ts
vendors.push({
  name: 'test',
  stackSetsInfos: [
    {
      account: DirectAccountId.AAA,
      readIps: ['192.168.1.1/24'],
    },
  ],
  groupInfos: [
    {
      users: ['test@example.com'],
      accountAuthMaps: [
        {
          account: DirectAccountId.AAA,
          authTypes: [AuthType.Read],
        },
      ],
    },
  ],
});
```

デプロイ完了後、指定したIPアドレスではない端末からスイッチロールしてみます。
正常にスイッチロールできないことを確認しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/02a0b96a-5e71-61a1-bea3-80bff6b782c2.png)


続いて、Permission Boundaryが付与されているかを確認します。
```typescript:vendor.ts
vendors.push({
  name: 'test',
  stackSetsInfos: [
    {
      account: DirectAccountId.AAA,
      permissionsBoundaryName: 'testboundrypolicy',
    },
  ],
  groupInfos: [
    {
      users: ['test@example.com'],
      accountAuthMaps: [
        {
          account: DirectAccountId.AAA,
          authTypes: [AuthType.Read],
        },
      ],
    },
  ],
});
```
問題なく付与されていました！:fist:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/3890246d-0eb9-97ae-9210-3186a8c84af2.png)



# さいごに
今回はIP制限やPermission Boundaryをスイッチ先のIAMロールに付与するカスタマイズを行いました。これにより、アカウント毎にセキュリティ要件を反映しつつ、ジャンプアカウントでIAMと権限を一元管理することができます。

この記事が同じような状況でお困りの方々のお役に立てれば幸いです。


# 弊社では一緒に働く仲間を募集中です！

現在、様々な職種を募集しております。
カジュアル面談も可能ですので、ご連絡お待ちしております！

募集内容等詳細は、是非採用サイトをご確認ください。

https://engineer.po-holdings.co.jp/