Fine-Grained Tokens

Fine-Grained Tokens

Restrict tunnel projects, tunnel creation, tunnel discovery, and tunnel connections.


Fine-grained tokens let a backend issue credentials that are limited to specific tunnel projects and specific tunnel operations. They are designed for delegation: a trusted backend keeps its personal access token or application credential, then gives a short-lived, narrower token to a browser, device, worker, or tenant service.

Tunnel grants

Tunnel grants restrict runtime access to tunnels. They do not hide workspaces, projects, credentials, or billing objects from hosted APIs. Hosted API access is controlled by API permissions, documented in Tokens.

A token with no tunnelsGrants field can use every tunnel project available to its owner, subject to its tunnel permissions. A token with tunnel grants can only use matching projects. A grant can target workspaces, target projects, or apply globally when no target field is present.

[
  {
    "workspaces": ["workspace-id"]
  },
  {
    "projects": ["project-id"],
    "scopes": {
      "tunnels": {
        "list": true
      }
    }
  },
  {
    "scopes": {
      "tunnels": {
        "connect": {
          "params": {
            "path": {
              "regex": "^/api"
            }
          }
        }
      }
    }
  }
]

The Dashboard exposes four modes for stored credentials: all projects, selected workspaces, selected projects, and advanced JSON. The first three modes cover the common cases. Advanced JSON is used when grants need operation-level scopes or different rules per project.

Tunnel grants are enforced by managed engines. Community Edition deployments do not enforce this model.

Scopes

The current scope root is tunnels. It has three capabilities: create, connect, and list. Each capability can be set to true or to an object that limits accepted tunnel properties, connection parameters, or returned fields. Operation-level scopes are available on Pro and Enterprise projects.

tunnels.create.filters restricts tunnel properties at creation time. If a filter forces a property, the engine applies that property when possible and rejects incompatible requests. tunnels.connect.filters restricts which existing tunnels can be reached. tunnels.connect.params.path restricts accepted HTTP request paths for HTTP tunnels. tunnels.list.select limits the fields returned during tunnel discovery.

[
  {
    "projects": ["project-id"],
    "scopes": {
      "tunnels": {
        "create": {
          "filters": {
            "protocol": "http",
            "publish": true,
            "token_auth": true
          }
        },
        "connect": {
          "params": {
            "path": {
              "regex": "^/api"
            }
          }
        },
        "list": {
          "select": {
            "id": true,
            "name": true,
            "protocol": true
          }
        }
      }
    }
  }
]

String filters accept exact strings, { "exact": "value" }, { "oneof": ["a", "b"] }, and { "regex": "^/api" }. Filter trees also support AND and OR. Label filters use the same matcher per label key.

Effective restrictions

Tunnel grants can exist in two places. Stored credentials can have tunnel grants in the database. Short-lived auth or application tokens can also carry tunnel grants in their signed claims.

When both are present, the engine applies both. A token derived from a restricted credential cannot escape the credential boundary. Project settings are also applied as runtime bounds, so the effective rule is the intersection of stored credential grants, token grants, and project settings.

For example, if a project requires public tunnels to use token auth or rstream Auth, a token cannot create an unauthenticated public tunnel. If a token limits HTTP paths to ^/api, requests outside that path are denied even if the tunnel itself is public.

Remote device example

A common backend flow is to issue a short-lived application token to a remote device. The device only needs to create a tunnel for one project. It does not need to read account resources, manage credentials, or open connections back to other tunnels.

import { createClientCredentialsToken } from "@rstreamlabs/rstream";
 
const { token } = createClientCredentialsToken(
  {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
  {
    expiresInSeconds: 60,
    claims: {
      permissions: ["tunnels.tunnels.create-delete"],
      tunnelsGrants: [
        {
          projects: ["project-id"],
          scopes: {
            tunnels: {
              create: {
                filters: {
                  protocol: "http",
                  publish: true,
                },
              },
            },
          },
        },
      ],
    },
  },
);

The resulting token can create matching HTTP tunnels for project-id. It cannot list projects through the hosted API unless API permissions are also granted, and it cannot connect to tunnels because no tunnels.streams.create-delete permission is present.

API example

The control plane can mint a short-lived auth token with the same model. Passing permissions narrows API and tunnel permissions. Passing tunnelsGrants narrows the tunnel projects and runtime scope.

curl -X POST "https://rstream.io/api/tokens" \
  -H "Authorization: Bearer <token-with-api.tokens.create>" \
  -H "Content-Type: application/json" \
  -d '{
    "permissions": ["tunnels.tunnels.create-delete"],
    "tunnelsGrants": [
      {
        "projects": ["project-id"],
        "scopes": {
          "tunnels": {
            "create": {
              "filters": {
                "protocol": "http",
                "publish": true,
                "token_auth": true
              }
            }
          }
        }
      }
    ]
  }'

Use short expirations for delegated tokens. A one-minute token is usually enough for browser bootstrap, device handoff, and other flows where a backend remains responsible for authorization.