Skip to content

bublik/scaled

Repository files navigation

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.

About

Read-only Ruby client for Tailscale API

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors