Introduction

Building secure, private networks across isolated cloud environments is a common challenge in modern infrastructure. While AWS offers solutions like VPC peering and Transit Gateway, these can become complex and expensive, especially when connecting resources across multiple AWS accounts, regions, or even different cloud providers.

In this guide, I'll show you how to build a private mesh VPN network using Headscale - a self-hosted, open-source alternative to Tailscale's control server. We'll connect three isolated AWS VPCs across different accounts and regions, coordinated by a self-hosted Headscale control plane, and fully provisioned using Ansible for infrastructure-as-code automation.

This architecture demonstrates how Headscale provides a simpler, more cost-effective approach to private networking without the complexity of traditional AWS networking solutions.

What is Headscale?

Headscale is an open-source, self-hosted implementation of the Tailscale control server. While Tailscale is a commercial SaaS offering that manages your mesh network's control plane, Headscale allows you to run your own control plane infrastructure, giving you complete ownership and control.

Key benefits of Headscale:

Architecture Overview

Our demo architecture consists of three AWS EC2 instances deployed across different regions and accounts:

Node Role Public IP Region VPC CIDR
headscale-coordination-server Control plane 15.152.149.240 ap-northeast-3 -
headscale-client-01 Mesh node / subnet router 34.237.124.11 us-east-1 10.100.0.0/16
headscale-client-02 Mesh node / subnet router 13.207.176.206 ap-south-1 172.31.0.0/16

Each client sits in its own VPC and advertises its local CIDR into the mesh network. The coordination server auto-approves these routes for nodes tagged with tag:gateway, so once a client connects, it's immediately reachable across the mesh - no manual route approval needed.

Network Architecture Diagram

Headscale mesh VPN network architecture showing coordination server and two client nodes across AWS VPCs Fig: Headscale mesh network architecture across AWS VPCs

Each client is deployed in a separate VPC, AWS account, and region. Headscale gives them a shared, encrypted overlay network (100.64.0.0/10) without VPC peering or transit gateways. Each node receives a stable mesh IP and can reach the other side's advertised subnet directly through the encrypted WireGuard tunnel.

Infrastructure Provisioning with Ansible

The entire deployment follows a layered approach using Ansible for infrastructure-as-code automation. This ensures repeatability, version control, and consistent configuration across all nodes.

Three-Tier Deployment Model

Our Ansible playbooks are organized into three distinct layers:

Deploying the Coordination Server

# Deploy Headscale coordination server
ansible-playbook -i inventories/dev/hosts.yml playbooks/site.yml --limit role_headscale_server

Deploying Client Nodes

# Deploy Headscale client nodes
ansible-playbook -i inventories/dev/hosts.yml playbooks/site.yml --limit role_headscale_client

Automated User Enrollment

User enrollment and pre-authentication key generation is fully automated through Ansible:

# Generate pre-auth keys for new nodes
ansible-playbook -i inventories/dev/hosts.yml playbooks/headscale_enroll.yml -e headscale_user_generate_preauth_keys=true

Ansible Inventory Configuration

The inventory defines our mesh topology with appropriate tags and groupings:

headscale_users:
  - name: dev-aws-ops-tky-dkr-headscale-client-01
    tags: [tag:gateway, tag:dev]
  - name: dev-aws-ops-tky-dkr-headscale-client-02
    tags: [tag:gateway, tag:stg]

dev:
  hosts:
    dev-aws-ops-tky-net-headscale-coordination-server:
      ansible_host: 15.152.149.240
    dev-aws-ops-tky-dkr-headscale-client-01:
      ansible_host: 34.237.124.11
    dev-aws-ops-tky-dkr-headscale-client-02:
      ansible_host: 13.207.176.206

role_headscale_server:
  hosts:
    dev-aws-ops-tky-net-headscale-coordination-server:

role_headscale_client:
  hosts:
    dev-aws-ops-tky-dkr-headscale-client-01:
    dev-aws-ops-tky-dkr-headscale-client-02:

Subnet Router Configuration

Each client node is configured as a subnet router (gateway) to advertise its VPC CIDR to the mesh network. This configuration is defined in the node's host_vars and pulled from Ansible Vault for sensitive data like authentication keys.

Client 01 Configuration (us-east-1)

service_headscale_client_router:
  hostname: "dev-aws-ops-tky-dkr-headscale-client-01"
  auth_key: "{{ vault_headscale_client_1_auth_key }}"
  advertise_routes: ["10.100.0.0/16"]
  advertise_tags: ["tag:gateway"]
  accept_dns: true
  accept_routes: true
  snat_subnet_routes: true
  enable_ipv4_forwarding: true
  manage_mss_clamping: true

Client 02 Configuration (ap-south-1)

service_headscale_client_router:
  hostname: "dev-aws-ops-tky-dkr-headscale-client-02"
  auth_key: "{{ vault_headscale_client_2_auth_key }}"
  advertise_routes: ["172.31.0.0/16"]
  advertise_tags: ["tag:gateway"]
  accept_dns: true
  accept_routes: true
  snat_subnet_routes: true
  enable_ipv4_forwarding: true
  manage_mss_clamping: true

Configuration Highlights:
advertise_routes: Specifies which CIDR blocks to advertise to the mesh
advertise_tags: Tags used for ACL policy matching and auto-approval
snat_subnet_routes: Enables NAT for traffic forwarding
manage_mss_clamping: Handles MTU issues for proper packet forwarding

Access Control Lists (ACL)

Access control in Headscale is policy-driven through ACL definitions written in HuJSON format. These policies are rendered and deployed via Ansible as infrastructure-as-code, ensuring consistent, auditable, and version-controlled access rules.

ACL Policy Structure

Our baseline ACL policy includes:

ACL Configuration Example

"tagOwners": {
  "tag:prod": ["group:admins"],
  "tag:staging": ["group:admins"],
  "tag:gateway": ["group:admins"]
},

"acls": [
  {
    "action": "accept",
    "src": ["group:admins"],
    "dst": ["*:*"]
  },
  {
    "action": "accept",
    "src": ["tag:prod", "tag:staging"],
    "dst": [
      "tag:prod:*",
      "tag:staging:*",
      "10.0.0.0/16:*",
      "172.31.0.0/16:*"
    ]
  },
  {
    "action": "accept",
    "src": ["*"],
    "dst": ["*:*"],
    "proto": "icmp"
  }
],

"autoApprovers": {
  "routes": {
    "10.100.0.0/16": ["tag:gateway"],
    "172.31.0.0/16": ["tag:gateway"]
  }
}

Note: This policy is a baseline for the demo and is fully configurable. In production environments, you would refine these rules based on specific team and service access requirements. ACL changes can be reloaded without disrupting existing connections.

Reloading ACL Configuration

After making ACL changes, reload the configuration without disrupting active connections:

ansible-playbook -i inventories/dev/hosts.yml playbooks/headscale_reload.yml

Verification and Testing

Once the infrastructure is deployed, it's important to verify that all components are functioning correctly and that the mesh network is operational.

Coordination Server Verification

On the Headscale coordination server, you can inspect users, nodes, routes, and pre-auth keys:

# List all users
docker exec headscale headscale users list

# List all connected nodes
docker exec headscale headscale nodes list

# List advertised routes
docker exec headscale headscale routes list

# List pre-auth keys
docker exec headscale headscale preauthkeys list
Headscale coordination server showing registered users, nodes, and pre-auth keys Fig: Headscale coordination server - users, nodes, and routes

This confirms:

Client Node Verification

On each client node, verify the Tailscale status and test connectivity:

# Check Tailscale status
tailscale status

# Ping peer by mesh IP or hostname
tailscale ping <peer-mesh-ip-or-name>

Client 01 Status (us-east-1)

Headscale client 01 showing tailscale status and successful ping to client 02 Fig: Client 01 - Connected to mesh and can ping Client 02

Client 02 Status (ap-south-1)

Headscale client 02 showing tailscale status and successful ping to client 01 Fig: Client 02 - Connected to mesh and can ping Client 01

The verification confirms:

Frequently Asked Questions (FAQ)

What is Headscale and how does it differ from Tailscale?

Headscale is an open-source, self-hosted implementation of the Tailscale control server. While Tailscale is a commercial SaaS offering, Headscale allows you to run your own control plane, giving you complete control over your mesh network infrastructure without relying on external services.

Why use Headscale instead of VPC peering or Transit Gateway?

Headscale provides a simpler, more cost-effective solution for connecting resources across VPCs, regions, or even cloud providers. Unlike VPC peering or Transit Gateway, Headscale works across different AWS accounts, regions, and even non-AWS environments without complex networking configuration. It also provides encrypted peer-to-peer connections and easier access control through ACL policies.

How does subnet routing work in Headscale?

Subnet routing allows a Headscale node to advertise routes to its local network. When configured as a subnet router (gateway), the node forwards traffic from the mesh network to its local VPC CIDR. With auto-approval policies, nodes tagged appropriately can automatically have their advertised routes approved, enabling seamless connectivity without manual intervention.

What are ACL policies in Headscale?

Access Control Lists (ACLs) in Headscale define who can access what resources in your mesh network. ACL policies use tags, groups, and users to control traffic flow between nodes. You can specify which nodes can communicate, which protocols are allowed, and which routes should be automatically approved. ACLs are defined in HuJSON format and can be managed as infrastructure-as-code.

Can Headscale connect VPCs across different AWS accounts and regions?

Yes, Headscale works seamlessly across AWS accounts, regions, and even different cloud providers. Each node connects to the Headscale coordination server over the internet using encrypted WireGuard tunnels, making it ideal for multi-account, multi-region, or hybrid cloud architectures without requiring VPC peering or complex networking setup.

How do you automate Headscale deployment with Ansible?

Headscale deployment with Ansible follows a layered approach: baseline (OS hardening, users, SSH), profile (Docker host setup), and service (Headscale server or client role). Ansible manages the entire lifecycle including node enrollment, pre-auth key generation, route configuration, and ACL policy deployment. All configuration is stored in inventory and host_vars, making it fully repeatable and version-controlled.

Current Status and Next Steps

The current deployment demonstrates a working Headscale mesh VPN with the following achievements:

Production Considerations

Before moving to production, consider the following enhancements:

Conclusion

Headscale provides a powerful, cost-effective alternative to traditional AWS networking solutions for connecting isolated environments. By leveraging WireGuard's modern encryption and Tailscale's mesh networking approach, Headscale enables secure, private networks across AWS accounts, regions, and even cloud providers without the complexity of VPC peering or Transit Gateways.

The infrastructure-as-code approach using Ansible ensures that the entire deployment is repeatable, version-controlled, and auditable. From initial provisioning to ACL management, every aspect of the mesh network can be managed through code, making it ideal for teams practicing DevOps and GitOps methodologies.

Whether you're building a development environment spanning multiple accounts, creating a staging network that mirrors production, or establishing secure connectivity for a hybrid cloud architecture, Headscale offers a flexible and maintainable solution that scales with your infrastructure needs.

Written By

Ujwal Budha

Hello, I am Ujwal Budha. Currently working as a DevOps Engineer. Expert in creating scalable cloud infrastructure and automating the workflow for deployment. An AWS Certified Solution Architect Associate, Ujwal enjoys sharing knowledge in the form of technical blogs and helping others to go through their cloud journey.