Skip to main content

WorkSpaces VPC Best Practices: 1 Public Subnet, 2 Private Subnets with NAT Gateway

Overview

This check ensures your Amazon WorkSpaces are deployed in a properly segmented VPC with at least 1 public subnet and 2 private subnets, with a NAT Gateway providing controlled internet access. WorkSpaces themselves should reside only in the private subnets.

Risk

Without proper network segmentation:

  • Direct internet exposure: WorkSpaces in public subnets are reachable from the internet, enabling credential attacks and session hijacking
  • Reduced availability: Missing NAT Gateway egress can cause updates and directory connectivity failures
  • Compliance gaps: Many security frameworks require network isolation for virtual desktop infrastructure

Remediation Steps

Prerequisites

You need:

  • AWS account access with permissions to create VPC resources and WorkSpaces
  • An existing WorkSpaces directory (AWS Managed Microsoft AD or Simple AD), or plan to create one
Required IAM permissions

Your IAM user or role needs these permissions:

  • ec2:CreateVpc, ec2:CreateSubnet, ec2:CreateInternetGateway
  • ec2:CreateNatGateway, ec2:AllocateAddress
  • ec2:CreateRouteTable, ec2:CreateRoute, ec2:AssociateRouteTable
  • workspaces:RegisterWorkspaceDirectory, workspaces:CreateWorkspaces

AWS Console Method

Step 1: Create the VPC

  1. Open the VPC Console
  2. Click Create VPC
  3. Select VPC and more (this creates subnets automatically)
  4. Configure:
    • Name tag: workspaces-vpc
    • IPv4 CIDR block: 10.0.0.0/16
    • Number of Availability Zones: 2
    • Number of public subnets: 1
    • Number of private subnets: 2
    • NAT gateways: In 1 AZ (for cost savings) or 1 per AZ (for high availability)
  5. Click Create VPC

Step 2: Verify the Architecture

After creation, confirm:

  • 1 public subnet with a route to the Internet Gateway (0.0.0.0/0 -> igw-xxxxx)
  • 2 private subnets with routes to the NAT Gateway (0.0.0.0/0 -> nat-xxxxx)
  • NAT Gateway is in the public subnet

Step 3: Register or Update WorkSpaces Directory

  1. Open the WorkSpaces Console
  2. Go to Directories
  3. If registering a new directory:
    • Click Register directory
    • Select your directory
    • Choose both private subnets (not the public one)
  4. If updating an existing directory:
    • Select the directory and review its subnet configuration
    • If using public subnets, you will need to re-register with private subnets

Step 4: Launch WorkSpaces in Private Subnets

When launching new WorkSpaces, they will automatically use the private subnets associated with your directory.

AWS CLI (optional)

Create the VPC and subnets:

# Create VPC
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--region us-east-1 \
--query 'Vpc.VpcId' \
--output text)

aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames

# Create Internet Gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--region us-east-1 \
--query 'InternetGateway.InternetGatewayId' \
--output text)

aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID --region us-east-1

# Create public subnet
PUBLIC_SUBNET=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--region us-east-1 \
--query 'Subnet.SubnetId' \
--output text)

# Create private subnets
PRIVATE_SUBNET_1=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1a \
--region us-east-1 \
--query 'Subnet.SubnetId' \
--output text)

PRIVATE_SUBNET_2=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.3.0/24 \
--availability-zone us-east-1b \
--region us-east-1 \
--query 'Subnet.SubnetId' \
--output text)

Create NAT Gateway:

# Allocate Elastic IP
EIP_ALLOC=$(aws ec2 allocate-address \
--domain vpc \
--region us-east-1 \
--query 'AllocationId' \
--output text)

# Create NAT Gateway in public subnet
NAT_GW=$(aws ec2 create-nat-gateway \
--subnet-id $PUBLIC_SUBNET \
--allocation-id $EIP_ALLOC \
--region us-east-1 \
--query 'NatGateway.NatGatewayId' \
--output text)

# Wait for NAT Gateway to become available
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_GW --region us-east-1

Configure route tables:

# Create and configure public route table
PUBLIC_RT=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--region us-east-1 \
--query 'RouteTable.RouteTableId' \
--output text)

aws ec2 create-route \
--route-table-id $PUBLIC_RT \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id $IGW_ID \
--region us-east-1

aws ec2 associate-route-table \
--subnet-id $PUBLIC_SUBNET \
--route-table-id $PUBLIC_RT \
--region us-east-1

# Create and configure private route table
PRIVATE_RT=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--region us-east-1 \
--query 'RouteTable.RouteTableId' \
--output text)

aws ec2 create-route \
--route-table-id $PRIVATE_RT \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id $NAT_GW \
--region us-east-1

aws ec2 associate-route-table \
--subnet-id $PRIVATE_SUBNET_1 \
--route-table-id $PRIVATE_RT \
--region us-east-1

aws ec2 associate-route-table \
--subnet-id $PRIVATE_SUBNET_2 \
--route-table-id $PRIVATE_RT \
--region us-east-1

Output the subnet IDs for WorkSpaces registration:

echo "Private Subnet 1: $PRIVATE_SUBNET_1"
echo "Private Subnet 2: $PRIVATE_SUBNET_2"
CloudFormation (optional)

Save this template and deploy it via the CloudFormation console or CLI.

AWSTemplateFormatVersion: '2010-09-09'
Description: VPC with 1 public subnet, 2 private subnets, and NAT Gateway for WorkSpaces

Parameters:
VpcCidr:
Type: String
Default: 10.0.0.0/16
Description: CIDR block for the VPC

PublicSubnetCidr:
Type: String
Default: 10.0.1.0/24
Description: CIDR block for the public subnet

PrivateSubnet1Cidr:
Type: String
Default: 10.0.2.0/24
Description: CIDR block for the first private subnet

PrivateSubnet2Cidr:
Type: String
Default: 10.0.3.0/24
Description: CIDR block for the second private subnet

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: workspaces-vpc

InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: workspaces-igw

InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC

PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: !Ref PublicSubnetCidr
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: workspaces-public-subnet

PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: !Ref PrivateSubnet1Cidr
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: workspaces-private-subnet-1

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs '']
CidrBlock: !Ref PrivateSubnet2Cidr
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: workspaces-private-subnet-2

NatGatewayEIP:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttachment
Properties:
Domain: vpc
Tags:
- Key: Name
Value: workspaces-nat-eip

NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet
Tags:
- Key: Name
Value: workspaces-nat

PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: workspaces-public-rt

DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway

PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet

PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: workspaces-private-rt

DefaultPrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway

PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet1

PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet2

Outputs:
VpcId:
Description: VPC ID
Value: !Ref VPC
Export:
Name: WorkspacesVpcId

PrivateSubnet1Id:
Description: Private Subnet 1 ID (for WorkSpaces)
Value: !Ref PrivateSubnet1
Export:
Name: WorkspacesPrivateSubnet1Id

PrivateSubnet2Id:
Description: Private Subnet 2 ID (for WorkSpaces)
Value: !Ref PrivateSubnet2
Export:
Name: WorkspacesPrivateSubnet2Id

Deploy the stack:

aws cloudformation create-stack \
--stack-name workspaces-vpc \
--template-body file://template.yaml \
--region us-east-1
Terraform (optional)
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
}

provider "aws" {
region = "us-east-1"
}

variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}

variable "public_subnet_cidr" {
description = "CIDR block for public subnet"
type = string
default = "10.0.1.0/24"
}

variable "private_subnet_1_cidr" {
description = "CIDR block for first private subnet"
type = string
default = "10.0.2.0/24"
}

variable "private_subnet_2_cidr" {
description = "CIDR block for second private subnet"
type = string
default = "10.0.3.0/24"
}

data "aws_availability_zones" "available" {
state = "available"
}

resource "aws_vpc" "workspaces" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true

tags = {
Name = "workspaces-vpc"
}
}

resource "aws_internet_gateway" "workspaces" {
vpc_id = aws_vpc.workspaces.id

tags = {
Name = "workspaces-igw"
}
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.workspaces.id
cidr_block = var.public_subnet_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true

tags = {
Name = "workspaces-public-subnet"
}
}

resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.workspaces.id
cidr_block = var.private_subnet_1_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = false

tags = {
Name = "workspaces-private-subnet-1"
}
}

resource "aws_subnet" "private_2" {
vpc_id = aws_vpc.workspaces.id
cidr_block = var.private_subnet_2_cidr
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = false

tags = {
Name = "workspaces-private-subnet-2"
}
}

resource "aws_eip" "nat" {
domain = "vpc"

tags = {
Name = "workspaces-nat-eip"
}

depends_on = [aws_internet_gateway.workspaces]
}

resource "aws_nat_gateway" "workspaces" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id

tags = {
Name = "workspaces-nat"
}

depends_on = [aws_internet_gateway.workspaces]
}

resource "aws_route_table" "public" {
vpc_id = aws_vpc.workspaces.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.workspaces.id
}

tags = {
Name = "workspaces-public-rt"
}
}

resource "aws_route_table" "private" {
vpc_id = aws_vpc.workspaces.id

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.workspaces.id
}

tags = {
Name = "workspaces-private-rt"
}
}

resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private_1" {
subnet_id = aws_subnet.private_1.id
route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private_2" {
subnet_id = aws_subnet.private_2.id
route_table_id = aws_route_table.private.id
}

output "vpc_id" {
description = "VPC ID"
value = aws_vpc.workspaces.id
}

output "private_subnet_1_id" {
description = "Private Subnet 1 ID (for WorkSpaces)"
value = aws_subnet.private_1.id
}

output "private_subnet_2_id" {
description = "Private Subnet 2 ID (for WorkSpaces)"
value = aws_subnet.private_2.id
}

Deploy:

terraform init
terraform plan
terraform apply

Verification

After remediation, verify your setup:

  1. In the VPC Console: Check that your VPC has:

    • 1 public subnet with route 0.0.0.0/0 pointing to an Internet Gateway
    • 2 private subnets with route 0.0.0.0/0 pointing to a NAT Gateway
    • NAT Gateway deployed in the public subnet
  2. In the WorkSpaces Console: Confirm your directory is registered with the private subnets only

  3. Re-run Prowler to confirm the check passes:

    prowler aws --checks workspaces_vpc_2private_1public_subnets_nat
CLI verification commands
# List subnets and their route table associations
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=<your-vpc-id>" \
--query 'Subnets[*].[SubnetId,CidrBlock,AvailabilityZone,MapPublicIpOnLaunch]' \
--output table \
--region us-east-1

# Check NAT Gateway status
aws ec2 describe-nat-gateways \
--filter "Name=vpc-id,Values=<your-vpc-id>" \
--query 'NatGateways[*].[NatGatewayId,SubnetId,State]' \
--output table \
--region us-east-1

# Verify WorkSpaces directory subnet configuration
aws workspaces describe-workspace-directories \
--query 'Directories[*].[DirectoryId,DirectoryName,SubnetIds]' \
--output table \
--region us-east-1

Additional Resources

Notes

  • Cost consideration: NAT Gateways incur hourly charges plus data processing fees. For high availability, deploy NAT Gateways in multiple AZs, but this increases cost.
  • Existing WorkSpaces: If you have WorkSpaces in public subnets, you cannot simply move them. You must terminate and recreate them in the private subnets after updating the directory registration.
  • Directory Service: AWS Managed Microsoft AD and Simple AD both support deployment in private subnets with NAT Gateway for outbound connectivity.
  • Two private subnets: The requirement for two private subnets ensures high availability across multiple Availability Zones, which is critical for production WorkSpaces deployments.