AEP-013: Security Hardening & Authentication Layer¶
| Field | Value |
|---|---|
| Status | completed |
| Priority | P0 |
| Effort | High (7-10 days) |
| Impact | Critical |
| Dependencies | AEP-011 (deployment hardening) |
Gap Analysis¶
Current Implementation¶
The project has solid security foundations: - RBAC: 3-role hierarchy (viewer/operator/admin) enforced via GuardrailsPlugin - Input validation: 5 validators with safety constants preventing injection attacks - Guardrails: @destructive and @confirm decorators for dangerous operations - Audit logging: Structured JSON with secret redaction - Authentication enforcement: set_user_role() marks server-trusted roles; ensure_default_role() forces viewer for unset roles
However, there is no authentication layer — the system trusts the integration layer (Slack bot, web UI) to set the user role correctly.
What ADK Provides¶
ADK's safety documentation recommends a multi-layered approach:
- Agent-Auth: Service account identity for tool calls to external systems
- User-Auth: OAuth-based identity delegation (agent acts as the user)
- In-tool guardrails: Policy enforcement via
ToolContextwith developer-set constraints - Gemini safety filters: Configurable content safety thresholds
- Callbacks/Plugins for guardrails: Pre-validation of model and tool I/O
- Gemini as a Judge: LLM-based safety screening of inputs/outputs
- PII Redaction Plugin: Before-tool callback to redact PII
Gap¶
- No authentication: Anyone with network access can send requests
- No OAuth/JWT verification: User identity is self-declared
- No API key management: LLM keys in environment variables
- No PII redaction: Infrastructure data (IPs, hostnames) may leak
- No content safety filters: No screening for prompt injection attempts
- No network isolation: No guidance for VPC/firewall configuration
Proposed Solution¶
Step 1: JWT Authentication Middleware¶
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer
import jwt
security = HTTPBearer()
async def verify_token(credentials=Depends(security)):
try:
payload = jwt.decode(
credentials.credentials,
os.getenv("JWT_SECRET"),
algorithms=["HS256"],
)
return payload
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# Map JWT claims to RBAC roles
def extract_role(token_payload: dict) -> str:
roles = token_payload.get("roles", [])
if "admin" in roles:
return "admin"
elif "operator" in roles:
return "operator"
return "viewer"
Step 2: Auth Plugin¶
class AuthPlugin(BasePlugin):
"""Validates authentication and sets user role from JWT claims."""
def __init__(self):
super().__init__(name="auth")
async def on_user_message_callback(self, *, invocation_context, user_message):
# Extract auth context from session state (set by HTTP middleware)
auth_context = invocation_context.session.state.get("_auth")
if not auth_context:
return types.Content(
parts=[types.Part(text="Authentication required.")],
role="model",
)
# Set verified role
set_user_role(invocation_context.session, auth_context["role"])
return None
Step 3: PII Redaction Plugin¶
Create a plugin that redacts infrastructure PII before tool output reaches the LLM:
class PIIRedactionPlugin(BasePlugin):
"""Redacts infrastructure-sensitive data from tool outputs."""
PATTERNS = [
(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", "[REDACTED_IP]"),
(r'password["\s:=]+\S+', "password=[REDACTED]"),
(r'token["\s:=]+\S+', "token=[REDACTED]"),
(r'(?i)api[_-]?key["\s:=]+\S+', "api_key=[REDACTED]"),
]
def __init__(self):
super().__init__(name="pii_redaction")
async def after_tool_callback(self, *, tool, args, tool_context, result):
if isinstance(result, dict):
result = self._redact_dict(result)
return result
def _redact_dict(self, data):
"""Recursively redact sensitive patterns in dict values."""
...
Step 4: Prompt Injection Detection¶
Add a safety screening plugin (inspired by ADK's "Gemini as a Judge" pattern):
class SafetyScreenPlugin(BasePlugin):
"""Screens user inputs for prompt injection attempts."""
INJECTION_PATTERNS = [
r"ignore previous instructions",
r"forget your instructions",
r"you are now",
r"system prompt",
r"reveal your",
]
async def on_user_message_callback(self, *, invocation_context, user_message):
text = " ".join(p.text for p in user_message.parts if p.text)
for pattern in self.INJECTION_PATTERNS:
if re.search(pattern, text, re.IGNORECASE):
logger.warning("Potential prompt injection detected", extra={"input": text[:200]})
return types.Content(
parts=[types.Part(text="I can only help with DevOps tasks.")],
role="model",
)
return None
Step 5: Gemini Safety Filters¶
For Gemini models, enable content safety filters:
from google.genai import types
agent = create_agent(
name="orrery_assistant",
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting(
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
),
],
),
)
Step 6: Secrets Management¶
Replace environment variables with a secrets manager:
# core/orrery_core/secrets.py
class SecretsManager:
"""Pluggable secrets management."""
@staticmethod
def get(key: str) -> str:
# Try vault first, fall back to env vars
if vault_client := _get_vault_client():
return vault_client.read(f"secret/data/agents/{key}")
return os.getenv(key, "")
Affected Files¶
| File | Change |
|---|---|
core/orrery_core/auth.py | New: JWT verification, AuthPlugin |
core/orrery_core/pii.py | New: PIIRedactionPlugin |
core/orrery_core/safety.py | New: SafetyScreenPlugin |
core/orrery_core/secrets.py | New: SecretsManager |
core/orrery_core/plugins.py | Add auth/PII/safety plugins to default_plugins() |
agents/slack-bot/slack_bot/app.py | Add Slack signature verification |
core/tests/test_auth.py | New: auth tests |
core/tests/test_pii.py | New: PII redaction tests |
core/tests/test_safety.py | New: prompt injection detection tests |
Acceptance Criteria¶
- [x] JWT authentication required for HTTP endpoints
- [x] User role derived from verified JWT claims (not self-declared)
- [x] PII redaction applied to all tool outputs
- [x] Prompt injection patterns detected and blocked
- [x] Gemini safety filters enabled for content screening
- [x] Secrets manager with pluggable backends (mounted-file backend shipped; env var fallback; the
SecretsBackendprotocol accepts a Vault adapter without core changes) - [x] Slack bot verifies request signatures (slack-bolt signing-secret verification; Socket Mode authenticates via app token)
- [x] All security features have comprehensive tests
- [x] Security documentation with threat model (Security & Authentication)
Implementation notes (as landed)¶
The landed implementation diverges from the sketches above where ADK 2.0 semantics required it:
- SafetyScreenPlugin blocks in
before_run_callback, noton_user_message_callback— in ADK 2.0 the latter can only replace the user message;before_run_callbackis the hook whose non-None return halts the runner, so a screened message never reaches the model or any tool. - PIIRedactionPlugin mutates tool results in place and returns
None— ADK's after-tool chain early-exits on the first non-None return, so returning a redacted copy would silence every observer registered later (audit outcome, activity, metrics, output cap). It registers beforeAuditPluginso the audit log records redacted values too. Key-based redaction (credential-named dict keys) is layered over value-pattern scanning (kv pairs, PEM blocks, AWS/GitHub/Slack/OpenAI token shapes, JWTs); IP redaction is opt-in (ORRERY_REDACT_IPS) because an SRE agent that cannot see pod/broker IPs cannot diagnose much. - Gemini safety filters default to
BLOCK_ONLY_HIGH, notBLOCK_LOW_AND_ABOVE— SRE conversations legitimately discuss killing pods and destroying volumes; the aggressive threshold trips on ops chatter. Threshold viaGEMINI_SAFETY_THRESHOLD; only attached to Gemini string models (LiteLLM providers ignore the genai config). - Both plugins are on by default in
default_plugins()(ORRERY_SAFETY_SCREEN=false/ORRERY_PII_REDACTION=falseto disable).
Notes¶
- Authentication is the most critical gap. Without it, RBAC is meaningless since anyone can claim to be an admin.
- Start with JWT + RBAC mapping. OAuth2 with a proper identity provider (Auth0, Keycloak, Google IAP) is the production target.
- The PII redaction plugin should be configurable — some users may need to see IPs and hostnames for debugging.
- Prompt injection detection via regex is a baseline. For production, consider the "Gemini as a Judge" pattern using a fast, cheap model (Gemini Flash Lite) to screen inputs.