Following on from a previous post, I wanted to explore a way to rapidly deploy exit nodes using a container.

Overview

The whole process is fairly straight forward.

  1. We run a task in AWS Elastic Container Service (ECS) to launch a container using Fargate
  2. The container is running the Tailscale Docker image
  3. The container is using an authkey to join our Tailnet
  4. We connect to the exit node from a client, then stop the task.

Tailscale configuration

While Headscale has improved quite a bit since my last post about it, I’m trying to maintain a minimal amount of services myself so I’ve settled for using Tailscale.

We need to set up our Tailnet to do a few things before we start up our containers. First lets adjust our access controls so that we automatically approve exit nodes with the tag exit.

1	"autoApprovers": {
2		"exitNode": ["tag:exit"],
3	},

We’ll then go an create an auth-key to use for adding our containers to the Tailnet. We’ll want it to be reusable, our containers aren’t going to persist so we’ll want them to be ephemeral, we want to pre-approve the devices so that we don’t need to log into the management console every time we spin one up, and lastly we’ll tag them as an exit.

ts-authkey

Copy your ts-authkey to somewhere safe, we’ll use this later in our ECS task definition. You’ll need to re-generate it every 90 days, in the future I plan to look into a way of automating the creation of this authkey as well as launching the container.

Create an ECS cluster and task definition

Log into the AWS console and navigate to ECS and create a cluster. Just give it a name and click create, it should already be set to use AWS Fargate.

Next navigate to Task definitions. Below is a JSON you can use, the key details are specifying the container as tailscale/tailscale:stable, and having the right environment variables. Refer to the read me of the container for which variables are accepted. I’m using the TS_EXTRA_ARGS variable to set --advertise-exit-node

 1{
 2    "taskDefinitionArn": "arn:aws:ecs:<REGION>:<ACCOUNT_ID>:task-definition/ts-exit-task:1",
 3    "containerDefinitions": [
 4        {
 5            "name": "ts-exit-container",
 6            "image": "tailscale/tailscale:stable",
 7            "cpu": 512,
 8            "memory": 1024,
 9            "memoryReservation": 512,
10            "portMappings": [],
11            "essential": true,
12            "environment": [
13                {
14                    "name": "TS_AUTHKEY",
15                    "value": "tskey-auth-aaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
16                },
17                {
18                    "name": "TS_EXTRA_ARGS",
19                    "value": "--advertise-exit-node"
20                },
21                {
22                    "name": "TS_HOSTNAME",
23                    "value": "ts-exit"
24                }
25            ],
26            "environmentFiles": [],
27            "mountPoints": [],
28            "volumesFrom": [],
29            "ulimits": [],
30            "systemControls": []
31        }
32    ],
33    "family": "ts-exit-task",
34    "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
35    "networkMode": "awsvpc",
36    "revision": 1,
37    "volumes": [],
38    "status": "ACTIVE",
39    "requiresAttributes": [
40        {
41            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"
42        },
43        {
44            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
45        },
46        {
47            "name": "ecs.capability.task-eni"
48        }
49    ],
50    "placementConstraints": [],
51    "compatibilities": [
52        "EC2",
53        "FARGATE"
54    ],
55    "requiresCompatibilities": [
56        "FARGATE"
57    ],
58    "cpu": "512",
59    "memory": "1024",
60    "runtimePlatform": {
61        "cpuArchitecture": "X86_64",
62        "operatingSystemFamily": "LINUX"
63    },
64    "registeredAt": "2024-11-19T23:09:07.162Z",
65    "registeredBy": "arn:aws:iam::<ACCOUNT_ID>:<USERNAME>",
66    "tags": []
67}

Start your exit node and connect

Now you can run your ECS task to suite your needs. For example you could point and click to manually spin up a container (deploy -> run task), but you could also run it on a schedule, or use the aws cli to kick it off. I’ll explore some other possibilities here in the future.

After launching the task, you should see a new exit node appear in your Tailnet in a few seconds.

ts-exit

Doing a speed test from my phone via the exit node yields a fairly decent speed and shows that we’re presenting from the IP address of our ECS container.

ts-speedtest

Once you stop the task in ECS, the node will be removed from the Tailnet as it was ephemeral.

Cool 😎