How to secure your DevOps tools with ALB authentication?

Andreas Wittig – 21 Jan 2020

Are you hosting any DevOps tools like GitLab, Jenkins, Kibana, Grafana, or phpMyAdmin yourself? On the one hand, it is convenient to provide access to those tools via the Internet. On the other hand, those tools add high-risk attack vectors to your infrastructure. Therefore, I recommend securing your DevOps tools by adding an extra layer of security: authentication provided by the Application Load Balancer (ALB).

How to secure your DevOps tools with ALB Authentication?

Use the ALB to make sure incoming requests are authenticated before they are forwarded to one of your DevOps tools. Right now, the ALB supports two authentication methods:

  • Cognito User Pool comes with a built-in user database and supports user federation (Google, Facebook, SAML, OICD, …) as well.
  • OpenID Connect (OICD) integrates with any OICD-compliant identity provider.

In short, the process works as follows:

  1. The engineer accesses Grafana by pointing the browser to https://grafana.example.com.
  2. The ALB redirects the engineer to the login page provided by AWS Cognito User Pool.
  3. The engineer authenticates with username, password, and optionally a one-time-password.
  4. AWS Cognito User Pool redirects the engineer to https://grafana.example.com.
  5. The ALB verifies the authentication information and forwards the request to one of the targets running Grafana.

ALB Authentication

In the following, you will learn how to add ALB authentication to protect your DevOps tools from all kinds of attacks. The implementations are intended for small organizations.

Requirements

Before you start, make sure the following requirements are met.

  • A custom domain name pointing to your DevOps tool (e.g., grafana.example.com).
  • A valid SSL certificate (e.g., Amazon Certificate Manager) for the custom domain name.

Example: Cognito User Pool

The following CloudFormation template shows how to configure an ALB to authenticate incoming requests against a Cognito User Pool. The Cognito User Pool provides a separate user database for your DevOps tools. Check out the # inline comments for details.

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ALB Authentication | Cognito | cloudonaut.io'
Parameters:
VpcId:
Type: 'AWS::EC2::VPC::Id'
SubnetIds:
Type: 'List<AWS::EC2::Subnet::Id>'
PublicDomainName:
Type: 'String' # the custom domain name (e.g., grafana.example.com)
CertificateArn:
Type: 'String' # the ARN of a ACM certificate valid for the custom domain name
Resources:
LoadBalancerSecurityGroup: # Security Group for Load Balancer allows incoming HTTPS requests
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'Load Balancer'
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: '0.0.0.0/0'
LoadBalancer:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets: !Ref SubnetIds
Type: application
Listener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
Certificates:
- CertificateArn: !Ref CertificateArn
DefaultActions: # 1. Authenticate against Cognito User Pool
- Type: 'authenticate-cognito'
AuthenticateCognitoConfig:
OnUnauthenticatedRequest: 'authenticate' # Redirect unauthenticated clients to Cognito login page
Scope: 'openid'
UserPoolArn: !GetAtt 'UserPool.Arn'
UserPoolClientId: !Ref UserPoolClient
UserPoolDomain: !Ref UserPoolDomain
Order: 1
- Type: forward # 2. Forward request to target group (e.g., EC2 instances)
TargetGroupArn: !Ref TargetGroup
Order: 2
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: 'HTTPS'
TargetGroup:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
Port: 80
Protocol: HTTP
TargetType: ip
VpcId: !Ref VpcId
UserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true # Disable self-registration
InviteMessageTemplate:
EmailSubject: !Sub '${AWS::StackName}: temporary password'
EmailMessage: 'Use the username {username} and the temporary password {####} to log in for the first time.'
SMSMessage: 'Use the username {username} and the temporary password {####} to log in for the first time.'
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 16
RequireLowercase: false
RequireNumbers: false
RequireSymbols: false
RequireUppercase: false
TemporaryPasswordValidityDays: 21
UserPoolDomain: # Provides Cognito Login Page
Type: 'AWS::Cognito::UserPoolDomain'
Properties:
UserPoolId: !Ref UserPool
Domain: !Select [2, !Split ['/', !Ref 'AWS::StackId']] # Generates a unique domain name
UserPoolClient:
Type: 'AWS::Cognito::UserPoolClient'
Properties:
AllowedOAuthFlows:
- code # Required for ALB authentication
AllowedOAuthFlowsUserPoolClient: true # Required for ALB authentication
AllowedOAuthScopes:
- email
- openid
- profile
- aws.cognito.signin.user.admin
CallbackURLs:
- !Sub https://${PublicDomainName}/oauth2/idpresponse # Redirects to the ALB
GenerateSecret: true
SupportedIdentityProviders: # Optional: add providers for identity federation
- COGNITO
UserPoolId: !Ref UserPool

What else is needed?

  1. Create a CNAME/Alias record for your custom domain name (e.g., grafana.example.com) pointing to the ALB’s DNS name (e.g., cloud-LoadB-MTNUUM0SEVMO-1111111111.eu-west-1.elb.amazonaws.com).
  2. Open the AWS Management Console and add a user to the Cognito User Pool created by CloudFormation.
  3. Browse to your custom domain name (e.g., grafana.example.com).
  4. Log in with the user you created in the penultimate step.

Login with Cognito User Pool

By the way, it is possible to create users with CloudFormation as well. Alternatively, you might want to use an existing user database. One option to do so is OpenID Connect, which you will learn about next.

Example: OpenID Connect (OICD) with Google

The following CloudFormation template shows how to configure an ALB use Google user accounts to authenticate incoming requests. Check out the # inline comments for details.

Before you proceed, you need to configure the OAuth consent screen of your application and create an OAuth Client. Open the Google API Console to do so.

Warning Choose user type Internal when configuring the OAuth consent screen. By doing so, only G Suite users within your organization are allowed to log in. When choosing External anyone with a Google account can log in.

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ALB Authentication | Google OICD | cloudonaut.io'
Parameters:
VpcId:
Type: 'AWS::EC2::VPC::Id'
SubnetIds:
Type: 'List<AWS::EC2::Subnet::Id>'
CertificateArn:
Type: 'String'
GoogleClientId:
Type: 'String' # The OAuth Client ID from the Google API Console
GoogleClientSecret:
Type: 'String' # The OAuth Client Secret from the Google API Console
NoEcho: true
Resources:
LoadBalancerSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'Load Balancer'
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: '0.0.0.0/0'
LoadBalancer:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets: !Ref SubnetIds
Type: application
Listener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
Certificates:
- CertificateArn: !Ref CertificateArn
DefaultActions:
- Type: 'authenticate-oidc' # 1. Authenticate with OICD
AuthenticateOidcConfig:
OnUnauthenticatedRequest: 'authenticate'
Issuer: 'https://accounts.google.com'
AuthorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth'
TokenEndpoint: 'https://oauth2.googleapis.com/token'
UserInfoEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo'
ClientId: !Ref GoogleClientId
ClientSecret: !Ref GoogleClientSecret
Order: 1
- Type: forward # 2. Forward request to target group (e.g., EC2 instances)
TargetGroupArn: !Ref TargetGroup
Order: 2
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: 'HTTPS'
TargetGroup:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
Port: 80
Protocol: HTTP
TargetType: ip
VpcId: !Ref VpcId

What else is needed?

  1. Create a CNAME/Alias record for your custom domain name (e.g., grafana.example.com) pointing to the ALB’s DNS name (e.g., cloud-LoadB-MTNUUM0SEVMO-1111111111.eu-west-1.elb.amazonaws.com).
  2. Browse to your custom domain name (e.g., grafana.example.com).
  3. Log in with your Google account.

Login with Google

See Google Identity Platform: Guides: OpenID Connect to learn more.

What is next?

Be aware that the ALB only handles authentication. Anyone with a user account can log in and access your DevOps tools. The ALB does not offer additional authorization. Make sure you understand which users can log in when configuring an OpenID Connect provider. It might be anyone with a user account. Or only user accounts that belong to your organization (see the warning for OpenID Connect (OICD) with Google).

By default, your DevOps tool will not integrate with the ALB authentication. However, the ALB adds headers with the user information to all forwarded requests (e.g., x-amzn-oidc-identity contains the user name of the authenticated user). You might be able to configure your DevOps tool to use these headers for authentication and authorization. See Authenticate Users Using an Application Load Balancer to learn more.

Summary

Use authentication on the load balancer to add another layer of security to your DevOps tools like GitLab, Jenkins, Kibana, Grafana, or phpMyAdmin. Doing so reduced the attack surface to your infrastructure. Implementing ALB authentication for small organizations is quite simple by using either Cognito User Pools or Google via OpenID Connect.

Andreas Wittig

Andreas Wittig

I’ve been building on AWS since 2012 together with my brother Michael. We are sharing our insights into all things AWS on cloudonaut and have written the book AWS in Action. Besides that, we’re currently working on bucketAV,HyperEnv for GitHub Actions, and marbot.

Here are the contact options for feedback and questions.