Configuring AWS SSO with AWS Client VPN

In this tutorial, I will describe how to easily and securely set up a multi-account infrastructure based on AWS, including SSO and a VPN solution from Amazon.


I have broken down the article into several main parts:

  • First, I will show you how to create an infrastructure in AWS from scratch, with a secure structure of accounts, networks, peering.

  • The second part of this article focuses on AWS SSO: Users, Groups, MFA, etc.

  • Part 3 describes the process of deploying an AWS VPN service using terraform and configuring it for previously created networks.

Let’s start!

AWS account structure

Multi-account structure for AWS has several advantages… I will not dwell on them, just show you an example:

Account structure
Account structure

Account root is the main one in the organization (billing is tied to it), all other accounts are added under the leadership of this account.

Each account is tied to a different postal address, but usually you can use postal aliases for easy management from one mailbox.

Let’s create an account named root and add new accounts to the organization. Log in to the accounts, enable MFA for users, and make sure our account structure is correct and we are in the same organization.

This is enough for the moment, we can go further.

Network structure

In my example, accounts dev, stage and prod isolated from each other.

Account common used for general services like CI / CD system, data storage (S3 buckets), etc. Therefore, in my case, in order to allow a network connection between common and dev/stage/prod accounts, we need to create a VPC peering (connection between virtual networks).

Let’s do it with terraform for accounts common and dev

First, let’s create a VPC:

data "aws_availability_zones" "available" {}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.7.0"

  name = "${var.env}-vpc"
  cidr = var.vpc_cidr
  azs  = data.aws_availability_zones.available.names

  private_subnets = var.vpc_private_subnets
  public_subnets  = var.vpc_public_subnets

  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true

And also a file with input parameters terraform.tfvars:

region = "eu-central-1"
env    = "common"

# каждая подсеть ~8190 хостов
# зарезервирована под VPN-клиентов
vpc_cidr            = ""
vpc_private_subnets = ["", "", ""]
vpc_public_subnets  = ["", "", ""]

If you want to apply the same for your account dev, just copy and paste the file and change the values ​​of networks and subnets, for example to etc. Remember that the subnets must be different to avoid crossing them after peering.

After that we have new virtual networks:

  • common-vpc in account common with CIDR:, three public and three private subnets

  • dev-vpc in account dev with CIDR:, three public and three private subnets

Let’s connect them using peering:

module "common_dev_peering" {
  source  = "grem11n/vpc-peering/aws"
  version = "4.0.1"

  providers = {
    aws.this = aws
    aws.peer =

  this_vpc_id = module.vpc.vpc_id
  peer_vpc_id = var.vpc_dev_accepter_id

  auto_accept_peering = true

Do not forget to indicate vpc_dev_accepter_id in file terraform.tfvars:

vpc_dev_accepter_id = "vpc-12345678"

The same procedure can be done, for example, for accounts stage and prod

Private network connection diagram
Private network connection diagram

To test the network connection, it is enough to create a virtual machine in both networks and try to check with some tool like ping or traceroute

Configuring SSO

Go to AWS in your account root, we find a service AWS SSO and create three groups:

The idea is to share access to different networks for different groups (RBAC) with VPN.

Groups created by us
Groups created by us

We will also create a user, let it be amet-umerov, after adding it to all previously created groups, we want the user to get access to all networks using VPN.

Remember to enable MFA for the user
Remember to enable MFA for the user

So, we have prepared the groups and the user, it’s time to create new SSO application called VPN:

  • Add a custom SAML 2.0 application

  • Loading AWS SSO SAML metadata file, it will be useful to us later

  • Set session duration: 12 hours

  • Application ACS URL:

  • Application SAML audience: urn:amazon:webservices:clientvpn

Let’s also create another SSO application named VPN Self-Service with the same settings, except:

Let’s add mapping of attributes for these applications:

  • Subjectuser:subjectemailAddress

  • NameID${user:email}unspecified

  • FirstName${user:name}unspecified

  • LastName${user:familyName}unspecified

  • memberOf${user:groups}unspecified

And we will attach all three groups to them (vpn-dev, vpn-stage, vpn-prod).

List of configured applications
List of configured applications

This is how it works:

Sharing access to networks
Sharing access to networks

Configuring VPN

Our final step is to create client hotspot for VPN in account common… But before that, you need to prepare some certificates and keys.

We generate certificates for the server and client:

$ git clone
$ cd easy-rsa/easyrsa3

$ ./easyrsa init-pki
$ ./easyrsa build-ca nopass
Common Name (eg: your user, host, or server name) [Easy-RSA CA]

$ ./easyrsa build-server-full vpn-aws-server nopass
$ ./easyrsa build-client-full vpn-aws-client nopass

Copy the generated certificates to a safe place and import the certificate for the server into AWS ACM:

$ mkdir ~/.vpn-assets/
$ cp pki/ca.crt ~/.vpn-assets/
$ cp pki/private/ca.key ~/.vpn-assets/
$ cp pki/issued/vpn-aws-*.crt ~/.vpn-assets/
$ cp pki/private/vpn-aws-*.key ~/.vpn-assets/

$ aws --profile common 
    --region eu-central-1 
    acm import-certificate 
    --certificate fileb://$HOME/.vpn-assets/vpn-aws-server.crt 
    --private-key fileb://$HOME/.vpn-assets/vpn-aws-server.key 
    --certificate-chain fileb://$HOME/.vpn-assets/ca.crt

# На всякий случай скопируем это все в S3-бакет
$ aws --profile=common s3 cp --recursive ~/.vpn-assets/ s3://my-bucket/vpn/

Create a new VPN connection using terraform:

# SAML провайдеры из метадата-файлов, загруженных нами ранее
resource "aws_iam_saml_provider" "vpn" {
  name                   = "vpn"
  saml_metadata_document = file("${path.module}/files/VPN_ins-mymetadata-file.xml")

resource "aws_iam_saml_provider" "vpn_self_service" {
  name                   = "vpn-self-service"
  saml_metadata_document = file("${path.module}/files/VPN Self-Service_ins-mymetadata-file.xml")

# Получаем импортированный нами сертификат и subnet_id
data "aws_acm_certificate" "vpn_aws_server_cert" {
  domain   = "vpn-aws-server"
  statuses = ["ISSUED"]

data "aws_subnet" "vpn_subnet_id" {
  filter {
    name   = "tag:Name"
    values = ["${var.env}-vpc-private"]
  availability_zone_id = "euc1-az1"

# Подготовка CloudWatch для логирования VPN
resource "aws_cloudwatch_log_group" "client_vpn" {
  name = "vpn_endpoint_cloudwatch_log_group"

resource "aws_cloudwatch_log_stream" "client_vpn" {
  name           = "vpn_endpoint_cloudwatch_log_stream"
  log_group_name =

# Точка доступа VPN
resource "aws_ec2_client_vpn_endpoint" "vpn" {
  description            = "VPN client for AWS"
  server_certificate_arn = data.aws_acm_certificate.vpn_aws_server_cert.arn

  client_cidr_block = var.vpn_client_cidr_block
  dns_servers       = var.vpn_dns_servers

  split_tunnel        = "true"
  self_service_portal = "enabled"
  transport_protocol  = "udp"

  authentication_options {
    type                           = "federated-authentication"
    saml_provider_arn              = aws_iam_saml_provider.vpn.arn
    self_service_saml_provider_arn = aws_iam_saml_provider.vpn_self_service.arn

  connection_log_options {
    enabled               = true
    cloudwatch_log_group  =
    cloudwatch_log_stream =

# Фаерволы для VPN
resource "aws_security_group" "vpn_main" {
  name        = "vpn_main"
  description = "Allow VPN all traffic"
  vpc_id      = module.vpc.vpc_id

  egress {
    description = "Allow all traffic for VPN"
    cidr_blocks = [""]
    from_port   = "0"
    protocol    = "-1"
    self        = "false"
    to_port     = "0"

  ingress {
    description = "Allow all traffic for VPN"
    cidr_blocks = [""]
    from_port   = "0"
    protocol    = "-1"
    self        = "false"
    to_port     = "0"

# Ассоциируем VPN-точку с подсетью
resource "aws_ec2_client_vpn_network_association" "vpn" {
  client_vpn_endpoint_id =
  subnet_id              =
  security_groups        = []

# Идентификаторы (access_group_id) можно найти тут (в аккаунте root):
resource "aws_ec2_client_vpn_authorization_rule" "vpn_dev" {
  client_vpn_endpoint_id =
  target_network_cidr    = ""
  access_group_id        = "1234-5678-..."
  description            = "vpn-dev"

# Разрешаем доступ в сеть common для группы vpn-dev
resource "aws_ec2_client_vpn_authorization_rule" "vpn_common" {
  client_vpn_endpoint_id =
  target_network_cidr    = ""
  access_group_id        = "1234-5678-..."
  description            = "vpn-common"

# Маршруты
resource "aws_ec2_client_vpn_route" "vpn_to_dev" {
  client_vpn_endpoint_id =
  destination_cidr_block = ""
  target_vpc_subnet_id   = aws_ec2_client_vpn_network_association.vpn.subnet_id

And modify the file terraform.tfvars:

vpn_client_cidr_block = ""
vpn_dns_servers       = ["", ""]

After applying this configuration, we get a client VPN access point with access control at the level of roles (groups).

We are testing.

  • We go to the self-service portal, the link looks like this:

  • We download the VPN client for our OS, as well as the configuration (file in the format .ovpn)

Self-service portal
Self-service portal
  • Launch the VPN client, create a new profile and import the VPN configuration

  • Making sure we are connected to a VPN

  • Checking the connection with traceroute/ping and all the same virtual machines

After all the previous manipulations, we have access to account networks common and dev using a VPN.

This configuration can be scaled for other accounts with different access rules and routes in our terraform code.

useful links

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *