Skip to main content

AWS Client VPN

main.tf
resource "tls_private_key" "this" {
algorithm = "RSA"
rsa_bits = 2048
}

resource "tls_self_signed_cert" "this" {
private_key_pem = tls_private_key.this.private_key_pem
validity_period_hours = 43800 # 5 years
early_renewal_hours = 168 # Generate new cert if Terraform is run within 1 week of expiry

subject {
common_name = "vpn.digital.canada.ca"
}

allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
"ipsec_end_system",
"ipsec_tunnel",
"any_extended",
"cert_signing",
]
}

resource "aws_acm_certificate" "this" {
private_key = tls_private_key.this.private_key_pem
certificate_body = tls_self_signed_cert.this.cert_pem
}
main.tf
resource "aws_iam_saml_provider" "aws-client-vpn" {
name = "aws-client-vpn"
saml_metadata_document = file("${path.module}/metadata/aws-client-vpn.xml")
}

resource "aws_iam_saml_provider" "aws-client-vpn-self-service" {
name = "aws-client-vpn-self-service"
saml_metadata_document = file("${path.module}/metadata/aws-client-vpn-self-service.xml")
}
main.tf
resource "aws_security_group" "postgresql_cluster" {
name = "test_database"
description = "Test database security group"
vpc_id = module.test_vpc.vpc_id
}

resource "aws_security_group_rule" "postgresql_cluster_ingress_vpn" {
description = "Ingress from VPN task to database"
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = aws_security_group.postgresql_cluster.id
source_security_group_id = aws_security_group.this.id
}
main.tf
resource "aws_ec2_client_vpn_endpoint" "this" {
description = var.endpoint_name
vpc_id = module.test_vpc.vpc_id
server_certificate_arn = aws_acm_certificate.this.arn
client_cidr_block = var.endpoint_cidr_block

session_timeout_hours = 8
split_tunnel = true
self_service_portal = "enabled"
transport_protocol = "udp"
security_group_ids = [aws_security_group.this.id]
dns_servers = [cidrhost(module.test_vpc.cidr_block, 2)]

authentication_options {
type = "federated-authentication"
saml_provider_arn = aws_iam_saml_provider.aws-client-vpn.arn
self_service_saml_provider_arn = aws_iam_saml_provider.aws-client-vpn-self-service.arn
}

connection_log_options {
enabled = true
cloudwatch_log_group = aws_cloudwatch_log_group.this.name
}

client_login_banner_options {
enabled = true
banner_text = "This is a private network. Take care when connecting."
}
}
main.tf
#
# Associate subnets and authorize access
# To save costs, the VPN endpoint is only associated with on availability zone's subnets.
# The resources to access through the VPN must be in these subnets.
#
data "aws_subnet" "private" {
for_each = toset(module.test_vpc.private_subnet_ids)
id = each.key
}

locals {
availability_zone_subnet_ids = {
for s in data.aws_subnet.private : s.availability_zone => s.id...
}
availability_zone_subnet_cidr_blocks = {
for s in data.aws_subnet.private : s.availability_zone => s.cidr_block...
}
}
main.tf
resource "aws_ec2_client_vpn_network_association" "this_private_subnets" {
for_each = toset(local.availability_zone_subnet_ids[var.availability_zone])
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.this.id
subnet_id = each.value
}

resource "aws_ec2_client_vpn_authorization_rule" "this_internal_dns" {
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.this.id
target_network_cidr = "${cidrhost(module.test_vpc.cidr_block, 2)}/32"
authorize_all_groups = true
description = "Authorization for ${var.endpoint_name} to DNS"
}

resource "aws_ec2_client_vpn_authorization_rule" "this_private_subnets" {
for_each = toset(local.availability_zone_subnet_cidr_blocks[var.availability_zone])
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.this.id
target_network_cidr = each.value
access_group_id = var.access_group_id
description = "Rule name: ${each.value}"
}

#
# VPN security group
#
resource "aws_security_group" "this" {
name = "client-vpn-endpoint-${var.endpoint_name}"
description = "Egress All. Used for other groups where VPN access is required."
vpc_id = module.test_vpc.vpc_id
}

resource "aws_security_group_rule" "egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.this.id
}

#
# Connection logging
#
resource "aws_cloudwatch_log_group" "this" {
name = "/aws/client-vpn-endpoint/${var.endpoint_name}"
retention_in_days = 14
}

VPN For EKS​

The above was just example code from Github. Now we will create a VPN for our EKS cluster.

main.tf
data "aws_eks_cluster" "cluster" {
name = var.cluster_name
}

Step One: Create and configure the Client VPN SAML applications in AWS IAM Identity Center​

Create two custom SAML 2.0 applications in AWS IAM Identity Center. One will be the IdP for the Client VPN software, the other will be a self-service portal that allows users to download their Client VPN software and client configuration file.

First:​

aws-client-vpn

  • AWS Client VPN: the IdP for the Client VPN software.
  • Application ACS URL: http://127.0.0.1:35001
  • Application SAML audience: urn:amazon:webservices:clientvpn

Second:​

aws-client-vpn-self-service

  • AWS Client VPN Self-Service: the self-service portal that allows users to download their Client VPN software and client configuration file.
main.tf
resource "aws_iam_saml_provider" "aws-client-vpn" {
name = "aws-client-vpn"
}

Issue​

I have used this tool: https://www.samltool.com/sp_metadata.php

Good evening Mr. Heard,
Hope you are enjoying your evening ^^

I came across this amazing repo, thank you for your efforts, while trying to use terraform to configure `AWS client VPN using AWS IAM Identity Center as the IdP`.

I have been through the hole process using ClickOps or the Console.

### My Question

I used to add the `Application Metadata` manually, as the docs mentioned **If you don’t have a metadata file, you can manually type your metadata values and enter the following values**:
#### E.g. for `aws-client-vpn`
- `Application ACS URL`: http://127.0.0.1:35001
- `Application SAML audience`: urn:amazon:webservices:clientvpn
#### E.g. for `aws-client-vpn-self-service`
- `Application ACS URL`: https://self-service.clientvpn.amazonaws.com/api/auth/sso/saml
- `Application SAML audience`: urn:amazon:webservices:clientvpn

But the terraform code is:
resource "aws_iam_saml_provider" "aws-client-vpn" {
name = "aws-client-vpn"
saml_metadata_document = file("${path.module}/metadata/aws-client-vpn.xml")
}

resource "aws_iam_saml_provider" "aws-client-vpn-self-service" {
name = "aws-client-vpn-self-service"
saml_metadata_document = file("${path.module}/metadata/aws-client-vpn-self-service.xml")
}

You mentioned in the repo `You will need to add the AWS SSO SAML Application metadata files to the terraform/metadata directory.`

How can I generate the `aws-client-vpn.xml` or `aws-client-vpn-self-service.xml` ? Or are there any sample or docs referring to the structure of the file ? Would I need to create the files manually of I should get them from somewhere ?

Thank you in advance ^^

Implementation​

  1. Use this tool to generate the metadata for the two SAML provider.
    • For aws-client-vpn gen aws-client-vpn.xml:
      • EntityId: urn:amazon:webservices:clientvpn.
      • Attribute Consume Service Endpoint (HTTP-POST): http://127.0.0.1:35001.
    • For aws-client-vpn-self-service gen aws-client-vpn-self-service.xml:
      • EntityId: urn:amazon:webservices:clientvpn.
      • Attribute Consume Service Endpoint (HTTP-POST): https://self-service.clientvpn.amazonaws.com/api/auth/sso/saml.
  2. Use aws_iam_saml_provider terraform resource to create the two SAML providers.
  3. Add attributes mappings according to the following table:
User attribute in the applicationMaps to this string value or user attribute in AWS IAM Identity CenterFormat
Subjectuser:emailemailAddress
Nameuser:emailunspecified
FirstNameuser:givenNameunspecified
LastNameuser:familyNameunspecified
memberOfuser:groupsunspecified
  1. Assign admins group to both SAML providers.
  2. Download the idp metadata. // The rest can be automated
  3. Navigate to IAM/Identity Providers and add two new providers:
    • Choose SAMl and name it aws-client-vpn. Then upload the idp metadata file.
    • Choose SAMl and name it aws-client-vpn-self-service. Then upload the idp metadata file.
  4. Import a CA certificate into AWS Certificate Manager (ACM).
./easyrsa init-pki
./easyrsa build-ca nopass
./easyrsa build-server-full server nopass

# Navigate to certificate manger, import:
# Certificate body: server.crt
# Certificate private key: server.key
# Certificate chain: ca.crt
  1. Create a Client VPN endpoint.
    • Under VPC/Client VPN endpoints, create a new endpoint.
    • Name: demo-vpn-endpoint.
    • Client IPv4 CIDR: 172.16.0.0/16.
    • Server certificate: server.crt.
    • Select Use user-based authentication and Federated authentication. Then select aws-client-vpn and aws-client-vpn-self-service.
    • Select VPC id which will be associated with this endpoint.
    • Select the security group.
    • Enable self-service portal and split-tunnel.
  2. Associate the endpoint with the two private subnets.
  3. Add Authorization rules for the endpoint.
    • Add Destination Network: 10.0.128.0/17. That contains the two private subnets CIDRs. Or include the whole VPC CIDR block 10.0.0.0/16.
    • Allow access to users in a specific group. Access Group ID: admins.
  4. Copy the Self Service Portal URL from the endpoint details. And navigate back to: IAM/Identity Center:
    • Click on applications then aws-client-vpn-self-service. Action/Edit Configuration.
    • Under Application Properties, paste the URL in the Application start URL.

REFERENCES​