Important
This is a demonstration, not a fully production-ready solution for isolating sensitive data. Ensure that you do a threat-model for your particular circumstances and environment.
This demo shows how to use Tailscale device posture attributes and Aperture to help mitigate the lethal trifecta.
Its web interface creates user-owned EC2 machines in one of two network classes:
noegressmachines receivecustom:hasEgress=falsewhen their single-use device auth key is created.openmachines receive thecustom:hasEgress=trueattribute.
The example tailnet policy grants members access to devices tagged
tag:aperture, with the Aperture connectors gated by device posture: a device
with the explicit custom:hasEgress=false value may use both
label:noCustomerData and label:hasCustomerData connectors, while a device
with custom:hasEgress=true is limited to label:noCustomerData.
cmd/egressd/ web service that allows users to create EC2 instances
cmd/allowlist/ discovers the Tailscale control and DERP endpoints for Terraform
terraform/ creates the egress-restricted infrastructure, and an optional jump host
tool/terraform downloads, verifies, and runs a pinned Terraform release
policy.hujson example posture-gated tailnet policy
The restricted VPC allows TCP 443 only to the Tailscale control and DERP IPv4 and IPv6 addresses discovered during each Terraform plan or apply, plus the regional S3 IPv4 and IPv6 prefix lists. A default-deny Route 53 Resolver DNS Firewall permits only the corresponding Tailscale hostnames and the four S3 names: the regional and dual-stack endpoints, each plain and bucket-qualified.
The Tailscale .deb package is staged in an endpoint-scoped S3 bucket because
restricted machines cannot reach the package repository.
The operator host needs Go, AWS credentials, Bash, curl, jq, unzip, and
either sha256sum or shasum.
-
Create the infrastructure:
tool/terraform -chdir=terraform init tool/terraform -chdir=terraform apply
To include the isolated SSH jump host, also set
jump_host_enable=true, an existingaws_key_name, and at least one ofjump_host_ssh_cidrsorjump_host_ssh_ipv6_cidrs.Terraform downloads a pinned Tailscale package, verifies its hash, and stages it in the S3 bucket during the apply. To upgrade the package, update the version, URL, and checksum in
terraform/bootstrap_s3.tf. -
Apply
policy.hujsonto a test tailnet, and add Aperture to the tailnet with thetag:aperturetag. -
Apply the
hasCustomerDataornoCustomerDatalabels to connectors in Aperture. -
Get a Tailscale API access token from the console: https://console.tailscale.com/admin/settings/keys
-
Create a device-provisioning OAuth app with the callback URL and allowed node attributes:
curl -X POST "https://api.tailscale.com/api/v2/tailnet/-/oauth-apps" \ -u "<api-access-token>:" \ -H "Content-Type: application/json" \ -o oauth-credentials.json \ -d '{ "name": "lethal-trifecta-mitigation-demo", "redirectUris": ["http://localhost:8080/oauth/callback"], "scopes": ["auth_keys:create:once"], "allowedNodeAttributes": ["custom:hasEgress"] }'
Custom posture attributes require a supported Tailscale plan.
-
Run the service:
export TS_OAUTH_APP_CLIENT_SECRET=tskey-app-... go run ./cmd/egressd \ --region us-east-2 \ --deb-url "$(tool/terraform -chdir=terraform output -raw bootstrap_deb_url)"
Also pass the
--aws-key-nameflag when using the jump host. -
Open http://localhost:8080, choose a machine class, and complete the Tailscale consent screen.
-
Wait a minute or two for it to boot, then use the displayed SSH command to connect to the VM via Tailscale SSH.
Create one machine of each class. In the Tailscale admin console, the
restricted device has the posture attribute custom:hasEgress=false; the open
device has custom:hasEgress=true instead.
If you configure Aperture as a MCP proxy in your agent of choice, you can see that:
- From the restricted machine, the agent is able to use any proxied MCP server
with either the label
hasCustomerDataornoCustomerData, but cannot reach the internet. - From an open machine, only MCP servers tagged with
noCustomerDatacan be reached, and the machine is able to reach the internet.
The restricted VPC currently allows communication with Tailscale's public DERP servers. A root-level agent could join another tailnet -- using, for example, a reusable auth key supplied through prompt injection -- and regain internet access through an exit node on a different tailnet.
Deployments concerned about this attack can run a custom DERP server with
--verify-clients, admit only devices from the intended tailnet, and allowlist
only that DERP server's IP addresses instead of the IPs for the public DERP
servers. This ensures that even if a prompt-injected device is able to join an
arbitrary tailnet, it will not be able to establish connectivity with anyone.
In order to allow full DERP functionality, ports other than TCP 443 should be allowed through the firewall; at the very least, UDP port 3478 (STUN) and TCP port 80 (HTTP) should be allowed. While the demo works without them, this is not a future guarantee.