This Terraform module deploys a Teleport cluster on AWS in a high-availability configuration. v2 of the module targets Teleport 18 and consolidates the network surface into a single Network Load Balancer running in TLS routing (multiplex) mode.
- High availability auth and proxy ASGs across multiple AZs.
- Single consolidated NLB fronting both auth and proxy.
- TLS routing (multiplex) by default — every client protocol (web UI,
tsh ssh, Kubernetes, databases, reverse tunnels) flows over the proxy's:443listener. - Defense-in-depth security groups: the public client listener is CIDR-scoped while the internal auth listener is reachable only from members of the cluster security group.
- Optional AWS PrivateLink so consumers in other VPCs/accounts can reach Teleport without traversing the public internet.
- CloudPosse label/context integration for consistent naming/tagging.
- Companion submodules for on-demand SSH tunnels and database login.
The default deployment puts the consolidated NLB in private subnets
(nlb_internal = true). Clients reach Teleport via PrivateLink, a VPN, Direct
Connect, or a bastion in the same VPC. Set nlb_internal = false and supply
vpc_public_subnet_ids to expose the cluster on the public internet.
module "teleport_cluster" {
source = "cruxstack/teleport-cluster/aws"
version = "2.0.0"
teleport_letsencrypt_email = "letsencrypt@example.com"
teleport_runtime_version = "18.0.0"
teleport_setup_mode = false
dns_parent_zone_id = "Z0000000000000000000"
dns_parent_zone_name = "demo.example.com"
vpc_id = "vpc-00000000000000"
vpc_private_subnet_ids = ["subnet-...", "subnet-...", "subnet-..."]
}The internal default pairs naturally with PrivateLink so consumers in other VPCs/accounts can reach Teleport without traversing the public internet.
module "teleport_cluster" {
# ... required inputs ...
nlb_privatelink_enabled = true
nlb_privatelink_config = {
acceptance_required = true
allowed_principals = [
"arn:aws:iam::111111111111:root",
"arn:aws:iam::222222222222:root",
]
}
}Consumers in other accounts create an aws_vpc_endpoint against the service
name returned in teleport_nlb_vpce_service_name. Only the public client
listener (:443) is reachable through the endpoint service — auth (:3025)
remains gated to the cluster security group and is rejected by the NLB.
If nlb_privatelink_config.private_dns_name is set, the module also creates the
Route 53 TXT verification record that AWS requires before consumers can enable
private_dns_enabled on their VPC endpoint. The record is placed in the parent
zone (dns_parent_zone_id).
Switch to a public NLB by setting nlb_internal = false and supplying public
subnets. When the proxy ASG lives in the same VPC as the NLB, also set
nlb_auth_allowed_cidrs to the VPC's NAT gateway EIPs so the proxy's reverse
tunnel to auth on :3025 is admitted (the source IP at the NLB is the NAT EIP,
not a cluster SG member, when traffic hairpins out and back).
module "teleport_cluster" {
# ... required inputs ...
nlb_internal = false
vpc_public_subnet_ids = ["subnet-...", "subnet-...", "subnet-..."]
nlb_allowed_cidrs = ["0.0.0.0/0"]
nlb_auth_allowed_cidrs = ["203.0.113.10/32", "203.0.113.11/32"] # VPC NAT EIPs
}In addition to the variables documented below, this module includes several
other optional variables (e.g., name, tags, etc.) provided by the
cloudposse/label/null module. See its
documentation
for details.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
teleport_runtime_version |
The runtime version of Teleport (v18 recommended). | string |
n/a | yes |
teleport_letsencrypt_email |
The email address to use for Let's Encrypt. | string |
n/a | yes |
teleport_setup_mode |
Toggle Teleport setup mode. | bool |
true |
no |
teleport_experimental_mode |
Toggle Teleport experimental mode. | bool |
false |
no |
teleport_auth_address |
Override the host[:port] proxies dial to reach auth. Empty ⇒ cluster FQDN (may be shadowed by PrivateLink private DNS — see notes). |
string |
"" |
no |
deletion_protection_enabled |
Enable deletion protection on the NLB and DynamoDB tables. null ⇒ follows !teleport_experimental_mode. |
bool |
null |
no |
instance_config |
Configuration for the auth and proxy ASGs (auth, proxy keys with count, sizes, spot). |
object |
{} |
no |
nlb_internal |
Use an internal NLB (private subnets) instead of internet-facing (public subnets). | bool |
true |
no |
nlb_allowed_cidrs |
CIDRs allowed on the public client port (443) of the NLB. | list(string) |
["0.0.0.0/0"] |
no |
nlb_auth_allowed_cidrs |
CIDRs allowed on the auth listener (3025). Defaults to empty (cluster SG only). See bootstrap notes. | list(string) |
[] |
no |
nlb_privatelink_enabled |
Expose the NLB as a PrivateLink endpoint service. | bool |
false |
no |
nlb_privatelink_config |
PrivateLink config: acceptance_required, allowed_principals, private_dns_name, supported_regions. |
object |
{} |
no |
artifacts_bucket_name |
The name of the S3 bucket for artifacts. | string |
"" |
no |
logs_bucket_name |
The name of the S3 bucket for logs. | string |
"" |
no |
dns_parent_zone_id |
The ID of the parent DNS zone. | string |
n/a | yes |
dns_parent_zone_name |
The name of the parent DNS zone. | string |
n/a | yes |
vpc_id |
The ID of the VPC to deploy resources into. | string |
n/a | yes |
vpc_private_subnet_ids |
The IDs of the private subnets in the VPC. | list(string) |
n/a | yes |
vpc_public_subnet_ids |
The IDs of the public subnets. Required when nlb_internal = false; may be empty otherwise. |
list(string) |
[] |
no |
aws_region_name |
The name of the AWS region. | string |
"" |
no |
aws_account_id |
The ID of the AWS account. | string |
"" |
no |
aws_kv_namespace |
The namespace or prefix for AWS SSM parameters and similar resources. | string |
"" |
no |
| Name | Description |
|---|---|
teleport_dns_name |
The DNS name of the Teleport service. |
teleport_auth_config |
The Teleport auth configuration. |
teleport_proxy_config |
The Teleport proxy configuration. |
teleport_nlb_dns_name |
DNS name of the consolidated Teleport NLB. |
teleport_nlb_arn |
ARN of the consolidated Teleport NLB. |
teleport_nlb_zone_id |
Hosted zone ID of the consolidated Teleport NLB. |
teleport_nlb_security_group_id |
Dedicated NLB security group (public CIDR ingress lives here). |
teleport_nlb_target_group_arns |
Map of target group ARNs (auth_ssh, proxy_web). |
teleport_nlb_vpce_service_name |
PrivateLink endpoint service name (empty when disabled). |
teleport_nlb_vpce_service_id |
PrivateLink endpoint service ID (empty when disabled). |
teleport_nlb_vpce_service_arn |
PrivateLink endpoint service ARN (empty when disabled). |
security_group_id |
The ID of the cluster security group. |
security_group_name |
The name of the cluster security group. |
The consolidated NLB has exactly two listeners:
| Listener | Backend target | Who can reach it |
|---|---|---|
:443 |
proxy ASG → :3080 |
nlb_allowed_cidrs plus any member of the cluster security group. |
:3025 |
auth ASG → :3025 |
Members of the cluster security group only. Never reachable from the public. |
This is enforced by attaching two security groups to the NLB:
- A dedicated NLB security group (managed by this module) that holds the public
CIDR ingress rules for
:443only. - The shared cluster security group, of which both auth and proxy instances are
members. Its existing self-ingress rule grants cluster members and the NLB
ENIs full reachability to each other on every port — which is what covers the
proxy ⇄ auth control channel on
:3025.
enforce_security_group_inbound_rules_on_private_link_traffic = on means the
same rules also apply to PrivateLink consumer traffic, so endpoint ENIs (which
are not members of the cluster SG) can never reach :3025.
The proxy_web target group has proxy_protocol_v2 = true, and the Teleport
proxy config carries proxy_service.proxy_protocol = "on". The two together
preserve the real client IP at L7 (for the audit log) while the L3 source on the
wire is the NLB ENI — which is exactly what makes the security-group based
design work.
The default (nlb_internal = true) puts the NLB in the private subnets.
Same-VPC proxy ⇄ auth traffic stays in-VPC: the proxy resolves the NLB DNS to
private ENI IPs and the cluster SG admits the connection on :3025. Public
clients reach the cluster via one of:
- PrivateLink. Set
nlb_privatelink_enabled = trueand have consumers create endpoints against the resulting endpoint service. - VPN / Direct Connect. Anything that puts the client in the cluster VPC's routable address space resolves the NLB DNS internally.
- In-VPC bastion or operator workstation. Same constraint as VPN: the resolver needs to be inside the VPC.
To expose Teleport on the public internet, set nlb_internal = false and supply
vpc_public_subnet_ids. When the proxy ASG lives in the same VPC and the NLB is
internet-facing, the proxy resolves the NLB's public DNS to its public ENI IPs
and the traffic egresses via the VPC NAT gateway. At the NLB, the apparent
source is the NAT EIP — not a member of the cluster security group — so the
SG-only :3025 rule rejects the connection and the proxy never registers with
auth. Two ways to resolve it, in order of preference:
- Use
nlb_internal = false+nlb_auth_allowed_cidrs. Set the variable to the VPC NAT gateway EIPs so the auth listener accepts traffic arriving from those specific IPs. Wider scopes work but increase the public attack surface on:3025. mTLS still gates the connection at Teleport, but defense-in-depth is reduced. - Use
nlb_internal = false+ PrivateLink for in-VPC consumers. Deploy anaws_vpc_endpointin the cluster VPC against the module's endpoint service and point the proxy at the endpoint DNS instead of the NLB DNS. More moving parts; only worth it if you have a wider PrivateLink story.
Enabling nlb_privatelink_enabled = true together with
nlb_privatelink_config.private_dns_name registers the cluster FQDN against the
endpoint service. AWS attaches that private DNS to every VPC that hosts a
consumer endpoint — including the cluster's own producer VPC if a consumer
endpoint exists there. In that VPC the cluster FQDN now resolves to the
PrivateLink ENI's private IPs and proxy ⇄ auth :3025 traffic enters the NLB
with the PrivateLink ENI IP as its source. With nlb_internal = false the NLB's
security group on :3025 admits the NAT EIPs, not the PrivateLink ENI IPs, and
the dials silently time out.
Pass teleport_auth_address = module.<self>.teleport_nlb_dns_name (or any other
address that PrivateLink does not shadow) to keep the proxy → auth path off
PrivateLink. The raw NLB DNS resolves to the NLB ENIs directly and falls inside
nlb_internal's SG-only admit rule.
v2 is a breaking change. The major items:
- Single consolidated NLB. The dedicated
authandproxyNLBs and their target groups/listeners are gone. AWS will replace the existing NLB(s) and target groups; Route53 proxy alias records are migrated in place viamoved {}blocks and update to point at the new NLB. - TLS routing (multiplex) is mandatory. The proxy now uses
version: v2and only opens itsweb_listen_addr(:3080, fronted by the NLB on:443). The auth advertisesproxy_listener_mode: multiplex. Both halves must agree — see Teleport issue #57009. All client protocols (SSH, k8s, databases) now ride:443. noderole removed.module.node_servers, theinstance_config.nodekey, and theteleport_node_configoutput are gone. Pre-existing node ASGs are destroyed on the nextterraform apply.vpc_security_group_allowed_cidrs/instance_config.*.allowed_cidrsremoved. Client allow-lists moved tonlb_allowed_cidrson the consolidated NLB.- PROXY protocol. Connections from the NLB to the proxy carry PROXY
protocol v2. Anything bypassing the NLB and hitting the proxy on
:3080directly will be rejected — by design. nlb_internaldefaults totrue. v1 was implicitly internet-facing for the proxy NLB. The v2 default is internal so same-VPC proxy ⇄ auth traffic works without extra config. Passnlb_internal = falseandvpc_public_subnet_idsto restore public exposure.- Deletion protection unified.
ddb_deletion_protection_enabledandnlb_deletion_protectionare removed. Usedeletion_protection_enabled(defaultnull⇒ follows!teleport_experimental_mode) instead. - Terraform
required_versionbumped to>= 1.9.0to use cross-variable references invalidationblocks.
We welcome contributions. See CONTRIBUTING for setup and contribution guidelines.