Project

scaled

0.0
The project is in a healthy, maintained state
Scaled provides a read-only Ruby SDK for Tailscale API endpoints: devices, keys, and logs. 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)
  • keys metadata (list, get)
  • logs (configuration, network)

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"

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"

List keys

response = client.keys.list
curl -sS \
  -H "Authorization: Bearer ${TAILSCALE_API_TOKEN}" \
  "https://api.tailscale.com/api/v2/tailnet/-/keys"

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"

Read network logs

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"

Example responses

Response shapes vary by account features and scopes. Examples:

Devices list (client.devices.list)

{
  "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 list (client.keys.list)

{
  "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
          }
        }
      }
    }
  ]
}

Configuration logs (client.logs.configuration)

{
  "events": [
    {
      "id": "evt_cfg_1",
      "time": "2026-03-12T08:11:00Z",
      "actor": "admin@example.com",
      "type": "policy.updated",
      "details": {
        "source": "api"
      }
    }
  ]
}

Network logs (client.logs.network)

{
  "events": [
    {
      "id": "evt_net_1",
      "time": "2026-03-12T08:15:00Z",
      "srcDeviceId": "n123456CNTRL",
      "dstDeviceId": "n998877CNTRL",
      "proto": "tcp",
      "dstPort": 443,
      "action": "accept"
    }
  ]
}

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.