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. -
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)"
Open http://localhost:8080, choose a machine class, and complete the Tailscale consent screen. Set
--aws-key-namewhen using the jump host.
Create one machine of each class. In the Tailscale admin console, the
restricted device has custom:hasEgress=false; the open device has
custom:hasEgress=true instead. From the restricted machine, TODO something
about Aperture, but it cannot reach the internet. The open machine can reach
the internet but cannot TODO something about Aperture.
The restricted VPC currently allows 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 there.
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 instead of the public servers.