Scoping InfluxDB tokens for least-privilege task execution

A shared InfluxDB organization with three teams — platform, billing, and ML features — starts life with one all-access token pasted into every downsampling task, every Airflow connection, and a couple of engineers’ shell profiles. It works, until the day the billing team’s rollup task has a Flux typo that writes into platform_raw, and nobody can prove it wasn’t the platform team. The token that ran the task could read every bucket, overwrite every bucket, and delete any of them; the blast radius of one mistake was the whole org. In InfluxDB 2.x an authorization (a token) carries an explicit list of (action, resource) grants, so a task never needs more than “read the source bucket, write the target bucket.” This page shows how to enumerate what each job actually touches, mint per-task scoped tokens through the CLI, HTTP API, and Python client, wire them into tasks and orchestrators, and audit the result. It is the operational counterpart to the controls described in Data Ingestion Security Frameworks.

Prerequisites

Why least-privilege scoping matters

A token’s danger is not how often it is used but how much it can do if it leaks or misfires. Treat the blast radius of a token as the product of the verbs it holds and the resources those verbs reach:

$$ B_{token} = P_{verbs} \times N_{buckets} $$

An all-access token in a nine-bucket org holds read, write, and delete over all of them, so $B \approx 3 \times 9 = 27$ distinct destructive capabilities behind one string. A downsampling task genuinely needs read on one bucket and write on one bucket — $B = 2$. Scoping is simply driving every automated identity toward the smallest $B$ that still lets it do its job, so a stolen or misconfigured task token cannot touch a neighbouring team’s data.

The payoff is not only reduced risk but faster incident response. When every job runs under a distinctly described token, a leaked credential is contained to two buckets and one team, and the audit log names exactly which authorization performed a write. Under the shared-token model the same investigation stalls at “some job with the all-access token did it,” which in a multi-team org means everyone is a suspect and nothing can be safely revoked without breaking unrelated pipelines.

One all-access token versus per-task scoped tokens On the left, a single all-access token fans out with read, write, and delete edges to four buckets belonging to different teams, a wide blast radius. On the right, two per-task tokens each reach only their own source and target bucket, a narrow blast radius. Before · one shared all-access token all-access token read + write + delete → every bucket platform_raw billing_raw ml_features audit_logs blast radius: 3 verbs × 4 buckets = 12 After · per-task scoped tokens downsample token billing token platform_rawread platform_1mwrite billing_rawread billing_1mwrite blast radius per token: 2 verbs × 2 buckets = 4

Start every scoping exercise by writing down the matrix of what each job actually touches. The four jobs in this shared org reduce to four narrow tokens:

Task / job Reads Writes Deletes Scoped token
Platform downsample (Flux task) platform_raw platform_1m downsample-platform
Billing rollup (Flux task) billing_raw billing_1m billing-rollup
Test-device purge (Python job) platform_raw platform_raw (predicate) platform-purge
Airflow feature export ml_features ml-export (read-only)

The matrix is the specification for the tokens you are about to mint — nothing in a token should exist that is not a cell in this table.

Solution walkthrough

Step 1 — Mint a scoped token with the CLI

The influx auth create command builds an authorization from repeatable --read-bucket/--write-bucket flags, each taking a bucket ID. Run it with the all-access operator token, which is used here only to create the narrow token — it never leaves your provisioning host.

bash
influx auth create \
  --org shared-timeseries \
  --description "downsample-platform task: read platform_raw, write platform_1m" \
  --read-bucket 0af1c3d5e7b90000 \
  --write-bucket 0bf2d4e6f8c10000 \
  --token "$INFLUX_OPERATOR_TOKEN"

Each --read-bucket/--write-bucket grants exactly one (read|write, buckets:<id>) permission; repeat a flag to add more buckets. Note there is no --delete-bucket verb — a write grant covers point deletion within that bucket via the delete API, which is why the purge job needs platform_raw as both a read and a write target rather than an org-wide delete. The command prints the token string once; capture it into your secret store immediately, because InfluxDB never displays it again. Give the --description real semantic content — it is the only human-readable label you will have when auditing dozens of tokens later. A convention such as <job-name>: read <src>, write <dst> lets an operator reconstruct the intended grant from the description alone and spot drift without decoding permission lists by eye.

Step 2 — Provision the same grant through the HTTP API

For environments stood up by CI/CD, the /api/v2/authorizations endpoint accepts the permission list directly, so token creation lives in the same automation that creates the buckets.

bash
curl -X POST "https://influxdb.example.com/api/v2/authorizations" \
  -H "Authorization: Token $INFLUX_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "orgID": "054b8c3f2a1d9000",
    "description": "billing-rollup task: read billing_raw, write billing_1m",
    "permissions": [
      { "action": "read",  "resource": { "type": "buckets", "id": "0cf3a5b7d9e20000", "orgID": "054b8c3f2a1d9000" } },
      { "action": "write", "resource": { "type": "buckets", "id": "0df4b6c8e0f30000", "orgID": "054b8c3f2a1d9000" } }
    ]
  }'

Scoping a permission to a specific id restricts it to one bucket; omitting id and keeping only type: "buckets" would grant the verb across all buckets in the org — the accidental over-grant that turns a “read” token back into a wide one. The endpoint returns 201 Created with a token field and a status: "active" flag; store the token, and assert every returned permission carries the id you sent before trusting the deployment.

Step 3 — Build the token programmatically in Python

Orchestrators that create their own workers can mint and attach tokens in the same code path. The influxdb-client library models the grant as Permission objects over a PermissionResource.

python
import os
from influxdb_client import InfluxDBClient, Permission, PermissionResource

def create_scoped_token(org_id: str, description: str,
                        read_bucket_id: str, write_bucket_id: str) -> str:
    client = InfluxDBClient(
        url=os.environ["INFLUX_URL"],
        token=os.environ["INFLUX_OPERATOR_TOKEN"],  # mint-only, never shipped to the task
        org=org_id,
    )
    try:
        read = Permission(
            action="read",
            resource=PermissionResource(type="buckets", id=read_bucket_id, org_id=org_id),
        )
        write = Permission(
            action="write",
            resource=PermissionResource(type="buckets", id=write_bucket_id, org_id=org_id),
        )
        auth = client.authorizations_api().create_authorization(
            org_id=org_id, permissions=[read, write], description=description,
        )
        return auth.token
    finally:
        client.close()

feature_export_token = create_scoped_token(
    org_id="054b8c3f2a1d9000",
    description="ml-export read-only: ml_features",
    read_bucket_id="0ef5c7d9f1040000",
    write_bucket_id="0ef5c7d9f1040000",  # same id twice = read+write on one bucket; drop the write Permission for read-only
)

For a genuinely read-only job such as the feature export, drop the write permission entirely and pass permissions=[read]. The reusable-client, batching, and backoff patterns that keep this provisioning resilient are covered in Python client orchestration patterns.

Step 4 — Attach the token to the task and the orchestrator

A native InfluxDB task runs under the authorization of the token that created it — there is no separate “run-as” field. So the way to make a task execute with least privilege is to create (or update) it while authenticating as the scoped token, not as your personal all-access session:

bash
influx task create \
  --org shared-timeseries \
  --token "$DOWNSAMPLE_PLATFORM_TOKEN" \
  --file downsample_platform.flux

Because the task inherits $DOWNSAMPLE_PLATFORM_TOKEN, its Flux can only read platform_raw and write platform_1m; a typo pointing to() at billing_1m fails at runtime with an authorization error instead of silently corrupting another team’s data. For external engines, inject the scoped token as the job’s secret — an Airflow Connection password, a Kubernetes secret, a CI variable — so each DAG or pod carries only its own grant. This keeps the least-privilege boundary intact across the process handoff: the orchestrator process itself never holds more than the union of the tokens for the jobs it launches, and a compromised worker exposes only that worker’s buckets. Wiring a scoped token into a scheduled Airflow run is exactly the connection setup described in Airflow + InfluxDB Integration.

Gotchas and edge cases

Grants bind to bucket IDs, so recreating a bucket silently orphans the token. A permission references buckets:0af1c3d5e7b90000, not the name platform_raw. If someone deletes and recreates the bucket — a common “reset the environment” move — the new bucket has a fresh ID, the old grant now points at nothing, and the task’s writes fail with a 404/403 that reads like a network fault. Reference bucket IDs from a single source of truth in your infrastructure code, and re-mint or re-grant any token after a bucket is recreated.

Operator tokens cannot be scoped and bypass permission checks entirely. The operator token (and, in OSS, the initial setup token) is all-powerful by design and ignores the permissions list. Never embed one in a task, a DAG connection, or a container image; use it only on a controlled host to mint scoped tokens, then store nothing but the scoped result. Auditing for the operator token’s fingerprint anywhere outside your provisioning path is a high-value security check.

A task created with the wrong session quietly inherits full access. Because a task runs as its creating token, building a task through the UI while logged in as an all-access user, or via influx task create with your personal token, gives that task every permission you hold — regardless of what its Flux actually needs. Always create and update scheduled tasks with the matching scoped token, and periodically reconcile each task’s owning authorization against the permission matrix. Rotating these credentials on a schedule without breaking the running tasks is covered in automating security token rotation for InfluxDB writes.

Verification

First, confirm each token holds only the grants the matrix allows. influx auth list prints the permission set per authorization:

bash
influx auth list --org shared-timeseries --token "$INFLUX_OPERATOR_TOKEN"
# For downsample-platform expect exactly:
#   read:orgs/054b8c3f2a1d9000/buckets/0af1c3d5e7b90000
#   write:orgs/054b8c3f2a1d9000/buckets/0bf2d4e6f8c10000
# Any extra line — especially a bare read:buckets or write:buckets — is an over-grant.

Then prove the boundary holds with a negative test: use the scoped token to attempt a write into a bucket it was not granted, and confirm InfluxDB refuses it.

bash
influx write \
  --bucket billing_1m \
  --org shared-timeseries \
  --token "$DOWNSAMPLE_PLATFORM_TOKEN" \
  "probe,src=verify value=1"
# expect: Error: unauthorized access (HTTP 403)
# A 403 here is success — the platform token cannot reach the billing team's bucket.

A 403 on the cross-team write, paired with a clean auth list, is the deterministic proof that least privilege is actually enforced rather than merely intended.

FAQ

Can one token be scoped to more than two buckets?

Yes. A single authorization holds any number of (action, resource) permissions, so a job that reads three source buckets and writes two targets gets one token with five grants. Keep the token aligned to a job, not a team — bundling every platform job into one token quietly rebuilds a wide-access credential.

How does scoping interact with token rotation?

Scoping and rotation are complementary: scoping limits what a token can do, rotation limits how long any single string is valid. Because a scoped token carries a fixed, documented permission set, its replacement can be minted with the identical grants and swapped in without guesswork — see automating security token rotation for InfluxDB writes for the zero-downtime swap.

Do bucket-scoped tokens work the same on InfluxDB Cloud?

Yes. InfluxDB Cloud exposes the same /api/v2/authorizations API and bucket-scoped permission model, so the CLI, curl, and Python flows here apply unchanged. Cloud additionally lets you manage tokens in the UI, but scripting them keeps the permission matrix under version control.

Up: Data Ingestion Security Frameworks