배경
AWS Client VPN 비용을 절감하기 위해, 직원들이 필요할 때 스스로 AWS Client VPN 연결을 설정할 수 있도록 계획했습니다.
해결책으로 Lambda 함수를 통해 연결을 실행하도록 결정했으며, 모든 직원이 이 Lambda 함수를 실행할 수 있도록 했습니다.
그러나 직원이 아닌 사람이 이를 실행하지 못하도록 하기 위해 Lambda 실행에 사용자 인증을 추가했습니다.
다음 기사에서 언급한 바와 같이, 저희 회사는 Active Directory를 사용하여 직원의 사용자 ID를 관리하고 있습니다.
따라서 Active Directory 사용자를 사용하여 인증을 구현할 수 있는지 검토하기 시작했습니다.
실패
실제 구성에 들어가기 전에 실패했던 시도를 공유하겠습니다.
처음 떠올린 방법은 API Gateway를 사용하여 Lambda 함수를 실행하고, API Gateway에서 Cognito 인증을 사용하는 것이었습니다.
Cognito의 콜백 URL을 API Gateway로 설정하면, Cognito의 로그인 URL을 통해 사용자를 인증하고 API Gateway가 요청을 실행할 수 있을 것이라 생각했습니다.
하지만 여기에는 문제가 있었습니다.
Cognito 로그인 URL에서 인증이 성공하면, id_token
이 콜백 URL의 쿼리 파라미터로 추가됩니다.
반면, API Gateway 인증에서는 토큰을 Authorization
헤더에 포함하여 요청해야 합니다.
즉, Cognito에서 API Gateway를 콜백 URL로 지정하더라도 API Gateway에서 인증이 성공하지 않는다는 것입니다.
Cognito 대신 Lambda 함수에서 사용자 정의 인증을 사용하는 것이 가능할 것 같았지만, 추가 개발 작업이 필요하기 때문에 일단 보류하기로 했습니다.
실패한 구성
방법
대안 구성으로 API Gateway 대신 ALB를 사용하기로 결정했습니다.
ALB에서도 Cognito를 이용한 사용자 인증이 가능합니다.
또한, Lambda 함수를 대상으로 지정하면 사용자 인증 후 요청을 전송하고 Lambda 함수를 실행할 수 있습니다.
게다가 Cognito 사용자로 IAM Identity Center를 통합할 수 있습니다.
IAM Identity Center는 Active Directory를 소스로 지정할 수 있으므로 IAM Identity Center를 통해 Cognito 인증에도 사용할 수 있습니다.
IAM Identity Center와 Amazon Cognito 사용자 풀을 통합하는 방법에 대한 자세한 내용은 다음을 참조하세요.
IAM Identity Center를 Amazon Cognito 사용자 풀과 통합하려면 어떻게 해야 하나요?
https://repost.aws/ko/knowledge-center/cognito-user-pool-iam-integration
구성은 다음과 같습니다.
관리자 단계
IAM Identity Center와 Active Directory의 연동은 이미 다른 기사에서 완료되었습니다.
따라서 이번에는 Amazon Cognito 및 ALB 설정에 초점을 맞추고 있습니다.
완료된 절차(파란색으로 표시됨)는 간단히 목적만 설명하며, 세부 단계는 아래 기사에서 확인할 수 있습니다.
Active Directory와 IAM Identity Center 연동
IAM Identity Center는 사용자를 AWS 서비스에 연결하는 서비스입니다.
IAM Identity Center 내에서 사용자를 직접 생성할 수도 있지만 Active Directory의 사용자와 동기화할 수도 있습니다.
저희 회사는 이미 다른 프로젝트에서 Active Directory 사용자와 IAM Identity Center를 동기화한 상태입니다.
자세한 단계는 아래 기사를 참조하십시오.
도메인 준비
ALB의 Cognito 인증은 HTTPS 리스너를 필요로 하므로 ACM을 통해 공인 인증서를 발급해야 합니다.
도메인이 있으면 절차가 간단해집니다. 본 기사에서는 awsexample.com
을 구입하여 사용했습니다.
- 다음 URL을 통해 Route 53에서 도메인을 구입합니다.
https://us-east-1.console.aws.amazon.com/route53/domains/home?region=us-east-1#/DomainSearch - 원하는 도메인이 사용 가능한지 확인합니다.
- 도메인의 가용성 확인:
awsexample.com
- 사용 가능하면 선택 후 결제로 진행합니다.
- 도메인의 가용성 확인:
이미지
- [1단계] 요금
- 기간: 1년
- 자동 갱신: ☑️ 활성
이미지
- [2단계] 연락처 정보
- 연락처 정보를 알파벳으로 입력합니다.
- 개인 정보 보호: ☑️
- 활성화 (연락처 공개 방지).
이미지
- [3단계] 검토 및 제출
이미지
- 도메인을 구입한 후, 공인 호스팅 영역이 생성되었는지 확인합니다.
https://us-east-1.console.aws.amazon.com/route53/v2/hostedzones?region=us-east-1#
ACM을 통해 인증서 발급
- 다음 URL에서 ACM 공인 인증서를 요청합니다.
https://us-east-1.console.aws.amazon.com/acm/home?region=us-east-1#/certificates/request/public- 완전히 정규화된 도메인 이름:
api.awsexample.com
- 이는 직원들이 시스템에 접근할 때 사용할 도메인입니다.
- 검증 방법: ⦿ DNS 검증 – 권장
- 완전히 정규화된 도메인 이름:
이미지
- [Route 53에서 레코드 생성]을 클릭하여
awsexample.com
호스팅 영역에 검증용 레코드가 자동으로 생성됩니다.- 검증이 완료되면 인증서가 발급됩니다.
이미지
Cognito 생성
- 다음 URL을 사용하여 Cognito 사용자 풀을 생성합니다:
https://us-east-1.console.aws.amazon.com/cognito/v2/idp/set-up-your-application?region=us-east-1- 애플리케이션 이름 지정: ALB for Client VPN
- 로그인 식별자에 대한 옵션: ☑️ 사용자 이름
- 가입에 필요한 필수 속성: email
- 리턴 URL:
https://api.awsexample.com/oauth2/idpresponse
- ALB에 할당할 도메인 이름 뒤에
/oauth2/idpresponse
를 추가합니다.
- ALB에 할당할 도메인 이름 뒤에
이미지
- Amazon Cognito 탐색 창에서 [개요] 및 [도메인]으로 이동하여 사용자 풀 ID와 Cognito 도메인을 확인합니다.
이러한 정보는 IAM Identity Center 통합 및 ALB 리스너 생성에 필요합니다.- 사용자 풀 ID:
u
s
-east-1_eXAMPLE
- Cognito 도메인:
https://
u
s
-east-1example.auth.us-east-1.amazoncognito.com
- 사용자 풀 ID:
이미지
IAM Identity Center 통합 준비
- 다음 URL을 사용하여 IAM Identity Center에서 애플리케이션을 생성합니다:
https://us-east-1.console.aws.amazon.com/singlesignon/applications/home?region=us-east-1#/add - [단계1] 애플리케이션 유형 선택
- 환경 설정: 설정하려는 애플리케이션이 있습니다
- 애플리케이션 유형: SAML 2.0
이미지
- [단계2] 애플리케이션 구성
- 애플리케이션 구성:
- 표시 이름: Amazon Cognito
- 설명: Amazon Cognito용 맞춤 SAML 2.0 애플리케이션
- 애플리케이션 메타데이터: ⦿ 메타데이터 값 수동 입력
- 애플리케이션 ACS URL:
https://us-east-1example.auth.us-east-1.amazoncognito.com/saml2/idpresponse
- Cognito 생성 [2단계]에서 확인한 Cognito 도메인에
/saml2/idpresponse
를 추가합니다.
- Cognito 생성 [2단계]에서 확인한 Cognito 도메인에
- 애플리케이션 SAML 대상자:
urn:amazon:cognito:sp:us-east-1_eXAMPLE
- Cognito 생성 [2단계]에서 확인한 사용자 풀 ID를 사용합니다.
- 애플리케이션 ACS URL:
- 이 시점에서 IAM Identity Center SAML 메타데이터 파일을 다운로드합니다.
이 파일은 Cognito 통합에 필요합니다.- IAM Identity Center SAML 메타데이터 파일:
https://portal.sso.us-east-1.amazonaws.com/saml/metadata/ExAmPlExAmPlExAmPlExAmPlExAmPlExAmPlExAmPlE
- IAM Identity Center SAML 메타데이터 파일:
- 애플리케이션 구성:
이미지
- 생성된 애플리케이션 세부정보에서 [작업] > [속성 매핑 편집]으로 이동하여 속성을 설정합니다.
애플리케이션 내 사용자 속성 | IAM Identity Center의 이 문자열 값 또는 사용자 속성으로 매핑 | 형식 |
---|---|---|
Subject | ${user:subject} | persistent |
${user:email} | basic |
이미지
- 생성된 애플리케이션 세부정보에서 [사용자 및 그룹 할당]을 통해 그룹을 할당합니다.
여기에서 할당된 그룹이 Amazon Cognito 인증에 성공하게 됩니다.
이 경우, 모든 직원이 포함된 그룹IAMIdentityCenterAllUsersAndGroups
를 할당합니다.
이미지
Cognito 통합 준비
- 다음 URL을 사용하여 IAM Identity Center를 ID 공급자로 추가합니다:
https://us-east-1.console.aws.amazon.com/cognito/v2/idp/user-pools/us-east-1_eXAMPLE/authentication/social-and-custom-providers/add?region=us-east-1- 연동 로그인 옵션: SAML
- 공급자 이름: IAMIdentityCenter
- 메타데이터 문서 소스: ⦿ 메타데이터 문서 엔드포인트 URL 입력
- https://portal.sso.ap-northeast-1.amazonaws.com/saml/metadata/ExAmPlExAmPlExAmPlExAmPlExAmPlExAmPlExAmPlE
- IAM Identity Center 통합 준비 [3단계]에서 확인한 IAM Identity Center SAML 메타데이터 파일 사용
- SAML 공급자와 사용자 풀 간 속성 매핑:
- 사용자 풀 속성:
email
- SAML 속성:
email
- 사용자 풀 속성:
이미지
- 다음 URL을 사용하여 애플리케이션 클라이언트 (
ALB for Client VPN
)의 ID 공급자를IAMIdentityCenter
로 변경합니다:
https://us-east-1.console.aws.amazon.com/cognito/v2/idp/user-pools/us-east-1_eXAMPLE/applications/app-clients/<App Client ID>/login-pages/edit/hosted-ui-settings?region=us-east-1ap-northeast-1_eXAMPLE
을 Cognito 생성 [2단계]에서 확인한 사용자 풀 ID로 바꿉니다.<App Client ID>
를 애플리케이션 클라이언트 ID로 바꿉니다.- ID 공급자: IAMIdentityCenter
이미지
Lambda 함수 생성
다음으로 ALB의 타겟이 될 Lambda 함수를 생성합니다.
- 다음 AWS CLI 명령을 실행하여 Lambda 함수용 IAM 역할을 생성합니다:
# Create IAM role
aws iam create-role \
--role-name LambdaRole-ClientVPN \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
# Attach the IAM policy required for Client VPN
aws iam attach-role-policy \
--role-name LambdaRole-ClientVPN \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess
# Attach the IAM policy required for logging
aws iam attach-role-policy \
--role-name LambdaRole-ClientVPN \
--policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
- 다음 URL을 사용하여 Lambda 함수를 생성합니다:
https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/create/function - 함수 설정:
- 함수 이름:
AWSClientVPN-AssociatingSubnets
- 런타임: Python 3.13
- 실행 역할:
LambdaRole-ClientVPN
- 함수 이름:
이미지
- 다음 URL을 사용하여 타임아웃을 1분으로 변경합니다:
https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/AWSClientVPN-AssociatingSubnets/edit/basic-settings?tab=configure- 타임아웃: 1분
이미지
- Lambda 함수의 코드를 추가합니다.
코드를 입력한 후 배포를 클릭하거나 Ctrl+Shift+U를 사용하여 배포해야 합니다.
코드의 대부분은 ChatGPT를 사용하여 생성되었으며 코드는 다음과 같습니다:
import boto3
import os
import json
def lambda_handler(event, context):
# Initialize the EC2 client
ec2 = boto3.client('ec2')
# Input parameters
client_vpn_endpoint_id = '<Your Client VPN endpoint ID>'
subnet_id = '<Your subnet ID for associating>'
try:
# Describe the Client VPN endpoint to check its status
endpoint_status_response = ec2.describe_client_vpn_endpoints(
ClientVpnEndpointIds=[client_vpn_endpoint_id]
)
endpoint_status = endpoint_status_response['ClientVpnEndpoints'][0]['Status']['Code']
print(f"Client VPN Endpoint Status: {endpoint_status}")
# Check if the status is 'pending-associate'
if endpoint_status.lower() == 'pending-associate':
# Associate the Client VPN endpoint with the target subnet
response = ec2.associate_client_vpn_target_network(
ClientVpnEndpointId=client_vpn_endpoint_id,
SubnetId=subnet_id
)
# Return response details
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json'
},
'body': json.dumps({
'Message': 'Successfully associated Client VPN endpoint with subnet.',
'AssociationId': response['AssociationId'],
'Status': response['Status']
})
}
else:
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json'
},
'body': json.dumps({
'Message': f'Client VPN Endpoint is not in pending-associate state. Current status: {endpoint_status}'
})
}
except Exception as e:
print(f"Error associating Client VPN endpoint: {str(e)}")
return {
'statusCode': 500,
'headers': {
'Content-Type': 'application/json'
},
'body': json.dumps({
'Message': 'Failed to associate Client VPN endpoint.',
'Error': str(e)
})
}
ALB 생성
타겟 그룹 생성부터 시작합니다.
ALB 생성은 본문의 주요 주제가 아니므로 여기서부터 AWS CLI 명령어를 사용하여 설정을 진행합니다.
- 다음 AWS CLI 명령어를 사용하여 타겟 그룹을 생성합니다
(참고: 관리 콘솔에서 타겟 그룹을 생성하면 리소스 기반 정책이 자동으로 생성되므로 세 번째 AWS CLI 명령어는 불필요합니다.)
LAMBDA_FUNCTION_NAME="AWSClientVPN-AssociatingSubnets"
# Create a target group
TARGET_GROUP_ARN=$(aws elbv2 create-target-group \
--name AssociatingSubnets-alb \
--target-type lambda \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
# Get the ARN of the Lambda function
LAMBDA_FUNCTION_ARN=$(aws lambda get-function \
--function-name $LAMBDA_FUNCTION_NAME \
--query 'Configuration.FunctionArn' \
--output text)
# Add permission for the ALB to invoke the Lambda function
aws lambda add-permission \
--function-name $LAMBDA_FUNCTION_NAME \
--statement-id "AWS-ALB_Invoke-targetgroup" \
--action lambda:InvokeFunction \
--principal elasticloadbalancing.amazonaws.com \
--source-arn $TARGET_GROUP_ARN
# Register the Lambda function
aws elbv2 register-targets \
--target-group-arn $TARGET_GROUP_ARN \
--targets Id=$LAMBDA_FUNCTION_ARN
- 다음 AWS CLI 명령어를 사용하여 HTTPS 트래픽을 허용하는 ALB용 보안 그룹을 생성합니다.
VPC_ID="<Your VPC ID>"
# Create a security group for the ALB
SECURITY_GROUP_ID=$(aws ec2 create-security-group \
--group-name ALB-sg \
--description "Security group for ALB allowing HTTPS traffic" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
# Add an inbound rule to allow HTTPS traffic
aws ec2 authorize-security-group-ingress \
--group-id $SECURITY_GROUP_ID \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
- 다음으로 아래 AWS CLI 명령어를 사용하여 ALB를 생성합니다.
ALB에는 최소 두 개의 AZ에 있는 서브넷을 지정해야 합니다.
PUBLIC_SUBNET1="<1st public subnet ID>"
PUBLIC_SUBNET2="<2nd public subnet ID>"
# Create an ALB
LOAD_BALANCER_ARN=$(aws elbv2 create-load-balancer \
--name AssociatingSubnets-alb \
--subnets $PUBLIC_SUBNET1 $PUBLIC_SUBNET2 \
--security-groups $SECURITY_GROUP_ID \
--scheme internet-facing \
--type application \
--ip-address-type ipv4 \
--query 'LoadBalancers[0].LoadBalancerArn' \
--output text)
- 다음 AWS CLI 명령어를 사용하여 HTTPS 리스너를 생성합니다.
Cognito 생성 [2단계] 에서 확인한 사용자 풀 ID 및 Cognito 도메인을 사용합니다.
DOMAIN="api.awsexample.com"
USER_POOL_ID="ap-northeast-1_eXAMPLE"
COGNITO_DOMAIN="https://ap-northeast-1example.auth.ap-northeast-1.amazoncognito.com"
COGNITO_DOMAIN_PREFIX=$(echo $COGNITO_DOMAIN | awk -F[/:] '{print $4}' | cut -d. -f1)
# Get the ARN of the ACM certificate
CERTIFICATE_ARN=$(aws acm list-certificates \
--query "CertificateSummaryList[?DomainName=='${DOMAIN}'].CertificateArn" \
--output text)
# Get the ARN of the Cognito User Pool
COGNITO_USER_POOL_ARN=$(aws cognito-idp describe-user-pool \
--user-pool-id $USER_POOL_ID \
--query 'UserPool.Arn' \
--output text)
# Get the App Client ID for the User Pool
COGNITO_APP_CLIENT_ID=$(aws cognito-idp list-user-pool-clients \
--user-pool-id $USER_POOL_ID \
--query 'UserPoolClients[0].ClientId' \
--output text)
# Create an HTTPS listener with Cognito authentication
aws elbv2 create-listener \
--load-balancer-arn $LOAD_BALANCER_ARN \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=$CERTIFICATE_ARN \
--default-actions Type=authenticate-cognito,Order=1,AuthenticateCognitoConfig="{UserPoolArn=$COGNITO_USER_POOL_ARN,UserPoolClientId=$COGNITO_APP_CLIENT_ID,UserPoolDomain=$COGNITO_DOMAIN_PREFIX}" Type=forward,Order=2,TargetGroupArn=$TARGET_GROUP_ARN
- 마지막으로
api.awsexample.com
에 접속했을 때 ALB로 라우팅되도록 Route 53 호스트 존에 레코드를 생성합니다.
이전에 ALB DNS의 호스트 존 ID를 공식 문서를 통해 확인하세요.
Service endpoints
https://docs.aws.amazon.com/ko_kr/general/latest/gr/elb.html
리전 이름 지역 엔드포인트 Route 53 호스팅 영역 ID(Application Load Balancers, Classic Load Balancers) Route 53 호스팅 영역 ID(Network Load Balancers) 미국 동부(버지니아 북부) us-east-1 elasticloadbalancing.us-east-1.amazonaws.com
elasticloadbalancing-fips.us-east-1.amazonaws.comZ35SXDOTRQ7X7K Z26RNL4JYFTOTI
HOSTEDZONE_ID_ALB="Z35SXDOTRQ7X7K"
# Get hosted zone ID of awsexample.com
HOSTEDZONE_ID_R53=$(aws route53 list-hosted-zones-by-name \
--dns-name awsexample.com \
--query "HostedZones[0].Id" \
--output text)
# Get domain name of ALB
ALB_DNS=$(aws elbv2 describe-load-balancers \
--query "LoadBalancers[?LoadBalancerName=='AssociatingSubnets-alb'].DNSName" \
--output text)
# Create record for ALB
aws route53 change-resource-record-sets \
--hosted-zone-id $HOSTEDZONE_ID_R53 \
--change-batch '{
"Comment": "Creating ALB alias for api.awsexample.com",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "api.awsexample.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "'"$HOSTEDZONE_ID_ALB"'",
"DNSName": "'"$ALB_DNS"'",
"EvaluateTargetHealth": false
}
}
}
]
}'
사용자 측 절차
직원들이 Lambda 함수를 통해 AWS Client VPN 엔드포인트를 연결합니다.
구체적인 절차는 다음과 같습니다.
- 브라우저에서 다음 URL에 접속합니다.
URL: https://api.awsexample.com - Active Directory에 등록된 사용자 ID와 비밀번호로 로그인합니다.
위 단계를 따르면 AWS Client VPN 엔드포인트가 연결됩니다.
최초 로그인 시에는 IAM Identity Center에서 MFA 등록이 기본으로 활성화되어 있으므로 MFA 장치를 등록해야 합니다.
최초 로그인 시에는 아래 절차를 따라 주세요.
- 스마트폰에 Google Authenticator(MFA 인증 앱)를 설치합니다.
[Google Play / App Store] - MFA 장치 등록에서 인증 앱을 선택하고 아래 단계를 진행합니다.
- QR 코드를 표시합니다.
- 스마트폰에서 Google Authenticator를 실행합니다.
- 앱 하단 오른쪽의 "+" 버튼을 눌러 QR 코드를 스캔합니다.
- 앱에 추가된 AWS SSO 항목의 6자리 인증 코드를 입력합니다.
직원들이 AWS Client VPN을 사용할 때 위 단계를 따르도록 안내했습니다.
이를 통해 연결 시간을 최소화하고 비용을 절감할 수 있었습니다.
자동으로 연결을 해제하는 시스템도 구현할 예정이며, 이는 다음 기사에서 소개할 예정입니다.