Introducing TailscaleHound: Mapping Tailscale Attack Paths in BloodHound

Read Time

12 mins

Published

May 21, 2026

Share

TL;DR: TailscaleHound is an OpenGraph collector for BloodHound that maps Tailscale users, devices, groups, tags, ACLs, grants, SSH rules, routes, app connectors, services, keys, invites, webhooks, and hybrid Azure identity relationships. The result is a graph that helps answer practical questions like, “Which users can reach this device?”, “Who can use this exit node?”, “Which routes are exposed through subnet routers?”, and “Which Azure users inherit Tailscale access?”

Take me to the repo.

Introduction

In Leveraging Tailscale Keys, Andrew Luke covered the Tailscale concepts and red team tradecraft that matter when authentication keys show up during an assessment. That post is the right place to start if you want a primer on tailnets, nodes, subnet routers, exit nodes, and why Tailscale access can matter so much during an operation.

This blog picks up where Andrew Luke left off.

Once Tailscale exists in an environment, the next problem is visibility. Tailscale access is not just a “VPN” but, rather, a set of identity, device, group, tag, route, SSH, posture, and policy relationships. A user may not have direct access to a sensitive host, but they may be in a group that has access to a tag. A tagged device may advertise routes to internal network spaces. An exit node may provide traffic egress from a privileged location. A Tailscale user may also line up cleanly with an Azure user that already exists in BloodHound. These can quickly turn into identity problems during an incident.

TailscaleHound turns Tailscale into OpenGraph data so those relationships can be queried and visualized as attack paths. The “Saved Queries Overview” section will cover each of these scenarios.

What TailscaleHound Collects

TailscaleHound currently models 39 node kinds and 64 relationship kinds under the TS namespace. The main object families include but are not limited to:

AreaNode Kinds
Tailnet metadataTS_Network, TS_IDP, TS_ExternalTailnet
IdentitiesTS_User, TS_ExternalUser, TS_Group, TS_AutoGroup, TS_Tag, TS_ExternalTag ,
Devices and routesTS_Device, TS_ExternalDevice, TS_Route, TS_Cidr, TS_HostAlias
PolicyTS_ACL, TS_Grant, TS_SSHRule, TS_ACLTest, TS_SSHTest , TS_PortSpec
PostureTS_PostureTS_DefaultSrcPostureTS_NodeAttr
Tailnet objectsTS_AuthKey, TS_APIKey, TS_ClientKey, TS_FederatedKey, TS_Service, TS_AppConnector, TS_Webhook, TS_UserInvite, TS_DeviceInvite
AreaRelationship Kinds
Identity and rolesTS_IsMemberOfTS_IsOwnerOfTS_IsAdminOfTS_IsNetworkAdminOfTS_IsITAdminOf
DevicesTS_RegisteredDeviceTS_HasTagTS_EnabledRouteTS_IsExitNode
ACLs and grantsTS_AclSourceTS_AclTargetsDeviceTS_GrantSourceTS_GrantTargetsRouteTS_RequiresPosture
SSHTS_SSHRuleSourceTS_SSHRuleTargetsDeviceTS_SSHRuleTargetsSelfTS_SSHRuleAllowsUser
Funnel and servicesTS_HasFunnelCapabilitiesTS_HasFunnelEnabledTS_HasServiceTS_ServiceRunsOnTS_ServiceHasTag
Hybrid identityTS_AZUserSyncedToUser

The collector has two main collection modes:

  • Remote collection through the Tailscale API, with optional admin-panel enrichment through a tailcontrol cookie.
  • Local collection from tailscale status --json, useful for quick user, device, route, tag, and shared-device visibility when API access is not available. A status file can be augmented with a local Access Policy file to model ACLs, grants, SSH rules, posture requirements, host aliases, ipsets, node attributes, and app connectors.

Remote collection gives the broadest graph because it can include ACL policy, grants, SSH rules, keys, services, invites, webhooks, DNS settings, contacts, and other tailnet metadata. Local collection is intentionally narrower, but it can still map registered devices, external users, external tailnets, externally scoped tags, advertised and enabled routes, exit node options, Funnel state visible in status, and policy-derived access paths when an Access Policy file is supplied.

TailscaleHound Account Setup

Read Only OAuth Credentials

TailscaleHound requires Read permissions to enumerate the environment. I recommend generating an OAuth token with only Read permissions to run TailscaleHound. To generate an OAuth token login to Tailscale as an Owner, Admin, IT Admin or Network Admin. Browse to “settings -> Trust Credentials -> click + Credential...

Provide the key read-only across all resources, click “Generate credential”, and record your credentials.

Tailscale Auditor Account

Some useful fields are not available from the public API paths this collector uses (e.g., external devices, which devices have an active funnel, or what IDP is being utilized). If you provide a tailcontrol cookie to TailscaleHound, it will try to enrich the graph with additional machine, user, key, identity provider, and settings data.

The minimum role I’ve seen that can provide this information is the Auditor role. To grant this role to a user, login with an Owner, Admin, or IT admin account and go to Users panel, select the ... next to the user, and select Edit Role. Grant them the Auditor role to give them read only access to the admin console.

To obtain a tailcontrol cookie, open your browser and launch the Network tab in the DevTool console by pressing the F12 key. Log into Tailscale and look for any network call to the /admin/api endpoint such as self, tailnets, etc.

Select one of those calls, such as machines and inspect grab your tailcontrol cookie from the Headers tab under to Cookie section

Remote Collection With OAuth + Tailcontrol

Using the OAuth credentials and tailcontrol cookie we created, run TailscaleHound with the following command. This will output a JSON file that can then be ingested into BloodHound.

python3 TailscaleHound.py --ts-client-id "$TAILSCALE_CLIENT_ID" --ts-oauth-secret "$TAILSCALE_OAUTH_SECRET" --tailcontrol "$TAILCONTROL_COOKIE" --output ./tailscalehound-output --verbose

Local Collection With Status and Policy

During an engagement, you may land on a host that already has Tailscale installed but not have Tailscale API credentials. You may also find a copy of the tailnet Access Policy, especially in environments where policy is managed through GitOps. In that situation, local collection lets you turn the host’s Tailscale view into graph data:

tailscale status --json > tailscale-status.json

python3 TailscaleHound.py --status-file tailscale-status.json --policy-file access-policy.json --output ./tailscalehound-output --verbose

A policy file is optional. With only the status file, local mode maps devices, users, external tailnets, external users, external tags, routes, and capabilities visible from the joined host. Adding the policy file connects that local inventory to ACL, grant, SSH, posture, host alias, ipset, app connector, and Funnel policy edges, which is where the attack-path context starts to show up.

BloodHound Setup

Next, we’ll need to enable the TailscaleHound extension in BloodHound as outlined here.

Once enabled, upload the TailscaleHound schema under Administration → OpenGraph Management:

Next, ingest the generated TailscaleHound JSON through BloodHound file ingest. Pathfinding through traversable nodes outlined in the schema should now be enabled!

Saved Queries Overview

TailscaleHound ships with saved queries under the SavedQueries folder. These queries can be imported using this guide. A useful review of the saved queries flow is:

QuestionSaved query family
What is in the tailnet?Network overview, users, devices, groups, tags
Who owns or administers the tailnet?Users – Admins and Owners, Users – Tailnet Roles
Who can reach devices?Access – Device Access Paths
Which ports are attached to access rules?Access – Device Port Access, ACL – Port Specs
Who can reach subnet routes?Routes – Access Via Device
Who can use exit nodes?Exit Nodes – Policy Paths, Exit Nodes – Device Options
Who can SSH and as whom?SSH – Rule Paths, SSH – As User
Which app connectors matter?App Connector – Usage Paths, App Connector – Runs On Devices
What is exposed through Funnel?Funnel – Capabilities, Funnel – Enabled
Which tagged devices are policy sources?Tags – Tagged Device Policy Sources, Tags – Tagged Device Access To Devices
Which shared devices came from outside the tailnet?Network – External Tailnets, Network – External User Devices, Network – External Device Tags
Which keys exist and who created them?Auth/API/Client/Federated Keys, Keys – Created By, Keys – Tags
Where do tests say access should fail?ACL Tests and SSH Tests
How does Azure identity connect to Tailscale?Hybrid – AZUser Synced, Hybrid – AZUser Device Access Paths

Below are some examples of how these queries can help identify attack paths

Scenario 1: Who Can Reach a Device?

The most direct graph question is: which Tailscale users can reach a device through ACLs or grants?

MATCH p=(u:TS_User)-[:TS_AclSource|TS_GrantSource]->(r)-[:TS_AclTargetsDevice|TS_GrantTargetsDevice]->(d)
RETURN p

Scenario 2: Which Routes Are Exposed?

Subnet routers change the shape of a tailnet. A user may not only access Tailscale device IPs; they may also reach private CIDRs or host routes advertised by a device.

MATCH p1=(u:TS_User)-[:TS_IsMemberOf]->(s)-[:TS_AclSource|TS_GrantSource]->(r)-[:TS_AclTargetsRoute|TS_GrantTargetsRoute]->(route:TS_Route)
MATCH p2=(d:TS_Device)-[:TS_EnabledRoute]->(route)
RETURN p1, p2

This query is useful for finding access to lab ranges, cloud VPC ranges, production subnets, or other internal spaces that might not be obvious from the machine list alone.

Scenario 3: Who Can Use Exit Nodes?

Exit nodes can matter when access to a third-party resource is restricted by source IP, network location, or expected egress path. TailscaleHound models exit-node-capable devices and policy paths that target those devices:

MATCH p=(u:TS_User)-[:TS_IsMemberOf]->(s)-[:TS_AclSource|TS_GrantSource]->(r)-[:TS_AclTargetsExitNode|TS_GrantTargetsExitNode]->(d:TS_Device)
RETURN p

This helps identify users who may be able to send traffic through an egress point that other systems (such as conditional access rules) trust.

Scenario 4: Who Can SSH, and as Which User?

Tailscale SSH rules deserve their own treatment because the interesting question is “Which users can SSH to which devices, and what user can they SSH as?”

MATCH p1=(u:TS_User)-[:TS_IsMemberOf]->(s)-[:TS_SSHRuleSource]->(r:TS_SSHRule)-[:TS_SSHRuleTargetsDevice]->(d:TS_Device)
MATCH p2=(r)-[:TS_SSHRuleAllowsUser]->(ssh:TS_SSHUser)
RETURN p1, p2

This can expose paths where a broad Tailscale group can SSH to a sensitive server as root, admin, or another privileged local account.

Scenario 5: Keys, Tags, Invites, and Admin Context

The previous Tailscale keys post explained why keys matter during assessments. TailscaleHound adds graph context around key objects where the data is available:

MATCH p=(k)-[:TS_KeyHasTag]->(t:TS_Tag)
RETURN p

The collector also models user and device invites, webhooks, tag ownership, auto approvers, host aliases, posture definitions, and default source posture. Not every one of these should be a traversable attack path edge, but they are useful context when deciding whether a path is expected, risky, or stale.

Scenario 6: Local Status and Policy Attack Paths

Local ingestion is where TailscaleHound can be useful even without control-plane credentials. Suppose you have tailscale status --json output from a joined workstation and a copy of the tailnet’s Access Policy. Status tells you which devices, users, external devices, tags, routes, and exit node options are visible from that host. The policy file tells you how ACLs, grants, SSH rules, posture, node attributes, host aliases, and ipsets are intended to work. TailscaleHound combines those into one graph.

One useful question is whether a local tagged device is not just present, but is itself a policy source that receives access to another device:

MATCH p=(src:TS_Device)-[:TS_HasTag]->(tag:TS_Tag)-[:TS_GrantSource|TS_AclSource]->(rule)-[:TS_GrantTargetsDevice|TS_AclTargetsDevice]->(dst:TS_Device)
RETURN p

That path can highlight scenarios like a lab box, app server, or automation host carrying a tag that the Access Policy trusts. If an operator has code execution on that tagged device, the graph can show which hosts the device may be able to reach because of its tag.

Hybrid Attack Paths

TailscaleHound can also create a bridge from Azure users to Tailscale users. If Azure and TailscaleHound data already exist in BloodHound, the hybrid mapper queries BloodHound for AZUser and TS_User nodes, then creates TS_AZUserSyncedToUser edges by matching AZUser.userPrincipalName to TS_User.LoginName.

python3 TailscaleHound.py --hybrid-attacks Windows --bh-url "$BH_URL" --bh-user "$BH_USER" --bh-password "$BLOODHOUND_SECRET" --output ./tailscalehound-output

Once those bridge edges exist, Azure identity paths can continue into Tailscale policy paths:

MATCH p=(az:AZUser)-[:TS_AZUserSyncedToUser]->(u:TS_User)-[:TS_IsMemberOf]->(s)-[:TS_AclSource|TS_GrantSource]->(r)-[:TS_AclTargetsDevice|TS_GrantTargetsDevice]->(d)
RETURN p

Or into Tailscale SSH:

MATCH p1=(az:AZUser)-[:TS_AZUserSyncedToUser]->(u:TS_User)-[:TS_IsMemberOf]->(s)-[:TS_SSHRuleSource]->(r:TS_SSHRule)-[:TS_SSHRuleTargetsDevice]->(d:TS_Device)
MATCH p2=(r)-[:TS_SSHRuleAllowsUser]->(ssh:TS_SSHUser)
RETURN p1, p2

This is where TailscaleHound becomes more than a Tailscale inventory tool. It can show how a cloud identity compromise, group membership, or synchronized user account may lead to network access, SSH access, route access, or exit-node access in the tailnet.

What to Do With the Results

The graph is useful for both offensive and defensive workflows.

For red teams, it helps identify:

  • Tailscale users with direct or group-based access to sensitive devices
  • Routes that expose internal networks through subnet routers
  • Exit nodes that provide useful egress points
  • SSH rules that allow privileged local users
  • Keys, tags, and invite paths that may support persistence or lateral movement
  • Azure identities that extend into Tailscale access

For defenders, it helps prioritize:

  • Overbroad ACL and grant sources
  • Stale groups, tags, keys, invites, and webhooks
  • Sensitive routes exposed to broad groups
  • Exit node usage and auto approval rules
  • Tailscale SSH rules that grant privileged local users
  • App connectors and Funnel devices that should be reviewed

The goal is not only to find attack paths, but to give teams a way to explain, reproduce, and reduce them. In the future this OpenGraph collector will be integrated into OpenHound as an extension but, for now, the collector can be found at: https://github.com/KingOfTheNOPs/TailscaleHound

Andrew Luke

Senior Consultant

Andrew Luke is a Senior Consultant at SpecterOps specializing in Adversary Simulation. He is also a support course instructor for Adversary Tactics: Red Team Operations. Areas of interests include Kubernetes, CI/CD, and application security.

Andrew Gomez

Consultant

Andrew is a Consultant at SpecterOps specializing in Adversary Simulation. He has over eight years of cyber security experience with four years as an offensive security consultant performing tasks such as red teaming, penetration testing, web application assessments, and tool development.

Ready to get started?

Book a Demo