Project

scaled

0.0
The project is in a healthy, maintained state
Scaled provides a read-only Ruby SDK for Tailscale API endpoints: devices, device routes, keys, logs, and the policy file (ACL). Supports API token and OAuth client credentials authentication.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

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.rb
  • examples/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 rspec and bundle exec rubocop are green
  • configure RubyGems credentials and MFA

Release:

bundle exec rake build
bundle exec rake release

License

MIT. See LICENSE.txt.