Onderzoek naar compromittering van API-sleutels
Het onderzoeken van incidenten met gecompromitteerde AI-API-sleutels, waaronder detectie, scopebeoordeling, gebruiksforensiek en remediatieprocedures.
Overzicht
AI-API-sleutels zijn waardevolle doelwitten voor aanvallers. Een gecompromitteerde API-sleutel van OpenAI, Anthropic, Google AI of Azure OpenAI verleent de aanvaller toegang tot krachtige taalmodellen op kosten van het slachtoffer. De gevolgen variëren van financieel: aanvallers hebben in één enkel weekend rekeningen van meer dan $100.000 opgebouwd met gestolen GPT-4-sleutels, tot operationeel: de aanvaller kan de sleutel gebruiken om gevoelige data via het model te verwerken en mogelijk informatie te exfiltreren uit RAG-verbonden systemen. In het ergste geval stelt een gecompromitteerde sleutel met fine-tuning-permissies de aanvaller in staat om modellen die aan het account zijn gekoppeld te vergiftigen.
Onderzoeken naar gecompromitteerde API-sleutels verschillen op verschillende belangrijke manieren van traditionele compromittering van credentials. Ten eerste hangt de "blast radius" sterk af van waar de sleutel mee is verbonden. Een sleutel die alleen voor eenvoudige completions wordt gebruikt, heeft een ander risicoprofiel dan een sleutel die is ingebouwd in een agentic systeem met toolgebruik. Ten tweede zijn de forensische artefacten primair API-gebruikslogs en factureringsrecords in plaats van systeemlogs en netwerkcaptures. Ten derde is het aanvalsoppervlak voor diefstal van credentials breed: sleutels belanden in git-repository's, client-side code, CI/CD-logs, dumps van omgevingsvariabelen en gedeelde configuratiebestanden.
Dit artikel doorloopt de volledige onderzoekslevenscyclus voor compromittering van AI-API-sleutels: initiële detectie, scopebeoordeling, forensische reconstructie van aanvalleractiviteit en remediatie.
Detectie van compromittering van API-sleutels
Detectie van gebruiksanomalieën
Het meest betrouwbare signaal voor compromittering van een API-sleutel is afwijkend gebruik. Dit manifesteert zich als pieken in verzoekvolume, onverwacht modelgebruik (de aanvaller die GPT-4-turbo gebruikt terwijl je applicatie alleen GPT-3.5 gebruikt), verzoeken van onbekende IP-adressen, of gebruik buiten normale kantooruren.
import json
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from collections import defaultdict
from typing import Optional
@dataclass
class UsageRecord:
"""Een enkele API-gebruiksrecord."""
timestamp: str
api_key_prefix: str # Laatste 4 tekens of gemaskeerde sleutel
model: str
input_tokens: int
output_tokens: int
endpoint: str
source_ip: Optional[str] = None
user_agent: Optional[str] = None
status_code: int = 200
cost_usd: float = 0.0
@dataclass
class AnomalyAlert:
"""Een anomalie gedetecteerd in API-gebruik."""
alert_type: str
severity: str # low, medium, high, critical
description: str
evidence: dict
timestamp: str
api_key_prefix: str
class APIKeyUsageAnalyzer:
"""Detecteer afwijkende gebruikspatronen van API-sleutels die
kunnen wijzen op compromittering."""
# Benaderende kosten per 1K tokens (USD) per begin 2026
MODEL_COSTS = {
"gpt-4": {"input": 0.03, "output": 0.06},
"gpt-4-turbo": {"input": 0.01, "output": 0.03},
"gpt-4o": {"input": 0.005, "output": 0.015},
"gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
"claude-3-opus": {"input": 0.015, "output": 0.075},
"claude-3.5-sonnet": {"input": 0.003, "output": 0.015},
"claude-3.5-haiku": {"input": 0.0008, "output": 0.004},
}
def __init__(self, baseline_window_days: int = 30):
self.baseline_window_days = baseline_window_days
self.baselines: dict[str, dict] = {} # key_prefix -> baseline-statistieken
def build_baseline(self, historical_records: list[UsageRecord]) -> None:
"""Bouw gebruiksbasislijnen op uit historische data."""
key_records: dict[str, list[UsageRecord]] = defaultdict(list)
for record in historical_records:
key_records[record.api_key_prefix].append(record)
for key_prefix, records in key_records.items():
daily_requests: dict[str, int] = defaultdict(int)
daily_costs: dict[str, float] = defaultdict(float)
models_used: set[str] = set()
ips_seen: set[str] = set()
hourly_distribution: dict[int, int] = defaultdict(int)
for r in records:
try:
dt = datetime.fromisoformat(r.timestamp)
except (ValueError, TypeError):
continue
day_key = dt.strftime("%Y-%m-%d")
daily_requests[day_key] += 1
daily_costs[day_key] += r.cost_usd
models_used.add(r.model)
if r.source_ip:
ips_seen.add(r.source_ip)
hourly_distribution[dt.hour] += 1
req_counts = list(daily_requests.values()) or [0]
cost_values = list(daily_costs.values()) or [0.0]
self.baselines[key_prefix] = {
"avg_daily_requests": sum(req_counts) / max(len(req_counts), 1),
"max_daily_requests": max(req_counts),
"std_daily_requests": self._std(req_counts),
"avg_daily_cost": sum(cost_values) / max(len(cost_values), 1),
"max_daily_cost": max(cost_values),
"known_models": models_used,
"known_ips": ips_seen,
"active_hours": {
h for h, c in hourly_distribution.items()
if c > len(records) * 0.01 # minstens 1% van het verkeer
},
}
def analyze_records(
self,
records: list[UsageRecord],
) -> list[AnomalyAlert]:
"""Analyseer gebruiksrecords tegen basislijnen om anomalieën te detecteren."""
alerts = []
# Groepeer per sleutel en dag
key_day_records: dict[str, dict[str, list[UsageRecord]]] = defaultdict(
lambda: defaultdict(list)
)
for r in records:
try:
day = datetime.fromisoformat(r.timestamp).strftime("%Y-%m-%d")
except (ValueError, TypeError):
continue
key_day_records[r.api_key_prefix][day].append(r)
for key_prefix, days in key_day_records.items():
baseline = self.baselines.get(key_prefix)
if not baseline:
continue
for day, day_records in days.items():
alerts.extend(
self._check_volume_anomaly(key_prefix, day, day_records, baseline)
)
alerts.extend(
self._check_cost_anomaly(key_prefix, day, day_records, baseline)
)
alerts.extend(
self._check_model_anomaly(key_prefix, day_records, baseline)
)
alerts.extend(
self._check_ip_anomaly(key_prefix, day_records, baseline)
)
alerts.extend(
self._check_time_anomaly(key_prefix, day_records, baseline)
)
return sorted(alerts, key=lambda a: a.timestamp)
def _check_volume_anomaly(
self, key_prefix: str, day: str,
records: list[UsageRecord], baseline: dict,
) -> list[AnomalyAlert]:
"""Controleer op anomalieën in verzoekvolume."""
alerts = []
count = len(records)
avg = baseline["avg_daily_requests"]
std = baseline["std_daily_requests"]
if std > 0 and count > avg + 3 * std:
severity = "critical" if count > avg * 10 else "high"
alerts.append(AnomalyAlert(
alert_type="volume_spike",
severity=severity,
description=(
f"Key {key_prefix}: {count} requests on {day}, "
f"baseline avg={avg:.0f}, std={std:.0f}"
),
evidence={
"day": day,
"request_count": count,
"baseline_avg": avg,
"baseline_std": std,
"multiplier": round(count / max(avg, 1), 1),
},
timestamp=records[0].timestamp,
api_key_prefix=key_prefix,
))
return alerts
def _check_cost_anomaly(
self, key_prefix: str, day: str,
records: list[UsageRecord], baseline: dict,
) -> list[AnomalyAlert]:
"""Controleer op kostenanomalieën."""
alerts = []
total_cost = sum(r.cost_usd for r in records)
avg_cost = baseline["avg_daily_cost"]
if total_cost > max(avg_cost * 5, 10.0): # 5x basislijn of minimaal $10
alerts.append(AnomalyAlert(
alert_type="cost_spike",
severity="critical",
description=(
f"Key {key_prefix}: ${total_cost:.2f} on {day}, "
f"baseline avg=${avg_cost:.2f}"
),
evidence={
"day": day,
"total_cost": total_cost,
"baseline_avg_cost": avg_cost,
},
timestamp=records[0].timestamp,
api_key_prefix=key_prefix,
))
return alerts
def _check_model_anomaly(
self, key_prefix: str,
records: list[UsageRecord], baseline: dict,
) -> list[AnomalyAlert]:
"""Controleer op gebruik van onverwachte modellen."""
alerts = []
known = baseline.get("known_models", set())
for r in records:
if r.model not in known:
alerts.append(AnomalyAlert(
alert_type="unknown_model",
severity="high",
description=(
f"Key {key_prefix}: model '{r.model}' not in baseline. "
f"Known models: {known}"
),
evidence={
"model": r.model,
"known_models": list(known),
},
timestamp=r.timestamp,
api_key_prefix=key_prefix,
))
break # Eén alert per model is voldoende
return alerts
def _check_ip_anomaly(
self, key_prefix: str,
records: list[UsageRecord], baseline: dict,
) -> list[AnomalyAlert]:
"""Controleer op verzoeken van onbekende IP-adressen."""
alerts = []
known_ips = baseline.get("known_ips", set())
if not known_ips:
return alerts
unknown_ips = set()
for r in records:
if r.source_ip and r.source_ip not in known_ips:
unknown_ips.add(r.source_ip)
if unknown_ips:
alerts.append(AnomalyAlert(
alert_type="unknown_source_ip",
severity="high",
description=(
f"Key {key_prefix}: requests from {len(unknown_ips)} "
f"unknown IP(s): {list(unknown_ips)[:5]}"
),
evidence={
"unknown_ips": list(unknown_ips),
"known_ips": list(known_ips)[:10],
},
timestamp=records[0].timestamp,
api_key_prefix=key_prefix,
))
return alerts
def _check_time_anomaly(
self, key_prefix: str,
records: list[UsageRecord], baseline: dict,
) -> list[AnomalyAlert]:
"""Controleer op verzoeken buiten normale kantooruren."""
alerts = []
active_hours = baseline.get("active_hours", set())
if not active_hours:
return alerts
off_hours_count = 0
for r in records:
try:
hour = datetime.fromisoformat(r.timestamp).hour
if hour not in active_hours:
off_hours_count += 1
except (ValueError, TypeError):
continue
if off_hours_count > len(records) * 0.3: # >30% buiten kantooruren
alerts.append(AnomalyAlert(
alert_type="off_hours_usage",
severity="medium",
description=(
f"Key {key_prefix}: {off_hours_count}/{len(records)} "
f"requests outside normal hours"
),
evidence={
"off_hours_count": off_hours_count,
"total_records": len(records),
"active_hours": list(active_hours),
},
timestamp=records[0].timestamp,
api_key_prefix=key_prefix,
))
return alerts
@staticmethod
def _std(values: list[float]) -> float:
"""Bereken de standaarddeviatie."""
if len(values) < 2:
return 0.0
mean = sum(values) / len(values)
variance = sum((x - mean) ** 2 for x in values) / (len(values) - 1)
return variance ** 0.5Monitoring van factureringsdashboards
Naast monitoring op API-niveau bieden factureringsdashboards een secundair detectiesignaal. De meeste AI-providers bieden factureringsalerts, maar de standaarddrempels zijn vaak te hoog ingesteld om compromittering in een vroeg stadium af te vangen. Configureer factureringsalerts op niveaus die betekenisvol zijn voor je werkelijke gebruik: als je doorgaans $50/dag uitgeeft bij OpenAI, stel dan alerts in op $75, $150 en $500. De eerste drempel vangt langzame lekken af, de tweede vangt actief misbruik af en de derde vangt grootschalig misbruik af.
Scopebeoordeling
Bepalen wat is blootgesteld
Zodra een gecompromitteerde sleutel is geïdentificeerd, is de directe vraag: wat kan de aanvaller ermee doen? Het antwoord hangt af van de permissies van de sleutel en waar deze mee is verbonden.
@dataclass
class KeyScopeAssessment:
"""Beoordeling van waar een gecompromitteerde sleutel toegang toe heeft."""
key_prefix: str
provider: str
permissions: list[str]
connected_services: list[str]
data_exposure_risk: str # low, medium, high, critical
financial_exposure: str
findings: list[str]
def assess_openai_key_scope(
key_prefix: str,
org_settings: dict,
usage_history: list[dict],
) -> KeyScopeAssessment:
"""
Beoordeel de scope van een gecompromitteerde OpenAI-API-sleutel.
Args:
key_prefix: De sleutelidentifier (laatste 4 tekens).
org_settings: Organisatie-instellingen uit het OpenAI-dashboard.
usage_history: Historische gebruiksrecords.
Returns:
KeyScopeAssessment met volledige scope-analyse.
"""
permissions = []
findings = []
connected_services = []
data_risk = "low"
financial_risk = "low"
# Controleer welke modellen toegankelijk zijn
models_used = {r.get("model") for r in usage_history if r.get("model")}
permissions.append(f"model_access: {models_used}")
# Controleer op fine-tuning-toegang
fine_tune_records = [
r for r in usage_history
if "fine_tun" in r.get("endpoint", "").lower()
]
if fine_tune_records:
permissions.append("fine_tuning")
findings.append(
"CRITICAL: Key has fine-tuning access. Attacker could "
"create poisoned models in your organization."
)
data_risk = "critical"
# Controleer op toegang tot file-uploads
file_records = [
r for r in usage_history
if "/files" in r.get("endpoint", "")
]
if file_records:
permissions.append("file_upload")
findings.append(
"HIGH: Key has file API access. Attacker could upload "
"training data or access existing uploaded files."
)
data_risk = max(data_risk, "high", key=_risk_ord)
# Controleer op toegang tot assistants/threads (kan gespreksgeschiedenis bevatten)
assistant_records = [
r for r in usage_history
if "/assistants" in r.get("endpoint", "")
or "/threads" in r.get("endpoint", "")
]
if assistant_records:
permissions.append("assistants_api")
connected_services.append("assistants")
findings.append(
"HIGH: Key has Assistants API access. Attacker could "
"read existing thread histories containing user data."
)
data_risk = max(data_risk, "high", key=_risk_ord)
# Schat de financiële blootstelling in
monthly_limit = org_settings.get("monthly_spend_limit")
if monthly_limit:
financial_risk = (
"critical" if monthly_limit > 10000
else "high" if monthly_limit > 1000
else "medium"
)
findings.append(
f"Monthly spend limit: ${monthly_limit}. "
f"Attacker could consume up to this amount."
)
else:
financial_risk = "critical"
findings.append(
"WARNING: No monthly spend limit configured. "
"Attacker has unlimited financial exposure."
)
return KeyScopeAssessment(
key_prefix=key_prefix,
provider="openai",
permissions=permissions,
connected_services=connected_services,
data_exposure_risk=data_risk,
financial_exposure=financial_risk,
findings=findings,
)
def _risk_ord(level: str) -> int:
"""Converteer risiconiveau naar ordinaal voor vergelijking."""
return {"low": 0, "medium": 1, "high": 2, "critical": 3}.get(level, -1)Verbonden systemen beoordelen
Een gecompromitteerde API-sleutel is vaak gevaarlijker vanwege waar deze mee verbonden is dan vanwege de directe API-toegang die hij biedt. Breng de integratiepunten van de sleutel in kaart. Wordt deze gebruikt door een applicatie die toegang heeft tot een vectordatabase? Heeft de applicatie tool-use-capaciteiten die het model toegang geven tot interne systemen? Wordt de sleutel gedeeld over meerdere diensten?
Documenteer voor elk verbonden systeem welke data de aanvaller via de AI-applicatie zou kunnen benaderen, welke acties ze via toolgebruik zouden kunnen triggeren, en of er rate limits of toegangscontroles zijn buiten de API-sleutel zelf.
Forensische reconstructie van aanvalleractiviteit
De aanvalstijdlijn opbouwen
Met de gecompromitteerde sleutel geïdentificeerd en de scope ervan beoordeeld, is de volgende stap het reconstrueren van precies wat de aanvaller deed. Dit vereist het correleren van API-gebruikslogs, factureringsrecords en eventuele logs op applicatieniveau.
from enum import Enum
class AttackPhase(Enum):
RECONNAISSANCE = "reconnaissance"
CAPABILITY_TESTING = "capability_testing"
EXPLOITATION = "exploitation"
PERSISTENCE = "persistence"
EXFILTRATION = "exfiltration"
@dataclass
class AttackerAction:
"""Een gereconstrueerde aanvalleractie."""
timestamp: str
phase: AttackPhase
action: str
details: dict
confidence: float # 0-1
class APIKeyForensicReconstructor:
"""Reconstrueer aanvalleractiviteit uit API-gebruikslogs
na compromittering van een sleutel."""
def reconstruct_timeline(
self,
usage_records: list[UsageRecord],
compromise_start: str,
known_legitimate_ips: set[str],
) -> list[AttackerAction]:
"""
Reconstrueer de tijdlijn van aanvalleractiviteit.
Args:
usage_records: Alle gebruiksrecords voor de gecompromitteerde sleutel.
compromise_start: ISO-tijdstempel van het geschatte begin van de compromittering.
known_legitimate_ips: IP's waarvan bekend is dat ze legitiem zijn.
Returns:
Geordende lijst van gereconstrueerde aanvalleracties.
"""
start_dt = datetime.fromisoformat(compromise_start)
# Filter naar records na de compromittering en van onbekende IP's
suspicious = []
for r in usage_records:
try:
r_dt = datetime.fromisoformat(r.timestamp)
except (ValueError, TypeError):
continue
if r_dt < start_dt:
continue
if r.source_ip and r.source_ip in known_legitimate_ips:
continue
suspicious.append(r)
suspicious.sort(key=lambda r: r.timestamp)
actions = []
# Fasedetectie op basis van temporele patronen en verzoektypen
for i, record in enumerate(suspicious):
phase = self._classify_phase(record, i, suspicious)
action_desc = self._describe_action(record, phase)
actions.append(AttackerAction(
timestamp=record.timestamp,
phase=phase,
action=action_desc,
details={
"model": record.model,
"endpoint": record.endpoint,
"input_tokens": record.input_tokens,
"output_tokens": record.output_tokens,
"source_ip": record.source_ip,
"status_code": record.status_code,
"cost_usd": record.cost_usd,
},
confidence=0.7, # Standaard; verfijnd door context
))
return actions
def _classify_phase(
self,
record: UsageRecord,
index: int,
all_records: list[UsageRecord],
) -> AttackPhase:
"""Classificeer de aanvalsfase van een gegeven record."""
# De eerste paar verzoeken zijn doorgaans reconnaissance
if index < 3:
return AttackPhase.RECONNAISSANCE
# Kleine tokenaantallen wijzen op testen/probing
if record.input_tokens < 50 and record.output_tokens < 50:
return AttackPhase.CAPABILITY_TESTING
# Fine-tuning- of file-endpoints wijzen op persistentie
if any(
kw in record.endpoint.lower()
for kw in ["fine-tun", "files", "batch"]
):
return AttackPhase.PERSISTENCE
# Grote output-tokenaantallen kunnen wijzen op exfiltratie
if record.output_tokens > 4000:
return AttackPhase.EXFILTRATION
return AttackPhase.EXPLOITATION
def _describe_action(
self,
record: UsageRecord,
phase: AttackPhase,
) -> str:
"""Genereer een door mensen leesbare beschrijving van de actie."""
descriptions = {
AttackPhase.RECONNAISSANCE: (
f"Probed {record.endpoint} with model {record.model}"
),
AttackPhase.CAPABILITY_TESTING: (
f"Tested {record.model} capabilities via {record.endpoint} "
f"({record.input_tokens} in, {record.output_tokens} out)"
),
AttackPhase.EXPLOITATION: (
f"Used {record.model} via {record.endpoint} "
f"({record.input_tokens}+{record.output_tokens} tokens, "
f"${record.cost_usd:.4f})"
),
AttackPhase.PERSISTENCE: (
f"Accessed {record.endpoint}, possible persistence mechanism"
),
AttackPhase.EXFILTRATION: (
f"Large output ({record.output_tokens} tokens) from "
f"{record.model}, possible data exfiltration"
),
}
return descriptions.get(phase, f"Unknown action on {record.endpoint}")
def generate_forensic_report(
self,
actions: list[AttackerAction],
key_scope: KeyScopeAssessment,
) -> str:
"""Genereer een forensisch rapport uit gereconstrueerde acties."""
lines = [
"=" * 70,
"API KEY COMPROMISE - FORENSIC INVESTIGATION REPORT",
"=" * 70,
"",
f"Compromised Key: ...{key_scope.key_prefix}",
f"Provider: {key_scope.provider}",
f"Data Exposure Risk: {key_scope.data_exposure_risk}",
f"Financial Exposure: {key_scope.financial_exposure}",
"",
"SCOPE FINDINGS:",
]
for finding in key_scope.findings:
lines.append(f" - {finding}")
lines.extend([
"",
f"ATTACKER ACTIONS: {len(actions)} total",
"",
])
# Groepeer per fase
phase_groups: dict[AttackPhase, list[AttackerAction]] = defaultdict(list)
for action in actions:
phase_groups[action.phase].append(action)
for phase in AttackPhase:
phase_actions = phase_groups.get(phase, [])
if not phase_actions:
continue
lines.append(f"--- {phase.value.upper()} ({len(phase_actions)} actions) ---")
for a in phase_actions[:10]: # Toon de eerste 10 per fase
lines.append(f" [{a.timestamp}] {a.action}")
if len(phase_actions) > 10:
lines.append(f" ... and {len(phase_actions) - 10} more")
lines.append("")
# Financiële impact
total_cost = sum(
a.details.get("cost_usd", 0) for a in actions
)
total_tokens = sum(
a.details.get("input_tokens", 0) + a.details.get("output_tokens", 0)
for a in actions
)
lines.extend([
"FINANCIAL IMPACT:",
f" Total attacker cost: ${total_cost:.2f}",
f" Total tokens consumed: {total_tokens:,}",
])
return "\n".join(lines)Bronidentificatie: hoe is de sleutel gelekt?
Veelvoorkomende lekvectoren
Het identificeren van hoe de sleutel is gecompromitteerd is essentieel voor remediatie. De meest voorkomende lekvectoren voor AI-API-sleutels zijn:
Broncode-repository's: Sleutels gecommit naar publieke of onjuist beveiligde repository's. Tools zoals GitHub's secret scanning, truffleHog en GitLeaks kunnen de commit-geschiedenis doorzoeken. Onthoud dat zelfs als de sleutel in een latere commit wordt verwijderd, deze in de git-geschiedenis blijft staan.
Client-side blootstelling: Sleutels ingebed in frontend-JavaScript, mobiele applicaties of browserextensies. Deze zijn triviaal te extraheren door elke gebruiker.
Logs van de CI/CD-pijplijn: Sleutels afgedrukt in build-logs, testoutput of deploymentscripts. Veel CI-systemen bewaren logs wekenlang of maandenlang.
Lekken van omgevingsvariabelen: Debug-endpoints, foutpagina's of serverstatuspagina's die omgevingsvariabelen dumpen.
Compromittering van een derde partij: Als de sleutel is opgeslagen in een secrets manager, configuratiedienst of gedeelde credential store die zelf is gecompromitteerd.
import subprocess
from pathlib import Path
def scan_git_history_for_key(
repo_path: str,
key_pattern: str,
) -> list[dict]:
"""
Doorzoek de git-geschiedenis op voorkomens van een API-sleutelpatroon.
Args:
repo_path: Pad naar de git-repository.
key_pattern: Regex-patroon dat overeenkomt met het sleutelformaat
(bijv. 'sk-[a-zA-Z0-9]{48}' voor OpenAI-sleutels).
Returns:
Lijst van commits waarin de sleutel werd aangetroffen.
"""
results = []
try:
# Gebruik git log -p om door diffs te zoeken
cmd = [
"git", "-C", repo_path, "log", "--all", "-p",
f"--grep-reflog={key_pattern}",
"--format=%H|%an|%ae|%aI|%s",
]
# Fallback: gebruik git log met pickaxe-zoekopdracht
cmd = [
"git", "-C", repo_path, "log", "--all",
f"-S{key_pattern}", "--format=%H|%an|%ae|%aI|%s",
]
output = subprocess.run(
cmd, capture_output=True, text=True, timeout=120,
)
for line in output.stdout.strip().split("\n"):
if not line or "|" not in line:
continue
parts = line.split("|", 4)
if len(parts) >= 5:
results.append({
"commit_hash": parts[0],
"author_name": parts[1],
"author_email": parts[2],
"date": parts[3],
"message": parts[4],
})
except subprocess.TimeoutExpired:
results.append({"error": "Git search timed out"})
except FileNotFoundError:
results.append({"error": "Git not found or invalid repo path"})
return resultsOnderzoek naar blootstelling door derden
Controleer of de sleutel voorkomt in publieke databreach-dumps, pastesites of AI-specifieke kanalen voor sleutelhandel. Diensten zoals Have I Been Pwned (voor bijbehorende e-mailadressen) en gespecialiseerde dark web-monitoring kunnen aangeven of de sleutel publiekelijk is blootgesteld. Als de sleutel via GitHub's secret scanning-alerts in een publieke repository wordt aangetroffen, helpt het blootstellingstijdstempel om het begin van het compromitteringsvenster vast te stellen.
Remediatieprocedures
Onmiddellijke respons
De onmiddellijke respons op een bevestigde compromittering van een API-sleutel volgt deze volgorde:
-
Roteer de sleutel onmiddellijk. Genereer een nieuwe sleutel en werk alle legitieme consumers bij. Schakel de oude sleutel niet zomaar uit als je eerst kunt roteren, omdat je daardoor consumers kunt bijwerken zonder downtime.
-
Stel uitgavelimieten in of verlaag ze. Als de provider dit ondersteunt, stel dan een harde uitgavenlimiet in om verdere financiële schade te beperken terwijl je de rotatie voltooit.
-
Beoordeel recente API-activiteit. Gebruik de hierboven beschreven forensische reconstructietechnieken om te begrijpen wat de aanvaller met de sleutel deed.
-
Controleer op persistentie. Als de aanvaller fine-tuning-toegang had, beoordeel dan alle fine-tuning-jobs. Als ze file-toegang hadden, beoordeel dan geüploade bestanden. Als ze assistant-toegang hadden, beoordeel dan assistants en threads op wijzigingen.
-
Stel betrokken partijen op de hoogte. Als gebruikersdata mogelijk is blootgesteld via de gecompromitteerde sleutel (bijv. via toegang tot gespreksgeschiedenis of RAG-verbonden data), initieer dan je procedures voor melding van datalekken.
Een post-incident review uitvoeren
Nadat de onmiddellijke respons is voltooid, voer je een gestructureerde post-incident review uit die verder gaat dan de technische details van de compromittering. Documenteer de volledige tijdlijn van de initiële blootstelling van de sleutel via detectie tot indamming. Bereken de totale financiële impact, inclusief zowel het verbruik van de aanvaller als de kosten van het onderzoek en de remediatie-inspanning.
Evalueer je detectiecapaciteiten kritisch. Hoe lang was de sleutel gecompromitteerd vóór detectie (dwell time)? Welk signaal triggerde uiteindelijk het onderzoek? Had dat signaal eerder gedetecteerd kunnen worden met andere alertdrempels? Veel organisaties ontdekken dat hun dwell time bij compromittering van API-sleutels in weken of maanden wordt gemeten, omdat hun monitoring niet granulair genoeg was om de vroege stadia van misbruik af te vangen.
Beoordeel de levenscyclus van sleutelbeheer die tot de blootstelling leidde. Breng elk systeem en elke persoon in kaart die toegang tot de sleutel had. Identificeer waar de sleutel was opgeslagen, hoe deze werd gedistribueerd en welke controls op elk punt aanwezig waren. De grondoorzaak is zelden "de sleutel werd gelekt" — het is "de sleutel bevond zich op een locatie waar deze gelekt kon worden, omdat ons sleutelbeheerproces dat toeliet".
Documenteer specifieke indicatoren van compromittering (IOC's) uit het onderzoek: de IP-adressen, user agents, verzoekpatronen en timingkenmerken van de aanvaller. Deel deze IOC's met je security operations-team zodat zij detecties kunnen bouwen die soortgelijke activiteit in de toekomst afvangen, en overweeg ze te delen met het abuse-team van je AI-provider.
Provider-specifieke onderzoeksbronnen
Elke grote AI-API-provider biedt andere tools voor het onderzoeken van gecompromitteerde sleutels. OpenAI biedt een gebruiksdashboard met uitsplitsingen per sleutel en een API voor programmatische gebruiksquery's. Het organisatie-auditlog legt aanmaak, wijziging en verwijdering van sleutels vast. De console van Anthropic biedt gebruikstracking per API-sleutel met kostenuitsplitsingen. Google Cloud's AI Platform integreert met Cloud Audit Logs voor gedetailleerde tracking op verzoekniveau. Voor Azure OpenAI bieden diagnostische logs in Azure Monitor het meest gedetailleerde overzicht, inclusief volledige verzoek- en responsinhoud wanneer ingeschakeld.
Geef bij het werken met support-teams van providers tijdens een onderzoek de identifier van de gecompromitteerde sleutel, het vermoedelijke compromitteringsvenster en eventuele door jou geïdentificeerde IP-adressen van de aanvaller. De meeste providers kunnen aanvullende forensische data van hun kant leveren, inclusief details op verzoekniveau die mogelijk niet beschikbaar zijn via je eigen logging.
Langetermijn-hardening
Implementeer na de oplossing van het onmiddellijke incident controls om herhaling te voorkomen:
- Gebruik scoped API-sleutels met de minimaal noodzakelijke permissies. Als je applicatie alleen chat completions nodig heeft, geef deze dan geen file- of fine-tuning-toegang.
- Implementeer rotatie van API-sleutels volgens een regelmatig schema (minimaal per kwartaal).
- Gebruik secrets managers (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) in plaats van omgevingsvariabelen of configuratiebestanden.
- Zet pre-commit hooks in die scannen op API-sleutelpatronen voordat code wordt gecommit.
- Configureer factureringsalerts op betekenisvolle drempels.
- Implementeer IP-allowlisting waar de provider dit ondersteunt.
- Monitor op gelekte sleutels met geautomatiseerd scannen van je repository's en monitoring van blootstelling door derden.
Proactieve beveiligingsmonitoring van API-sleutels
Architectuur voor continue monitoring
Een robuust beveiligingsprogramma voor API-sleutels vereist continue monitoring in plaats van reactief onderzoek. Zet een geautomatiseerde monitoringpijplijn in die API-gebruiksdata in vrijwel-realtime opneemt, de hierboven beschreven anomaliedetectie-algoritmen toepast en alerts naar je security operations-team routeert.
De monitoringpijplijn moet gebruiksrecords binnen enkele minuten na hun generatie verwerken. De meeste AI-API-providers bieden gebruiksdata met een vertraging van 5-30 minuten. Configureer de pijplijn om gebruiksdata in dit tempo op te halen, de basislijnvergelijking toe te passen en alerts te genereren voor anomalieën die je geconfigureerde drempels overschrijden. Voer de alerts in je bestaande SIEM- of beveiligingsalert-infrastructuur in, zodat ze samen met andere beveiligingsgebeurtenissen worden getrieerd.
Roteer de basislijnen die worden gebruikt voor anomaliedetectie regelmatig. Naarmate het gebruik van je applicatie groeit en verandert, moet de basislijn meeveranderen om false positives door legitieme groei en false negatives door geleidelijk misbruik te voorkomen. Herbereken basislijnen wekelijks met de meest recente 30 dagen aan data, met uitsluiting van dagen waarop bevestigde incidenten plaatsvonden.
Referenties
- Lasso Security (2023). "Thousands of API keys and credentials found in public LLM training data." https://www.lasso.security/blog/thousands-of-api-keys-found-in-llm-training-data
- OWASP Foundation (2025). "OWASP Top 10 for LLM Applications - LLM06: Sensitive Information Disclosure." https://owasp.org/www-project-top-10-for-large-language-model-applications/
- Truffle Security (2024). "TruffleHog: Find and verify credentials in git repositories." https://github.com/trufflesecurity/trufflehog