Skip to main content

Security model

This document describes every security mechanism in tsink — TLS, bearer-token authentication, role-based access control (RBAC), OIDC/JWT validation, cluster mTLS, multi-tenant isolation, secret rotation, and audit logging. Security is enforced entirely at the server layer. The embedded library (tsink crate) has no authentication of its own; access control there is the responsibility of the embedding application.

Contents

  1. Transport security (TLS)
  2. Authentication — bearer tokens
  3. Role-based access control (RBAC)
  4. OIDC / JWT authentication
  5. Cluster security
  6. Multi-tenant isolation
  7. Secret rotation
  8. Audit logging
  9. Spoofing-resistant header model
  10. CLI security flags reference

1. Transport security (TLS)

tsink uses rustls for all TLS — no OpenSSL dependency. To enable TLS on the public listener, supply a PEM certificate and key:
tsink-server \
  --tls-cert /etc/tsink/server.crt \
  --tls-key  /etc/tsink/server.key \
  ...
Both files are read at startup. Certificates can be hot-reloaded or rotated at runtime without restarting the server (see Secret rotation). When neither --tls-cert/--tls-key nor the cluster mTLS flags are given, the server listens in plain HTTP mode. Plain HTTP is acceptable for development or when TLS is terminated upstream (e.g. by a load balancer).

2. Authentication — bearer tokens

2.1 Request scopes

Every incoming HTTP request is classified into one of four scopes before authentication is checked:
ScopePath prefixAccess rule
Probe/healthz, /readyAlways allowed — no token required.
Internal/internal/v1/*Cluster-peer traffic; authenticated at the transport layer by mTLS or the internal bearer token. Not accessible from the public network.
Admin/api/v1/admin/*Requires the admin token (or the public token when no separate admin token is configured). Enabled only with --enable-admin-api.
Publiceverything elseRequires the public token when one is configured.

2.2 Public and admin tokens

Two independent bearer tokens can be configured:
RoleFlagDescription
Public--auth-token / --auth-token-fileRequired for all data-plane endpoints (/api/v1/query, /write, /v1/metrics, …).
Admin--admin-auth-token / --admin-auth-token-fileRequired for admin endpoints (/api/v1/admin/*). Falls back to the public token if no dedicated admin token is set.
The token is carried in the Authorization header:
Authorization: Bearer <token>
If the public token is configured and the request provides no token or a wrong token, the server returns 401 Unauthorized. Presenting a public token to an admin endpoint when a separate admin token is configured returns 403 Forbidden (auth_scope_denied). When no public token is configured, the public endpoints are unauthenticated. Securing the listener with TLS is still strongly recommended even without a token.

3. Role-based access control (RBAC)

RBAC is an optional layer on top of bearer-token authentication. Enable it by pointing --rbac-config at a JSON file. When an RBAC config is present every request is additionally authorised against the role graph; when it is absent the simple token check described in §2 applies.

3.1 Config file structure

{
  "roles": {
    "<role-name>": {
      "grants": [
        { "action": "Read|Write", "resource": { "kind": "Tenant|Admin|System", "name": "<name-or-wildcard>" } }
      ]
    }
  },
  "principals": [
    {
      "id": "<principal-id>",
      "token": "<bearer-token>",
      "bindings": [
        { "role": "<role-name>", "scopes": [ { "kind": "Tenant", "name": "acme" } ] }
      ]
    }
  ],
  "service_accounts": [
    {
      "id": "<sa-id>",
      "token": "<bearer-token>",
      "bindings": [ { "role": "<role-name>" } ]
    }
  ],
  "oidc_providers": [ ... ]
}
The config is hot-reloaded via POST /api/v1/admin/rbac/reload.

3.2 Roles and grants

A role is a named set of grants. Each grant pairs an action with a resource:
FieldValues
actionRead or Write
resource.kindTenant — scoped to data-plane operations for a named tenant. Admin — administrative operations. System — system-level operations.
resource.nameExact name, prefix wildcard (metrics*), or * for all resources of that kind.
Example — a role that can write to any tenant and read from the ops tenant only:
{
  "data-writer": {
    "grants": [
      { "action": "Write", "resource": { "kind": "Tenant", "name": "*" } },
      { "action": "Read",  "resource": { "kind": "Tenant", "name": "ops" } }
    ]
  }
}

3.3 Principals

A principal is a named identity backed by a static bearer token. Principals are defined in the RBAC config file and are typically used for long-lived service identities.
{
  "id": "ingestor-1",
  "token": "tok_ingestor_abc123",
  "bindings": [
    { "role": "data-writer", "scopes": [ { "kind": "Tenant", "name": "acme" } ] }
  ]
}
A binding links a principal to a role, optionally restricted to a subset of resources via scopes. Omitting scopes means the binding applies to all resources the role grants access to.

3.4 Service accounts

Service accounts are managed programmatically through the admin API and support token rotation without reloading the config file:
POST   /api/v1/admin/rbac/service-accounts          Create
GET    /api/v1/admin/rbac/service-accounts/{id}     Describe
POST   /api/v1/admin/rbac/service-accounts/{id}/rotate   Rotate token
POST   /api/v1/admin/rbac/service-accounts/{id}/disable  Disable
Service account tokens are prefixed tsa_ and are generated from 32 cryptographically random bytes (base64url, no padding).

3.5 Authorization flow

When a request arrives and RBAC is active:
  1. The bearer token is extracted from the Authorization header.
  2. An O(1) lookup in the token index resolves the token to a principal or service account identity.
  3. If no match is found and the token contains two . separators it is treated as a JWT → OIDC path (see §4).
  4. Otherwise the server returns 401 auth_token_invalid.
  5. If the resolved identity is disabled the server returns 403 auth_principal_disabled.
  6. The binding graph is walked; if any role grant covers the requested action and resource the request is admitted and the identity is stamped onto the request context.
  7. If no grant matches the server returns 403 auth_scope_denied.
All decisions — both allow and deny — are written to the RBAC audit ring (see §8.1).

4. OIDC / JWT authentication

OIDC providers are declared in the RBAC config file’s oidc_providers array. This allows identity tokens issued by external providers (Keycloak, Okta, Auth0, Google, etc.) to be used as bearer tokens.

4.1 Provider configuration

{
  "oidc_providers": [
    {
      "name": "my-idp",
      "issuer": "https://idp.example.com",
      "audiences": ["tsink"],
      "username_claim": "email",
      "jwks_url": "https://idp.example.com/.well-known/jwks.json",
      "claim_mappings": [
        {
          "claim": "groups",
          "value": "tsink-admins",
          "bindings": [ { "role": "admin" } ]
        }
      ]
    }
  ]
}
FieldRequiredDescription
nameYesUnique provider identifier used in audit entries and the derived principal ID.
issuerYesExpected iss claim value.
audiencesNoIf non-empty, at least one value must match the JWT aud claim.
username_claimNoClaim to use as the display name; defaults to sub.
jwks_urlNo*URL of the provider’s JWKS endpoint. Fetched once at config load (5 s timeout).
jwksNo*Inline array of JWK objects. Use instead of jwks_url for air-gapped deployments.
claim_mappingsNoRules that map JWT claim values to RBAC role bindings.
* At least one of jwks_url or jwks must be provided.

4.2 Supported algorithms

JWT signature verification is implemented directly with the ring cryptography library. No third-party JWT library is used.
AlgorithmKey type
RS256RSA PKCS#1 (2048–8192 bit)
ES256ECDSA P-256
HS256HMAC-SHA256 (symmetric)
The algorithm is taken from the JWK’s alg field; no algorithm downgrade is possible because only the algorithm that matches the configured key’s type is attempted.

4.3 Claim validation

ClaimValidation
expRequired; token must not be expired. A 60-second clock-skew tolerance is applied.
nbfOptional; if present, token must have reached its valid-from time (60-second skew).
iatOptional; checked as a sanity bound (60-second skew).
issMust match the issuer field of the provider definition.
audMust contain at least one configured audience value (if audiences are configured).
An expired token returns 401 auth_oidc_token_expired. A token with an unrecognised issuer or algorithm returns 401 auth_token_invalid.

4.4 Claim-to-role mappings

A claim_mapping entry matches a single JWT claim against a value pattern and, when it matches, injects RBAC role bindings for the request:
{
  "claim": "groups",      // claim name (scalar string or JSON array in the token)
  "value": "ops-*",       // exact, prefix wildcard, or "*"
  "bindings": [ { "role": "observer" } ]
}
If the claim value is an array all elements are tested. The derived principal ID is oidc:<provider-name>:<sub>.

5. Cluster security

5.1 Internal mTLS

When clustering is enabled, peer-to-peer RPC traffic on /internal/v1/* can be protected with mutual TLS using a dedicated internal CA:
tsink-server \
  --cluster-internal-mtls-enabled true \
  --cluster-internal-mtls-ca-cert /etc/tsink/cluster-ca.crt \
  --cluster-internal-mtls-cert    /etc/tsink/node.crt \
  --cluster-internal-mtls-key     /etc/tsink/node.key \
  ...
FlagDescription
--cluster-internal-mtls-ca-certPEM CA bundle used to verify client certs presented by peer nodes.
--cluster-internal-mtls-certPEM certificate presented by this node when connecting to peers as a client.
--cluster-internal-mtls-keyCorresponding PEM private key.
The same CA bundle is also used as the client-CA on the public listener, so inbound /internal/v1/* requests are rejected at the TLS handshake unless they present a certificate signed by the cluster CA. The public listener and the cluster listener share one acceptor; the client-cert requirement applies only when cluster-internal-mtls is enabled.

5.2 Internal bearer token

As an alternative to mTLS, cluster peers can authenticate to each other with a shared bearer token:
--cluster-internal-auth-token-file /run/secrets/cluster-token
The internal token is checked only on /internal/v1/* paths. It is never accepted on public endpoints.
mTLS and the internal bearer token are independent — you can use either, both, or neither (the last option is appropriate when the internal network is trusted, e.g. a private Kubernetes namespace).

5.3 Verified node-ID propagation

When a cluster peer connects over mTLS the server extracts the node ID from the client certificate (Common Name or first DNS SAN label) and stamps it into the internal header x-tsink-internal-verified-node-id. This header is always stripped from inbound requests before any handler sees it, so it can never be spoofed by an external client. Similarly, all RBAC-verified identity headers (x-tsink-rbac-verified, x-tsink-auth-principal-id, x-tsink-auth-role, etc.) are stripped on ingress. See §9 for the full list.

6. Multi-tenant isolation

6.1 Tenant identification

Each request carries a tenant identifier in the x-tsink-tenant header. When the header is absent the request is attributed to the default tenant. The tenant ID is injected as the internal label __tsink_tenant__ on every stored series, providing hard data-plane isolation between tenants that share a single server.

6.2 Per-tenant auth tokens

The tenant config file (supplied via --tenant-config) supports per-tenant bearer tokens with granular scope:
{
  "tenants": [
    {
      "id": "acme",
      "auth": {
        "tokens": [
          { "token": "acme-write-token", "scopes": ["Write"] },
          { "token": "acme-read-token",  "scopes": ["Read"] }
        ]
      }
    }
  ]
}
A request carrying a valid per-tenant Write token for tenant acme is allowed to ingest data into that tenant only; it cannot read data or access other tenants. Per-tenant token auth is evaluated before the global SecurityManager token check.

6.3 Per-tenant request and admission policies

Each tenant can have independent request size limits and concurrency budgets:
{
  "id": "acme",
  "request_policy": {
    "max_write_rows_per_request": 10000,
    "max_read_queries_per_request": 5,
    "max_query_length_bytes": 4096,
    "max_range_points_per_query": 1000000,
    "admission": {
      "ingest":    { "max_inflight_requests": 50, "max_inflight_units": 100000 },
      "query":     { "max_inflight_requests": 20 },
      "metadata":  { "max_inflight_requests": 10 },
      "retention": { "max_inflight_requests": 5  }
    }
  }
}
Admission budgets are enforced with tokio semaphores. When the budget is exhausted new requests for that tenant are rejected with 429 Too Many Requests rather than queuing indefinitely, preventing one tenant’s load from starving others.

7. Secret rotation

All security material — bearer tokens and TLS certificates — can be rotated at runtime without restarting the server. The rotation API is available at POST /api/v1/admin/security/rotate (requires the admin token and --enable-admin-api).

7.1 Rotation targets

TargetCovers
PublicAuthTokenThe public bearer token (--auth-token / --auth-token-file).
AdminAuthTokenThe admin bearer token.
ClusterInternalAuthTokenThe shared internal cluster auth token.
ListenerTlsThe TLS certificate and key for the public listener.
ClusterInternalMtlsThe client certificate and key used for cluster peer connections.

7.2 Reload vs. rotate

ModeBehaviour
ReloadRe-reads the material from its original source (file or exec command). The new material replaces the current one; the previous value is retained as a fallback during the overlap window.
RotateInvokes the rotateCommand from the exec manifest (or generates a new random token) to produce new material, writes it to disk atomically, then reloads.

7.3 Material backends

A secret path is interpreted as:
  • Plain file — the file content is read as the secret value.
  • Exec manifest — if the file begins with {, it is parsed as JSON:
{
  "kind": "exec",
  "command": ["vault", "read", "-field=token", "secret/tsink/public"],
  "rotateCommand": ["vault", "write", "secret/tsink/public"]
}
command is executed to load or reload the current value. rotateCommand is executed during a Rotate operation; if empty the target is not rotatable via the exec path. This allows integration with any secrets management system (HashiCorp Vault, AWS Secrets Manager via CLI, etc.). Atomic file writes during rotation use a .tmp-<timestamp> temporary file followed by rename, ensuring no reader ever sees a partial write.

7.4 Overlap window

When material is replaced (either by reload or rotation) the previous value is kept alive for a configurable overlap period (default 300 seconds). During this window the server accepts both the old and the new credential, enabling zero-downtime rotation when clients are updated gradually. After the overlap window expires the old credential is discarded. The overlap duration can be overridden per rotation call via the overlap_seconds request field.

8. Audit logging

tsink maintains three independent audit systems.

8.1 RBAC audit ring

An in-memory ring buffer of the last 256 authorization decisions is maintained by the RBAC engine. Every authorize() call — whether it succeeds or is denied — produces an entry. Administrative operations (service account create/update/rotate/disable, config reload) are also recorded. Query the ring:
GET /api/v1/admin/rbac/audit?limit=100
Each entry contains:
FieldDescription
sequenceMonotonically increasing event counter.
timestamp_unix_msEvent time.
eventEvent type (e.g. Authorize, ServiceAccountCreated, ConfigReloaded).
outcomeAllow or Deny.
principal_idResolved identity (oidc:<provider>:<sub> for OIDC tokens).
roleRole that matched (allow path).
actionRead or Write.
resourceResource kind and name.
codeError code for denied requests (e.g. auth_scope_denied).
auth_methodToken, ServiceAccount, or Oidc.
providerOIDC provider name (OIDC path only).
subjectJWT sub claim (OIDC path only).
detailFree-text supplementary information.

8.2 Security audit ring

A separate in-memory ring of the last 128 entries covers all secret lifecycle events:
FieldDescription
sequenceMonotonically increasing event counter.
timestamp_unix_msEvent time.
targetWhich secret was affected (PublicAuthToken, ListenerTls, etc.).
operationReload or Rotate.
outcomeSuccess or Failure.
actorPrincipal that triggered the operation.
detailError message on failure.
Query via GET /api/v1/admin/security/audit.

8.3 Cluster audit log

A persistent JSONL file records cluster-level control-plane events. Every record is fsynced to disk on write. Default limits (all tunable via environment variables):
VariableDefaultDescription
TSINK_CLUSTER_AUDIT_RETENTION_SECS2 592 000 (30 days)Records older than this are pruned on each append.
TSINK_CLUSTER_AUDIT_MAX_LOG_BYTES134 217 728 (128 MiB)When exceeded the log is compacted (rewritten without pruned records).
TSINK_CLUSTER_AUDIT_MAX_QUERY_LIMIT1 000Maximum records returned per query.
Each ClusterAuditRecord contains:
FieldDescription
idUnique record ID.
timestamp_unix_msEvent time.
operationOperation name (e.g. RebalanceStarted, SnapshotCompleted).
actor.idPrincipal that initiated the operation.
actor.auth_scopePublic or Admin.
targetArbitrary JSON payload describing the affected resource.
outcome.statusSuccess or Failure.
outcome.http_statusHTTP status code.
outcome.error_typeError classifier on failure.
Query the log:
GET /api/v1/admin/cluster/audit
    ?operation=RebalanceStarted
    &actor_id=admin-principal
    &status=Success
    &since_unix_ms=1700000000000
    &until_unix_ms=1800000000000
    &limit=500

9. Spoofing-resistant header model

Several internal headers carry security-sensitive metadata set by the server during request processing. To prevent clients from injecting forged values, all of these headers are stripped from every inbound request before any handler executes.
HeaderSet byCarries
x-tsink-rbac-verifiedRBAC enginePresence flag when RBAC authorisation succeeded.
x-tsink-auth-principal-idRBAC engineResolved principal or service account ID.
x-tsink-auth-roleRBAC engineMatched role name.
x-tsink-auth-methodRBAC engineToken, ServiceAccount, or Oidc.
x-tsink-auth-providerRBAC engineOIDC provider name (OIDC path only).
x-tsink-auth-subjectRBAC engineJWT sub claim (OIDC path only).
x-tsink-internal-verified-node-idTransport layerPeer node ID extracted from mTLS client certificate CN / SAN.
x-tsink-public-auth-requiredSecurity layerWhether a public auth token is configured.
x-tsink-public-auth-verifiedSecurity layerWhether the request passed public token check.
None of these headers should be forwarded by reverse proxies or load balancers. If tsink sits behind a proxy, ensure the proxy does not pass client-supplied headers with these names.

10. CLI security flags reference

FlagTypeDescription
--tls-cert PATHPathPEM certificate file for the public listener. Enables TLS when combined with --tls-key.
--tls-key PATHPathPEM private key for the public listener.
--auth-token TOKENStringInline public bearer token. Mutually exclusive with --auth-token-file.
--auth-token-file PATHPathPublic bearer token loaded from a plain file or exec manifest.
--admin-auth-token TOKENStringInline admin bearer token. When set, admin endpoints require this token instead of the public token.
--admin-auth-token-file PATHPathAdmin bearer token from a plain file or exec manifest.
--rbac-config PATHPathPath to the RBAC JSON configuration file (roles, principals, service accounts, OIDC providers).
--tenant-config PATHPathPath to the multi-tenant JSON configuration file.
--enable-admin-apiFlagUnlock the /api/v1/admin/* endpoint group. Off by default.
--admin-path-prefix PATHPathRestrict admin file-system operations (e.g. cert rotation) to files under this prefix.
--cluster-internal-auth-token TOKENStringShared bearer token for cluster peer RPC.
--cluster-internal-auth-token-file PATHPathSame, from a plain file or exec manifest.
--cluster-internal-mtls-enabled BOOLBoolEnable mutual TLS for /internal/v1/* cluster traffic.
--cluster-internal-mtls-ca-cert PATHPathPEM CA bundle used to verify client certificates from peer nodes.
--cluster-internal-mtls-cert PATHPathPEM client certificate presented by this node on outbound cluster connections.
--cluster-internal-mtls-key PATHPathPEM private key for the cluster client certificate.