auth-security-patterns
Authentication, secret management, and security patterns for VoxAgent. Use when handling API keys, implementing authentication, validating user input, managing sessions, or auditing security. Triggers
SKILL.md.claude/.claude/skills/auth-security-patterns/SKILL.md
GitHubAuthentication & Security Patterns for VoxAgent
Guidelines for secure secret management, input validation, API authentication, and defense-in-depth across VoxAgent's Python backend and API layer.
When to Apply
Reference these guidelines when:
- Storing or retrieving API keys (
core/keyring_manager.py) - Validating user input or LLM-generated commands (
core/safety.py) - Adding authentication to API endpoints
- Implementing session management
- Auditing code for security vulnerabilities
- Reviewing PRs that touch secrets, auth, or user input
Rule Categories by Priority
| Priority | Category | Impact | Applies to |
|---|---|---|---|
| 1 | Secret Management | CRITICAL | All modules |
| 2 | Input Validation | CRITICAL | core/, api/ |
| 3 | API Authentication | HIGH | api/ |
| 4 | Session Security | HIGH | api/, core/ |
| 5 | Defense in Depth | MEDIUM | All modules |
Quick Reference
1. Secret Management (CRITICAL)
secret-keyring-only— ALWAYS store API keys in the OS keyring viakeyringlibrary. Never in config files, env vars in code, or hardcoded strings:import keyring # Good: OS keyring def get_api_key(provider: str) -> str: key = keyring.get_password("voxagent", f"{provider}_api_key") if not key: raise ProviderConfigError(f"No API key found for {provider}") return key # Bad: hardcoded or config file API_KEY = "sk-abc123..." # NEVER config = json.load(open("config.json"))["api_key"] # NEVERsecret-no-log— NEVER log, print, or include secrets in error messages. Mask them:# Good: masked logger.info("Using provider %s with key %s...", provider, key[:8] + "***") # Bad: full key in logs logger.info("API key: %s", api_key)secret-no-serialize— Never include secrets in JSON responses, Pydantic model exports, or dataclass repr. UseSecretStr:from pydantic import BaseModel, SecretStr class ProviderConfig(BaseModel): name: str api_key: SecretStr # won't appear in .model_dump() or .json() model_config = ConfigDict(json_schema_extra={"properties": {"api_key": {"writeOnly": True}}})secret-rotation— Design key access to support rotation. Read from keyring on each use, don't cache indefinitely:# Good: fresh read async def get_headers(self) -> dict[str, str]: key = keyring.get_password("voxagent", f"{self.name}_api_key") return {"Authorization": f"Bearer {key}"} # Bad: cached forever in __init__ def __init__(self): self._key = keyring.get_password(...) # stale after rotationsecret-scope— Use separate keyring entries per provider and per environment. Namespace withvoxagent/{provider}/{env}.
2. Input Validation (CRITICAL)
input-sanitize-llm— Always sanitize LLM-generated commands before execution. Use allowlists, not blocklists:ALLOWED_COMMANDS = frozenset({"open", "search", "play", "pause", "volume"}) def validate_intent(intent: str) -> bool: base_command = intent.split()[0].lower() return base_command in ALLOWED_COMMANDSinput-validate-pydantic— All external input MUST pass through Pydantic validation. Never trust raw dicts or strings:class SkillRequest(BaseModel): skill_name: str = Field(min_length=1, max_length=64, pattern=r"^[a-z_]+$") parameters: dict[str, str] = Field(default_factory=dict, max_length=10) @router.post("/execute") async def execute(request: SkillRequest): ... # request is already validatedinput-rate-limit— Apply rate limiting to all public API endpoints. Use sliding window per client:from fastapi import Request from collections import defaultdict REQUEST_LIMIT = 60 # per minute _request_counts: dict[str, list[float]] = defaultdict(list)input-size-limit— Set maximum sizes for all inputs. Reject oversized payloads early:MAX_TRANSCRIPT_LENGTH = 10_000 # characters MAX_FILE_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 MB
3. API Authentication (HIGH)
auth-bearer-token— Use Bearer token authentication for API endpoints. Validate in a FastAPI dependency:from fastapi import Depends, HTTPException, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer security = HTTPBearer() async def verify_token( credentials: HTTPAuthorizationCredentials = Security(security), ) -> str: token = credentials.credentials if not await validate_token(token): raise HTTPException(status_code=401, detail="Invalid token") return tokenauth-dependency— Apply auth as a FastAPI dependency, not inline in each endpoint:# Good: dependency @router.post("/execute", dependencies=[Depends(verify_token)]) async def execute(request: SkillRequest): ... # Bad: inline check @router.post("/execute") async def execute(request: SkillRequest, token: str = Header(...)): if not valid(token): raise ... # repeated in every endpointauth-provider-init— Verify API keys are available at provider initialization, not at first use:class LLMProvider: async def initialize(self) -> None: key = keyring.get_password("voxagent", f"{self.name}_api_key") if not key: raise ProviderConfigError(f"Missing API key for {self.name}")auth-no-query-params— Never pass tokens or secrets as URL query parameters. They leak in logs and browser history.
4. Session Security (HIGH)
session-id-random— Generate session IDs withsecrets.token_urlsafe(32). Never use sequential or predictable IDs.session-expiry— All sessions MUST have a TTL. Default 30 minutes, configurable:SESSION_TTL_SECONDS = 1800 # 30 minutes @dataclass class Session: id: str created_at: float last_active: float @property def is_expired(self) -> bool: return time.time() - self.last_active > SESSION_TTL_SECONDSsession-isolation— Each voice session is isolated. Never share state (conversation history, context) between sessions.session-cleanup— Implement periodic cleanup of expired sessions. Useasyncio.create_task()with a background loop:async def cleanup_sessions(store: SessionStore, interval: int = 300) -> None: while True: await asyncio.sleep(interval) expired = [s for s in store.sessions if s.is_expired] for session in expired: await store.remove(session.id)
5. Defense in Depth (MEDIUM)
defense-cors— Configure CORS strictly. Only allow known origins, not*:app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], # dashboard only allow_methods=["GET", "POST"], allow_headers=["Authorization", "Content-Type"], )defense-headers— Set security headers on all responses:@app.middleware("http") async def security_headers(request: Request, call_next): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return responsedefense-dependency-audit— Regularly audit dependencies withpip-auditorsafety. Pin versions inpyproject.toml.defense-least-privilege— Skills declare required permissions inpermissionsfield. Reject skills that request more than needed.
Operational Procedure
Before coding
- Check if
core/keyring_manager.pyalready handles the secret - Identify if the change touches any user input or external data
- Review the existing security boundary in
core/safety.py
During coding
- Use the OS keyring for ALL secrets — no exceptions
- Validate all input with Pydantic models
- Apply auth as FastAPI dependencies, not inline checks
- Mask secrets in all log output
After coding
- Search for hardcoded strings that look like keys or tokens
- Verify no secrets appear in error messages or API responses
- Check that all new endpoints have auth dependencies
- Run
ruff checkfor security-related linting rules