こんにちは、広野です。
AWS Cloud9 に代わる、使い勝手の良い IDE on AWS を作れないものか、、、日々模索しています。
今回は、code-server を HTTPS 公開して MFA 認証を付ける構成の実装編として AWS CloudFormation テンプレートを紹介します。
前回の記事
背景やアーキテクチャ説明については、前回記事をご覧ください。
アーキテクチャ
前回記事からの再掲で、図を載せておきます。
AWS CloudFormation テンプレート
テンプレートは大きく 2 つに分かれています。
- ユーザー共通で使用する Amazon Cognito ユーザープール 【既存ユーザープールも条件付きで使用可】
- ユーザー単位で作成する Amazon EC2 インスタンス、他
以下は本テンプレートを使用する際に前提として必要となるリソースです。
- Amazon Route 53 パブリックホストゾーン: 証明書作成やレコード登録に必要
- Amazon VPC: 既存のものを使用する想定、パブリックサブネットが必要
- Amazon SES ID および 設定セット: Amazon SES で Amazon Cognito からの通知メールを送るために必要
本記事のテンプレートを加工すれば 既存の Amazon Cognito ユーザープールを使用可能ですが、メールアドレスを属性として持っていること、Amazon Cognito マネージドログインのドメイン設定ができていることが前提となります。Amazon Cognito ID プールは不要です。
Amazon SES に関する説明は以下の記事をご覧ください。
AWS CloudFormation テンプレートの記述に関する補足は # コメントでインラインで追加します。
【ユーザー共通】Amazon Cognito ユーザープール
他の記事でも紹介していますが、より簡略化した設定になっています。セルフサインアップ可能なメールアドレスをドメイン名で制限するための AWS Lambda 関数を組み込んでいます。
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a Cognito user pool with Managed Login, self-signup by email, and forced MFA. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 SesId: Type: String Description: Amazon SES ID for sending emails. (email addreess or domain) Default: xxxx.xxx MaxLength: 100 MinLength: 5 SesConfigurationSet: Type: String Description: Amazon SES configuration set for sending emails. Default: xxxxxxxxxxxxxxxx MaxLength: 100 MinLength: 5 CognitoReplyTo: Type: String Description: Cognito Reply-to email address. (e.g. xxx@xxx.xxx) Default: xxxxx@xxx.xxx AllowedPattern: "[^\\s@]+@[^\\s@]+\\.[^\\s@]+" CognitoEmailFrom: Type: String Description: Cognito e-mail from address. (e.g. xxx@xxx.xxx) Default: xxxxx@xxx.xxx AllowedPattern: "[^\\s@]+@[^\\s@]+\\.[^\\s@]+" AllowedUserEmailDomains: Description: Domain list to allow user sign up. Each domains must be comma delimited and double quoted. Type: String Default: '"xxx.xxx"' Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Configuration" Parameters: - SystemName - SubName - Label: default: "Email Configuration" Parameters: - SesId - SesConfigurationSet - CognitoReplyTo - CognitoEmailFrom - Label: default: "Security Configuration" Parameters: - AllowedUserEmailDomains Resources: # ------------------------------------------------------------# # Lambda (triggered from Cognito) Role (IAM) # ------------------------------------------------------------# LambdaTriggeredFromCognitoRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-LambdaTriggeredFromCognitoRole-${SubName} Description: This role grants Lambda functions triggered from Cognito basic priviledges. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess - arn:aws:iam::aws:policy/AmazonCognitoPowerUser # ------------------------------------------------------------# # Cognito Lambda Invocation Permission # ------------------------------------------------------------# CognitoLambdaInvocationPermissionPresignup: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt LambdaCognitoPresignup.Arn Action: lambda:InvokeFunction Principal: cognito-idp.amazonaws.com SourceAccount: !Sub ${AWS::AccountId} SourceArn: !GetAtt UserPool.Arn DependsOn: - LambdaCognitoPresignup - UserPool CognitoLambdaInvocationPermissionPostconfirm: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt LambdaCognitoPostconfirm.Arn Action: lambda:InvokeFunction Principal: cognito-idp.amazonaws.com SourceAccount: !Sub ${AWS::AccountId} SourceArn: !GetAtt UserPool.Arn DependsOn: - LambdaCognitoPostconfirm - UserPool # ------------------------------------------------------------# # Cognito # ------------------------------------------------------------# UserPool: Type: AWS::Cognito::UserPool Properties: UserPoolName: !Sub ${SystemName}-${SubName} MfaConfiguration: "ON" EnabledMfas: - SOFTWARE_TOKEN_MFA Policies: PasswordPolicy: MinimumLength: 8 RequireUppercase: true RequireLowercase: true RequireNumbers: true RequireSymbols: false TemporaryPasswordValidityDays: 180 AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: AllowAdminCreateUserOnly: false AutoVerifiedAttributes: - email DeviceConfiguration: ChallengeRequiredOnNewDevice: false DeviceOnlyRememberedOnUserPrompt: false EmailConfiguration: ConfigurationSet: !Ref SesConfigurationSet EmailSendingAccount: DEVELOPER From: !Sub "${SystemName}-${SubName} admin <${CognitoEmailFrom}>" ReplyToEmailAddress: !Ref CognitoReplyTo SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/${SesId} EmailVerificationMessage: !Sub "${SystemName}-${SubName} Verification code: {####}" EmailVerificationSubject: !Sub "${SystemName}-${SubName} Verification code" LambdaConfig: PreSignUp: !GetAtt LambdaCognitoPresignup.Arn PostConfirmation: !GetAtt LambdaCognitoPostconfirm.Arn UsernameAttributes: - email UsernameConfiguration: CaseSensitive: false UserPoolAddOns: AdvancedSecurityMode: "OFF" UserPoolTags: Cost: !Sub ${SystemName}-${SubName} # この UserPoolDomain 設定は、Amazon Cognito マネージドログインを使用するために必要です。 # Version 2 つまりマネージドログインを使用する設定にしています。 # Version 1 だと旧型の hosted UI になります。 UserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Sub ${SystemName}-${SubName} ManagedLoginVersion: 2 UserPoolId: !Ref UserPool UserPoolGroupBasic: Type: AWS::Cognito::UserPoolGroup Properties: Description: The User Group for standard users. GroupName: BASIC Precedence: 101 UserPoolId: !Ref UserPool # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaCognitoPresignup: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${SystemName}-CognitoPresignup-${SubName} Description: !Sub Lambda Function triggered from Cognito before user self signup to check the user's email domain for ${SystemName}-${SubName} Runtime: python3.13 Timeout: 3 MemorySize: 128 Role: !GetAtt LambdaTriggeredFromCognitoRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} Code: ZipFile: !Sub | import re def lambda_handler(event, context): try: print(event) triggersource = event['triggerSource'] email = event['request']['userAttributes']['email'] print(email) if triggersource == 'PreSignUp_SignUp': print('via self signup') domain = email.split('@')[1] allowedDomains = [ ${AllowedUserEmailDomains} ] if domain in allowedDomains: print('allowed domain and email account') return event else: print('prohibited domain') return None else: print('via admin console') return event except Exception as e: print(str(e)) DependsOn: LambdaTriggeredFromCognitoRole LambdaCognitoPostconfirm: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${SystemName}-CognitoPostconfirm-${SubName} Description: !Sub Lambda Function triggered from Cognito after user confirmation to add the user in BASIC group for ${SystemName}-${SubName} Runtime: python3.13 Timeout: 3 MemorySize: 128 Role: !GetAtt LambdaTriggeredFromCognitoRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} Code: ZipFile: !Sub | import boto3 client = boto3.client('cognito-idp', '${AWS::Region}') def lambda_handler(event, context): USERPOOLID = event['userPoolId'] USERNAME = event['userName'] try: print(event) res = client.admin_add_user_to_group( UserPoolId=USERPOOLID, Username=USERNAME, GroupName='BASIC' ) return event except Exception as e: print(str(e)) DependsOn: LambdaTriggeredFromCognitoRole # ------------------------------------------------------------# # Output Parameters この出力は次のテンプレートで使用します。 # ------------------------------------------------------------# Outputs: # Cognito CognitoUserPoolId: Value: !Ref UserPool Export: Name: !Sub CognitoUserPoolId-${SystemName}-${SubName} CognitoUserPoolArn: Value: !GetAtt UserPool.Arn Export: Name: !Sub CognitoUserPoolArn-${SystemName}-${SubName} CognitoUserPoolProviderName: Value: !GetAtt UserPool.ProviderName Export: Name: !Sub CognitoUserPoolProviderName-${SystemName}-${SubName} CognitoUserPoolProviderUrl: Value: !GetAtt UserPool.ProviderURL Export: Name: !Sub CognitoUserPoolProviderUrl-${SystemName}-${SubName}
【ユーザー単位】Amazon EC2 インスタンス、他付属リソース
ユーザー単位で Amazon EC2 インスタンスを作成するとともに、その EC2 インスタンス専用の Amazon Cognito ユーザープール「アプリケーションクライアント」を作成します。アプリケーションクライアントは上述のテンプレートで作成した Amazon Cognito ユーザープールに関連付けます。ここで作成したアプリケーションクライアントにはクライアントシークレットを作成しており、その Amazon EC2 インスタンスしか知りません。(まあ、マネジメントコンソールを見てしまえばわかるんですが、外部の人が簡単にわかるものではない) それを知ろうとするために、Amazon Cognito ユーザープールを参照可能な IAM ロール権限を付与しています。
Amazon EC2 インスタンスには Elastic IP アドレスを割り当て、それを指定した Amazon Route 53 パブリックホストゾーンに A レコード登録します。FQDN に合わせた SSL 証明書を Let’s Encrypt で作成します。Let’s Encrypt に証明書を作成依頼するときの認証方法は DNS-01 という方式を選定しています。これは Amazon Route 53 パブリックホストゾーンに登録した認証用レコードを Let’s Encrypt のシステム側が確認することでドメインの所有者確認をする方法です。そのため、この EC2 インスタンスの IAM ロールにレコード登録可能な権限を付与しています。
などなど、いろいろと複合技を仕掛けています。内容が盛り沢山で既に私もおなかいっぱいです。
テンプレート内にインラインの説明コメントが多くなると思いますが、ご容赦ください。
AWSTemplateFormatVersion: "2010-09-09" Description: The CloudFormation template that creates an Amazon Linux 2023 x86_64 EC2 instance with code-server and Cognito authentication. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. xxxxx.xxx Default: example.com AllowedPattern: "^(?!-)(?:[a-zA-Z0-9-]{0,62}[a-zA-Z0-9])(?:\\.(?!-)(?:[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]))*$" # VPC やパブリックサブネットは既存のものを選択してください。 VpcId: Type: AWS::EC2::VPC::Id Description: Choose a existing VPC ID you deploy the EC2 instance in. InstanceSubnet: Type: AWS::EC2::Subnet::Id Description: Choose an existing Public Subnet ID you deploy the EC2 instance in. # EC2 インスタンスタイプは適宜変更してください。節約のため t3a にしているだけです。medium でもやや重いと思います。 InstanceType: Type: String Description: EC2 instance type (AMD/t3a) Default: t3a.medium AllowedValues: - t3a.medium - t3a.large - t3a.xlarge # EBS のサイズも適宜変更してください。 InstanceVolumeSize: Type: Number Description: The volume size in GB Default: 20 # アクセスを許可するソースグローバル IP アドレスのサブネットを指定してください。 AllowedSubnet: Type: String Description: Allowed source IPv4 subnet and subnet mask. (e.g. xxx.xxx.xxx.xxx/32) Default: 111.111.111.111/32 AllowedPattern: "^((25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})\\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})\\/([0-9]|[1-2][0-9]|3[0-2])$" TimeZone: Type: String Description: The specified time zone. Default: Asia/Tokyo AllowedPattern: "^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/[A-Za-z0-9_/-]+$" Route53HostZoneId: Type: String Description: Route 53 public host zone ID for the SSL certificate. Default: XXXXXXXXXXXXXXXXXXXX MaxLength: 30 MinLength: 1 YourId: Type: String Description: The individual instance id (or number) specified from the administrator. Default: client01 MaxLength: 20 MinLength: 1 # ここで指定したメールアドレスで Amazon Cognito ユーザーを作成する必要があります。 YourEmail: Type: String Description: Your email address for the access control. Default: xxx@xxx.xxx AllowedPattern: "[^\\s@]+@[^\\s@]+\\.[^\\s@]+" # OAuth2 Proxy のバージョンを入れてください。7.12.0 で動作確認済みですが、 # バージョンが変わるとコマンド仕様が変わる恐れがあります。 Oauth2ProxyVersion: Type: String Description: oauth2-proxy version. Default: 7.12.0 MaxLength: 10 MinLength: 4 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Configuration" Parameters: - SystemName - SubName - YourId - YourEmail - Label: default: "Domain Configuration" Parameters: - DomainName - Route53HostZoneId - Label: default: "Network Configuration" Parameters: - VpcId - InstanceSubnet - AllowedSubnet - Oauth2ProxyVersion - Label: default: "EC2 Configuration" Parameters: - InstanceType - InstanceVolumeSize - Label: default: "OS Configuration" Parameters: - TimeZone Resources: # ------------------------------------------------------------# # EC2 Launch template # ------------------------------------------------------------# EC2LaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateName: !Sub codeserver-${SystemName}-${SubName}-${YourId} LaunchTemplateData: InstanceType: !Ref InstanceType # 使用する AMI は Amazon Linux 2023 x86_64 の最新版なのですが、スタックを変更するときに # 新しい AMI ID がリリースされていると EC2 インスタンスを再作成してしまうので注意が必要です。 # 正直この指定は良くないと思っていますが楽さに負けて使ってしまいます。 ImageId: >- {{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}} BlockDeviceMappings: - Ebs: VolumeSize: !Ref InstanceVolumeSize VolumeType: gp3 DeleteOnTermination: true Encrypted: true DeviceName: /dev/xvda NetworkInterfaces: - SubnetId: !Ref InstanceSubnet Groups: - !Ref Ec2SecurityGroup DeviceIndex: 0 AssociatePublicIpAddress: true MetadataOptions: HttpTokens: required Monitoring: Enabled: true TagSpecifications: - ResourceType: volume Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} DependsOn: - Ec2SecurityGroup # ------------------------------------------------------------# # EC2 Security Group # ------------------------------------------------------------# Ec2SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref VpcId GroupDescription: Allow HTTPS access from specified subnets SecurityGroupIngress: # セキュリティグループは 443 だけ開けていますが、必要に応じて変更してください。 - Description: Allow HTTPS from the specified client FromPort: 443 IpProtocol: tcp ToPort: 443 CidrIp: !Ref AllowedSubnet SecurityGroupEgress: - Description: Allow all outbound traffic IpProtocol: -1 CidrIp: 0.0.0.0/0 Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} - Key: Name Value: !Sub SG-codeserver-${SystemName}-${SubName}-${YourId} # ------------------------------------------------------------# # EC2 Role / Instance Profile (IAM) # ------------------------------------------------------------# Ec2Role: Type: AWS::IAM::Role Properties: RoleName: !Sub Ec2Role-codeserver-${SystemName}-${SubName}-${YourId} Description: This role allows EC2 instance to invoke S3 and SSM. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: # Session Manager で CUI ログインできるようにしています。トラブルシューティング時に。 - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore # AWS CodeCommit リポジトリにアクセスできるようにしています。 - arn:aws:iam::aws:policy/AWSCodeCommitPowerUser - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy Policies: - PolicyName: !Sub Ec2Policy-codeserver-${SystemName}-${SubName}-${YourId} PolicyDocument: Version: 2012-10-17 Statement: # Amazon Q Developer の権限を付けています。 - Effect: Allow Action: - codewhisperer:GenerateRecommendations Resource: "*" # Amazon Route 53 の必要な権限です。Let's Encrypt の SSL 証明書作成に必要です。 - Effect: Allow Action: - route53:ListHostedZones - route53:GetChange Resource: - "*" - Effect: Allow Action: - route53:ChangeResourceRecordSets Resource: - !Sub arn:aws:route53:::hostedzone/${Route53HostZoneId} # Amazon Cognito ユーザープールアプリケーションクライアントのクライアントシークレット取得用です。 - Effect: Allow Action: - cognito-idp:DescribeUserPoolClient Resource: - Fn::ImportValue: !Sub CognitoUserPoolArn-${SystemName}-${SubName} Ec2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Ref Ec2Role Path: / Roles: - !Ref Ec2Role DependsOn: - Ec2Role # ------------------------------------------------------------# # Elastic IP address for EC2 instance # ------------------------------------------------------------# EipEc2: Type: AWS::EC2::EIP Properties: Domain: vpc Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} - Key: Name Value: !Sub eip-codeserver-${SystemName}-${SubName}-${YourId} # ------------------------------------------------------------# # EIP Association # ------------------------------------------------------------# EIPAssociation: Type: AWS::EC2::EIPAssociation Properties: AllocationId: !GetAtt EipEc2.AllocationId InstanceId: !Ref Ec2Instance DependsOn: - Ec2Instance - EipEc2 # ------------------------------------------------------------# # EC2 # ------------------------------------------------------------# Ec2Instance: Type: AWS::EC2::Instance Properties: IamInstanceProfile: !Ref Ec2InstanceProfile LaunchTemplate: LaunchTemplateId: !Ref EC2LaunchTemplate Version: !GetAtt EC2LaunchTemplate.LatestVersionNumber UserData: Fn::Base64: !Sub - | #!/bin/bash set -euxo pipefail # Locale & timezone timedatectl set-timezone ${TimeZone} # Install packages dnf update -y # とりあえず Git, Node.js, Python をインストールしています。 dnf install -y git nodejs gcc make zlib-devel bzip2-devel xz-devel curl-devel libffi-devel python3.13 python3-pip python3-boto3 python3-pytest # Install code-server export HOME=/home/ec2-user curl -fsSL https://code-server.dev/install.sh | bash mkdir -p /home/ec2-user/.config/code-server mkdir -p /home/ec2-user/.local/share/code-server/User mkdir -p /home/ec2-user/environment chown -R ec2-user:ec2-user /home/ec2-user/.config /home/ec2-user/.local /home/ec2-user/environment # Update code-server config # code-server は 8008 ポートで起動しています。よく使う 80 や 8080 と重複しないようにするため。 # code-server ネイティブの認証は無し(none)にしています。Amazon Cognito を使用するため。 tee /home/ec2-user/.config/code-server/config.yaml << EOF bind-addr: 127.0.0.1:8008 auth: none cert: false EOF chown ec2-user:ec2-user /home/ec2-user/.config/code-server/config.yaml tee /home/ec2-user/.local/share/code-server/User/settings.json << EOF { "extensions.autoUpdate": false, "extensions.autoCheckUpdates": false, "terminal.integrated.cwd": "/home/ec2-user/environment", "telemetry.telemetryLevel": "off", "security.workspace.trust.startupPrompt": "never", "security.workspace.trust.enabled": false, "security.workspace.trust.banner": "never", "security.workspace.trust.emptyWindow": false, "editor.indentSize": "tabSize", "editor.tabSize": 2, "python.testing.pytestEnabled": true, "auto-run-command.rules": [ { "command": "workbench.action.terminal.new" } ] } EOF chown ec2-user:ec2-user /home/ec2-user/.local/share/code-server/User/settings.json # Service start systemctl enable --now code-server@ec2-user # Create SSL cert # Let's Encrypt で SSL 証明書を作成するためのモジュール、Amazon Route 53 対応のものをインストールしています。 dnf install -y certbot python3-certbot-dns-route53 # 証明書を DNS-01 という認証方式で作成するコマンドです。 certbot certonly --dns-route53 -d codeserver-${SystemName}-${SubName}-${YourId}.${DomainName} --agree-tos -m ${YourEmail} --non-interactive --preferred-challenges dns-01 # Install oauth2-proxy curl -L -o /tmp/oauth2-proxy.tar.gz https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v${Oauth2ProxyVersion}/oauth2-proxy-v${Oauth2ProxyVersion}.linux-amd64.tar.gz tar -xzf /tmp/oauth2-proxy.tar.gz -C /tmp cp /tmp/oauth2-proxy-v${Oauth2ProxyVersion}.linux-amd64/oauth2-proxy /usr/local/bin/ chmod +x /usr/local/bin/oauth2-proxy # ここで、Amazon Cognito ユーザープールアプリケーションクライアントのクライアントシークレットを取得します。 CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \ --user-pool-id ${UserPoolId} \ --client-id ${UserPoolAppClient} \ --region ${AWS::Region} \ --query 'UserPoolClient.ClientSecret' \ --output text) # クッキーシークレットを生成します。 COOKIE_SECRET=$(python3.13 -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())') mkdir -p /etc/oauth2-proxy chown ec2-user:ec2-user /etc/oauth2-proxy chmod 700 /etc/oauth2-proxy # 以下は自分のメールアドレス以外ではログインを許可しない設定です。 echo ${YourEmail} > /etc/oauth2-proxy/allowed_emails.txt chmod 600 /etc/oauth2-proxy/allowed_emails.txt chown ec2-user:ec2-user /etc/oauth2-proxy/allowed_emails.txt # 以下は OAuth2 Proxy をサービスとして起動するための設定です。 # Amazon Cognito とやり取りするために、この通り記述する必要があります。 cat <<EOF > /etc/systemd/system/oauth2-proxy.service [Unit] Description=oauth2-proxy After=network.target [Service] Type=simple Environment=CLIENT_SECRET=${!CLIENT_SECRET} Environment=COOKIE_SECRET=${!COOKIE_SECRET} ExecStart=/usr/local/bin/oauth2-proxy \ --provider=oidc \ --oidc-issuer-url=${UserPoolProviderUrl} \ --client-id=${UserPoolAppClient} \ --client-secret=$CLIENT_SECRET \ --redirect-url=https://codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/oauth2/callback \ --login-url=https://${SystemName}-${SubName}.auth.${AWS::Region}.amazoncognito.com/oauth2/authorize \ --profile-url=https://${SystemName}-${SubName}.auth.${AWS::Region}.amazoncognito.com/oauth2/userInfo \ --redeem-url=https://${SystemName}-${SubName}.auth.${AWS::Region}.amazoncognito.com/oauth2/token \ --scope=openid \ --cookie-secure=true \ --cookie-expire=72h \ --cookie-refresh=1h \ --cookie-secret=$COOKIE_SECRET \ --authenticated-emails-file=/etc/oauth2-proxy/allowed_emails.txt \ --http-address=127.0.0.1:4180 Restart=on-failure User=ec2-user Group=ec2-user [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now oauth2-proxy # Install nginx # nginx は code-server を HTTPS 化するためにインストールします。SSL 証明書はここで使われます。 dnf install -y nginx systemctl enable --now nginx mkdir -p /etc/nginx/ssl chmod 700 /etc/nginx/ssl cp /etc/letsencrypt/live/codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/fullchain.pem /etc/nginx/ssl/nginx-chain.crt cp /etc/letsencrypt/live/codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/privkey.pem /etc/nginx/ssl/nginx.key chmod 644 /etc/nginx/ssl/nginx-chain.crt chmod 600 /etc/nginx/ssl/nginx.key # nginx が OAuth2 Proxy とやり取りするためにこの通りパスの設定をする必要があります。 # proxy_buffer 系の設定が重要で、これが大きくないとエラーになります。 tee /etc/nginx/conf.d/codeserver.conf << EOF server { listen 443 ssl; server_name codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}; large_client_header_buffers 4 16k; ssl_certificate_key /etc/nginx/ssl/nginx.key; ssl_certificate /etc/nginx/ssl/nginx-chain.crt; location / { auth_request /oauth2/auth; error_page 401 = /oauth2/sign_in; proxy_pass http://127.0.0.1:8008; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } location /oauth2/ { proxy_pass http://127.0.0.1:4180; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Auth-Request-Redirect \$request_uri; proxy_buffers 16 16k; proxy_buffer_size 16k; } location = /oauth2/auth { proxy_pass http://127.0.0.1:4180; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Scheme \$scheme; proxy_set_header Content-Length ""; proxy_pass_request_body off; proxy_buffers 16 16k; proxy_buffer_size 16k; } } EOF systemctl restart nginx # Create cert renew script # 以下は Let's Encrypt の SSL 証明書を更新するためのスクリプトです。ec2-user のホームディレクトリに配置します。 cat << 'EOF' > /home/ec2-user/renew-cert.sh #!/bin/bash set -e sudo certbot renew sudo cp -f /etc/letsencrypt/live/codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/fullchain.pem /etc/nginx/ssl/nginx-chain.crt sudo cp -f /etc/letsencrypt/live/codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/privkey.pem /etc/nginx/ssl/nginx.key sudo chmod 644 /etc/nginx/ssl/nginx-chain.crt sudo chmod 600 /etc/nginx/ssl/nginx.key sudo systemctl restart nginx echo "Certificate renewed." EOF chmod +x /home/ec2-user/renew-cert.sh chown ec2-user:ec2-user /home/ec2-user/renew-cert.sh # Create sign out file # 以下はサインアウト用の URL を md ファイルにして code-server のホームディレクトリに配置します。 tee /home/ec2-user/environment/signout.md > /dev/null << EOF [Sign Out](https://${SystemName}-${SubName}.auth.${AWS::Region}.amazoncognito.com/logout?client_id=${UserPoolAppClient}&logout_uri=https%3A%2F%2Fcodeserver-${SystemName}-${SubName}-${YourId}.${DomainName}%2Foauth2%2Fsign_out) EOF chown ec2-user:ec2-user /home/ec2-user/environment/signout.md chmod 644 /home/ec2-user/environment/signout.md # Download Amazon Q Developer CLI # Amazon Q Developer CLI のインストーラをダウンロードしています。 # インストールはサインインが必要なため、ダウンロードにとどめています。 sudo -i -u ec2-user bash << 'EOF' curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip" -o "q.zip" unzip q.zip EOF # Install nvm # Node.js のバージョン管理ツール nvm をインストールします。 # インストールは OS 再起動が必要なので最後に入れています。 # 必要なバージョンを nvm install コマンドで手動で入れましょう。 sudo -u ec2-user bash -c "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash" # Finally, reboot reboot - UserPoolProviderUrl: Fn::ImportValue: !Sub CognitoUserPoolProviderUrl-${SystemName}-${SubName} UserPoolId: Fn::ImportValue: !Sub CognitoUserPoolId-${SystemName}-${SubName} Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} - Key: Name Value: !Sub codeserver-${SystemName}-${SubName}-${YourId} DependsOn: - Ec2InstanceProfile # Route 53 に Elastic IP アドレスの A レコードが登録されないと Let's Encrypt の SSL 証明書作成が # 失敗するので、明示的に DependsOn 設定を入れています。 - Route53RecordA - UserPoolAppClient # ------------------------------------------------------------# # Route 53 # ------------------------------------------------------------# Route53RecordA: Type: AWS::Route53::RecordSet Properties: HostedZoneName: !Sub ${DomainName}. Name: !Sub codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}. Type: A TTL: 300 ResourceRecords: - !GetAtt EipEc2.PublicIp DependsOn: EipEc2 # ------------------------------------------------------------# # Cognito User Pool App Client # ------------------------------------------------------------# UserPoolAppClient: Type: AWS::Cognito::UserPoolClient Properties: UserPoolId: Fn::ImportValue: !Sub CognitoUserPoolId-${SystemName}-${SubName} ClientName: !Sub ${SystemName}-${SubName}-${YourId} GenerateSecret: true RefreshTokenValidity: 3 AccessTokenValidity: 6 IdTokenValidity: 6 ExplicitAuthFlows: - ALLOW_USER_SRP_AUTH - ALLOW_REFRESH_TOKEN_AUTH PreventUserExistenceErrors: ENABLED SupportedIdentityProviders: - COGNITO # Amazon Cognito マネージドログインを使用するには、以下の URL 系の設定が必須です。 # ここの設定は繊細で、EC2 内でコマンド実行している nginx や OAuth2 Proxy の設定ときっちり # 合わせる必要があります。 # 特に LogoutURLs に指定した URL を get パラメータの1つとして完全一致させないと # ログアウト時に使用しないとログアウトに失敗します。 # ここでは意味不明かもしれませんがやってみるとわかります。 # その他、AuthFlow 系の設定も繊細だと思うので、ここに書いてある通りに書いておかないと # 動かなそうな気がします。 CallbackURLs: - !Sub https://codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/oauth2/callback LogoutURLs: - !Sub https://codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/oauth2/sign_out DefaultRedirectURI: !Sub https://codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/oauth2/callback AllowedOAuthFlows: - code AllowedOAuthFlowsUserPoolClient: true AllowedOAuthScopes: - openid - email - profile # Amazon Cognito マネージドログインを使用する場合、以下の設定が必要になります。 # デザインを細かくカスタマイズできそうなのですが、大変なので完全デフォルトにしました。 ManagedLoginBranding: Type: AWS::Cognito::ManagedLoginBranding Properties: ClientId: !Ref UserPoolAppClient ReturnMergedResources: true UseCognitoProvidedValues: true UserPoolId: Fn::ImportValue: !Sub CognitoUserPoolId-${SystemName}-${SubName} # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: # EC2 # 完成した code-server の URL です。 codeserverUrl: Value: !Sub "https://codeserver-${SystemName}-${SubName}-${YourId}.${DomainName}/?folder=/home/ec2-user/environment" # ログアウト時にアクセスする URL です。get パラメータがかなり繊細です。 signoutUrl: Value: !Sub "https://${SystemName}-${SubName}.auth.${AWS::Region}.amazoncognito.com/logout?client_id=${UserPoolAppClient}&logout_uri=https%3A%2F%2Fcodeserver-${SystemName}-${SubName}-${YourId}.${DomainName}%2Foauth2%2Fsign_out"
まとめ
いかがでしたでしょうか?
今回の題材は code-server でしたが、他の認証機能のない (または弱い) Web アプリケーションによりセキュアな認証機能を付けたいニーズにぴったりはまると思います。
本件は CloudFormation 化するのに苦労しました。しかし、ユーザーデータスクリプトが dnf install でしばしば失敗するのは勘弁して欲しいです。その度に /var/log/cloud-init-output.log を見るはめになります。とりあえず動くものを作りましたが、今後はもっと信頼性の高い仕組みを作りたいです。AMI 化すればいい、では済まない処理もあるので。
さすがにここまでの内容を独力で作れるわけもなく、生成 AI にかなり助けていただきました。例によって AI の回答のままでは動かないことが多かったので各種ツールのリファレンスと見比べながらの作業を強いられましたが、総合的に見て開発、勉強の両面で非常に効率が良かったことに間違いはないでしょう。
一連の動くコマンドや設定はここにありますので、要件に合わせて部分的に活用いただければと思っております。
本記事が皆様のお役に立てれば幸いです。
雑談
Let’s Encrypt はいいですね。AWS Certificate Manager にも証明書をエクスポートできる機能が新たにできましたが、金額が結構高いので Amazon EC2 インスタンスに証明書を組み込むなら Let’s Encrypt 一択かなと思います。更新もスクリプト用意しておけば思ったより楽でしたし。
nginx も便利すぎます。とりあえず HTTPS 化、なら Let’s Encrypt と nginx だけでいいですね。
OAuth2 Proxy は神です。Amazon Cognito マネージドログインとの組み合わせ構成は私の中で標準化されました。w