こんにちは、SCSKでAWSの内製化支援『テクニカルエスコートサービス』を担当している貝塚です。
- 社内のAD(Active Directory)で管理しているユーザーIDとパスワードで、クラウド上のWebアプリケーションにログインさせたい
- ただし、社員IDはメールアドレスではなくsAMAccountName(Active Directoryでユーザーを一意に識別するログイン名属性。例: testuser01)を使用している
CognitoとADを連携させるには
Cognito User Poolは外部のSAML 2.0 Identity Provider(IdP)と連携することで、SSOを実現できます。
しかし、AD自体はLDAP/Kerberosベースのディレクトリサービスであり、SAML IdPの機能を持ちません。そのため、CognitoとADを連携させるには、ADの認証情報をSAML Assertionに変換して発行できるIdPを別途用意する必要があります。
AWS Directory Service AD Connectorは使えるか
このケースでAWS Directory Service AD Connectorは使えるでしょうか?AD ConnectorはオンプレミスADへのプロキシとして動作し、AWSマネジメントコンソールへのSSO、Amazon WorkSpaces、RDSのWindows認証などに利用できます。
しかし、AD ConnectorはSAML IdPではありません。Cognito User PoolのSAMLフェデレーションに必要なSAMLメタデータやSAML Assertionを発行する機能を持たないため、CognitoとADの連携には使用できないということが分かります。
結論: AWS上にADFSを構築する
AD Connectorの検討を経て、AWS上にADFSサーバーを構築する方式を選択しました。
ADFSはWindows Serverの役割(ロール)の一つで、ADの認証情報をSAML 2.0やWS-Federationといったフェデレーションプロトコルで外部に提供します。つまり、ADをSAML IdPとして機能させるのがADFSの中核的な役割です。
今回のケースでは、このADFSをSAML IdPとしてCognitoと連携させます。
全体アーキテクチャの検討
ADFSとCognitoを連携させるアーキテクチャを検討します。
当初案: ALB → ADFS → AD
当初は、Internet → ALB → ADFS → AD という構成を検討していました。ADFSサーバはSSL証明書を持つので、ALBでHTTPSを終端した後、再びHTTPSでADFSサーバと通信する想定です。ALBにAWS WAFをアタッチすればセキュリティ面も安心です。
ALBからADFSへの通信
ALBはHTTPSターゲットグループを使えばバックエンドへの再暗号化自体は可能です。
しかし、ADFSはSNI(Server Name Indication)の処理が特殊で、ALBのようなL7ロードバランサーの背後に配置すると、ヘルスチェックやプロキシ動作が正しく機能しないことが分かりました。
修正案: NLB → ADFS → AD
そこで、NLBを使い Internet → NLB → ADFS → AD という 構成に変更しました。
NLBはL4(TCP)パススルーで動作するため、TLS終端はADFSが行います。SNIの問題も発生しません。ただし、NLBにはAWS WAFを直接アタッチできないという制約があります。
暫定アーキテクチャ
インターネットからADFSへの通信のセキュリティ確保は別途考えることにして、まずは以下の構成としました。
こちらは完全に検証用の構成であることにご注意ください。実際はウェブサイトと同じところ(またはその「奥」)に認証サイトがあるべきですが、Cognito、ADFS、ウェブサーバ間の通信要件が明確になるようにあえてVPCを分けました。また実際には可用性要件も踏まえて構成する必要があります。
認証データフロー
認証データの流れとしては クライアントPC → Cognito → ADFS → AD → ADFS → Cognito → Client ですが、実際の通信はクライアントPCのブラウザがCognitoとADFSの間をHTTPリダイレクトで仲介する形になります。CognitoとADFSの間に直接の通信は発生しません。
通信フロー
具体的な通信フローは以下のとおりです。
- Client → ALB(Webアプリ): Webアプリにアクセス
- ALB → Client: Cognitoの認証エンドポイントへリダイレクト(HTTP 302)
- Client → Cognito: 認証リクエスト
- Cognito → Client: ADFS(SAML IdP)へリダイレクト(HTTP 302、SAMLRequestを含む)
- Client → ADFS: SAMLRequestを送信(ADFSのログイン画面が表示される)
- ADFS ←→ AD: ADFSがADに対してユーザー認証を実行(サーバー間通信)
- ADFS → Client: SAMLResponseを返却(認証成功時)
- Client → Cognito: SAMLResponseをCognitoのACSエンドポイントにPOST
- Cognito → Client: 認証トークンを発行し、ALBのコールバックURLへリダイレクト
- Client → ALB(Webアプリ): 認証済みとしてWebアプリにアクセス
重要なのは、CognitoとADFSの間に直接の通信は発生しないという点です。唯一の例外は、CognitoがADFSのメタデータXMLを取得する際のみ、Cognito → ADFSの直接通信が発生します。ただしメタデータXML取得は後述の通り構築時にADFSで出力されたメタデータをCognitoにアップロードすることで代替が可能です。
自己署名証明書とCognitoのメタデータ取得
今回の検証環境ではADFSに信頼できる第三者機関認証局(CA)の証明書は準備できず自己署名証明書を使用しました。
CognitoがADFSのメタデータURLにアクセスする方式では自己署名証明書のSSL検証に失敗するためエラーが発生し連携ができません。この問題を回避するため、メタデータのファイルをCognitoにアップロードする方式を採用しました。ADFSのメタデータXMLをS3バケットに格納し登録するようにしています。
sAMAccountNameでの認証
今回の要件では、メールアドレスではなくsAMAccountName(Active Directoryでユーザーを一意に識別するログイン名属性)でログインする必要がありました。
ADFSの Claim Rulesで sAMAccountNameを Name IDとして発行するよう設定し、Cognito側でこの値をユーザー識別子として使用しています。ウェブサイト側では認証済みリクエストの x-amzn-oidc-data ヘッダー(JWT形式)をデコードすることで、sAMAccountName を取得できます。
アーキテクチャ修正:Web Application Proxy (WAP)の導入
上記構成で一旦動作を確認した後、インターネットにさらされることになったADFSのセキュリティ強化を検討することになりました。
Web Application Proxy (WAP)はMicrosoft推奨のADFS公開方式で、ADFSのSNI処理を正しくハンドリングするリバースプロキシです。WAPはドメイン非参加で運用するため、万が一WAPが侵害されてもADドメインへの影響を限定できます。WAPが侵害された場合は、ADFS側で「プロキシ信頼の取り消し」を実行することで、侵害されたプロキシからのリクエストを即座に拒否できます。
本案件はクライアントPCのグローバルIPアドレスが限定できるためセキュリティグループに頼る選択肢もありましたが、ネットワーク層以外の防御を組み合わせるという視点からWAPを導入する方向で検討を進めました。とはいえ顧客運用対象のEC2サーバが増えるため手放しでは喜べない結果となりました。
まとめ
本記事では、CognitoとオンプレミスADをSAML連携するために、AWS上にADFSを構築し、WAPを経由して安全にインターネットに公開する構成を紹介しました。
ADFSの前にALBを置けないことからNLBを採用し、さらにWAPを導入することで、セキュリティと可用性を両立しています。自己署名証明書環境でのCognitoメタデータ取得の回避策や、sAMAccountNameによる認証の実現方法など、実装時に直面した課題とその解決策も共有しました。これらのノウハウが皆様のお役に立てば幸いです。
実装手順
本検証構成をデプロイするためのCloudFormationテンプレートとコマンドを以下に掲載します。
デプロイは5つのCloudFormationテンプレートと、テンプレート以外で必要な対応手順から構成されます。
- [Step 1] CloudFormation: スタック1 (擬似オンプレミスVPC) デプロイ
- [Step 2] CloudFormation: スタック2 (AD/ADFSサーバ) デプロイ
- [自動実行] UserDataスクリプト(ADドメイン作成、ADFSドメイン参加)
- [Step 3] CloudFormation: スタック3 (WAP) デプロイ
- [Step 4] SSM Run Command: ADテストユーザー作成
- [Step 5] 手動: ADFSファーム作成(Fleet Manager 経由)
- [Step 6] ADFSメタデータXMLのS3アップロード
- [Step 7] CloudFormation: スタック4 (ウェブアプリ用VPC) デプロイ
- [Step 8] CloudFormation: スタック5 (Cognito/ウェブアプリ) デプロイ
- [Step 9] WAP構成(Fleet Manager 経由)
Step 1: スタック1(擬似オンプレミスVPC)のデプロイ
コマンド
aws cloudformation create-stack \ --stack-name adfs-pseudo-vpc \ --template-body file://cfn-pseudo-onprem-vpc.yaml \ --parameters file://cfn-pseudo-onprem-vpc_adfs-pseudo-vpc.json \ --region ap-northeast-1
cfn-pseudo-onprem-vpc.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack 1: Pseudo On-Premises VPC with NLB for ADFS access'
Parameters:
VpcCidr:
Type: String
Description: 'CIDR block for the VPC'
Default: '10.0.0.0/16'
PublicSubnet1aCidr:
Type: String
Description: 'CIDR block for the public subnet in AZ 1a'
Default: '10.0.10.0/24'
PublicSubnet1cCidr:
Type: String
Description: 'CIDR block for the public subnet in AZ 1c'
Default: '10.0.11.0/24'
WapSubnet1aCidr:
Type: String
Description: 'CIDR block for the WAP subnet in AZ 1a'
Default: '10.0.20.0/24'
WapSubnet1cCidr:
Type: String
Description: 'CIDR block for the WAP subnet in AZ 1c'
Default: '10.0.21.0/24'
InternalNlbSubnet1aCidr:
Type: String
Description: 'CIDR block for the Internal NLB subnet in AZ 1a'
Default: '10.0.30.0/24'
InternalNlbSubnet1cCidr:
Type: String
Description: 'CIDR block for the Internal NLB subnet in AZ 1c'
Default: '10.0.31.0/24'
PrivateSubnet1aCidr:
Type: String
Description: 'CIDR block for the private subnet in AZ 1a'
Default: '10.0.40.0/24'
PrivateSubnet1cCidr:
Type: String
Description: 'CIDR block for the private subnet in AZ 1c'
Default: '10.0.41.0/24'
Resources:
# VPC
PseudoOnPremVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-vpc'
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-igw'
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref PseudoOnPremVpc
InternetGatewayId: !Ref InternetGateway
# Public Subnet 1a
PublicSubnet1a:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref PublicSubnet1aCidr
AvailabilityZone: 'ap-northeast-1a'
MapPublicIpOnLaunch: true
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-public-subnet-1a'
# Public Subnet 1c
PublicSubnet1c:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref PublicSubnet1cCidr
AvailabilityZone: 'ap-northeast-1c'
MapPublicIpOnLaunch: true
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-public-subnet-1c'
# WAP Subnet 1a
WapSubnet1a:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref WapSubnet1aCidr
AvailabilityZone: 'ap-northeast-1a'
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-wap-subnet-1a'
# WAP Subnet 1c
WapSubnet1c:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref WapSubnet1cCidr
AvailabilityZone: 'ap-northeast-1c'
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-wap-subnet-1c'
# Internal NLB Subnet 1a
InternalNlbSubnet1a:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref InternalNlbSubnet1aCidr
AvailabilityZone: 'ap-northeast-1a'
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-internal-nlb-subnet-1a'
# Internal NLB Subnet 1c
InternalNlbSubnet1c:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref InternalNlbSubnet1cCidr
AvailabilityZone: 'ap-northeast-1c'
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-internal-nlb-subnet-1c'
# Private Subnet 1a
PrivateSubnet1a:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref PrivateSubnet1aCidr
AvailabilityZone: 'ap-northeast-1a'
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-private-subnet-1a'
# Private Subnet 1c
PrivateSubnet1c:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PseudoOnPremVpc
CidrBlock: !Ref PrivateSubnet1cCidr
AvailabilityZone: 'ap-northeast-1c'
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-private-subnet-1c'
# Elastic IP for NAT Gateway
NatGatewayEIP:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttachment
Properties:
Domain: vpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-nat-eip'
# NAT Gateway
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet1a
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-nat-gateway'
# Public Route Table
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref PseudoOnPremVpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-public-rt'
PublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
PublicSubnet1aRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1a
RouteTableId: !Ref PublicRouteTable
PublicSubnet1cRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1c
RouteTableId: !Ref PublicRouteTable
# Private Route Table
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref PseudoOnPremVpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-private-rt'
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId: !Ref NatGateway
PrivateSubnet1aRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1a
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet1cRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1c
RouteTableId: !Ref PrivateRouteTable
# WAP Route Table
WapRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref PseudoOnPremVpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-wap-rt'
WapRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref WapRouteTable
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId: !Ref NatGateway
WapSubnet1aRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref WapSubnet1a
RouteTableId: !Ref WapRouteTable
WapSubnet1cRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref WapSubnet1c
RouteTableId: !Ref WapRouteTable
# Internal NLB Route Table (VPC local only, no internet route)
InternalNlbRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref PseudoOnPremVpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-internal-nlb-rt'
InternalNlbSubnet1aRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref InternalNlbSubnet1a
RouteTableId: !Ref InternalNlbRouteTable
InternalNlbSubnet1cRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref InternalNlbSubnet1c
RouteTableId: !Ref InternalNlbRouteTable
# Security Group for AD
ADSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Security group for Active Directory server'
VpcId: !Ref PseudoOnPremVpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: '0.0.0.0/0'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-ad-sg'
# Security Group for ADFS
ADFSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Security group for ADFS server'
VpcId: !Ref PseudoOnPremVpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: '0.0.0.0/0'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-adfs-sg'
# Security Group Ingress Rules (defined separately to avoid circular dependency)
ADSecurityGroupIngressFromADFS:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref ADSecurityGroup
IpProtocol: -1
SourceSecurityGroupId: !Ref ADFSSecurityGroup
Description: 'Allow all traffic from ADFS Security Group'
ADFSSecurityGroupIngressFromAD:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref ADFSSecurityGroup
IpProtocol: -1
SourceSecurityGroupId: !Ref ADSecurityGroup
Description: 'Allow all traffic from AD Security Group'
ADFSSecurityGroupIngressAllTraffic:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref ADFSSecurityGroup
IpProtocol: -1
CidrIp: '0.0.0.0/0'
Description: 'Allow all traffic (test environment)'
# Security Group for WAP
WapSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Security group for WAP server'
VpcId: !Ref PseudoOnPremVpc
SecurityGroupIngress:
- IpProtocol: -1
CidrIp: '0.0.0.0/0'
Description: 'Allow all traffic (test environment)'
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: '0.0.0.0/0'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-wap-sg'
# External Network Load Balancer
ExternalNetworkLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${AWS::StackName}-ext-nlb'
Type: network
Scheme: internet-facing
Subnets:
- !Ref PublicSubnet1a
- !Ref PublicSubnet1c
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-ext-nlb'
# External NLB Target Group (WAP Target)
ExternalNlbTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${AWS::StackName}-wap-tg'
VpcId: !Ref PseudoOnPremVpc
Port: 443
Protocol: TCP
TargetType: instance
HealthCheckProtocol: TCP
HealthCheckPort: '443'
TargetGroupAttributes:
- Key: preserve_client_ip.enabled
Value: 'false'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-wap-tg'
# External NLB Listener
ExternalNlbListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ExternalNetworkLoadBalancer
Port: 443
Protocol: TCP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ExternalNlbTargetGroup
# Internal Network Load Balancer
InternalNetworkLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${AWS::StackName}-int-nlb'
Type: network
Scheme: internal
Subnets:
- !Ref InternalNlbSubnet1a
- !Ref InternalNlbSubnet1c
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-int-nlb'
# Internal NLB Target Group (ADFS Target)
InternalNlbTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${AWS::StackName}-adfs-tg'
VpcId: !Ref PseudoOnPremVpc
Port: 443
Protocol: TCP
TargetType: instance
HealthCheckProtocol: TCP
HealthCheckPort: '443'
TargetGroupAttributes:
- Key: preserve_client_ip.enabled
Value: 'false'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-adfs-tg'
# Internal NLB Listener
InternalNlbListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref InternalNetworkLoadBalancer
Port: 443
Protocol: TCP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref InternalNlbTargetGroup
Outputs:
VpcId:
Description: 'VPC ID'
Value: !Ref PseudoOnPremVpc
Export:
Name: !Sub '${AWS::StackName}-VpcId'
PublicSubnet1aId:
Description: 'Public Subnet 1a ID'
Value: !Ref PublicSubnet1a
Export:
Name: !Sub '${AWS::StackName}-PublicSubnet1aId'
PrivateSubnet1aId:
Description: 'Private Subnet 1a ID'
Value: !Ref PrivateSubnet1a
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet1aId'
WapSubnet1aId:
Description: 'WAP Subnet 1a ID'
Value: !Ref WapSubnet1a
Export:
Name: !Sub '${AWS::StackName}-WapSubnet1aId'
ADSecurityGroupId:
Description: 'AD Security Group ID'
Value: !Ref ADSecurityGroup
Export:
Name: !Sub '${AWS::StackName}-ADSecurityGroupId'
ADFSSecurityGroupId:
Description: 'ADFS Security Group ID'
Value: !Ref ADFSSecurityGroup
Export:
Name: !Sub '${AWS::StackName}-ADFSSecurityGroupId'
WapSecurityGroupId:
Description: 'WAP Security Group ID'
Value: !Ref WapSecurityGroup
Export:
Name: !Sub '${AWS::StackName}-WapSecurityGroupId'
ExternalNlbTargetGroupArn:
Description: 'External NLB Target Group ARN'
Value: !Ref ExternalNlbTargetGroup
Export:
Name: !Sub '${AWS::StackName}-ExternalNlbTargetGroupArn'
InternalNlbTargetGroupArn:
Description: 'Internal NLB Target Group ARN'
Value: !Ref InternalNlbTargetGroup
Export:
Name: !Sub '${AWS::StackName}-InternalNlbTargetGroupArn'
ExternalNlbDnsName:
Description: 'External NLB DNS Name'
Value: !GetAtt ExternalNetworkLoadBalancer.DNSName
Export:
Name: !Sub '${AWS::StackName}-ExternalNlbDnsName'
cfn-pseudo-onprem-vpc_adfs-pseudo-vpc.json
[
{
"ParameterKey": "VpcCidr",
"ParameterValue": "10.0.0.0/16"
},
{
"ParameterKey": "PublicSubnet1aCidr",
"ParameterValue": "10.0.10.0/24"
},
{
"ParameterKey": "PublicSubnet1cCidr",
"ParameterValue": "10.0.11.0/24"
},
{
"ParameterKey": "WapSubnet1aCidr",
"ParameterValue": "10.0.20.0/24"
},
{
"ParameterKey": "WapSubnet1cCidr",
"ParameterValue": "10.0.21.0/24"
},
{
"ParameterKey": "InternalNlbSubnet1aCidr",
"ParameterValue": "10.0.30.0/24"
},
{
"ParameterKey": "InternalNlbSubnet1cCidr",
"ParameterValue": "10.0.31.0/24"
},
{
"ParameterKey": "PrivateSubnet1aCidr",
"ParameterValue": "10.0.40.0/24"
},
{
"ParameterKey": "PrivateSubnet1cCidr",
"ParameterValue": "10.0.41.0/24"
}
]
Step 2: スタック2(AD/ADFS)のデプロイ
ADサーバ、ADFSサーバを作成します。ADFSDomainNameに、ADFSを外部公開するためのFQDNを指定し、Route 53でそのDNS名をStep1で作成される外側NLBのFQDNに向けるレコードを作成しておいてください。
コマンド
aws cloudformation create-stack \ --stack-name adfs-ad \ --template-body file://cfn-ad-adfs.yaml \ --parameters file://cfn-ad-adfs_adfs-ad.json \ --capabilities CAPABILITY_IAM \ --region ap-northeast-1
cfn-ad-adfs.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack 2: AD/ADFS Servers for Cognito ADFS SSO Integration'
Parameters:
PseudoOnPremVpcStackName:
Type: String
Description: 'Name of the Pseudo On-Premises VPC stack'
Default: 'adfs-pseudo-vpc'
ADInstanceType:
Type: String
Description: 'Instance type for AD server'
Default: 't3.medium'
AllowedValues:
- t3.medium
- t3.large
- t3.xlarge
ADFSInstanceType:
Type: String
Description: 'Instance type for ADFS server'
Default: 't3.medium'
AllowedValues:
- t3.medium
- t3.large
- t3.xlarge
ADAdminPassword:
Type: String
Description: 'Administrator password for AD domain'
NoEcho: true
MinLength: 8
ADDomainName:
Type: String
Description: 'AD domain name (e.g., corp.local)'
Default: 'corp.local'
ADFSDomainName:
Type: String
Description: 'ADFS federation service name (e.g., adfs.example.com)'
Default: 'adfs.example.com'
WindowsAMI:
Type: AWS::SSM::Parameter::Value
Description: 'Latest Windows Server 2022 AMI from SSM Parameter Store'
Default: '/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base'
Resources:
# IAM Role for SSM
SSMInstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-ssm-role'
- Key: Cost
Value: 'cognitoadfs'
# Instance Profile for SSM
SSMInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref SSMInstanceRole
# AD Server Instance
ADInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref WindowsAMI
InstanceType: !Ref ADInstanceType
IamInstanceProfile: !Ref SSMInstanceProfile
SubnetId:
Fn::ImportValue: !Sub '${PseudoOnPremVpcStackName}-PrivateSubnet1aId'
SecurityGroupIds:
- Fn::ImportValue: !Sub '${PseudoOnPremVpcStackName}-ADSecurityGroupId'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-ad-server'
- Key: Cost
Value: 'cognitoadfs'
PropagateTagsToVolumeOnCreation: true
UserData:
Fn::Base64: !Sub |
# Get instance metadata
$instanceId = Invoke-RestMethod -uri http://169.254.169.254/latest/meta-data/instance-id
# Install AD DS role
Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools
# Disable Network Level Authentication for RDP (for Fleet Manager access)
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 0
# Set local Administrator password before creating domain
# This password will become the domain Administrator password after domain creation
$AdminUser = [ADSI]"WinNT://./Administrator,user"
$AdminUser.SetPassword("${ADAdminPassword}")
# Create AD domain (computer name will be the default AWS name)
$SafeModePassword = ConvertTo-SecureString "${ADAdminPassword}" -AsPlainText -Force
Install-ADDSForest -DomainName "${ADDomainName}" -DomainNetbiosName "CORP" -SafeModeAdministratorPassword $SafeModePassword -InstallDns -Force -NoRebootOnCompletion:$false
# Server will restart automatically after domain creation
# ADFS Server Instance
ADFSInstance:
Type: AWS::EC2::Instance
DependsOn: ADInstance
Properties:
ImageId: !Ref WindowsAMI
InstanceType: !Ref ADFSInstanceType
IamInstanceProfile: !Ref SSMInstanceProfile
SubnetId:
Fn::ImportValue: !Sub '${PseudoOnPremVpcStackName}-PrivateSubnet1aId'
SecurityGroupIds:
- Fn::ImportValue: !Sub '${PseudoOnPremVpcStackName}-ADFSSecurityGroupId'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-adfs-server'
- Key: Cost
Value: 'cognitoadfs'
PropagateTagsToVolumeOnCreation: true
UserData:
Fn::Base64:
Fn::Sub:
- |
# Get instance metadata
$instanceId = Invoke-RestMethod -uri http://169.254.169.254/latest/meta-data/instance-id
# Install ADFS role
Install-WindowsFeature -Name ADFS-Federation -IncludeManagementTools
# Disable Network Level Authentication for RDP (for Fleet Manager access)
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 0
# Generate self-signed certificate
$cert = New-SelfSignedCertificate -DnsName "${ADFSDomainName}" -CertStoreLocation "cert:\LocalMachine\My" -KeySpec KeyExchange
# Wait for AD server to be ready (20 minutes for domain creation and restart)
Start-Sleep -Seconds 1200
# Get AD server IP
$ADServerIP = "${ADInstancePrivateIp}"
# Set DNS server to AD server
# Get the first active network adapter (dynamically)
$adapter = Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | Select-Object -First 1
Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ServerAddresses $ADServerIP
# Clear DNS cache after changing DNS server
Clear-DnsClientCache
# Wait for DNS resolution with retry (max 10 attempts, 10 seconds interval)
$maxRetries = 10
$retryCount = 0
do {
Start-Sleep -Seconds 10
$result = Resolve-DnsName ${ADDomainName} -ErrorAction SilentlyContinue
$retryCount++
Write-Output "DNS resolution attempt $retryCount : $($result.IPAddress)"
} while (-not $result -and $retryCount -lt $maxRetries)
if (-not $result) {
Write-Error "DNS resolution for ${ADDomainName} failed after $maxRetries attempts"
exit 1
}
# Join domain with error handling
$password = ConvertTo-SecureString "${ADAdminPassword}" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential("CORP\Administrator", $password)
try {
Add-Computer -DomainName "${ADDomainName}" -Credential $credential -Force -ErrorAction Stop
Write-Output "Domain join successful"
} catch {
Write-Error "Domain join failed: $_"
exit 1
}
Restart-Computer -Force
- ADInstancePrivateIp: !GetAtt ADInstance.PrivateIp
# Lambda execution role for NLB target registration
NLBTargetRegistrationRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
Policies:
- PolicyName: 'ELBv2TargetRegistration'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'elasticloadbalancing:RegisterTargets'
- 'elasticloadbalancing:DeregisterTargets'
Resource: '*'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-nlb-target-reg-role'
- Key: Cost
Value: 'cognitoadfs'
# Lambda function for NLB target registration
NLBTargetRegistrationFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-nlb-target-reg'
Runtime: python3.12
Handler: index.handler
Role: !GetAtt NLBTargetRegistrationRole.Arn
Timeout: 60
Code:
ZipFile: |
import json
import boto3
import cfnresponse
def handler(event, context):
try:
target_group_arn = event['ResourceProperties']['TargetGroupArn']
instance_id = event['ResourceProperties']['InstanceId']
client = boto3.client('elbv2')
if event['RequestType'] in ['Create', 'Update']:
client.register_targets(
TargetGroupArn=target_group_arn,
Targets=[{'Id': instance_id}]
)
cfnresponse.send(event, context, cfnresponse.SUCCESS, {'InstanceId': instance_id})
elif event['RequestType'] == 'Delete':
try:
client.deregister_targets(
TargetGroupArn=target_group_arn,
Targets=[{'Id': instance_id}]
)
except Exception:
pass
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
except Exception as e:
print(f'Error: {e}')
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-nlb-target-reg-func'
- Key: Cost
Value: 'cognitoadfs'
# Custom resource to register ADFS instance to Internal NLB target group
NLBTargetRegistration:
Type: Custom::NLBTargetRegistration
DependsOn: ADFSInstance
Properties:
ServiceToken: !GetAtt NLBTargetRegistrationFunction.Arn
TargetGroupArn:
Fn::ImportValue: !Sub '${PseudoOnPremVpcStackName}-InternalNlbTargetGroupArn'
InstanceId: !Ref ADFSInstance
Outputs:
ADInstanceId:
Description: 'AD Server Instance ID'
Value: !Ref ADInstance
Export:
Name: !Sub '${AWS::StackName}-ADInstanceId'
ADFSInstanceId:
Description: 'ADFS Server Instance ID'
Value: !Ref ADFSInstance
Export:
Name: !Sub '${AWS::StackName}-ADFSInstanceId'
ADInstancePrivateIp:
Description: 'AD Server Private IP Address'
Value: !GetAtt ADInstance.PrivateIp
Export:
Name: !Sub '${AWS::StackName}-ADInstancePrivateIp'
ADFSInstancePrivateIp:
Description: 'ADFS Server Private IP Address'
Value: !GetAtt ADFSInstance.PrivateIp
Export:
Name: !Sub '${AWS::StackName}-ADFSInstancePrivateIp'
cfn-ad-adfs_adfs-ad.json
[
{
"ParameterKey": "PseudoOnPremVpcStackName",
"ParameterValue": "adfs-pseudo-vpc"
},
{
"ParameterKey": "ADInstanceType",
"ParameterValue": "t3.medium"
},
{
"ParameterKey": "ADFSInstanceType",
"ParameterValue": "t3.medium"
},
{
"ParameterKey": "ADAdminPassword",
"ParameterValue": "ChangeMe123!"
},
{
"ParameterKey": "ADDomainName",
"ParameterValue": "corp.local"
},
{
"ParameterKey": "ADFSDomainName",
"ParameterValue": "adfs.example.com"
},
{
"ParameterKey": "WindowsAMI",
"ParameterValue": "/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base"
}
]
スタック2のデプロイ完了後、UserDataスクリプトの実行完了まで約25-30分の待機が必要です。UserDataではADドメインの作成とADFSサーバーのドメイン参加が自動実行されます。
Step 3: スタック3(WAP)のデプロイ
コマンド
aws cloudformation create-stack \ --stack-name adfs-wap \ --template-body file://cfn-wap.yaml \ --parameters file://cfn-wap_adfs-wap.json \ --capabilities CAPABILITY_IAM \ --region ap-northeast-1
cfn-wap.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack 3: WAP Server for ADFS Proxy Security Enhancement'
Parameters:
VpcStackName:
Type: String
Description: 'Name of the Pseudo On-Premises VPC stack'
Default: 'adfs-pseudo-vpc'
InstanceType:
Type: String
Description: 'Instance type for WAP server'
Default: 't3.medium'
AllowedValues:
- t3.medium
- t3.large
- t3.xlarge
LatestWindowsAmiId:
Type: AWS::SSM::Parameter::Value
Description: 'Latest Windows Server 2019 Japanese AMI from SSM Parameter Store'
Default: '/aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base'
KeyPairName:
Type: String
Description: 'EC2 Key Pair name for WAP server (optional)'
Default: ''
Conditions:
HasKeyPair: !Not [!Equals [!Ref KeyPairName, '']]
Resources:
# IAM Role for SSM (Fleet Manager)
WapSSMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-ssm-role'
- Key: Cost
Value: 'cognitoadfs'
# Instance Profile for SSM
WapSSMInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref WapSSMRole
# WAP Server Instance
WapInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref LatestWindowsAmiId
InstanceType: !Ref InstanceType
KeyName: !If [HasKeyPair, !Ref KeyPairName, !Ref 'AWS::NoValue']
IamInstanceProfile: !Ref WapSSMInstanceProfile
SubnetId:
Fn::ImportValue: !Sub '${VpcStackName}-WapSubnet1aId'
SecurityGroupIds:
- Fn::ImportValue: !Sub '${VpcStackName}-WapSecurityGroupId'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-wap-server'
- Key: Cost
Value: 'cognitoadfs'
PropagateTagsToVolumeOnCreation: true
UserData:
Fn::Base64: |
# Install WAP role
Install-WindowsFeature Web-Application-Proxy -IncludeManagementTools
# Disable Network Level Authentication for RDP (for Fleet Manager access)
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 0
# Lambda execution role for NLB target registration
WapNLBTargetRegistrationRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
Policies:
- PolicyName: 'ELBv2TargetRegistration'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'elasticloadbalancing:RegisterTargets'
- 'elasticloadbalancing:DeregisterTargets'
Resource: '*'
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-nlb-target-reg-role'
- Key: Cost
Value: 'cognitoadfs'
# Lambda function for NLB target registration
WapNLBTargetRegistrationFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-nlb-target-reg'
Runtime: python3.12
Handler: index.handler
Role: !GetAtt WapNLBTargetRegistrationRole.Arn
Timeout: 60
Code:
ZipFile: |
import json
import boto3
import cfnresponse
def handler(event, context):
try:
target_group_arn = event['ResourceProperties']['TargetGroupArn']
instance_id = event['ResourceProperties']['InstanceId']
client = boto3.client('elbv2')
if event['RequestType'] in ['Create', 'Update']:
client.register_targets(
TargetGroupArn=target_group_arn,
Targets=[{'Id': instance_id}]
)
cfnresponse.send(event, context, cfnresponse.SUCCESS, {'InstanceId': instance_id})
elif event['RequestType'] == 'Delete':
try:
client.deregister_targets(
TargetGroupArn=target_group_arn,
Targets=[{'Id': instance_id}]
)
except Exception:
pass
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
except Exception as e:
print(f'Error: {e}')
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-nlb-target-reg-func'
- Key: Cost
Value: 'cognitoadfs'
# Custom resource to register WAP instance to External NLB target group
WapNLBTargetRegistration:
Type: Custom::NLBTargetRegistration
DependsOn: WapInstance
Properties:
ServiceToken: !GetAtt WapNLBTargetRegistrationFunction.Arn
TargetGroupArn:
Fn::ImportValue: !Sub '${VpcStackName}-ExternalNlbTargetGroupArn'
InstanceId: !Ref WapInstance
Outputs:
WapInstanceId:
Description: 'WAP Server Instance ID'
Value: !Ref WapInstance
Export:
Name: !Sub '${AWS::StackName}-WapInstanceId'
WapInstancePrivateIp:
Description: 'WAP Server Private IP Address'
Value: !GetAtt WapInstance.PrivateIp
Export:
Name: !Sub '${AWS::StackName}-WapInstancePrivateIp'
cfn-wap_adfs-wap.json
[
{
"ParameterKey": "VpcStackName",
"ParameterValue": "adfs-pseudo-vpc"
},
{
"ParameterKey": "InstanceType",
"ParameterValue": "t3.medium"
},
{
"ParameterKey": "LatestWindowsAmiId",
"ParameterValue": "/aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base"
},
{
"ParameterKey": "KeyPairName",
"ParameterValue": ""
}
]
Step 4: ADテストユーザーの作成
SSM Run Commandを使用して、ADサーバー上にテストユーザーを作成します。
| ユーザ名 | パスワード |
| testuser01@corp | TestPass123! |
| testuser02@corp | TestPass123! |
| testuser03@corp | TestPass123! |
aws ssm send-command \ --instance-ids "<ADインスタンスID>" \ --document-name "AWS-RunPowerShellScript" \ --parameters file://ad-create-test-users.json \ --region ap-northeast-1
ad-create-test-users.json
{
"commands": [
"Import-Module ActiveDirectory",
"$password = ConvertTo-SecureString 'TestPass123!' -AsPlainText -Force",
"New-ADUser -Name 'testuser01' -SamAccountName 'testuser01' -UserPrincipalName 'testuser01@corp.local' -AccountPassword $password -Enabled $true -PasswordNeverExpires $true -Path 'CN=Users,DC=corp,DC=local'",
"New-ADUser -Name 'testuser02' -SamAccountName 'testuser02' -UserPrincipalName 'testuser02@corp.local' -AccountPassword $password -Enabled $true -PasswordNeverExpires $true -Path 'CN=Users,DC=corp,DC=local'",
"New-ADUser -Name 'testuser03' -SamAccountName 'testuser03' -UserPrincipalName 'testuser03@corp.local' -AccountPassword $password -Enabled $true -PasswordNeverExpires $true -Path 'CN=Users,DC=corp,DC=local'",
"Write-Output 'Test users created. Verifying...'",
"Get-ADUser -Filter * | Select-Object Name,SamAccountName,Enabled | Format-Table -AutoSize"
]
}
Step 5: ADFSファームの作成
Fleet Manager でADFSサーバーに接続し、PowerShellでADFSファームを作成します。adfs.example.comのところ(2か所)はADFSを外部公開するFQDNに置き換えてください。
| ユーザ名 | パスワード |
| CORP\Administrator | ChangeMe123! |
# 証明書サムプリント取得
$cert = Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object {$_.Subject -like "*adfs.example.com*"}
# ADFSファーム作成
$password = ConvertTo-SecureString "ChangeMe123!" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential(
"CORP\Administrator", $password)
Install-AdfsFarm `
-CertificateThumbprint $cert.Thumbprint `
-FederationServiceName "adfs.example.com" `
-ServiceAccountCredential $credential `
-OverwriteConfiguration
Step 6: ADFSメタデータXMLのS3アップロード
自己署名証明書環境では、CognitoのMetadataURL方式が使えないため、メタデータXMLをS3にアップロードします。
# メタデータXML取得。ファイル名 adfs-metadata.xml として保存 curl -sk https://<ADFSのFQDN>/FederationMetadata/2007-06/FederationMetadata.xml \ -o /tmp/adfs-metadata.xml # S3バケット作成 aws s3 mb "s3://<バケット名>" --region ap-northeast-1 # S3にアップロード aws s3 cp tmp/adfs-metadata.xml "s3://<バケット名>/adfs-metadata.xml" \ --region ap-northeast-1
Step 7: スタック4(本番VPC)のデプロイ
デプロイには、ウェブサーバの前に配置するALBにインストールするACMサーバ証明書のARNが必要になります。
コマンド
aws cloudformation create-stack \ --stack-name adfs-prod-vpc \ --template-body file://cfn-production-vpc.yaml \ --parameters file://cfn-production-vpc_adfs-prod-vpc.json \ --region ap-northeast-1
cfn-production-vpc.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack 4: Production VPC with ALB for Web Application'
Parameters:
VpcCidr:
Type: String
Description: 'CIDR block for the VPC'
Default: '10.1.0.0/16'
PublicSubnet1Cidr:
Type: String
Description: 'CIDR block for the first public subnet'
Default: '10.1.1.0/24'
PublicSubnet2Cidr:
Type: String
Description: 'CIDR block for the second public subnet'
Default: '10.1.2.0/24'
PrivateSubnet1Cidr:
Type: String
Description: 'CIDR block for the first private subnet'
Default: '10.1.3.0/24'
PrivateSubnet2Cidr:
Type: String
Description: 'CIDR block for the second private subnet'
Default: '10.1.4.0/24'
AvailabilityZone1:
Type: String
Description: 'First Availability Zone'
Default: 'ap-northeast-1a'
AvailabilityZone2:
Type: String
Description: 'Second Availability Zone'
Default: 'ap-northeast-1c'
ALBCertificateArn:
Type: String
Description: 'ARN of the ACM certificate for ALB HTTPS listener'
Resources:
# VPC
ProductionVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-vpc'
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-igw'
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref ProductionVpc
InternetGatewayId: !Ref InternetGateway
# Public Subnet 1
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVpc
CidrBlock: !Ref PublicSubnet1Cidr
AvailabilityZone: !Ref AvailabilityZone1
MapPublicIpOnLaunch: true
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-public-subnet-1'
# Public Subnet 2
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVpc
CidrBlock: !Ref PublicSubnet2Cidr
AvailabilityZone: !Ref AvailabilityZone2
MapPublicIpOnLaunch: true
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-public-subnet-2'
# Private Subnet 1
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVpc
CidrBlock: !Ref PrivateSubnet1Cidr
AvailabilityZone: !Ref AvailabilityZone1
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-private-subnet-1'
# Private Subnet 2
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVpc
CidrBlock: !Ref PrivateSubnet2Cidr
AvailabilityZone: !Ref AvailabilityZone2
MapPublicIpOnLaunch: false
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-private-subnet-2'
# Elastic IP for NAT Gateway
NatGatewayEIP:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttachment
Properties:
Domain: vpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-nat-eip'
# NAT Gateway (in Public Subnet 1)
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-nat-gateway'
# Public Route Table
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ProductionVpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-public-rt'
PublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# Private Route Table
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ProductionVpc
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-private-rt'
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId: !Ref NatGateway
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
# Security Group for ALB
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Security group for Application Load Balancer'
VpcId: !Ref ProductionVpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: '0.0.0.0/0'
Description: 'Allow HTTPS from Internet'
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: '0.0.0.0/0'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-alb-sg'
# Security Group for Web Application
WebAppSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Security group for Web Application server'
VpcId: !Ref ProductionVpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: '0.0.0.0/0'
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-webapp-sg'
# Security Group Ingress Rule for WebApp (from ALB)
WebAppSecurityGroupIngressFromALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref WebAppSecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
Description: 'Allow HTTP from ALB'
# Application Load Balancer
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${AWS::StackName}-alb'
Type: application
Scheme: internet-facing
IpAddressType: ipv4
SecurityGroups:
- !Ref ALBSecurityGroup
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-alb'
# ALB Target Group
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${AWS::StackName}-alb-tg'
VpcId: !Ref ProductionVpc
Port: 80
Protocol: HTTP
TargetType: instance
HealthCheckEnabled: true
HealthCheckPath: /health
HealthCheckProtocol: HTTP
HealthCheckPort: traffic-port
HealthyThresholdCount: 5
UnhealthyThresholdCount: 2
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 6
Tags:
- Key: Cost
Value: cognitoadfs
- Key: Name
Value: !Sub '${AWS::StackName}-alb-tg'
# ALB HTTPS Listener
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 443
Protocol: HTTPS
SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
Certificates:
- CertificateArn: !Ref ALBCertificateArn
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
Outputs:
VpcId:
Description: 'VPC ID'
Value: !Ref ProductionVpc
Export:
Name: !Sub '${AWS::StackName}-VpcId'
PrivateSubnet1Id:
Description: 'Private Subnet 1 ID'
Value: !Ref PrivateSubnet1
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet1Id'
PrivateSubnet2Id:
Description: 'Private Subnet 2 ID'
Value: !Ref PrivateSubnet2
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet2Id'
PublicSubnet1Id:
Description: 'Public Subnet 1 ID'
Value: !Ref PublicSubnet1
Export:
Name: !Sub '${AWS::StackName}-PublicSubnet1Id'
PublicSubnet2Id:
Description: 'Public Subnet 2 ID'
Value: !Ref PublicSubnet2
Export:
Name: !Sub '${AWS::StackName}-PublicSubnet2Id'
ALBTargetGroupArn:
Description: 'ALB Target Group ARN'
Value: !Ref ALBTargetGroup
Export:
Name: !Sub '${AWS::StackName}-ALBTargetGroupArn'
ALBListenerArn:
Description: 'ALB Listener ARN'
Value: !Ref ALBListener
Export:
Name: !Sub '${AWS::StackName}-ALBListenerArn'
WebAppSecurityGroupId:
Description: 'Web Application Security Group ID'
Value: !Ref WebAppSecurityGroup
Export:
Name: !Sub '${AWS::StackName}-WebAppSecurityGroupId'
ALBDnsName:
Description: 'ALB DNS Name'
Value: !GetAtt ApplicationLoadBalancer.DNSName
Export:
Name: !Sub '${AWS::StackName}-ALBDnsName'
cfn-production-vpc_adfs-prod-vpc.json
[
{
"ParameterKey": "VpcCidr",
"ParameterValue": "10.1.0.0/16"
},
{
"ParameterKey": "PublicSubnet1Cidr",
"ParameterValue": "10.1.1.0/24"
},
{
"ParameterKey": "PublicSubnet2Cidr",
"ParameterValue": "10.1.2.0/24"
},
{
"ParameterKey": "PrivateSubnet1Cidr",
"ParameterValue": "10.1.3.0/24"
},
{
"ParameterKey": "PrivateSubnet2Cidr",
"ParameterValue": "10.1.4.0/24"
},
{
"ParameterKey": "AvailabilityZone1",
"ParameterValue": "ap-northeast-1a"
},
{
"ParameterKey": "AvailabilityZone2",
"ParameterValue": "ap-northeast-1c"
},
{
"ParameterKey": "ALBCertificateArn",
"ParameterValue": "<ACMで作成したウェブアプリ用サーバ証明書のARN>"
}
]
Step 8: スタック5(Cognito/WebApp)のデプロイ
- Step 6で作成したADFSメタデータXMLファイルを格納したS3バケット名をパラメータファイルで指定する必要があります。
- ウェブサイトのDNS名を決定し、Route 53で名前解決設定(CNAMEまたはALIASをStep 7で作成したALBに向ける)をしてから実行してください。またそのDNS名をパラメータファイルで指定する必要があります。
- パラメータファイルで指定するCognitoのドメインプレフィックスはAWSアカウント横断でグローバルに一意である必要があります。”AlreadyExists”エラーが発生した場合は、プレフィックスを変更してください。
コマンド
aws cloudformation create-stack \ --stack-name adfs-webapp \ --template-body file://cfn-cognito-webapp.yaml \ --parameters file://cfn-cognito-webapp_adfs-webapp.json \ --capabilities CAPABILITY_NAMED_IAM \ --region ap-northeast-1
cfn-cognito-webapp.yaml
表示崩れるため別掲します。
cfn-cognito-webapp_adfs-webapp.json
[
{
"ParameterKey": "ProductionVpcStackName",
"ParameterValue": "adfs-prod-vpc"
},
{
"ParameterKey": "PseudoOnPremVpcStackName",
"ParameterValue": "adfs-pseudo-vpc"
},
{
"ParameterKey": "ADFSMetadataS3Bucket",
"ParameterValue": "your-adfs-metadata-xml-bucket-name"
},
{
"ParameterKey": "ADFSMetadataS3Key",
"ParameterValue": "adfs-metadata.xml"
},
{
"ParameterKey": "CognitoDomainPrefix",
"ParameterValue": "your-cognito-domain-prefix"
},
{
"ParameterKey": "ALBDnsName",
"ParameterValue": "<ウェブサーバのDNS名>"
},
{
"ParameterKey": "WebAppInstanceType",
"ParameterValue": "t3.micro"
},
{
"ParameterKey": "WebAppAmiId",
"ParameterValue": "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
]
Step 9: WAP構成
Fleet Manager でWAPサーバーに接続し、以下の手順でWAPを構成します。
9-1. DNS解決設定
WAPサーバーのhostsファイルに、ADFS FQDNとInternal NLBのIPアドレスのマッピングを追加します。WAPはADFS FQDNに対してHTTPS通信を行いますが、通常のDNS解決ではExternal NLBに解決されてしまうため、hostsファイルでInternal NLB経由に向ける必要があります。
$internalNlbDns = "<Internal NLB DNS名>" $adfsIps = [System.Net.Dns]::GetHostAddresses($internalNlbDns) | Select-Object -ExpandProperty IPAddressToString $hostsEntry = "$($adfsIps[0]) adfs.example.com" Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $hostsEntry
9-2. ADFS証明書のインポート
ADFSサーバーから証明書をPFXファイルとしてエクスポートし、S3経由でWAPサーバーに転送してインポートします。
# WAPサーバーで証明書をインポート $password = ConvertTo-SecureString -String "<エクスポートパスワード>" ` -Force -AsPlainText Import-PfxCertificate -FilePath "C:\adfs-cert.pfx" ` -CertStoreLocation Cert:\LocalMachine\My -Password $password
9-3. Proxy Trust確立
WAPとADFS間の信頼関係を確立します。
$cred = Get-Credential # CORP\Administrator Install-WebApplicationProxy ` -FederationServiceName "adfs.example.com" ` -FederationServiceTrustCredential $cred ` -CertificateThumbprint "<証明書Thumbprint>"
9-4. ADFSアプリケーション公開
ADFSをWAP経由で外部に公開します。
Add-WebApplicationProxyApplication ` -Name "ADFS" ` -ExternalUrl "https://adfs.example.com/adfs/ls/" ` -ExternalCertificateThumbprint "<証明書Thumbprint>" ` -BackendServerUrl "https://adfs.example.com/adfs/ls/" ` -ExternalPreauthentication PassThrough
以上



