DEV Community

Andrii Shykhov
Andrii Shykhov

Posted on • Edited on • Originally published at Medium

CloudFormation nested stack with Cognito, ALB, EC2, Route53 for different environments

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

The infrastructure schema:

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:

  1. Clone the repository.
    git clone https://gitlab.com/Andr1500/cognito_alb_ec2.git

  2. Create an S3 bucket for nested stack templates.
    aws s3api create-bucket --bucket bucket-name --region YOUR_REGION \
    --create-bucket-configuration LocationConstraint=YOUR_REGION

  3. Fill 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/*"

  4. 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-keypair

  5. Create 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.pem

  6. Browse to your custom domain name (e.g., instance.example.com) with the necessary path ( /development or /testing), and log in with the necessary user.

  7. Delete the CloudFormation stack
    aws cloudformation delete-stack --stack-name cognito-alb-ec2

  8. Delete all files from the S3 bucket
    aws s3 rm s3://bucket-name--recursive

  9. 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)