-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity.html
More file actions
395 lines (348 loc) · 18.1 KB
/
Copy pathsecurity.html
File metadata and controls
395 lines (348 loc) · 18.1 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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
---
layout: default
title: "Security Model"
description: "Sandlock's threat model: trust boundaries, what each enforcement layer guarantees, how TOCTOU is handled, default-deny posture, and the limits of the sandbox."
permalink: /security.html
---
<style>
.boundary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-5);
margin-top: var(--space-10);
}
.boundary {
padding: var(--space-6);
background: white;
border: 1px solid var(--gray-200);
border-top: 2px solid var(--accent-500);
border-radius: var(--radius-lg);
}
.boundary .tier {
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--gray-400);
display: block;
margin-bottom: var(--space-2);
}
.boundary h3 {
font-size: var(--text-base);
font-weight: 700;
color: var(--gray-900);
margin-bottom: var(--space-3);
}
.boundary p {
font-size: var(--text-sm);
line-height: var(--leading-relaxed);
color: var(--gray-600);
}
.claims {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-5);
margin-top: var(--space-8);
}
.claim-col {
padding: var(--space-8);
border-radius: var(--radius-lg);
border: 1px solid var(--gray-200);
background: white;
}
.claim-col.does {
border-left: 3px solid var(--success);
}
.claim-col.does-not {
border-left: 3px solid var(--error);
}
.claim-col h3 {
font-size: var(--text-lg);
font-weight: 700;
color: var(--gray-900);
margin-bottom: var(--space-5);
display: flex;
align-items: center;
gap: var(--space-2);
}
.claim-col.does h3 i {
color: var(--success);
width: 18px;
height: 18px;
}
.claim-col.does-not h3 i {
color: var(--error);
width: 18px;
height: 18px;
}
.claim-col ul {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.claim-col li {
font-size: var(--text-sm);
line-height: var(--leading-relaxed);
color: var(--gray-600);
padding-left: var(--space-5);
position: relative;
}
.claim-col li::before {
content: '';
position: absolute;
left: 0;
top: 0.55em;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--gray-300);
}
.claim-col.does li::before {
background: var(--success);
}
.claim-col.does-not li::before {
background: var(--error);
}
.claim-col li strong {
color: var(--gray-900);
font-weight: 600;
}
.prose-block {
max-width: 76ch;
}
.prose-block h3 {
font-size: var(--text-lg);
font-weight: 700;
color: var(--gray-900);
margin-top: var(--space-8);
margin-bottom: var(--space-3);
}
.prose-block p {
font-size: var(--text-base);
line-height: var(--leading-relaxed);
color: var(--gray-600);
margin-bottom: var(--space-4);
}
.prop-list {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-4);
margin: var(--space-6) 0;
}
.prop-list li {
font-size: var(--text-sm);
line-height: var(--leading-relaxed);
color: var(--gray-600);
padding-left: var(--space-5);
border-left: 2px solid var(--accent-500);
}
.prop-list li strong {
color: var(--gray-900);
font-weight: 600;
}
.prose-block strong {
color: var(--gray-900);
font-weight: 600;
}
@media (max-width: 900px) {
.boundary-grid,
.claims {
grid-template-columns: 1fr;
}
}
</style>
<section class="hero hero-page">
<div class="hero-container">
<div class="hero-badge">
<i data-lucide="shield-check"></i>
<span>Security model</span>
</div>
<h1>What Sandlock Guarantees, and What It Does Not</h1>
<p class="subtitle">A sandbox is only useful if you know its edges. This page states the trust boundaries, the guarantees each layer provides, and the attacks that are explicitly out of scope.</p>
</div>
</section>
<section class="section">
<div class="section-container">
<div class="section-header">
<div class="section-badge">
<i data-lucide="layers"></i>
<span>Trust boundaries</span>
</div>
<h2 class="section-title">Three Tiers of Trust</h2>
<p class="section-subtitle">Sandlock's design principle is that the amount of trust placed in userspace should be as small as the policy allows.</p>
</div>
<div class="boundary-grid">
<div class="boundary">
<span class="tier">Fully trusted</span>
<h3>The host kernel</h3>
<p>Landlock rules and the seccomp-bpf filter are evaluated by the kernel. If the kernel is compromised, so is every guarantee on this page. Sandlock shares a kernel with its workload, and that is the fundamental limit of the model.</p>
</div>
<div class="boundary">
<span class="tier">Partially trusted</span>
<h3>The supervisor</h3>
<p>Runs in the parent process, outside the sandbox. It decides on the syscalls the kernel hands it: destination IPs, resource accounting, COW writes. Its handler chain is fixed, with built-ins first, so a custom handler can extend confinement but never relax it.</p>
</div>
<div class="boundary">
<span class="tier">Untrusted</span>
<h3>The workload</h3>
<p>Assumed hostile. It never executes an unconfined instruction: <code class="inline">NO_NEW_PRIVS</code>, Landlock, and the seccomp filter are all installed before <code class="inline">exec</code>, and inherited descriptors above stderr are closed first.</p>
</div>
</div>
</div>
</section>
<section class="section section-alt">
<div class="section-container">
<div class="section-header">
<div class="section-badge">
<i data-lucide="check-check"></i>
<span>Scope</span>
</div>
<h2 class="section-title">In Scope and Out of Scope</h2>
</div>
<div class="claims">
<div class="claim-col does">
<h3><i data-lucide="shield-check"></i>Sandlock defends against</h3>
<ul>
<li><strong>Filesystem escape.</strong> Only paths reachable through the granted Landlock rules can be opened. Grants are recursive and denials override them.</li>
<li><strong>Unapproved network egress.</strong> Default-deny. With no rules Landlock refuses every TCP connect, and UDP, ICMP, and raw socket creation are refused at the seccomp layer.</li>
<li><strong>Exfiltration on an approved host.</strong> HTTP rules match method, host, and path, so an agent allowed one endpoint cannot repurpose the connection.</li>
<li><strong>Credential theft by the workload.</strong> The secret stays in the supervisor and is attached after the ACL check; an <code class="inline">env:</code> source is stripped from the child.</li>
<li><strong>Privilege escalation via setuid.</strong> <code class="inline">NO_NEW_PRIVS</code> is set before the filter, so a setuid binary confers nothing.</li>
<li><strong>Reaching sibling processes.</strong> Landlock ABI v6 scopes deny abstract UNIX socket connections and signals outside the sandbox.</li>
<li><strong>Host resource exhaustion.</strong> Memory, concurrent processes, open files, CPU share, and COW disk usage are all capped.</li>
<li><strong>Unintended writes.</strong> Copy-on-write stages writes and discards them on error, so a failed run leaves the tree untouched.</li>
</ul>
</div>
<div class="claim-col does-not">
<h3><i data-lucide="shield-off"></i>Sandlock does not defend against</h3>
<ul>
<li><strong>Kernel vulnerabilities.</strong> The workload runs on your kernel. An escalation bug in a permitted syscall defeats the sandbox. This is the price of no hypervisor.</li>
<li><strong>Hardware side channels.</strong> Spectre-class attacks and cache timing are out of scope. CPU pinning reduces sharing but is not a mitigation.</li>
<li><strong>A policy that grants too much.</strong> <code class="inline">--net-allow '*'</code> permits any destination. Sandlock enforces the policy you wrote, not the one you meant.</li>
<li><strong>A hostile launcher.</strong> An attacker who already controls the process that starts Sandlock controls the policy.</li>
<li><strong>The workload starving itself.</strong> Limits protect the host, not the workload's own progress. A sandbox can still spin or wedge inside its budget.</li>
<li><strong>Open-file limits as a boundary.</strong> <code class="inline">RLIMIT_NOFILE</code> can be raised again by a sandbox launched with the privilege to do so. Treat it as a budget.</li>
<li><strong>Covert channels between sandboxes.</strong> Two sandboxes on one host share timing, disk, and memory bandwidth.</li>
<li><strong>Packet crafting.</strong> Raw ICMP sockets are never exposed. A deliberate reduction in capability, not an oversight.</li>
</ul>
</div>
</div>
</div>
</section>
<section class="section">
<div class="section-container narrow">
<div class="section-header left">
<div class="section-badge">
<i data-lucide="clock-alert"></i>
<span>Design detail</span>
</div>
<h2 class="section-title">Time-of-Check to Time-of-Use</h2>
</div>
<div class="prose-block">
<p>Seccomp user notification has a well-known hazard, documented in <code class="inline">seccomp_unotify(2)</code>: after the supervisor returns a continue verdict, <strong>the kernel re-reads any argument the syscall passed by pointer</strong>. A supervisor that inspects a path string and then allows the call has validated a string the workload is free to overwrite in the interval. Filtering on path strings in userspace is therefore unsound, no matter how carefully it is written.</p>
<p>Sandlock addresses this in two ways rather than papering over it.</p>
<h3>Path strings are never exposed to policy callbacks</h3>
<p>Policy events carry the syscall name, category, PID, network destination, and argv, but never a path. Path-based access control belongs in static Landlock rules, where the kernel resolves the path itself at access time and no window exists. For decisions that genuinely have to be made at runtime, the policy context offers <code class="inline">deny_path()</code> and <code class="inline">allow_path()</code>, which add Landlock-side restrictions rather than filtering strings.</p>
<h3>argv is exposed, and it is made safe first</h3>
<p>Command lines are too useful to withhold, so Sandlock makes them safe instead. Before exposing <code class="inline">argv</code> for an <code class="inline">execve</code>, the supervisor <strong>freezes every task in its process index</strong>, including peer processes that could alias the argv memory through a shared mapping. While a policy callback is active, fork-like syscalls are traced for one creation event so that children are registered before they can run user code.</p>
<p>If the freeze or the creation tracking cannot be established, for instance because a YAMA policy blocks ptrace, <strong>the syscall is denied with <code class="inline">EPERM</code></strong>. The safety invariant is never silently relaxed in favour of letting the workload proceed.</p>
</div>
</div>
</section>
<section class="section section-alt">
<div class="section-container narrow">
<div class="section-header left">
<div class="section-badge">
<i data-lucide="toggle-left"></i>
<span>Defaults</span>
</div>
<h2 class="section-title">Strict by Default, Explicit to Weaken</h2>
</div>
<div class="prose-block">
<p>Sandlock starts from deny and requires you to open things up. This matters because the failure mode of a permissive default is silent: nothing breaks, and you learn about the gap from an incident rather than from an error.</p>
</div>
<div class="data-table-wrap" style="margin-top: var(--space-6);">
<table class="data-table">
<thead>
<tr>
<th scope="col">Surface</th>
<th scope="col">With no policy</th>
<th scope="col">To open it up</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Filesystem</th>
<td>Nothing readable or writable. No implicit grant for <code>/usr</code> or <code>/lib</code>.</td>
<td>Name every path, which is why every example on this site does.</td>
</tr>
<tr>
<th scope="row">Network</th>
<td>Landlock denies every TCP connect; UDP, ICMP, and raw socket <em>creation</em> are denied at the seccomp layer.</td>
<td><code>--net-allow '*'</code> explicitly. ICMP still needs its own <code>icmp://</code> rule on top.</td>
</tr>
<tr>
<th scope="row">Syscalls</th>
<td>The default blocklist applies unconditionally and cannot be turned off.</td>
<td>Named groups only, such as System V IPC. An unknown group name is rejected, so a typo fails loudly.</td>
</tr>
<tr>
<th scope="row">Kernel protections</th>
<td>Every protection the host supports is enforced, and Sandlock <strong>refuses to start</strong> when one it expects is missing.</td>
<td>Per protection, explicitly: <code>allow_degraded</code> skips it where the kernel lacks it, <code>disable</code> turns it off outright.</td>
</tr>
</tbody>
</table>
</div>
<p class="table-note">Protocol availability follows rule presence: with no UDP rule, UDP sockets cannot be created at all. A sandbox's resolved protection posture is part of its checkpoint, so a restored sandbox returns with the protections it was built with rather than whatever the new host offers. <code>sandlock check</code> reports the host's Landlock ABI before you deploy.</p>
</div>
</section>
<section class="section">
<div class="section-container narrow">
<div class="section-header left">
<div class="section-badge">
<i data-lucide="key-round"></i>
<span>Secrets</span>
</div>
<h2 class="section-title">Credential Handling</h2>
</div>
<div class="prose-block">
<p>A workload can use a secret without ever holding it. The value is loaded into the supervisor from an environment variable, a file, or a file descriptor, and attached to a matching request inside the proxy <strong>strictly after the ACL check has passed</strong>, so a request the policy would reject never carries it.</p>
<ul class="prop-list">
<li><strong>The child never carries the value.</strong> An <code class="inline">env:</code> source is stripped from the child's environment, so a compromised agent cannot read its own key back. An <code class="inline">fd:</code> source never touches disk.</li>
<li><strong>Cleartext HTTP warns rather than failing silently.</strong> Attaching a credential over plain HTTP sends it upstream in the clear, so Sandlock emits a one-per-run warning instead of treating the transport as safe.</li>
<li><strong>The MITM CA's private key stays in memory.</strong> <code class="inline">--http-inject-ca</code> generates an ephemeral CA, writes only the public certificate into the trust bundles you name, and never persists the key.</li>
</ul>
<p>Sandlock ships no secret-manager client, deliberately. An external fetcher materializes the value into a <code class="inline">file:</code> or <code class="inline">fd:</code> source, keeping it off <code class="inline">ps</code>, out of shell history, and out of the child's environment, and letting you use Vault, a cloud secret store, or a CSI driver without Sandlock having an opinion.</p>
</div>
</div>
</section>
<section class="section section-dark">
<div class="section-container narrow">
<div class="section-header">
<div class="section-badge">
<i data-lucide="bug"></i>
<span>Disclosure</span>
</div>
<h2 class="section-title">Reporting a Vulnerability</h2>
<p class="section-subtitle">If you believe you have found a way to escape a Sandlock policy, we want to hear about it before anyone else does.</p>
</div>
<div class="text-center">
<p style="color: rgba(255,255,255,0.6); font-size: var(--text-base); line-height: var(--leading-relaxed); max-width: 60ch; margin: 0 auto var(--space-8);">
Email <a href="mailto:contact@multikernel.io" style="color: var(--accent-400)">contact@multikernel.io</a> with a description and, where possible, a reproducer. Please do not open a public issue for a suspected escape until we have had a chance to respond.
</p>
<div class="button-group">
<a href="mailto:contact@multikernel.io" class="btn btn-primary">
<i data-lucide="mail"></i>
Report Privately
</a>
<a href="https://github.com/multikernel/sandlock/issues" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
<i data-lucide="github"></i>
Non-Security Issues
</a>
</div>
</div>
</div>
</section>