［AWS] Verified AccessとOIDCによるアクセス制御

## はじめに

- Verified Accessはプライベートアプリケーション(LB等)に対するVPNレスでのアクセス制御を実施するためのAWSサービスです。
- OIDCのIdPと連携することで、認証ステータスやIdP側のユーザグループに応じた細かな認証認可を実装することも出来ます。
- OIDCのIdPとなるKeycloak及びAWS資源を実際に設定し、認証認可の動作確認をしてみます。(AWS資源設定にはAWS CDKを利用します。）

## OIDC認証基盤設定

- 既存の構築済みKeycloakに対して、Verfied Accessと連携するための設定をしていきます。
:::note info
本記事ではKeycloak実行環境構築の詳細は割愛します。
:::


### Client設定

- Keycloak管理コンソールの`Clients`から、OIDC Clientを作成します。
    - Valid redirect URIs
        - https://Verified Accessで設定するカスタムドメイン/oauth2/idpresponse
- Keycloak管理コンソールの`Clients scopes`から、client scopeを作成します。
    - Mapper type
        - Group Membership
    - Name
        - groups
    - Token Claim Name
        - groups

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/2ae374dd-4bdb-1fa3-7526-f9ab6f63f55f.png)
    
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/8ed1e726-f2d8-65ca-eb46-9c20d842de65.png)
- Keycloak管理コンソールの`Clients`から、上記で作成したOIDC Clientを選択して`Client scopes`タブを選択し、groupsを`Add client scope`します。
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/12888697-3671-73c2-b17f-6231457c071f.png)


### ユーザ設定

- Keycloak管理コンソールの`Groups`から、動作確認用グループを作成します。
- Keycloak管理コンソールの`Users`から、動作確認用ユーザを作成します。
- 動作確認用ユーザの詳細画面にて`Groups`タブを選択し、動作確認用グループをアタッチします。
- Keycloak管理コンソールの`Clients`から、上記で作成したOIDC Clientを選択して`Client scopes`タブを選択し、`Evaluate`を選択します。動作確認用ユーザを選択すると、各種トークンやUserInfoエンドポイント提供情報にgroups情報が内包されていることが分かります。

### OIDC情報確認

- Keycloak管理コンソールの`Clients`から、先ほど作成したClientを選択し、clientIdを控えておきます。
- `Credentials`タブを選択し、clientSecretを控えておきます。
- Keycloak管理コンソールの`Realm settings` から、`OpenID Endpoint Configuration` にアクセスします。
- 遷移先のOpenID Configurationエンドポイントで、issuerと各種エンドポイント情報を控えておきます。
    - authorizationEndpoint
    - issuer
    - tokenEndpoint
    - userInfoEndpoint

## ネットワーク設定
- ここからはCDKを用いて、AWS資源を設定していきます。
- BleaのNetworkingコンストラクトを用いて、VPC資源を設定します。
    - https://github.com/aws-samples/baseline-environment-on-aws/blob/main/usecases/blea-guest-ecs-app-sample/lib/construct/networking.ts

## バックエンドアプリ設定

- 内部向けALB及びLambda関数から構成されるバックエンドアプリ資源を設定します。
    - 検証用のため、内部向けALBのプロトコルはHTTPにしています。

```tsx
// ---- Lambda
const itemLambda = new lambda.Function(this, "ItemLambda", {
  functionName: `${props.prefix}-lambda-backend-item`,
  runtime: lambda.Runtime.PYTHON_3_12,
  code: lambda.Code.fromAsset('lambda/python/backend'),
  handler: 'test.lambda_handler',
  memorySize: 256,
  tracing: lambda.Tracing.ACTIVE,
  insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_98_0,
  vpc: props.vpc,
  vpcSubnets: props.vpc.selectSubnets({
    subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
  })
})
// ---- ALB
// Security Group
const backendAlbSecurityGroup = new ec2.SecurityGroup(this, "BackendAlbSecurityGroup", {
  securityGroupName: `${props.prefix}-sg-alb-back`,
  vpc: props.vpc,
  description: "Security Group for Backend ALB"
})
backendAlbSecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(80),
  "Allow Inbound HTTP Access"
)
backendAlbSecurityGroup.addEgressRule(
  ec2.Peer.ipv4(props.vpc.vpcCidrBlock),
  ec2.Port.tcp(443),
  "Allow Outbound HTTPS Access to inside of VPC"
)
backendAlbSecurityGroup.addEgressRule(
  ec2.Peer.ipv4(props.vpc.vpcCidrBlock),
  ec2.Port.tcp(80),
  "Allow Outbound HTTP Access to inside of VPC"
)
// Alb
this.alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', {
  vpc: props.vpc,
  vpcSubnets: props.vpc.selectSubnets({
    subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
  }),
  loadBalancerName: `${props.prefix}-alb-back`,
  internetFacing: false,
  securityGroup: backendAlbSecurityGroup,
});
// Listener
const albListener = this.alb.addListener('AlbListener', {
  protocol: elbv2.ApplicationProtocol.HTTP,
  port: 80,
});
// Target
albListener.addTargets(
  "LambdaTarget",
  {
    targetGroupName: `${props.prefix}-tg-back-lambda`,
    targets: [new elbv2Targets.LambdaTarget(itemLambda)],
    healthCheck: {
      enabled: true,
    }
  }
);
```

- 検証用のため、Lambda関数はrepostに記載のコードを流用しています。(且つ、Lambda関数のレスポンス形式が不正な場合、502エラーが返却されることに留意ください。)
    - https://repost.aws/ja/knowledge-center/lambda-troubleshoot-targets-for-albs

```tsx
import json

def lambda_handler(event, context):
  return {
    "statusCode": 200,
    "statusDescription": "200 OK",
    "headers": {
      "Content-Type": "text/html"
    },
    "isBase64Encoded": False,
    "body": "<h1>Hello from Lambda!</h1>"
  }
```

## Verified Accessの設定

### 事前準備

- Verified Accessエンドポイントで利用するSecurity Groupを作成します。

```tsx
    // Security Group　　　　	
    const verifiedSecurityGroup = new ec2.SecurityGroup(this, "VerifiedSecurityGroup", {
      securityGroupName: `${props.prefix}-sg-verified`,
      vpc: props.vpc,
      description: "Security Group for Verified Access"
    })  verifiedSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443),
      "Allow Inbound HTTPS Access"
    )
```

- Verified Accessエンドポイントで利用するパブリック証明書をACMで作成します。

```tsx
    // ACM
    const verifiedAcm = new acm.Certificate(this, "VerifiedAcm", {
      domainName: props.domainName,
      validation: acm.CertificateValidation.fromDns(props.hostedZone)
    })
```

### 信頼プロバイダー設定

- Keycloak管理コンソールで取得した情報を用いて、信頼プロバイダーを作成します。

```tsx
    // Trust Provider
    const verifiedAccessTrustProvider = new ec2.CfnVerifiedAccessTrustProvider(this, "VerifiedAccessTrustProvider", {
      tags: [{
        key: 'Name',
        value: `${props.prefix}-verified-access-trust-provider-oidc`,
      }],
      description: "OIDC VerifiedAccessTrustProvider",
      policyReferenceName: `${props.prefix}_user_trust_policy`,
      trustProviderType: "user",
      userTrustProviderType: "oidc",
      oidcOptions: {
        authorizationEndpoint: props.authorizationEndpoint,
        clientId: props.clientId,
        clientSecret: props.clientSecret,
        issuer: props.issuer,
        scope: "openid profile groups",
        tokenEndpoint: props.tokenEndpoint,
        userInfoEndpoint: props.userInfoEndpoint,
      }
    })
```

### インスタンス設定

- 上記で作成した信頼プロバイダーをアタッチしたインスタンスを作成します。

```tsx
    // Instance
    const verifiedAccessInstance = new ec2.CfnVerifiedAccessInstance(this, "VerifiedAccessInstance", {
      tags: [{
        key: 'Name',
        value: `${props.prefix}-verified-access-instance-oidc`,
      }],
      description: "OIDC VerifiedAccessInstance",
      verifiedAccessTrustProviderIds: [verifiedAccessTrustProvider.attrVerifiedAccessTrustProviderId]
    })
```

### グループ設定

- 上記で作成したインスタンスをアタッチしたVerified Accessグループを作成します。
- まずは、Keycloak認証済みユーザに対してアクセスを許可するよう、設定します。

```tsx
    // Access Group
    const verifiedAccessGroup = new ec2.CfnVerifiedAccessGroup(this, "VerifiedAccessGroup", {
      tags: [{
        key: 'Name',
        value: `${props.prefix}-verified-access-group-oidc`,
      }],
      description: "OIDC VerifiedAccessGroup",
      verifiedAccessInstanceId: verifiedAccessInstance.attrVerifiedAccessInstanceId,
      policyDocument: "permit(principal,action,resource) when {true};"
    }
```

### エンドポイント設定

上記で作成したグループ・ACM・Security Group・ALBをアタッチしたエンドポイントを作成します。

```tsx
    // Access Endpoint
    const verifiedAccessEndpoint = new ec2.CfnVerifiedAccessEndpoint(this, "VerifiedAccessEndpoint", {
      tags: [{
        key: 'Name',
        value: `${props.prefix}-verified-access-endpoint-oidc`,
      }],
      description: "OIDC VerifiedAccessEndpoint",
      verifiedAccessGroupId: verifiedAccessGroup.attrVerifiedAccessGroupId,
      applicationDomain: props.domainName,
      domainCertificateArn: verifiedAcm.certificateArn,
      attachmentType: "vpc",
      securityGroupIds: [verifiedSecurityGroup.securityGroupId],
      endpointDomainPrefix: props.domainName.split(".")[0],
      endpointType: "load-balancer",
      loadBalancerOptions: {
        loadBalancerArn: props.LoadBalancerArn,
        port: 80,
        protocol: "http",
        subnetIds: props.vpc.selectSubnets({
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
        }).subnetIds
      }
    })
```

### Route53設定

- Verified Accessにアクセスするためのカスタムドメインについて、CNAMEレコードとして設定します。

```tsx
    // Route53 CNAME Record
    new route53.CnameRecord(this, "VerifiedAccessCnameRecord", {
      zone: props.hostedZone,
      recordName: props.domainName,
      domainName: verifiedAccessEndpoint.attrEndpointDomain,
    });
```


## 動作確認

- カスタムドメインに対してアクセスすると、Keycloakの認証画面にリダイレクトされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/bd3b57de-7a2b-82b8-ffc0-27b4da11068f.png)

- Keycloakで認証を済ますと、無事にLambda関数からレスポンスが返却されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/c0aedfbe-05d4-f119-a396-287998c4acd0.png)

- グループポリシーを設定変更し、特定のグループに所属している場合のみアクセスを許可するよう設定します。

```tsx
permit(principal,action,resource)
when {
    context.poc_user_trust_policy.groups.contains("/XXX")
}; 
```

- 上記設定後、当該グループに未所属のユーザでアクセスした場合には403エラーが返却されました。(反映までタイムラグがありました。)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/967ffd9c-bfbd-82bb-22fb-4db798ea7414.png)

- 特定のグループに所属のユーザでは、引き続きアクセス可能なことも確認済みです。


## 注意事項
- 本記事は万全を期して作成していますが、お気づきの点がありましたら、ご連絡よろしくお願いします。
- なお、本記事の内容を利用した結果及び影響について、筆者は一切の責任を負いませんので、予めご了承ください。