Terraform module to deploy a self-hosted Headscale VPN server on AWS with automated user onboarding, ACL-based access control, and subnet routing.
Single EC2 instance running both Headscale (coordination server) and Tailscale (subnet router)
Automated user creation and auth key generation via terraform apply
Auth keys stored in AWS SSM Parameter Store (SecureString)
ACL policy with user groups for granular access control
Subnet routing to access all private IPs in your VPC over VPN
headscale-terraform-registry/
├── configs/ # Configuration templates
│ ├── headscale_config.tpl # Headscale server configuration template
│ └── user_data.tpl # EC2 instance bootstrap script
├── scripts/ # Automation scripts
│ └── create-user.sh # User creation and auth key generation via SSM
├── examples/ # Usage examples
│ ├── complete/ # Full configuration with all options
│ └── minimal/ # Minimal working configuration
├── main.tf # Core infrastructure (EC2, EIP, VPC lookup)
├── variables.tf # Module input variables
├── outputs.tf # Module output values
├── iam.tf # IAM roles and policies for SSM access
├── users.tf # User provisioning via null_resource
├── security_groups.tf # Network security group rules
└── versions.tf # Terraform and provider version constraints
This module deploys a complete VPN solution using Headscale (open-source Tailscale control server) on a single EC2 instance:
┌─────────────────────────────────────────────────────────────┐
│ AWS VPC │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Headscale EC2 Instance │ │
│ │ ┌──────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Headscale │ │ Tailscale │ │ │
│ │ │ (Controller) │◄─┤ (Subnet Router) │ │ │
│ │ │ Port: 8080 │ │ Routes: VPC CIDR │ │ │
│ │ └────────┬─────────┘ └─────────────────────────┘ │ │
│ │ │ │ │
│ │ │ Elastic IP (Public) │ │
│ └───────────┼────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────▼──────────────┐ │
│ │ Private Resources │ │
│ │ (RDS, EC2, EKS, etc.) │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲
│ VPN Connection (WireGuard)
│
┌──────────┴──────────┐
│ Client Devices │
│ (Laptop, Mobile) │
│ w/ Tailscale │
└─────────────────────┘
External Services:
┌─────────────────────┐
│ AWS SSM Parameter │ ← Auth keys stored as SecureStrings
│ Store │
└─────────────────────┘
Headscale Server : Open-source coordination server that manages VPN clients, assigns IPs, and enforces ACL policies
Tailscale Client : Acts as a subnet router on the same EC2 instance to forward traffic to private resources
Security Groups : Allow inbound connections on port 8080 (Headscale) and SSH from specified CIDR
IAM Role : Grants EC2 instance permissions to store/retrieve auth keys in SSM Parameter Store
User Provisioning : Automated via null_resource + SSM Run Command to create users and generate auth keys
1. Infrastructure Provisioning
Terraform creates an EC2 instance in your specified public subnet
An Elastic IP is allocated and associated with the instance for stable public access
Security groups allow inbound traffic on port 8080 (Headscale API) and SSH
IAM instance profile grants SSM permissions for remote command execution
2. Server Bootstrap (user_data.tpl)
Installs Headscale binary and creates systemd service
Configures Headscale with the public IP and ACL policy
Installs Tailscale client on the same instance
Creates a subnet-router user and connects Tailscale to Headscale locally
Enables IP forwarding and advertises VPC CIDR routes
Deploys helper script for user creation via SSM
3. User Creation (create-user.sh)
For each user defined in user_groups, Terraform triggers a null_resource
The script uses AWS SSM Send Command to remotely execute user creation on the EC2
Creates Headscale user, generates a reusable preauth key (365-day expiration)
Stores the auth key securely in SSM Parameter Store as a SecureString
User groups are defined in Terraform variables
ACL policy controls which users can access which IPs in the VPC
Applied at Headscale server startup and enforced for all connections
5. Client Connection Flow
Client Device → Tailscale App → Headscale Server (Port 8080)
↓
Validates Auth Key
↓
Assigns VPN IP (100.64.x.x)
↓
Applies ACL Rules
↓
Routes traffic to subnet-router
↓
Forwards to VPC resources
Name
Version
aws
~> 5.0
null
~> 3.0
Name
Description
Type
Default
Required
allowed_ssh_cidr
Your IP for SSH (e.g. 1.2.3.4/32)
string
n/a
yes
ami_id
AMI ID (Amazon Linux 2023)
string
"ami-0317b0f0a0144b137"
no
aws_region
AWS region for SSM parameter storage
string
"ap-south-1"
no
headscale_version
Version of headscale to install on the server
string
"0.23.0"
no
instance_type
EC2 instance type for the headscale server
string
"t3.micro"
no
public_subnet_id
ID of the public subnet for the headscale EC2 instance
string
n/a
yes
ssh_key_name
Name of the AWS SSH key pair for EC2 instance access
string
n/a
yes
tags
Map of tags to assign to all resources
map(string)
{}
no
user_groups
Map of group name to users and allowed IPs. Use ["*"] for full access.
map(object({ users = list(string) allowed_ips = list(string) }))
{}
no
vpc_id
ID of the VPC where headscale server will be deployed
string
n/a
yes