Skip to content
slopscale
Esc
↑↓navigate↵open⌘Jpreview
On this page

Device trust

Suspend a machine without deleting it, and gate traffic on device posture, hardware attestation and MDM or EDR integrations through postures.

Approval decides whether a machine may join at all (see Device and user approval). Device trust is what an administrator can do with a machine after that: cut it off for a while without deleting it, and let the policy look at what the machine is before letting its traffic through.

Suspending a machine

A suspended machine stays registered and keeps its addresses and its key, but it gets no peers, no peer sees it, its packet filter and SSH policy are empty, and its client is told it is not authorized. The Tailscale client shows the same state as a machine waiting for approval, and a health message, “This device is suspended”, says why. Lifting the suspension gives everything back live; nobody has to sign in on the device.

Suspension is the reversible alternative to expiring the key or removing the machine: a lost laptop, a contractor on leave, a server under investigation. It is separate from approval, so a suspended machine is still approved and a suspended machine that is later unsuspended does not wait for approval again.

slopscale nodes list                        # the Approved column reads "suspended"
slopscale nodes suspend --identifier 7
slopscale nodes suspend --identifier 7 --revoke

Through the API, POST /api/v1/node/{id}/suspend, with {"suspended": false} to lift it; the node object carries suspended and suspendedAt. It needs the devices:core scope. The console has Suspend in a machine’s menu and in the danger zone of its page, where a suspended machine shows a red Suspended badge. Both edges are audited as node.suspension.set and raise the nodeSuspended and nodeUnsuspended webhook events.

The v2 API has no counterpart, because Tailscale’s API has none; a suspended machine reads as authorized: true there.

Device posture

A posture is what the policy can check about a machine before it lets traffic through, following Tailscale’s device posture. Each machine carries a set of attributes: most are derived from what its client reports, the serial numbers are collected from the client on request, and custom: attributes are set by an operator.

Attribute Source
node:os Hostinfo, lowercased the way Tailscale writes it: linux, macos, windows, ios, android
node:osVersion Hostinfo
node:tsVersion Hostinfo, the client version without the build suffix
node:tsReleaseTrack stable or unstable, from the client version
node:tsAutoUpdate Whether the client has auto-update on
node:hostname The name the client reports
node:machine The CPU architecture
node:distro, node:distroVersion Linux distribution and release
node:deviceModel The hardware model on macOS, iOS and Android
node:package How the client was installed
node:tagged Whether the node is tagged
node:hardwareAttested Whether the machine proved its hardware attestation key on its last map request
node:tpm Whether the client found a TPM on the machine
node:serialNumber The serial numbers the client collected, once identity collection is on
custom:... Set through the API, the CLI or the console
ip:address The address the machine’s control connection comes from, as seen by the server
ip:country The ISO country code of that address; needs policy.geoip_database

GET /api/v1/node/{id}/posture returns the whole map, the identity report and the custom attributes; the v2 API has Tailscale’s GET /api/v2/device/{id}/attributes. Both need devices:posture_attributes:read. The console shows the map in a Device posture section of the machine’s page.

Identity collection

The client only hands over serial numbers when asked, and only when its user allowed it with tailscale set --posture-checking=true. The server asks over the control connection (a “c2n” request carried by the map stream, answered over the noise channel), so the machine has to be connected. Collection is off by default; the postureIdentityOn setting, slopscale settings set --posture-identity=true or Collect device identity under the console’s Settings turns it on. While it is on, the server asks each machine when it connects and again once a day, and POST /api/v1/node/{id}/posture/collect, slopscale nodes posture collect or the Refresh button asks now. A client with posture checking off answers that it is disabled, which the posture shows so the operator knows to ask the user.

Custom attributes

A custom attribute has a key of the form custom:name, a value that is a string, a number or a boolean, an optional comment and an optional expiry. An expired attribute stops counting at once and is deleted by a sweep; the expiry is how a temporary marker such as an on-call rotation is made. Setting one recomputes the policy for the machine.

slopscale nodes posture show --identifier 7
slopscale nodes posture set --identifier 7 custom:oncall=true --expiry 8h --comment "pager week"
slopscale nodes posture set --identifier 7 custom:tier=3
slopscale nodes posture delete --identifier 7 custom:oncall

Through the API, PUT /api/v1/node/{id}/attributes/{key} with {"value": true, "expiry": "2026-09-08T09:00:00Z", "comment": "..."} and DELETE on the same path; the v2 API has Tailscale’s POST and DELETE /api/v2/device/{id}/attributes/{key}. Both need devices:posture_attributes, which the owner, admins, network admins and IT admins hold. The audit log records node.attribute.set, node.attribute.delete and node.posture.collect.

Attributes set by the machine

A machine can set its own custom attributes over its control connection, the way Tailscale’s experimental device attributes do: a management agent on the machine reports what it found, and the policy checks it. The client’s local API takes a patch, custom: keys with a string, number or boolean value, or null to delete:

curl --unix-socket /var/run/tailscale/tailscaled.sock -X PATCH \
  -H 'Content-Type: application/json' \
  -d '{"custom:disk-encrypted": true, "custom:agent": "1.4"}' \
  http://local-tailscaled.sock/localapi/v0/alpha-set-device-attrs

The server accepts this only while the deviceAttributesOn setting is on, slopscale settings set --device-attributes=true or Machines set their own attributes under the console’s Settings; it is off by default, because whoever is root on a machine can then give it any attribute a policy trusts. An attribute the machine sets is a custom attribute like any other, with the comment Set by the machine, and the audit log records it as node.attribute.set with the machine as the actor.

Hardware attestation

A posture attribute is only as good as the machine reporting it. Hardware attestation binds a machine’s identity to hardware it cannot copy: the client generates an ECDSA P-256 key inside the machine’s TPM, where the private half never becomes readable, and signs every map request with it. The server checks the signature and records what it proved, so the policy can insist that traffic comes from the physical machine that first presented the key rather than from a copied node key.

The client only does this when it is started with the flag:

tailscaled --hardware-attestation

It needs a build with TPM support (the stock Linux and Windows packages have it) and a TPM 2.0 device. The Apple and Android clients never send one, so their machines are never attested; node:tpm tells them apart from a Linux machine that has no TPM at all.

What the server verifies

Every map request from such a client carries the public key, a timestamp and an ECDSA signature over the SHA-256 of <unix seconds>|<node key>, with the node key in its nodekey: form. The server verifies the signature against the node key it holds for the machine, and rejects a timestamp more than a day from its own clock: the client stamps its own time, so the window bounds a replay rather than checks a clock.

The machine is attested for exactly as long as it keeps signing. A client that is restarted without the flag, or whose TPM stops answering, loses the attribute on its next map request, and every rule that requires it closes at once.

A signature under a key other than the stored one replaces it, because a cleared TPM and a reinstall both produce a new key legitimately. The change is recorded on the machine as keyChangedAt and audited, since it also looks like a machine being impersonated.

Requiring it in a policy

The two attributes are ordinary posture attributes:

node:hardwareAttested == true
node:tpm == true

node:hardwareAttested is always present, true or false, so a rule can require it with == true and nothing else. node:tpm is present as soon as the client has reported anything about itself.

slopscale postures create --name "Attested hardware" \
  --expr "node:hardwareAttested == true"
slopscale access-rules create --name "SSH from attested machines" \
  --src 2 --dst 3 --protocol tcp --ports 22 --posture 1

In the policy file the same expression goes into a postures entry and is named from srcPosture.

Console and API

A machine’s hardwareAttestation in the API holds attested, the key as hwattestpub:<hex>, attestedAt (when attestation was gained, not the last request) and keyChangedAt; it is absent for a machine that never sent a key. tpm holds what the client reported about the device: manufacturer, vendor, model, firmware version and specification revision. Both are on the node object of GET /api/v1/node/{id}, and the machine’s page shows them under Device posture.

DELETE /api/v1/node/{id}/hardware-attestation forgets the record, so the next map request that carries a valid signature starts it again. The client is not touched and keeps its key; this is what to do after a TPM was cleared, so that the change of key is not recorded as one. It needs the devices:core scope and is audited as node.attestation.reset, next to the node.attestation.attested, node.attestation.lost and node.attestation.key_changed entries the server writes on its own.

slopscale nodes attestation reset --identifier 7

Posture integrations

Posture integrations query external endpoint management (MDM) and endpoint detection and response (EDR) services for device state and turn the answers into prefixed posture attributes, following Tailscale’s device posture integrations. The attribute names match Tailscale’s naming conventions, so existing postures work without changes.

Providers and requirements

Slopscale supports six posture providers:

  • CrowdStrike Falcon (falcon:): requires an OAuth client ID (clientId) and secret (clientSecret). The API base URL (baseUrl) defaults to https://api.crowdstrike.com.
  • SentinelOne (sentinelOne:): requires the console URL (baseUrl) and an API token (apiToken).
  • Microsoft Intune (intune:): requires the Entra tenant ID (tenantId), client ID (clientId), and client secret (clientSecret). The API base URL defaults to https://graph.microsoft.com.
  • Jamf Pro (jamfPro:): requires the Jamf Pro server URL (baseUrl), client ID (clientId), and client secret (clientSecret).
  • Kandji (kandji:): requires the tenant URL (baseUrl) and an API token (apiToken).
  • Kolide (kolide:): requires an API token (apiToken). The API base URL defaults to https://api.kolide.com.

Any provided base URL must be an http or https origin.

Attributes written by providers

Each provider writes attributes prefixed with its namespace:

  • CrowdStrike Falcon (falcon:):
    • falcon:ztaScore (number): The Zero Trust Assessment score, 0 to 100.
  • SentinelOne (sentinelOne:):
    • sentinelOne:operationalState (string): The agent’s operational state.
    • sentinelOne:activeThreats (number): Unresolved threats on the device.
    • sentinelOne:agentVersion (string): The installed agent version.
    • sentinelOne:encryptedApplications (boolean): Whether disk encryption is on.
    • sentinelOne:firewallEnabled (boolean): Whether the firewall is on.
    • sentinelOne:infected (boolean): Whether the agent reports an infection.
  • Microsoft Intune (intune:):
    • intune:complianceState (string): compliant, noncompliant, inGracePeriod or unknown.
    • intune:azureADRegistered (boolean): Whether the device is registered in Entra.
    • intune:deviceRegistrationState (string): registered, notRegistered or revoked.
    • intune:isSupervised (boolean): Whether the device is supervised.
    • intune:isEncrypted (boolean): Whether the disk is encrypted.
    • intune:managedDeviceOwnerType (string): company, personal or unknown.
  • Jamf Pro (jamfPro:):
    • jamfPro:remoteManaged (boolean): Whether MDM manages the computer.
    • jamfPro:supervised (boolean): Whether the computer is supervised.
    • jamfPro:firewallEnabled (boolean): Whether the firewall is on.
    • jamfPro:fileVaultStatus (string): ALL_ENCRYPTED, SOME_ENCRYPTED or NOT_ENCRYPTED.
    • jamfPro:SIPEnabled (string): ENABLED or DISABLED.
  • Kandji (kandji:):
    • kandji:mdmEnabled (boolean): Whether MDM is enabled on the device.
    • kandji:agentInstalled (boolean): Whether the Kandji agent is installed.
  • Kolide (kolide:):
    • kolide:authState (string): Good, Notified, Will Block or Blocked.

Machine matching and synchronization

Machines are matched to provider records by the serial number the client reports. Identity collection must be on (slopscale settings set --posture-identity=true or Collect device identity in console Settings), and the client must report its serial number with posture checking enabled (tailscale set --posture-checking=true).

Only one enabled integration per provider is allowed at a time, because each provider owns its attribute prefix.

  • Scheduled sync: A worker goroutine syncs every enabled provider every 15 minutes.
  • Sync on save: Creating or updating an enabled integration syncs it at once so the attributes exist before an operator writes rules against them.
  • Sync now: An operator can trigger an immediate sync at any time via Sync now in the console or POST /api/v1/posture-integration/{id}/sync.

If a provider sync fails (for example, if the provider API is unreachable or credentials are rejected), the server keeps the previous attributes on machines so a transient failure does not disconnect devices from rules. The error is recorded on the integration in lastError and shown in the console.

Example postures

Integration attributes are used directly in posture expressions:

falcon:ztaScore >= 80
intune:complianceState == 'compliant'

Console and API

Posture integrations are managed in the console under Integrations › Device posture.

slopscale posture-integrations providers
slopscale posture-integrations list
slopscale posture-integrations check --provider kolide --name "Kolide Fleet" --api-token "kld_..."
slopscale posture-integrations create --provider kolide --name "Kolide Fleet" --api-token "kld_..."
slopscale posture-integrations create --provider intune --name "Entra Intune" \
  --tenant-id "00000000-0000-0000-0000-000000000000" \
  --client-id "11111111-1111-1111-1111-111111111111" \
  --client-secret "sec_..."
slopscale posture-integrations sync --id 1
slopscale posture-integrations delete --id 1

slopscale posture-integrations check verifies credentials with the provider before saving. An integration syncs on save and every 15 minutes, or immediately with sync.

Through the API:

  • GET /api/v1/posture-integrations: list integrations (requires devices:posture_attributes:read).
  • POST /api/v1/posture-integrations: create an integration (requires devices:posture_attributes).
  • GET /api/v1/posture-integration/{id}: get an integration (requires devices:posture_attributes:read).
  • PUT /api/v1/posture-integration/{id}: update an integration (requires devices:posture_attributes). An omitted secret keeps the stored value.
  • DELETE /api/v1/posture-integration/{id}: delete an integration and clear its attributes (requires devices:posture_attributes).
  • POST /api/v1/posture-integration/{id}/sync: sync an integration immediately (requires devices:posture_attributes).
  • POST /api/v1/posture-integrations/check: verify credentials against the provider without saving (requires devices:posture_attributes).
  • GET /api/v1/posture-integrations/providers: list supported providers and their attributes catalog (requires devices:posture_attributes:read).

Postures

A posture names conditions a machine must meet. It has a list of expressions, all of which must hold, and optionally a weekly schedule outside of which it does not hold at all. A posture is attached to access rules, where it narrows the rule’s sources to the machines that satisfy it, and it can be written into the policy file the way Tailscale’s postures and srcPosture work. Postures are evaluated on the server from the attributes above, so a client cannot claim one.

Expressions

An expression is an attribute, an operator and a value, in Tailscale’s syntax:

node:os == 'macos'
node:tsVersion >= '1.80'
node:os IN ['macos', 'windows']
node:serialNumber NOT IN ['C02XYZ123']
custom:oncall == true
custom:blocked NOT SET
ip:address IN ['203.0.113.0/24', '198.51.100.9']
ip:country == 'VN'

The operators are ==, !=, <, <=, >, >=, IN, NOT IN, IS SET and NOT SET. Values are strings in single or double quotes, numbers, true and false, or a list in brackets. Version strings such as node:tsVersion compare segment by segment, so '1.9' is smaller than '1.10'. A string that is a CIDR matches an address inside it. An attribute the machine does not have satisfies only NOT SET; a list-valued attribute such as node:serialNumber satisfies ==, IN and the ordered operators when any element does, and != and NOT IN when none does.

ip:address is the address the machine’s control connection comes from, so behind a reverse proxy it is the proxy’s address unless the proxy is configured to preserve the client address. ip:country needs a MaxMind GeoLite2 or GeoIP2 country database at policy.geoip_database; without one the server refuses a posture that uses it. A machine’s source address counts once it has connected; a posture that uses one of the ip: attributes recomputes the policy for a machine when its address changes.

POST /api/v1/posture/check and slopscale postures check --expr ... parse expressions without storing them. The console’s posture editor colours each line, underlines a parse error where it is as it is typed, warns about an attribute the server never reports or a value node:os never takes, and completes attributes, operators and known values.

Schedules

A schedule is a set of weekdays, a start and an end as HH:MM, and an IANA time zone, UTC when empty. An end before the start wraps past midnight, so sat 22:00–06:00 runs into Sunday morning. The server recomputes the policy when a schedule boundary passes, to the minute, so the rules open and close on their own. A posture may have a schedule and no expressions, which makes a plain time window.

Attaching postures to rules

A rule with postures admits a source machine only when it satisfies at least one of them; a rule without postures admits every machine in its source groups. The destinations are never narrowed. A posture a rule names cannot be deleted; remove it from the rule first.

slopscale postures create --name "Current client" \
  --expr "node:tsVersion >= '1.80'" --expr "custom:blocked NOT SET"
slopscale postures create --name "Office hours" \
  --days mon,tue,wed,thu,fri --start 09:00 --end 18:00 --timezone Asia/Ho_Chi_Minh
slopscale postures list
slopscale access-rules create --name "SSH from current clients" \
  --src 2 --dst 3 --protocol tcp --ports 22 --posture 1
slopscale nodes posture show --identifier 7

Through the API, GET, POST /api/v1/posture, GET, PUT, DELETE /api/v1/posture/{id} and the postureIds field of an access rule, under policy_file (policy_file:read to list). GET /api/v1/node/{id}/postures lists the postures a machine satisfies right now, and the console shows them under Device posture on the machine’s page. The audit log records posture.create, posture.update and posture.delete. In the console, postures live on the Postures page under Access controls, next to the rules and groups, and a rule’s editor has a Required postures picker.

Postures in the policy file

The policy file takes Tailscale’s postures, srcPosture and defaultSrcPosture:

{
  "postures": {
    "posture:latestMac": ["node:os == 'macos'", "node:tsVersion >= '1.80'"],
    "posture:office": ["ip:address IN ['203.0.113.0/24']"]
  },
  "defaultSrcPosture": ["posture:latestMac"],
  "grants": [
    { "src": ["group:eng"], "dst": ["tag:prod"], "ip": ["22"], "srcPosture": ["posture:office"] },
    { "src": ["autogroup:member"], "dst": ["tag:web"], "ip": ["443"], "srcPosture": [] }
  ]
}

Posture names start with posture:. srcPosture on an ACL or a grant narrows its sources to the machines that satisfy any of the named postures; every expression inside one posture must hold. defaultSrcPosture applies to every rule without srcPosture, and an explicit empty srcPosture turns the default off for that rule. Postures in the file have no schedule; use a database posture for that. The posture:# prefix is reserved for the postures the access rules use.

Last updated on September 27, 2026

Was this page helpful?