SkillAgentSearch skills...

003-zero-trust-authentication

A self-hosted AI platform that keeps your data on your infrastructure. Busibox integrates document processing, semantic search, AI agents, and custom applications into a single platform — running on Docker with enterprise-grade security baked in. Think of it as a Linux distribution for AI.

Install / Use

npx skills add jazzmind/busibox

Installs into whichever agent you are using.

About this skill
📐

Cursor Rules

Cursor IDE rules (v2)

Quality Score

69/100

Category

Security

Supported Platforms

Cursor

Zero Trust Authentication Rules

Overview

Busibox uses a Zero Trust authentication model where user identity is cryptographically proven by JWT tokens, not by service-to-service credentials. All services trust tokens signed by the authz service.

Core Principles

1. NO Client Credentials for User Operations

NEVER use client_id/client_secret for operations on behalf of users:

# ❌ WRONG - Don't use client credentials
token_client = TokenExchangeClient(
    client_id="agent-api",
    client_secret="secret123",
)
token = await token_client.get_token_for_service(
    requested_subject=user_id,  # Impersonation!
    target_audience="data-api"
)

# ✅ CORRECT - Use Zero Trust token exchange
token = await exchange_token_zero_trust(
    subject_token=user_token,  # User's actual token
    target_audience="data-api",
    user_id=user_id  # For logging only
)

2. Token Exchange Flow

Any valid authz-signed token can be exchanged for another audience:

User Session JWT (aud=ai-portal)
    ↓ token exchange
Agent API Token (aud=agent-api)
    ↓ token exchange (THIS IS NOW ALLOWED)
Ingest API Token (aud=ingest-api)

The security comes from:

  1. Signature verification - Token must be signed by authz
  2. Expiration check - Token must not be expired
  3. RBAC from authz DB - User's scopes/roles come from database, not the incoming token
  4. Session revocation - Session/delegation tokens are checked for revocation

3. Scopes Come from RBAC, Not Tokens

When exchanging tokens, the scopes in the issued token come from the user's roles in the authz database, not from the incoming token's scopes:

# In authz token exchange:
roles = await db.get_user_roles(user_id)
all_scopes = set()
for r in roles:
    all_scopes.update(r.get("scopes") or [])
# New token gets aggregated scopes from all user's roles

Exception: Source-Gated Scopes

Some scopes are never in any role's scope list but are injected conditionally by authz based on the origin (aud) of the incoming subject token. This is an approved pattern called "source gating".

Currently source-gated scopes:

| Scope | Injected when subject token aud is | Target audience | |-------|--------------------------------------|-----------------| | config.secrets.read | agent-api | config-api |

This allows agent-api to read raw LLM credentials from config-api without any other caller being able to do the same — even an admin user with a direct config-api token exchange cannot obtain this scope.

Implementation: srv/authz/src/routes/oauth.py_AGENT_API_CONFIG_EXTRA_SCOPES constant.

4. Pass Tokens Through the Call Chain

Services should store and pass along user tokens for downstream calls:

# In FastAPI dependency
async def get_principal(authorization: str = Header(...)) -> Principal:
    token = authorization.split(" ", 1)[1]
    principal = await validate_bearer(token)
    principal.token = token  # Store for downstream use
    return principal

# In service code
async def call_downstream_service(principal: Principal):
    downstream_token = await exchange_token_zero_trust(
        subject_token=principal.token,
        target_audience="downstream-api",
        user_id=principal.sub
    )

Implementation Patterns

Python Services (busibox_common)

from app.auth.token_exchange import exchange_token_zero_trust

# Exchange user's token for downstream service
ingest_token = await exchange_token_zero_trust(
    subject_token=principal.token,
    target_audience="ingest-api",
    user_id=principal.sub
)

if ingest_token:
    headers = {"Authorization": f"Bearer {ingest_token}"}
    # Make downstream call

TypeScript Services (busibox-app)

import { exchangeTokenZeroTrust } from '@jazzmind/busibox-app/lib/authz';

const result = await exchangeTokenZeroTrust({
  sessionJwt: userToken,
  audience: 'agent-api',
  scopes: ['agents:read'],
});

Token Types

| Type | Audience | Purpose | Revocable | |------|----------|---------|-----------| | Session | ai-portal | User browser session | Yes (via session_id) | | Delegation | ai-portal | Background tasks | Yes (via delegation_id) | | Access | service-specific | API calls | No (short-lived) |

All types can be used as subject_token for token exchange.

Anti-Patterns to Avoid

❌ Service-to-Service Authentication (HARD RULE)

NEVER create service accounts, shared secrets, API keys, or client credentials for one service to call another. This violates Zero Trust. All inter-service calls must carry a user's identity via JWT token exchange.

If a service needs data from another service:

  • During a user request: exchange the user's token for the target audience
  • During startup/background: use delegation tokens created while a user was present, or read directly from the shared database layer (Postgres) which both services can access
  • During deployment: use Ansible which has direct database/filesystem access
# ❌ NEVER - service-to-service API keys
CONFIG_API_KEY = "shared-secret-123"
headers = {"X-Service-Key": CONFIG_API_KEY}
resp = await client.get(f"{config_api_url}/admin/config", headers=headers)

# ❌ NEVER - client credentials grant
token = await get_client_credentials_token(
    client_id="agent-api",
    client_secret="secret",
    audience="config-api"
)

# ✅ OK - user token exchange during request
config_token = await get_service_token(principal.token, principal.sub, "config-api")

# ✅ OK - direct database read at startup (same Postgres cluster)
conn = await asyncpg.connect(config_database_url)
row = await conn.fetchrow("SELECT value FROM config_entries WHERE key = $1", key)

# ✅ OK - Ansible reads config DB and injects into service env at deploy time

❌ Service Account Impersonation

# DON'T DO THIS
client = ServiceClient(client_id="agent-api", secret="...")
token = client.get_token_as(user_id)  # Impersonation

❌ Hardcoded Audience Restrictions

# DON'T DO THIS
if token.audience != "ai-portal":
    raise Error("wrong audience")  # Over-restrictive

❌ Passing Scopes from Incoming Token

# DON'T DO THIS
new_token = exchange(subject_token, scopes=subject_token.scopes)
# Scopes should come from RBAC, not the incoming token

When to Use Delegation Tokens

For background tasks that run without user presence (e.g., scheduled jobs, webhooks):

  1. User explicitly creates a delegation token while authenticated
  2. Delegation token has limited scopes and expiration
  3. Background job uses delegation token as subject_token for exchanges
# Create delegation token (user must be present)
delegation = await create_delegation_token(
    session_jwt=user_session,
    name="nightly-sync",
    scopes=["ingest:read"],
    expires_in_seconds=86400
)

# Later, in background job
token = await exchange_token_zero_trust(
    subject_token=delegation.token,
    target_audience="ingest-api",
    user_id=delegation.user_id
)

Related Files

  • srv/authz/src/routes/oauth.py - Token exchange implementation
  • srv/agent/app/auth/token_exchange.py - Agent service token exchange helper
  • srv/shared/busibox_common/auth.py - Shared auth utilities
  • busibox-app/src/lib/authz/zero-trust.ts - TypeScript Zero Trust client

Related Skills

View on GitHub
GitHub Stars0
CategorySecurity
UpdatedNaNy ago
Forks0

Security Score

68/100

Audited on Invalid Date

2 medium1 low