Guidelines for secure secret management, input validation, API authentication, and defense-in-depth across VoxAgent's Python backend and API layer.
-
secret-keyring-only — ALWAYS store API keys in the OS keyring via keyring library. Never in config files, env vars in code, or hardcoded strings:
import 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
API_KEY = "sk-abc123..."
config = json.load(open("config.json"))["api_key"]
-
secret-no-log — NEVER log, print, or include secrets in error messages. Mask them:
logger.info("Using provider %s with key %s...", provider, key[:8] + "***")
logger.info("API key: %s", api_key)
-
secret-no-serialize — Never include secrets in JSON responses, Pydantic model exports, or dataclass repr. Use SecretStr:
from pydantic import BaseModel, SecretStr
class ProviderConfig(BaseModel):
name: str
api_key: SecretStr
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:
async def get_headers(self) -> dict[str, str]:
key = keyring.get_password("voxagent", f"{self.name}_api_key")
return {"Authorization": f"Bearer {key}"}
def __init__(self):
self._key = keyring.get_password(...)
-
secret-scope — Use separate keyring entries per provider and per environment. Namespace with voxagent/{provider}/{env}.
-
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_COMMANDS
-
input-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):
...
-
input-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
_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
MAX_FILE_UPLOAD_SIZE = 10 * 1024 * 1024
-
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 token
-
auth-dependency — Apply auth as a FastAPI dependency, not inline in each endpoint:
@router.post("/execute", dependencies=[Depends(verify_token)])
async def execute(request: SkillRequest):
...
@router.post("/execute")
async def execute(request: SkillRequest, token: str = Header(...)):
if not valid(token): raise ...
-
auth-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.
-
defense-cors — Configure CORS strictly. Only allow known origins, not *:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
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 response
-
defense-dependency-audit — Regularly audit dependencies with pip-audit or safety. Pin versions in pyproject.toml.
-
defense-least-privilege — Skills declare required permissions in permissions field. Reject skills that request more than needed.