Skip to content

Commit 674dda5

Browse files
JAORMXdmjb
andauthored
Add .thvignore proposal for secure bind mount filtering (#1055)
Co-authored-by: Don Browne <[email protected]>
1 parent 5f36e34 commit 674dda5

File tree

1 file changed

+346
-0
lines changed

1 file changed

+346
-0
lines changed

docs/proposals/thvignore.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# **🧱 Technical Design Proposal: `.thvignore`\-Driven Bind Mount Filtering in ToolHive**
2+
3+
---
4+
5+
## **🎯 Goals**
6+
7+
| Objective | Solution |
8+
| ----- | ----- |
9+
| Exclude secrets (e.g., `.ssh`, `.env`) from containers while using bind mounts | Use `.thvignore` to drive tmpfs overlays |
10+
| Maintain real-time access to files like SQLite DBs | Bind mount full directory |
11+
| Support both global ignore patterns (e.g., user-wide) and per-project rules | Combine global and local `.thvignore` |
12+
| Provide a consistent, secure experience across all runtimes | Abstract runtime-specific mount behavior in ToolHive's execution layer |
13+
14+
---
15+
16+
## **🗂 Config File Design**
17+
18+
### **🧭 Per-folder config: `.thvignore`**
19+
20+
Lives **next to the files being mounted**.
21+
22+
```shell
23+
my-folder/
24+
├── database.db
25+
├── .ssh/
26+
└── .thvignore
27+
```
28+
29+
`.thvignore`:
30+
31+
```
32+
.ssh/
33+
*.bak
34+
.env
35+
```
36+
37+
### **🌍 Global config: `~/.config/toolhive/thvignore`**
38+
39+
Example:
40+
41+
```
42+
node_modules/
43+
.DS_Store
44+
.idea/
45+
```
46+
47+
These patterns apply to **all** mounts unless explicitly disabled.
48+
49+
---
50+
51+
## **🧠 Behavior Overview**
52+
53+
### **✅ At runtime:**
54+
55+
1. User runs:
56+
57+
```shell
58+
thv run --volume ./my-folder:/app server-name
59+
```
60+
61+
2.
62+
ToolHive does the following:
63+
64+
* Load global ignore file from `~/.config/toolhive/thvignore`
65+
66+
* Load `./my-folder/.thvignore` (if present)
67+
68+
* Combine and normalize both sets of patterns
69+
70+
* For each pattern:
71+
72+
* Determine full container path (e.g. `/app/.ssh`)
73+
74+
* Add a `tmpfs` mount over it to the runtime configuration
75+
76+
---
77+
78+
## **🧱 Component Design**
79+
80+
### **🔹 `IgnoreProcessor` (new module in `pkg/ignore/`)**
81+
82+
```go
83+
package ignore
84+
85+
type IgnoreProcessor struct {
86+
GlobalPatterns []string
87+
LocalPatterns []string
88+
}
89+
90+
func NewIgnoreProcessor() *IgnoreProcessor
91+
func (ip *IgnoreProcessor) LoadGlobal() error
92+
func (ip *IgnoreProcessor) LoadLocal(sourceDir string) error
93+
func (ip *IgnoreProcessor) GetOverlayPaths(bindMount, containerPath string) []string
94+
func (ip *IgnoreProcessor) ShouldIgnore(path string) bool
95+
```
96+
97+
*
98+
Reads `.gitignore`\-style files using existing Go libraries
99+
100+
* Integrates with ToolHive's existing mount processing pipeline
101+
102+
* Converts ignore patterns into **container absolute paths** (e.g. `/app/.ssh`)
103+
104+
---
105+
106+
### **🔹 Enhanced `runtime.Mount` (in `pkg/container/runtime/types.go`)**
107+
108+
Extend the existing Mount struct to support tmpfs:
109+
110+
```go
111+
type Mount struct {
112+
Source string
113+
Target string
114+
ReadOnly bool
115+
Type string // NEW: "bind" or "tmpfs"
116+
}
117+
```
118+
119+
Integration with existing mount processing in `pkg/runner/config_builder.go`:
120+
121+
```go
122+
func (b *RunConfigBuilder) processVolumeMounts() error {
123+
// Existing mount processing...
124+
125+
// NEW: Process ignore patterns
126+
ignoreProcessor := ignore.NewIgnoreProcessor()
127+
ignoreProcessor.LoadGlobal()
128+
ignoreProcessor.LoadLocal(sourceDir)
129+
130+
overlayPaths := ignoreProcessor.GetOverlayPaths(source, target)
131+
for _, overlayPath := range overlayPaths {
132+
b.addTmpfsOverlay(overlayPath)
133+
}
134+
}
135+
```
136+
137+
---
138+
139+
## **🧪 Runtime Support**
140+
141+
Enhanced `convertMounts` function in `pkg/container/docker/client.go`:
142+
143+
```go
144+
func convertMounts(mounts []runtime.Mount) []mount.Mount {
145+
result := make([]mount.Mount, 0, len(mounts))
146+
for _, m := range mounts {
147+
if m.Type == "tmpfs" {
148+
result = append(result, mount.Mount{
149+
Type: mount.TypeTmpfs,
150+
Target: m.Target,
151+
TmpfsOptions: &mount.TmpfsOptions{
152+
SizeBytes: 1024 * 1024, // 1MB tmpfs for security overlays
153+
},
154+
})
155+
} else {
156+
result = append(result, mount.Mount{
157+
Type: mount.TypeBind,
158+
Source: m.Source,
159+
Target: m.Target,
160+
ReadOnly: m.ReadOnly,
161+
})
162+
}
163+
}
164+
return result
165+
}
166+
```
167+
168+
| Runtime | Bind Mount | Tmpfs Overlay |
169+
| ----- | ----- | ----- |
170+
| Docker |`mount.TypeBind` |`mount.TypeTmpfs` |
171+
| Podman |`--mount type=bind` |`--mount type=tmpfs` |
172+
173+
---
174+
175+
## **🧰 CLI Integration**
176+
177+
Extend existing `thv run` command flags:
178+
179+
```go
180+
// In cmd/thv/app/run.go
181+
var (
182+
runIgnoreGlobally bool
183+
runPrintOverlays bool
184+
runIgnoreFile string
185+
)
186+
187+
func init() {
188+
runCmd.Flags().BoolVar(&runIgnoreGlobally, "ignore-globally", true,
189+
"Load global ignore patterns from ~/.config/toolhive/thvignore")
190+
runCmd.Flags().BoolVar(&runPrintOverlays, "print-resolved-overlays", false,
191+
"Debug: show resolved container paths for tmpfs overlays")
192+
runCmd.Flags().StringVar(&runIgnoreFile, "ignore-file", ".thvignore",
193+
"Name of the ignore file to look for in source directories")
194+
}
195+
```
196+
197+
---
198+
199+
## **🔐 Security Considerations**
200+
201+
* Warn users if sensitive-looking files (`.ssh`, `.env`) are present but not excluded
202+
203+
* Validate ignore patterns to prevent overly broad exclusions
204+
205+
* Integrate with existing permission profile system for defense-in-depth
206+
207+
* Log overlay mount creation for audit purposes
208+
209+
---
210+
211+
## **🎯 Use Cases**
212+
213+
### **🔑 Cloud Provider Credentials**
214+
215+
**Scenario**: Developer working on a project with AWS/GCP credentials that should never be accessible to MCP servers.
216+
217+
```shell
218+
my-project/
219+
├── src/
220+
├── .aws/credentials
221+
├── .gcp/service-account.json
222+
├── .env.production
223+
└── .thvignore
224+
```
225+
226+
**`.thvignore`**:
227+
```
228+
.aws/
229+
.gcp/
230+
*.pem
231+
.env.production
232+
```
233+
234+
**Result**: MCP server analyzes code in `src/` but cloud credentials are hidden via tmpfs overlays.
235+
236+
---
237+
238+
### **🏢 SSH Keys and Development Secrets**
239+
240+
**Scenario**: Developer's home directory mounted for MCP server to access project files while protecting personal credentials.
241+
242+
```shell
243+
~/dev-project/
244+
├── code/
245+
├── .ssh/id_rsa
246+
├── .gnupg/
247+
├── .docker/config.json
248+
└── .thvignore
249+
```
250+
251+
**`.thvignore`**:
252+
```
253+
.ssh/
254+
.gnupg/
255+
.docker/config.json
256+
.kube/config
257+
```
258+
259+
**Result**: MCP server can access project code but personal authentication credentials remain protected.
260+
261+
---
262+
263+
### **🤖 AI/ML Model Protection**
264+
265+
**Scenario**: Data scientist using MCP servers for code analysis while protecting sensitive datasets and production models.
266+
267+
```shell
268+
ml-project/
269+
├── notebooks/
270+
├── src/
271+
├── data/customer-data.csv
272+
├── models/production-model.pkl
273+
└── .thvignore
274+
```
275+
276+
**`.thvignore`**:
277+
```
278+
data/*.csv
279+
models/production-*
280+
*.pkl
281+
.kaggle/
282+
```
283+
284+
**Result**: MCP server can analyze notebooks and source code but cannot access sensitive data or production models.
285+
286+
---
287+
288+
## **📄 Example: Final Runtime Command**
289+
290+
If user runs:
291+
292+
```shell
293+
thv run --volume ./my-folder:/app server-name
294+
```
295+
296+
And:
297+
298+
```shell
299+
# ~/.config/toolhive/thvignore
300+
node_modules/
301+
302+
# ./my-folder/.thvignore
303+
.ssh/
304+
.env
305+
```
306+
307+
ToolHive generates runtime configuration with:
308+
309+
```go
310+
// Main bind mount
311+
runtime.Mount{
312+
Source: "/absolute/path/my-folder",
313+
Target: "/app",
314+
Type: "bind",
315+
ReadOnly: false,
316+
}
317+
318+
// Tmpfs overlays
319+
runtime.Mount{Target: "/app/.ssh", Type: "tmpfs"}
320+
runtime.Mount{Target: "/app/.env", Type: "tmpfs"}
321+
runtime.Mount{Target: "/app/node_modules", Type: "tmpfs"}
322+
```
323+
324+
Which converts to Docker commands:
325+
326+
```shell
327+
docker run \
328+
-v /absolute/path/my-folder:/app \
329+
--tmpfs /app/.ssh:rw,nosuid,nodev,noexec \
330+
--tmpfs /app/.env:rw,nosuid,nodev,noexec \
331+
--tmpfs /app/node_modules:rw,nosuid,nodev,noexec \
332+
my-image
333+
```
334+
335+
---
336+
337+
## **✅ Summary**
338+
339+
| Feature | Outcome |
340+
| ----- | ----- |
341+
| Real-time file access | ✅ via full bind mount |
342+
| Hidden files (e.g. `.ssh`, `.env`) | ✅ overlaid with tmpfs |
343+
| Config flexibility | ✅ per-folder \+ global `.thvignore` |
344+
| Runtime compatibility | ✅ Docker, Podman |
345+
| Integration | ✅ Works with existing permission profiles |
346+

0 commit comments

Comments
 (0)