Introduction:
AWS offers a large amount of services to create scalable and secure applications. One of the key challenges in this realm is crafting an infrastructure that not only facilitates seamless user access but also manages data security and authentication. In this blog post, we delve into architecting an AWS infrastructure using CloudFormation. We’ll walk through the process of setting up an environment with EC2 instance, Security Groups, Route53, Application Load Balancer, ALB Listeners and Target Groups, and Amazon Cognito User Pools.
About the project:
The main idea of the project is to separate and secure access to EC2 instance for different categories of users. We have 1 user for the development environment and 1 user for the testing environment and every user can sign in only to his environment. All infrastructure in AWS (except the S3 bucket) is created with CloudFormation. S3 bucket is necessary for storing nested stack templates, more information about nested stack is here. The EC2 instance has preinstalled and preconfigured IIS service under */development *and */testing *paths. We configured user data and public key material as Parameters and defined that during the process of creating the CloudFormation stack. Cognito user email addresses should be valid because temporary password will be sent on these emails, more information about Amazon Cognito is here. GitLab repository with all nested stack templates is here.
In short, the process works as follows:
The development or testing user accesses Environment by pointing the browser to https://instance.example.com/environment.
The ALB redirects the user to the login page provided by AWS Cognito User Pool.
The engineer authenticates with username, one-time-password, and provides a new password.
AWS Cognito User Pool redirects the engineer to https://instance.example.com/environment.
The ALB verifies the authentication information and forwards the request to the necessary target group.
The project structure:
├── nestedALB.yaml
├── nestedCognito.yaml
├── nestedEC2.yaml
├── nestedNSG.yaml
├── nestedRoute53.yaml
├── root.yaml
└── user_data.txt
root.yaml template:
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy infrastructure with EC2, ALB, Cognito, Route53
Parameters:
TemplateUrlBase:
Type: String
Description: Base URL for CloudFormation templates
Default: ''
VpcId:
Type: AWS::EC2::VPC::Id
Description: ID of VPC
Default: ''
SubnetIds:
Type: String
Description: Comma-separated IDs of the subnets (minimum 2)
Default: ''
PublicDomainName:
Type: String
Description: The custom domain name
Default: ''
CertificateArn:
Type: String
Description: The ARN of a ACM certificate valid for the custom domain name
Default: ''
AmiID:
Type: String
Description: The ID of the AMI image
Default: ''
UserData:
Type: String
Description: User Data content for the ec2 instance
Default: ''
PublicKeyMaterial:
Type: String
Description: SSH Public Key content
Default: ''
CognitoUserEmailDev:
Type: String
Description: Email address for dev. Cognito user
Default: ''
CognitoUserEmailTest:
Type: String
Description: Email address for test Cognito user
Default: ''
Resources:
nestedNSG:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub '${TemplateUrlBase}/nestedNSG.yaml'
Parameters:
VpcId: !Ref VpcId
nestedEC2:
DependsOn:
- nestedNSG
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub '${TemplateUrlBase}/nestedEC2.yaml'
Parameters:
UserData: !Ref UserData
PublicKeyMaterial: !Ref PublicKeyMaterial
SubnetIds: !Ref SubnetIds
AmiID: !Ref AmiID
nestedCognitoDev:
DependsOn:
- nestedEC2
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub '${TemplateUrlBase}/nestedCognito.yaml'
Parameters:
Environment: 'dev'
PublicDomainName: !Ref PublicDomainName
CognitoUserEmail: !Ref CognitoUserEmailDev
nestedCognitoTest:
DependsOn:
- nestedEC2
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub '${TemplateUrlBase}/nestedCognito.yaml'
Parameters:
Environment: 'test'
PublicDomainName: !Ref PublicDomainName
CognitoUserEmail: !Ref CognitoUserEmailTest
nestedALB:
DependsOn:
- nestedNSG
- nestedEC2
- nestedCognitoDev
- nestedCognitoTest
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub '${TemplateUrlBase}/nestedALB.yaml'
Parameters:
VpcId: !Ref VpcId
SubnetIds: !Ref SubnetIds
CertificateArn: !Ref CertificateArn
nestedRoute53:
DependsOn:
- nestedALB
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub '${TemplateUrlBase}/nestedRoute53.yaml'
Parameters:
PublicDomainName: !Ref PublicDomainName
nestedALB.yaml template:
AWSTemplateFormatVersion: '2010-09-09'
Description: ALB with Target Groups and Listeners
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
CertificateArn:
Type: String
Resources:
# development TG for access to EC2
TargetGroupDev:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckProtocol: HTTP
HealthCheckPort: 80
HealthCheckPath: "/development"
Name: 'TG-access-to-ec2-dev'
TargetType: instance
Targets:
- Id: !ImportValue WSWebAppInstanceID
Protocol: HTTP
Port: 80
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: 'TG-access-to-ec2-dev'
# testing TG for access to EC2
TargetGroupTest:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckProtocol: HTTP
HealthCheckPort: 80
HealthCheckPath: "/testing"
Name: 'TG-access-to-ec2-test'
TargetType: instance
Targets:
- Id: !ImportValue WSWebAppInstanceID
Protocol: HTTP
Port: 80
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: 'TG-access-to-ec2-test'
# ALB
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: ApplicationLoadBalancer
Subnets: !Ref SubnetIds
SecurityGroups:
- !ImportValue AlbSecurityGroupId
Type: application
Scheme: internet-facing
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: '300'
Tags:
- Key: Name
Value: ApplicationLoadBalancer
# ALB Listener redirects to HTTPS
AlbListenerHTTP:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: redirect
RedirectConfig:
Host: "#{host}"
Path: "/#{path}"
Port: "443"
Protocol: HTTPS
Query: "#{query}"
StatusCode: HTTP_301
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
# ALB listener on HTTPS
AlbListenerHTTPS:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 443
Protocol: HTTPS
SslPolicy: 'ELBSecurityPolicy-TLS13-1-2-2021-06'
Certificates:
- CertificateArn: !Ref CertificateArn
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
ContentType: "text/plain"
MessageBody: "Page not found"
StatusCode: "404"
# Listener rules for development access
ListenerRuleDev:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref AlbListenerHTTPS
Priority: 1
Conditions:
- Field: path-pattern
Values:
- /development
- /development/*
Actions:
- Type: authenticate-cognito
Order: 1
AuthenticateCognitoConfig:
UserPoolArn: !ImportValue dev-UserPoolArn
UserPoolClientId: !ImportValue dev-UserPoolClientId
UserPoolDomain: !ImportValue dev-UserPoolDomain
SessionCookieName: devAppCookie
OnUnauthenticatedRequest: authenticate
- Type: forward
Order: 2
TargetGroupArn: !Ref TargetGroupDev
# Listener rules for test access
ListenerRuleTest:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref AlbListenerHTTPS
Priority: 2
Conditions:
- Field: path-pattern
Values:
- /testing
- /testing/*
Actions:
- Type: authenticate-cognito
Order: 1
AuthenticateCognitoConfig:
UserPoolArn: !ImportValue test-UserPoolArn
UserPoolClientId: !ImportValue test-UserPoolClientId
UserPoolDomain: !ImportValue test-UserPoolDomain
SessionCookieName: testAppCookie
OnUnauthenticatedRequest: authenticate
- Type: forward
Order: 2
TargetGroupArn: !Ref TargetGroupTest
Outputs:
LoadBalancerDNSName:
Value: !GetAtt ApplicationLoadBalancer.DNSName
Description: The DNS name of the load balancer
Export:
Name: LoadBalancerDNSName
AlbCanonicalHostedZoneID:
Value: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID
Description: The host ID of the ALB
Export:
Name: AlbCanonicalHostedZoneID
nestedCognito.yaml template:
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Cognito configuration
Parameters:
Environment:
Type: String
PublicDomainName:
Type: String
CognitoUserEmail:
Type: String
Resources:
UserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
UserPoolName: !Sub '${Environment}-user-pool-ec2-web-access'
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 20
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
TemporaryPasswordValidityDays: 10
# Define Cognito User Pool Domain
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub
- ${Environment}-${StackId}
- StackId: !Select [2, !Split ['/', !Ref 'AWS::StackId']]
UserPoolId: !Ref UserPool
UserPoolClient:
Type: 'AWS::Cognito::UserPoolClient'
Properties:
RefreshTokenValidity: 30
IdTokenValidity: 30
AccessTokenValidity: 30
TokenValidityUnits:
AccessToken: minutes
IdToken: minutes
RefreshToken: days
AllowedOAuthFlows:
- code
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- email
- openid
- profile
- aws.cognito.signin.user.admin
CallbackURLs:
- !Sub https://instance.${PublicDomainName}/oauth2/idpresponse
SupportedIdentityProviders:
- COGNITO
GenerateSecret: true
UserPoolId: !Ref UserPool
# create user
UserPoolUser:
Type: 'AWS::Cognito::UserPoolUser'
Properties:
UserPoolId: !Ref UserPool
Username: !Ref CognitoUserEmail
DesiredDeliveryMediums:
- EMAIL
UserAttributes:
- Name: email
Value: !Ref CognitoUserEmail
- Name: email_verified
Value: "true"
Outputs:
UserPoolArn:
Value: !GetAtt UserPool.Arn
Description: ARN of the User pool
Export:
Name: !Sub '${Environment}-UserPoolArn'
UserPoolClientId:
Value: !Ref UserPoolClient
Description: ID of Userpool client
Export:
Name: !Sub '${Environment}-UserPoolClientId'
UserPoolDomain:
Value: !Ref UserPoolDomain
Description: Domain of Userpool
Export:
Name: !Sub '${Environment}-UserPoolDomain'
The infrastructure schema:
Prerequisites:
Before you start, make sure the following requirements are met:
An AWS account with permissions to create resources, a DNS domain in Route53, and a SSL/TLS certificate in Certificate Manager.
AWS CLI installed on your local machine.
Deployment:
Clone the repository.
git clone https://gitlab.com/Andr1500/cognito_alb_ec2.git
Create an S3 bucket for nested stack templates.
aws s3api create-bucket --bucket bucket-name --region YOUR_REGION \
--create-bucket-configuration LocationConstraint=YOUR_REGIONFill in all necessary Parameters in root.yaml file and send all nested stack files to the S3 bucket.
take AMI:
aws ec2 describe-images \
--filters "Name=name,Values=Windows_Server-2022-English-Full-Base-*" "Name=architecture,Values=x86_64" "Name=root-device-type,Values=ebs" "Name=virtualization-type,Values=hvm" \
--query "Images | sort_by(@, &CreationDate) | [-1]" \
--output json
send files to the S3 bucket:
aws s3 cp . s3://bucket-name --recursive --exclude ".git/*" \
--exclude README.md --exclude user_data.txt --exclude ".images/*"Create ssh key and convert it into RSA format (if you don’t have it).
generation ssh key and converting the key into RSA format:
ssh-keygen -t rsa -b 2048 -f ~/.ssh/ws-ec2-keypair
ssh-keygen -p -m PEM -f ~/.ssh/ws-ec2-keypairCreate a CloudFormation stack.
create stack:
aws cloudformation create-stack \
--stack-name cognito-alb-ec2 \
--template-body file://root.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=UserData,ParameterValue="$(base64 -i user_data.txt)" \
ParameterKey=PublicKeyMaterial,ParameterValue="$(cat ~/.ssh/ws-ec2-keypair.pub)" \
--capabilities CAPABILITY_NAMED_IAM \
--disable-rollback
take the EC2 instance password for Admin access:
aws ec2 get-password-data --instance-id i-1234567890abcdef0 \
--priv-launch-key C:\Keys\ws-ec2.pemBrowse to your custom domain name (e.g., instance.example.com) with the necessary path ( /development or /testing), and log in with the necessary user.
Delete the CloudFormation stack
aws cloudformation delete-stack --stack-name cognito-alb-ec2
Delete all files from the S3 bucket
aws s3 rm s3://bucket-name--recursive
delete the S3 bucket
aws s3 rb s3://bucket-name --force
Conclusion:
Usage of authentication on the load balancer add another layer of security to Web services handled on EC2 instances. Doing so reduced the attack surface to infrastructure. Implementing ALB authentication can be quite simple by using Cognito User Pools.
If you found this post helpful and interesting, please click the reaction button below to show your support for the author. Feel free to use and share this post!
You can also support me with a virtual coffee https://www.buymeacoffee.com/andrworld1500 .
Top comments (0)