docs/code: clarify A2H2A prototype status and disable execution
- Add Implementation Status section to A2H2A spec - Clarify Google Chat is notification-only - Add target approval architecture requirements - Add critical A2H2A safety principle to RUNBOOK.md - Remove unsafe approval/rejection endpoints from server.py - Enforce server-side parameter_hash calculation - Disable execution for all tools in A2H2A_TOOL_ALLOW_LIST
This commit is contained in:
parent
79b100caff
commit
3b6e4e4890
212
docs/A2H2A_TICKETING_SPEC.md
Normal file
212
docs/A2H2A_TICKETING_SPEC.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# OSVxCC A2H2A Ticketing System Specification
|
||||
|
||||
## 1. Introduksjon
|
||||
|
||||
A2H2A (Agent-to-Human-to-Agent) er en uomgåelig Approval Gate mellom autonome observatører/analytikere og operative eksekveringsagenter. Ingen destruktive eller privilegerte handlinger skal utføres i GCP-infrastrukturen uten at autorisert operatør eksplisitt har validert og godkjent handlingen via et kryptografisk signert grensesnitt.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
**Current prototype — proposal recording only; approval and execution disabled.**
|
||||
|
||||
- The prototype may accept an allow-listed ticket proposal and store it as `PENDING`.
|
||||
- Google Chat is notification-only. It does not verify or prove the identity of an approver.
|
||||
- The current `/a2h2a/review/{ticket_id}` link is conceptual only.
|
||||
- No working approval or rejection endpoint currently exists.
|
||||
- No one-time action token currently exists.
|
||||
- No execution worker currently exists.
|
||||
- No proposed tool is executable through A2H2A at this stage.
|
||||
|
||||
## 2. JSON Datamodell (a2h2a-ticket-v1.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"ticket_id": "A2H2A-20260904-8B3D",
|
||||
"timestamp": "2026-09-04T14:20:05Z",
|
||||
"severity": "CRITICAL",
|
||||
"category": "SECURITY",
|
||||
"source": {
|
||||
"reporter": "tyr-compliance-agent",
|
||||
"trigger": "Secret scanned in public repository",
|
||||
"affected_service": "mcp-server-key",
|
||||
"project_id": "propane-will-491900-m5",
|
||||
"region": "global"
|
||||
},
|
||||
"context": {
|
||||
"summary": "En aktiv Secret Manager-nøkkel ble funnet i en offentlig Gitea-commit.",
|
||||
"evidence_logs": [
|
||||
"gitea.commit.url: https://git.vauco.no/chris/OSVauco/commit/a1b2c3d4...",
|
||||
"secret.name: projects/357036551735/secrets/mcp-server-key/versions/2"
|
||||
]
|
||||
},
|
||||
"proposed_action": {
|
||||
"action_type": "AUTOMATED_REMEDIATION",
|
||||
"runbook_reference": "docs/RUNBOOK.md#hendelse-eksponert-hemmelighet",
|
||||
"execution_tool": "opax.secrets.rotate",
|
||||
"parameters": {
|
||||
"secret_name": "mcp-server-key",
|
||||
"disable_old_versions": true,
|
||||
"notify_channel": "#security-alerts"
|
||||
},
|
||||
"parameter_hash": "sha256:f4a2...d8e0",
|
||||
"rollback_plan": "Manuell re-aktivering av forrige hemmelighetsversjon via `gcloud secrets versions enable` etter verifisering."
|
||||
},
|
||||
"governance": {
|
||||
"approval_status": "PENDING",
|
||||
"authorized_approver": "chris.christiansen@vauco.no",
|
||||
"requires_mfa": true,
|
||||
"timeout_minutes": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`The ticket model is a proposal model. Fields required for secure human approval—such as approval_token_hash, verified_approver_identity, approval timestamp, token_used_at, and immutable audit-event references—are target-design fields and are not implemented in the current prototype.`
|
||||
|
||||
## 3. Sikkerhetsgarantier
|
||||
|
||||
### 3.1 Constant-Time Authentication
|
||||
- Sammenligning av alle signaturer og tokens **skal** utføres med `secrets.compare_digest` eller tilsvarende for å forhindre timing-angrep.
|
||||
|
||||
### 3.2 Anti-Tampering
|
||||
- En SHA-256 hash av `proposed_action.parameters` **skal** beregnes før ticketen sendes til operatør.
|
||||
- Før eksekvering **skal** OPAX verifisere at hashen av parameterne den mottok for eksekvering stemmer overens med `parameter_hash` i den godkjente ticketen.
|
||||
|
||||
### 3.3 Replay-Beskyttelse
|
||||
- Hver callback fra Slack/Discord **skal** inneholde et timestamp.
|
||||
- Serveren **skal** avvise callbacks hvor `current_time - callback_timestamp > 300` sekunder for å forhindre gjenbruk av gamle godkjenninger.
|
||||
|
||||
### 3.4 Time-To-Live (TTL)
|
||||
- Ubehandlede tickets utløper automatisk basert på alvorlighetsgrad:
|
||||
- **CRITICAL:** 15 minutter
|
||||
- **HIGH:** 60 minutter
|
||||
- **MEDIUM/LOW:** 24 timer
|
||||
|
||||
## 4. API-Endepunkter (Prototype)
|
||||
|
||||
### 4.1 POST /api/v1/a2h2a/tickets
|
||||
1. Motta ticket-payload fra en autorisert intern agent (f.eks. TYR).
|
||||
2. Valider payloaden mot en Pydantic-modell basert på schemaet over.
|
||||
3. Beregn `parameter_hash` server-side.
|
||||
4. Lagre ticketen i en Firestore-collection (`a2h2a_tickets`) med status `PENDING`.
|
||||
5. Formater og send en Google Chat-melding med et konseptuelt link til en fremtidig godkjennings-UI.
|
||||
|
||||
`Google Chat delivers a notification and review link only; it must never be used as evidence of the approver's identity or as the authority to execute a tool.`
|
||||
|
||||
## Target Approval Architecture — Required Before Production
|
||||
|
||||
The eventual review URL may use:
|
||||
`/a2h2a/review/{ticket_id}?token=<opaque-random-token>`
|
||||
|
||||
- The token is generated with a cryptographically secure random generator.
|
||||
- The raw token is returned once in the notification link and is never persisted.
|
||||
- Only the SHA-256 token hash is stored server-side.
|
||||
- Approval/rejection backend verifies IAP-verified identity, PENDING state, TTL, unused matching token hash, authorized approver, server-side canonical parameter hash, and tool allow-list.
|
||||
- Approval only updates the ticket state.
|
||||
- A separate worker later executes only server-stored, immutable, approved actions.
|
||||
- Every audit event is a target requirement; do not claim it is already implemented.
|
||||
|
||||
## Target audit requirements
|
||||
`Immutable audit events for proposal, approval, rejection, expiry, execution start, execution success, execution failure, and rollback are required before production activation; they are not implemented in the current prototype.`
|
||||
|
||||
## 5. Google Chat / Slack Block Kit Payload (Konseptuelt Eksempel)
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "🚨 A2H2A Godkjenning Kreves: CRITICAL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{"type": "mrkdwn", "text": "*Kategori:*
|
||||
SECURITY"},
|
||||
{"type": "mrkdwn", "text": "*Tjeneste:*
|
||||
mcp-server-key"},
|
||||
{"type": "mrkdwn", "text": "*Handling:*
|
||||
`opax.secrets.rotate`"},
|
||||
{"type": "mrkdwn", "text": "*Tidsfrist:*
|
||||
15 minutter"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Sammendrag:*
|
||||
En aktiv Secret Manager-nøkkel ble funnet i en offentlig Gitea-commit."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "✅ Godkjenn",
|
||||
"emoji": true
|
||||
},
|
||||
"style": "primary",
|
||||
"value": "approve_A2H2A-20260904-8B3D"
|
||||
},
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "❌ Avvis",
|
||||
"emoji": true
|
||||
},
|
||||
"style": "danger",
|
||||
"value": "reject_A2H2A-20260904-8B3D"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Eksempel-Ticket (SECRET_EXPOSURE)
|
||||
|
||||
```json
|
||||
{
|
||||
"ticket_id": "A2H2A-20260904-1C4F",
|
||||
"timestamp": "2026-09-04T18:05:10Z",
|
||||
"severity": "CRITICAL",
|
||||
"category": "SECURITY",
|
||||
"source": {
|
||||
"reporter": "tyr-scanner-agent",
|
||||
"trigger": "Audit log anomaly: Unexpected access to mcp-server-key v2",
|
||||
"affected_service": "mcp-server-key",
|
||||
"project_id": "propane-will-491900-m5",
|
||||
"region": "global"
|
||||
},
|
||||
"context": {
|
||||
"summary": "Mistenkelig tilgang til mcp-server-key v2 fra en ukjent IP-adresse (35.22.88.101) utenfor GCP-nettverket.",
|
||||
"evidence_logs": [
|
||||
"log_explorer_link: https://console.cloud.google.com/logs/query;query=..."
|
||||
]
|
||||
},
|
||||
"proposed_action": {
|
||||
"action_type": "AUTOMATED_REMEDIATION",
|
||||
"runbook_reference": "docs/RUNBOOK.md#hendelse-eksponert-hemmelighet",
|
||||
"execution_tool": "opax.secrets.rotate",
|
||||
"parameters": {
|
||||
"secret_name": "mcp-server-key",
|
||||
"disable_old_versions": true,
|
||||
"notify_channel": "#security-alerts"
|
||||
},
|
||||
"parameter_hash": "sha256:f4a26901a511e6cb716b5a86552631521a0e71383921034293739886313ad8e0",
|
||||
"rollback_plan": "Manuell re-aktivering av forrige hemmelighetsversjon (v3) via `gcloud secrets versions enable` etter verifisering."
|
||||
},
|
||||
"governance": {
|
||||
"approval_status": "PENDING",
|
||||
"authorized_approver": "chris.christiansen@vauco.no",
|
||||
"requires_mfa": true,
|
||||
"timeout_minutes": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -229,3 +229,17 @@ En Cloud Build-pipeline feiler på det siste "deploy to Cloud Run"-steget. Feilm
|
|||
### Status
|
||||
|
||||
🟡 **MODERAT TIL AVKLART.** Vanligvis en operasjonell feil forårsaket av en compliance-endring.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sikkerhet og Hendelseshåndtering
|
||||
|
||||
Se følgende dokumenter for detaljerte prosedyrer:
|
||||
|
||||
- `docs/SECURITY.md`: Overordnet sikkerhetsarkitektur.
|
||||
- `docs/INCIDENT_RESPONSE.md`: Prosedyrer for håndtering av sikkerhetshendelser.
|
||||
- `docs/SECRET_MANAGEMENT.md`: Retningslinjer for håndtering av secrets.
|
||||
|
||||
### Kritisk Sikkerhetsprinsipp for A2H2A
|
||||
|
||||
**No privileged action may be authorized by a notification, a URL parameter, or a client-supplied identity. Approval requires a verified identity boundary and server-side validation of an immutable ticket.**
|
||||
|
|
|
|||
|
|
@ -48,10 +48,57 @@ except Exception as e:
|
|||
from email.mime.text import MIMEText
|
||||
from email.mime.text import MIMEText
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Dict, List
|
||||
import logging
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
import hashlib
|
||||
|
||||
# --- A2H2A Pydantic Models ---
|
||||
class TicketSource(BaseModel):
|
||||
reporter: str
|
||||
trigger: str
|
||||
affected_service: str
|
||||
project_id: str
|
||||
region: str
|
||||
|
||||
class TicketContext(BaseModel):
|
||||
summary: str
|
||||
evidence_logs: List[str]
|
||||
|
||||
class ProposedAction(BaseModel):
|
||||
action_type: str
|
||||
runbook_reference: str
|
||||
execution_tool: str
|
||||
parameters: Dict
|
||||
parameter_hash: Optional[str] = None
|
||||
rollback_plan: str
|
||||
|
||||
class Governance(BaseModel):
|
||||
approval_status: str = "PENDING"
|
||||
authorized_approver: str
|
||||
requires_mfa: bool = True
|
||||
timeout_minutes: int = 15
|
||||
|
||||
class A2H2ATicket(BaseModel):
|
||||
ticket_id: str
|
||||
timestamp: datetime
|
||||
severity: str # CRITICAL, HIGH, MEDIUM, LOW
|
||||
category: str
|
||||
source: TicketSource
|
||||
context: TicketContext
|
||||
proposed_action: ProposedAction
|
||||
governance: Governance
|
||||
|
||||
def calculate_parameter_hash(self) -> str:
|
||||
"""Beregn SHA-256 hash av parameters i kanonisk form."""
|
||||
param_json = json.dumps(self.proposed_action.parameters, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(param_json.encode()).hexdigest()
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Sjekk om ticket er utløpt"""
|
||||
from datetime import datetime, timedelta
|
||||
expiry = self.timestamp + timedelta(minutes=self.governance.timeout_minutes)
|
||||
return datetime.utcnow() > expiry.replace(tzinfo=None)
|
||||
|
||||
class ConfirmationRequired(BaseModel):
|
||||
message: str
|
||||
|
|
@ -97,6 +144,118 @@ async def sse_endpoint(request: Request):
|
|||
"""Handles Server-Sent Events for external clients like Perplexity."""
|
||||
return StreamingResponse(sse_event_stream(request), media_type="text/event-stream")
|
||||
|
||||
# --- A2H2A API Endpoints ---
|
||||
|
||||
# --- A2H2A Secure Design Placeholder ---
|
||||
#
|
||||
# The following logic is a placeholder for a secure A2H2A (Agent-to-Human-to-Agent)
|
||||
# approval workflow. The key principles are:
|
||||
#
|
||||
# 1. NOTIFICATION, NOT ACTION: Google Chat messages are for notification only. They
|
||||
# do not carry any identity or authorization guarantee.
|
||||
# 2. VERIFIED IDENTITY: Human approval must happen through a protected surface that
|
||||
# verifies the user's identity (e.g., a web app protected by Google IAP).
|
||||
# 3. ATOMIC, IMMUTABLE ACTIONS: The ticket stored in the backend (Firestore) is the
|
||||
# source of truth. The proposed action, its parameters, and the server-generated
|
||||
# parameter hash are immutable.
|
||||
# 4. ONE-TIME USE TOKENS: The approval UI should be accessed via a link containing a
|
||||
# cryptographically random, single-use token that is invalidated after the first
|
||||
# approval or rejection, or after the ticket's TTL expires.
|
||||
# 5. DECOUPLED EXECUTION: The approval action only changes the ticket's state to
|
||||
# 'APPROVED'. A separate, secure worker process will poll for approved tickets and
|
||||
# execute them, ensuring a clean separation of concerns. This worker validates the
|
||||
# ticket's integrity (non-expired, parameter hash match) before execution.
|
||||
#
|
||||
# This implementation only covers the safe recording of a proposed ticket.
|
||||
# Execution is explicitly disabled.
|
||||
|
||||
# A strict allow-list of tools that can be proposed in an A2H2A ticket.
|
||||
# `execution_enabled: False` means the tool can be proposed but not executed.
|
||||
A2H2A_TOOL_ALLOW_LIST = {
|
||||
"example_tool_1": {"execution_enabled": False, "description": "A sample tool that does something harmless."},
|
||||
"example_tool_2": {"execution_enabled": False, "description": "Another sample tool."},
|
||||
# Add real, vetted tools here with execution_enabled: True ONLY after extensive security review.
|
||||
}
|
||||
|
||||
@app.post("/api/v1/a2h2a/tickets")
|
||||
async def create_a2h2a_ticket(ticket: A2H2ATicket, request: Request):
|
||||
"""
|
||||
Opprett ny A2H2A ticket og send til Google Chat for godkjenning.
|
||||
Dette er kun en prototype for sikker registrering, ingen verktøy blir kjørt.
|
||||
"""
|
||||
try:
|
||||
await _verify_auth(request)
|
||||
|
||||
# Security check: Reject if caller provides a parameter_hash.
|
||||
if ticket.proposed_action.parameter_hash is not None:
|
||||
raise HTTPException(status_code=400, detail="Do not provide a parameter_hash. The server will generate it.")
|
||||
|
||||
# Security check: Ensure the proposed tool is in the allow-list.
|
||||
tool_name = ticket.proposed_action.execution_tool
|
||||
if tool_name not in A2H2A_TOOL_ALLOW_LIST:
|
||||
raise HTTPException(status_code=400, detail=f"Tool '{tool_name}' is not on the approved A2H2A allow-list.")
|
||||
|
||||
# Generate parameter hash server-side for integrity.
|
||||
ticket.proposed_action.parameter_hash = ticket.calculate_parameter_hash()
|
||||
|
||||
from google.cloud import firestore
|
||||
db = firestore.Client(project=GOOGLE_CLOUD_PROJECT)
|
||||
ticket_data = ticket.model_dump(mode='json')
|
||||
ticket_data['created_at'] = datetime.utcnow().isoformat()
|
||||
ticket_data['status'] = 'PENDING'
|
||||
doc_ref = db.collection('a2h2a_tickets').document(ticket.ticket_id)
|
||||
doc_ref.set(ticket_data)
|
||||
|
||||
# The approval URL now points to a conceptual, IAP-protected UI.
|
||||
approval_ui_url = f"https://opax.vauco.no/a2h2a/review/{ticket.ticket_id}"
|
||||
|
||||
google_chat_webhook = os.getenv('A2H2A_GOOGLE_CHAT_WEBHOOK')
|
||||
if google_chat_webhook:
|
||||
chat_payload = {
|
||||
"cards": [
|
||||
{
|
||||
"header": {
|
||||
"title": f"🚨 A2H2A Proposal: {ticket.severity}",
|
||||
"subtitle": "OSVxCC Security (Prototype - No Execution)",
|
||||
"image": {"imageUrl": "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png"}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"widgets": [
|
||||
{
|
||||
"textParagraph": {
|
||||
"text": f"<b>Category:</b> {ticket.category}<br>"
|
||||
f"<b>Service:</b> {ticket.source.affected_service}<br>"
|
||||
f"<b>Tool:</b> {ticket.proposed_action.execution_tool}<br>"
|
||||
f"<b>Summary:</b> {ticket.context.summary}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"textButton": {
|
||||
"text": "REVIEW IN APPROVAL UI",
|
||||
"onClick": {"openLink": {"url": approval_ui_url}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
import httpx
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(google_chat_webhook, json=chat_payload)
|
||||
|
||||
return {"status": "success", "ticket_id": ticket.ticket_id, "parameter_hash": ticket.proposed_action.parameter_hash}
|
||||
except Exception as e:
|
||||
logger.error(f"Feil ved opprettelse av A2H2A ticket: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ── Service discovery ───────────────────────────────────────────────────────
|
||||
OSVAUCO_AGENT_URL = os.environ.get("OSVAUCO_AGENT_URL", "") # f.eks. https://osvauco-agent-....run.app
|
||||
MCP_SECRET = os.environ.get("MCP_SECRET", "")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user