Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
192 lines (178 loc) · 11.8 KB
/
Copy pathindex.html
File metadata and controls
192 lines (178 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secrets That Even Your Own Code Can't Steal</title>
<link rel="stylesheet" href="../vendor/reveal/reset.css">
<link rel="stylesheet" href="../vendor/fonts/fonts.css">
<link rel="stylesheet" href="../vendor/reveal/reveal.css">
<link rel="stylesheet" href="../vendor/reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="../shared/theme.css">
</head>
<body>
<div class="reveal">
<img class="brand-mark" src="../assets/png/last9-icon-secondary.png" alt="Last9">
<div class="deck-foot"><span><b>Last9</b> · compartmentalizing secrets</span></div>
<div class="slides">
<!-- TITLE -->
<section class="title" data-background-color="#0e0f13">
<div class="kicker">Security & cryptography</div>
<h1>Secrets that even<br>your own code<br><span style="color:var(--lime)">can't steal</span></h1>
<p class="sub">Compartmentalizing secrets in multi-tenant agent systems.</p>
<p class="byline">Shekhar Patil · Last9</p>
</section>
<!-- MANDATORY · what is an agent -->
<section data-background-color="#0e0f13">
<div class="kicker">Required baseline</div>
<h2>What <em>is</em> an agent?</h2>
<p class="dim" style="font-size:.74em;margin-bottom:.4em">An <strong style="color:var(--ink)">LLM in a loop</strong> that takes <strong style="color:var(--ink)">actions</strong>, keeps <strong style="color:var(--ink)">state</strong>, and acts in the world, not a chatbot, not RPA.</p>
<svg class="diagram" viewBox="0 0 1120 312" fill="none" style="max-height:218px;margin:0 auto">
<defs>
<marker id="a" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7.5" markerHeight="7.5" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#6b6b78"/></marker>
<marker id="ad" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7.5" markerHeight="7.5" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#ff6b5e"/></marker>
<marker id="as" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7.5" markerHeight="7.5" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#c4f000"/></marker>
</defs>
<rect x="28" y="104" width="216" height="92" rx="14" fill="#1c1c22" stroke="#33333c" stroke-width="2"/>
<text x="136" y="146" fill="#fafafa" font-size="22" font-weight="600" text-anchor="middle">user · event</text>
<text x="136" y="174" fill="#a3a3ad" font-size="14" text-anchor="middle">slack · cron · webhook</text>
<circle cx="560" cy="150" r="82" fill="#14160a" stroke="#c4f000" stroke-width="2.5"/>
<text x="560" y="145" fill="#c4f000" font-size="34" font-weight="700" text-anchor="middle">LLM</text>
<text x="560" y="173" fill="#a3a3ad" font-size="15" text-anchor="middle">reason → decide</text>
<rect x="900" y="100" width="200" height="100" rx="14" fill="#2a140f" stroke="#ff6b5e" stroke-width="2.5"/>
<text x="1000" y="138" fill="#ff6b5e" font-size="20" font-weight="700" text-anchor="middle">TOOL CALL</text>
<text x="1000" y="164" fill="#fafafa" font-size="14" text-anchor="middle">shell · cloud API</text>
<text x="1000" y="186" fill="#fafafa" font-size="14" text-anchor="middle">db · message · $$</text>
<ellipse cx="560" cy="298" rx="66" ry="14" fill="#1c1c22" stroke="#33333c" stroke-width="2"/>
<path d="M494 298 v-50 a66 14 0 0 0 132 0 v50" fill="#1c1c22" stroke="#33333c" stroke-width="2"/>
<ellipse cx="560" cy="248" rx="66" ry="14" fill="#16161b" stroke="#33333c" stroke-width="2"/>
<text x="560" y="278" fill="#a3a3ad" font-size="14" text-anchor="middle">memory · state</text>
<path d="M560 246 V 240" stroke="#6b6b78" stroke-width="2" marker-end="url(#a)"/>
<path d="M248 150 H 472" stroke="#6b6b78" stroke-width="2.5" marker-end="url(#a)"/>
<text x="360" y="136" fill="#a3a3ad" font-size="14" text-anchor="middle">prompt + state</text>
<path d="M648 134 H 718" stroke="#ff6b5e" stroke-width="3" marker-end="url(#ad)"/>
<text x="683" y="120" fill="#ff6b5e" font-size="12" font-weight="600" text-anchor="middle">proposes</text>
<rect x="722" y="115" width="98" height="38" rx="10" fill="#14160a" stroke="#c4f000" stroke-width="2"/>
<text x="771" y="139" fill="#c4f000" font-size="13" font-weight="600" text-anchor="middle">approve?</text>
<path d="M824 134 H 896" stroke="#ff6b5e" stroke-width="3" marker-end="url(#ad)"/>
<text x="860" y="120" fill="#ff6b5e" font-size="12" font-weight="600" text-anchor="middle">execute</text>
<path d="M900 178 C 800 234, 662 222, 642 170" stroke="#c4f000" stroke-width="2.5" marker-end="url(#as)"/>
<text x="778" y="240" fill="#c4f000" font-size="14" text-anchor="middle">result</text>
<path d="M514 66 C 534 32, 586 32, 606 66" stroke="#6b6b78" stroke-width="2" stroke-dasharray="4 5" marker-end="url(#a)"/>
<text x="560" y="26" fill="#6b6b78" font-size="13" text-anchor="middle">repeat</text>
</svg>
<div class="row" style="gap:.7em;background:var(--danger-tint);border:1px solid var(--danger);border-radius:12px;padding:.45em .8em;margin-top:.3em">
<span class="chip danger">it holds the keys</span>
<span style="margin:0;font-size:.95em">That loop holds <strong>real credentials</strong>: AWS keys, Slack & Linear tokens.</span>
</div>
</section>
<!-- THESIS -->
<section class="divider" data-background-color="#0b0c10">
<div class="actno">THE ONE IDEA</div>
<p class="thesis">Isolation must be <span class="struct">structural</span>, not <span class="conv">conventional</span>.</p>
<p class="lead dim" style="max-width:28ch;margin-top:.5em">For secrets: bind access in the
<em>key and the constructor</em>, not in a query you have to remember to write.</p>
</section>
<!-- ATTACK -->
<section class="attack" data-background-color="#0e0f13">
<div class="kicker">The nightmare</div>
<h2>Agent A reads Agent B's API key.</h2>
<p class="lead">engineer9 for Acme and the instance for Globex share a process pool and one
Postgres.</p>
<p>If reading a neighbor's secret is one forgotten <span class="id">WHERE</span> clause away,
it <em>will</em> happen.</p>
</section>
<!-- conventional -->
<section class="attack" data-background-color="#0e0f13">
<div class="kicker">The usual answer</div>
<h2>"Just scope every query."</h2>
<pre><code class="language-sql" data-trim data-noescape>
SELECT value FROM secrets WHERE tenant_id = $1 -- hope nobody forgets this
</code></pre>
<p>That's <em>convention</em>: correctness rides on every developer, every query, forever.
One missing predicate and the wall is gone.</p>
<p class="hand-note" style="font-size:var(--step-0)">a rule you must remember is a rule you will eventually forget.</p>
</section>
<!-- DEFENSE 1 -->
<section class="defense" data-background-color="#0e0f13">
<div class="kicker">Structural control · bound at construction</div>
<h3>You can't <em>name</em> another agent's secret.</h3>
<pre><code class="language-go" data-trim data-noescape>
// Cross-agent access is structurally impossible: workspace_id and
// agent_id are bound at construction; the key is derived from them.
func NewPGStore(pool, masterKey, workspaceID, agentID) (Store, error)
type Store interface { // no method takes another agent's id
Get(ctx, name) (string, error)
GetMasked(ctx, name) (string, error)
}
</code></pre>
<span class="verdict">✓ contained, by the constructor, not by discipline</span>
</section>
<!-- DEFENSE 2 -->
<section class="defense" data-background-color="#0e0f13">
<div class="kicker">Defense in depth · encrypted at rest</div>
<h3>A unique key <em>per credential</em>, derived, never stored.</h3>
<pre><code class="language-go" data-trim data-noescape>
// Argon2id: time=1, memory=64MB, threads=4, keyLen=32 (AES-256)
key := argon2.IDKey([]byte(masterKey), salt, 1, 64*1024, 4, 32)
// stored blob: salt(32) || nonce(12) || AES-256-GCM(ciphertext)
</code></pre>
<div class="row"><span class="chip safe">random salt / credential</span>
<span class="chip safe">random nonce / write</span>
<span class="chip">master key never logged</span></div>
</section>
<!-- UX -->
<section data-background-color="#0e0f13">
<div class="kicker">The ergonomics that keep it safe</div>
<h2>Make the safe path the easy path.</h2>
<div class="cols">
<div class="card safe"><h3>Masked reads</h3>
<p><span class="id">GetMasked</span> returns <span class="id">AKIAIOSF...MPLE</span>
(first 8 + last 4). UIs never touch plaintext; values ≤ 12 chars become <span class="id">****</span>.</p></div>
<div class="card safe"><h3>Return-secret-once</h3>
<p>Webhook secrets come back only on creation. Later reads return empty: lose it, rotate it.
No "GET my secret" endpoint to leak.</p></div>
</div>
</section>
<!-- the snapshot problem (research) -->
<section data-background-color="#0e0f13">
<div class="kicker">The newer exposure · credentials in the snapshot</div>
<h2>Fast restore writes your token to disk.</h2>
<div class="figure">
<img src="../assets/research/credentials-in-the-snapshot.svg" alt="Credentials in the snapshot" style="max-height:340px">
</div>
<p class="figcap">One token, <strong style="color:var(--danger)">65</strong> plaintext copies in a 256MB memfile.
SEV-SNP hides RAM from the host, but not from the agent's own code.</p>
</section>
<!-- DEMO -->
<section class="divider" data-background-color="#0b0c10">
<div class="actno">DEMO</div>
<h2>Two agents. One tries to steal.</h2>
<p class="lead dim" style="max-width:32ch">From Acme's store, reach for Globex's token. There's no
API for it, and the bytes on disk are gibberish without Globex's derived key.</p>
</section>
<!-- limits + close -->
<section data-background-color="#0e0f13">
<div class="kicker">Honest limits</div>
<h2>What this does <em>not</em> solve</h2>
<div class="cols">
<div class="card danger"><h3>master-key compromise</h3><p>One master key derives every per-credential key. Guard it like crown jewels (KMS / Vault).</p></div>
<div class="card danger"><h3>no rotation yet</h3><p>No mechanism to re-encrypt all credentials under a new master key. On the roadmap.</p></div>
</div>
<p class="lead" style="margin-top:.5em">The zero-trust maturity path: <strong>short-lived tokens, hardware-bound, reference-not-value.</strong>
Anthropic's line: <span class="dim">"rotating a credential you can grep out of a lockfile doesn't raise the cost."</span></p>
<p style="margin-top:.2em">Still, the headline holds: leaking a neighbor's secret isn't a bug to avoid.
<strong>It's an operation that doesn't exist.</strong></p>
</section>
</div>
</div>
<script src="../vendor/reveal/reveal.js"></script>
<script src="../vendor/reveal/plugin/highlight/highlight.js"></script>
<script src="../vendor/reveal/plugin/notes/notes.js"></script>
<script>
Reveal.initialize({ hash:true, slideNumber:'c/t', transition:'fade', transitionSpeed:'fast',
width:1280, height:720, margin:0.04, center:false, controlsTutorial:false,
plugins:[ RevealHighlight, RevealNotes ] });
</script>
</body>
</html>