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) - keys metadata (
list,get) - logs (
configuration,network)
No create/update/delete actions are exposed in resource wrappers.
Add to your Gemfile:
gem "scaled"Or install directly:
gem install scaledEnvironment variables are documented in .env.example. Змінні середовища задокументовані в .env.example.
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 })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.listNotes:
- OAuth access tokens are fetched from
https://api.tailscale.com/api/v2/oauth/token. - Tokens are cached and refreshed automatically before expiration.
# Gemfile
gem "scaled"bundle installStore 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: "-"# 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# 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# 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
endReady-to-copy templates are included:
examples/rails/scaled_initializer.rbexamples/rails/tailscale_client.rb
Examples below do the same read-only operations via gem and curl.
client = Scaled.client(api_token: ENV.fetch("TAILSCALE_API_TOKEN"), tailnet: "-")
response = client.devices.listcurl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/devices"response = client.devices.get("device-id")curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/device/device-id"response = client.keys.listcurl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/keys"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"response = client.logs.network(query: { limit: 100 })curl -sS \
-H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
"https://api.tailscale.com/api/v2/tailnet/-/logging/network?limit=100"Response shapes vary by account features and scopes. Examples:
{
"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
}
]
}{
"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
}
}
}
}
]
}{
"events": [
{
"id": "evt_cfg_1",
"time": "2026-03-12T08:11:00Z",
"actor": "admin@example.com",
"type": "policy.updated",
"details": {
"source": "api"
}
}
]
}{
"events": [
{
"id": "evt_net_1",
"time": "2026-03-12T08:15:00Z",
"srcDeviceId": "n123456CNTRL",
"dstDeviceId": "n998877CNTRL",
"proto": "tcp",
"dstPort": 443,
"action": "accept"
}
]
}Integration tests are opt-in and run only when RUN_INTEGRATION=1.
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.
RUN_INTEGRATION=1 \
TAILSCALE_API_TOKEN=tskey-api-... \
TAILNET=- \
bundle exec rspec spec/integration/read_only_smoke_spec.rbRUN_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.rbbundle install
bundle exec rspec
bundle exec rubocopgit init
git add .
git commit -m "Initial read-only Tailscale client"
git remote add origin <YOUR_GITHUB_REPO_URL>
git push -u origin masterBefore 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 releaseMIT. See LICENSE.txt.