Scaled
Scaled is a read-only Ruby client for the Tailscale API.
Scaled це read-only Ruby клієнт для Tailscale API.
Current scope of the gem:
- devices inventory (
list,get) - device routes (
device_routes.get) — advertised/enabled subnet routes and exit nodes - keys metadata (
list,get) - logs (
configuration,network) - policy file / ACL (
acl.get) —acls/grants,tagOwners,groups
No create/update/delete actions are exposed in resource wrappers.
Installation
Add to your Gemfile:
gem "scaled"
Or install directly:
gem install scaled
Usage
Environment variables are documented in .env.example. Змінні середовища задокументовані в .env.example.
API token auth
require "scaled"
client = Scaled.client(
api_token: ENV.fetch("TAILSCALE_API_TOKEN"),
tailnet: ENV.fetch("TAILNET", "-")
)
devices = client.devices.list
key = client.keys.get("key-id")
logs = client.logs.configuration(query: { limit: 100 })
OAuth client credentials auth
require "scaled"
client = Scaled.client(
oauth: {
client_id: ENV.fetch("TAILSCALE_OAUTH_CLIENT_ID"),
client_secret: ENV.fetch("TAILSCALE_OAUTH_CLIENT_SECRET"),
scopes: %w[devices:core:read auth_keys:read logs:configuration:read logs:network:read]
},
tailnet: ENV.fetch("TAILNET", "-")
)
devices = client.devices.list
Notes:
- OAuth access tokens are fetched from
https://api.tailscale.com/api/v2/oauth/token. - Tokens are cached and refreshed automatically before expiration.
Rails integration
1. Add gem to Rails app
# Gemfile
gem "scaled"
bundle install
2. Configure credentials or env
Store secrets in Rails credentials (recommended) or environment variables.
Example credentials keys:
tailscale:
api_token: tskey-api-...
tailnet: "-"
For OAuth mode:
tailscale:
oauth_client_id: ...
oauth_client_secret: ...
oauth_scopes: "devices:core:read auth_keys:read logs:configuration:read logs:network:read"
tailnet: "-"
3. Create initializer
# config/initializers/scaled.rb
Rails.application.config.x.scaled_client =
if Rails.application.credentials.dig(:tailscale, :api_token).present?
Scaled.client(
api_token: Rails.application.credentials.dig(:tailscale, :api_token),
tailnet: Rails.application.credentials.dig(:tailscale, :tailnet) || "-"
)
else
Scaled.client(
oauth: {
client_id: Rails.application.credentials.dig(:tailscale, :oauth_client_id),
client_secret: Rails.application.credentials.dig(:tailscale, :oauth_client_secret),
scopes: Rails.application.credentials.dig(:tailscale, :oauth_scopes).to_s.split
},
tailnet: Rails.application.credentials.dig(:tailscale, :tailnet) || "-"
)
end
4. Add service object
# app/services/tailscale_client.rb
class TailscaleClient
def self.client
Rails.configuration.x.scaled_client
end
def self.devices
client.devices.list
end
def self.keys
client.keys.list
end
def self.configuration_logs(limit: 100)
client.logs.configuration(query: { limit: limit })
end
end
5. Use in Rails code
# rails console
TailscaleClient.devices
TailscaleClient.keys
# app/jobs/sync_tailscale_devices_job.rb
class SyncTailscaleDevicesJob < ApplicationJob
queue_as :default
def perform
devices = TailscaleClient.devices
Rails.logger.info("tailscale_devices_count=#{devices.fetch('devices', []).size}")
end
end
Ready-to-copy templates are included:
examples/rails/scaled_initializer.rbexamples/rails/tailscale_client.rb
Gem and curl examples
Examples below do the same read-only operations via gem and curl.
List devices
client = Scaled.client(api_token: ENV.fetch("TAILSCALE_API_TOKEN"), tailnet: "-")
response = client.devices.list
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/devices"
Example server response:
{
"devices": [
{
"id": "n123456CNTRL",
"name": "macbook-pro.tailnet.ts.net",
"addresses": ["100.101.102.103", "fd7a:115c:a1e0::abcd:1234"],
"user": "user@example.com",
"os": "macOS",
"created": "2026-03-12T07:12:30Z",
"lastSeen": "2026-03-12T08:25:44Z",
"authorized": true
}
]
}
Get one device
response = client.devices.get("device-id")
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/device/device-id"
Example server response:
{
"id": "n123456CNTRL",
"name": "macbook-pro.tailnet.ts.net",
"hostname": "macbook-pro",
"addresses": ["100.101.102.103", "fd7a:115c:a1e0::abcd:1234"],
"user": "user@example.com",
"os": "macOS",
"created": "2026-03-12T07:12:30Z",
"lastSeen": "2026-03-12T08:25:44Z",
"authorized": true
}
List keys
response = client.keys.list
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/keys"
Example server response:
{
"keys": [
{
"id": "key_abc123",
"description": "CI read-only key",
"created": "2026-03-11T09:00:00Z",
"expires": "2026-06-09T09:00:00Z",
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": true
}
}
}
}
]
}
Read configuration logs
response = client.logs.configuration(query: { limit: 100 })
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/logging/configuration?limit=100"
Example server response:
{
"events": [
{
"id": "evt_cfg_1",
"time": "2026-03-12T08:11:00Z",
"actor": "admin@example.com",
"type": "policy.updated",
"details": {
"source": "api"
}
}
]
}
Read network logs
Network flow logs require an RFC 3339 start and end time window (there is no
limit parameter). Source and destination are reported as addr:port strings.
response = client.logs.network(
query: { start: "2026-03-12T00:00:00Z", end: "2026-03-12T23:59:59Z" }
)
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/logging/network?start=2026-03-12T00:00:00Z&end=2026-03-12T23:59:59Z"
Example server response (logs is an array of NetworkFlowLog objects):
{
"logs": [
{
"logged": "2026-03-12T08:15:26Z",
"nodeId": "n123456CNTRL",
"start": "2026-03-12T08:15:25Z",
"end": "2026-03-12T08:15:26Z",
"virtualTraffic": [
{
"proto": "tcp",
"src": "100.101.102.103:52343",
"dst": "100.110.120.130:443",
"txPkts": 10,
"txBytes": 1200,
"rxPkts": 8,
"rxBytes": 900
}
],
"subnetTraffic": [],
"exitTraffic": [],
"physicalTraffic": []
}
]
}
Traffic is split into virtualTraffic (tailnet peer-to-peer), subnetTraffic
(through subnet routers), exitTraffic (through exit nodes), and
physicalTraffic (underlying physical endpoints).
Read device routes
response = client.device_routes.get("device-id")
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/device/device-id/routes"
Example server response:
{
"advertisedRoutes": ["10.0.0.0/24", "0.0.0.0/0", "::/0"],
"enabledRoutes": ["10.0.0.0/24"]
}
Read policy file (ACL)
By default the policy file is returned as JSON. Pass details: true to get a
JSON object with a base64-encoded huJSON acl plus warnings and errors.
policy = client.acl.get
detailed = client.acl.get(query: { details: true })
curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
-H "Accept: application/json" \
"https://api.tailscale.com/api/v2/tailnet/-/acl"
Example server response:
{
"groups": { "group:eng": ["alice@example.com"] },
"tagOwners": { "tag:server": ["group:eng"] },
"acls": [
{ "action": "accept", "src": ["group:eng"], "dst": ["tag:server:443"] }
]
}
OAuth note: reading routes needs devices:routes:read; reading the ACL needs an
ACL read scope (acl:read). Add them to your OAuth scopes when using those calls.
Integration smoke tests
Integration tests are opt-in and run only when RUN_INTEGRATION=1.
Environment variables
Main variables used by the gem and tests:
TAILSCALE_API_TOKEN- API token for Bearer auth mode.TAILSCALE_OAUTH_CLIENT_ID- OAuth client ID for client credentials flow.TAILSCALE_OAUTH_CLIENT_SECRET- OAuth client secret for client credentials flow.TAILSCALE_OAUTH_SCOPES- space-separated OAuth scopes.TAILNET- target tailnet (-means token-owned tailnet).RUN_INTEGRATION- enables/disables integration smoke specs.
See full descriptions and defaults in .env.example.
API token smoke
RUN_INTEGRATION=1 \
TAILSCALE_API_TOKEN=tskey-api-... \
TAILNET=- \
bundle exec rspec spec/integration/read_only_smoke_spec.rb
OAuth smoke
RUN_INTEGRATION=1 \
TAILSCALE_OAUTH_CLIENT_ID=... \
TAILSCALE_OAUTH_CLIENT_SECRET=... \
TAILSCALE_OAUTH_SCOPES='devices:core:read auth_keys:read logs:configuration:read logs:network:read' \
TAILNET=- \
bundle exec rspec spec/integration/read_only_smoke_spec.rb
Development
bundle install
bundle exec rspec
bundle exec rubocop
GitHub push and gem release
Push to GitHub
git init
git add .
git commit -m "Initial read-only Tailscale client"
git remote add origin <YOUR_GITHUB_REPO_URL>
git push -u origin master
Publish to RubyGems
Before release:
- update
scaled.gemspec(summary,description,homepage,source_code_uri) - update version in
lib/scaled/version.rb - ensure
bundle exec rspecandbundle exec rubocopare green - configure RubyGems credentials and MFA
Release:
bundle exec rake build
bundle exec rake release
License
MIT. See LICENSE.txt.