#2

Design low-latency authentication and authorization for tenant-scoped cache nodes

Openpepicrftopened 6mo ago

Context

We need to add authentication and authorization to cache-next, but the data plane cannot afford a hot-path round trip to the main server on every request.

The existing tuist/cache service is useful as a reference, but it is not the right target model:

  • It uses standard bearer auth at the edge.
  • It verifies some JWTs locally.
  • For opaque tokens, it falls back to the main server (/api/projects) and caches the result.
  • It depends on a bespoke projects claim shape in JWTs.
  • It shares the Guardian signing secret with the cache service.

That works as a compatibility bridge, but it is still a mixed model and not a clean long-term boundary.

What we want

  • No main-server round trip in the cache hot path.
  • Standard-ish authn/authz model.
  • Authorization that works for more than just projects.
  • Tenant isolation from cluster placement.
  • Support for cache namespaces that are not project-bound, e.g. mise tool caches.

Important observation

In a cluster-per-tenant model, tenant/account isolation is implicit from the cluster itself.

That means we do not need to authorize account_handle in every request.

However, we still need to authorize access to the requested cache namespace unless we intentionally want any tenant token to access any cache entry in that tenant.

Proposed direction

1. Use a control-plane/data-plane split

  • The main server remains the control plane.
  • Cache nodes are the data plane.
  • The main server issues short-lived cache access tokens.
  • Cache nodes verify them locally on every request.

2. Do not send opaque account/project tokens directly to cache nodes

Opaque long-lived tokens should be exchanged once for a short-lived cache JWT.

That gives us:

  • no hot-path introspection
  • no DB lookup on cache nodes
  • no repeated authorization callback to the main server

3. Use asymmetric signing for cache access tokens

Cache nodes should verify with public keys / JWKS, not with the same symmetric signing secret used by the main server.

This avoids distributing the token-signing secret to every cache deployment.

4. Generalize authz from project to namespace

project should not be the universal authorization primitive.

We need a cache namespace model such as:

  • project:<id>
  • domain:mise-tools
  • possibly more later

5. Token claims should authorize namespace access locally

Use standard JWT claims plus a small private claim for cache permissions.

Example shape:

{
  "iss": "...",
  "aud": "cache",
  "sub": "...",
  "exp": 123,
  "iat": 123,
  "nbf": 123,
  "jti": "...",
  "scp": ["cache:read", "cache:write", "cache:clean"],
  "cache_access": {
    "all_projects": false,
    "project_ids": ["p1", "p2"],
    "all_domains": false,
    "domains": ["mise-tools", "swiftpm-binaries"]
  }
}

The cache node then only needs to check:

  • token signature / expiry / audience
  • required action scope for the endpoint
  • whether the requested namespace is allowed by the claims

API implications

The cache API should stop assuming every request is project-scoped.

Instead, requests should carry or resolve a namespace such as:

  • namespace_type=project, namespace_id=<project-id>
  • namespace_type=domain, namespace_id=mise-tools

We should also stop treating account_handle as an authorization input in the cache data plane.

Storage implications

Storage identity should include namespace, not just project.

Suggested structure:

  • tenant + namespace_type + namespace_id + artifact_kind + artifact_key

Examples:

  • tenant-a / project / p1 / xcode / hash-123
  • tenant-a / domain / mise-tools / tool / python-3.13.2

Implementation sketch

  • Add a cache auth plug in cache-next.
  • Verify bearer JWTs locally.
  • Map endpoints to required scopes:
    • read paths -> cache:read
    • write/multipart paths -> cache:write
    • clean paths -> cache:clean or cache:write
  • Resolve the target namespace from the request.
  • Store a normalized auth context in conn.assigns.
  • Bind multipart uploads to the authorized namespace at creation time.

Open questions

  • Should namespace IDs be stable internal IDs only, or can some domain namespaces remain string-based?
  • Do we want cache:clean as a separate scope from cache:write?
  • Should the CLI exchange all auth methods into a cache-specific token up front, or should only CI/account tokens be exchanged?
  • Do we want one cache audience for all cache nodes, or audience per tenant / environment?

Acceptance criteria

  • No cache-node call to the main server in the request hot path.
  • No dependency on the old bespoke projects JWT claim.
  • No cache-node dependency on the main server’s symmetric signing secret.
  • Authorization works for both project-scoped and domain-scoped cache entries.
  • The resulting pattern is standard bearer + JWT verification, with only minimal private claims for namespace restrictions.

Activity

0

No activity yet.