Borg/rfc/RFC-004-TIM.md

330 lines
8 KiB
Markdown

# RFC-004: Terminal Isolation Matrix (TIM)
**Status**: Draft
**Author**: [Snider](https://github.com/Snider/)
**Created**: 2026-01-13
**License**: EUPL-1.2
**Depends On**: RFC-003
---
## Abstract
TIM (Terminal Isolation Matrix) is an OCI-compatible container bundle format. It packages a runtime configuration with a root filesystem (DataNode) for execution via runc or compatible runtimes.
## 1. Overview
TIM provides:
- OCI runtime-spec compatible bundles
- Portable container packaging
- Integration with DataNode filesystem
- Encryption via STIM (RFC-005)
## 2. Implementation
### 2.1 Core Type
```go
// pkg/tim/tim.go:28-32
type TerminalIsolationMatrix struct {
Config []byte // Raw OCI runtime specification (JSON)
RootFS *datanode.DataNode // In-memory filesystem
}
```
### 2.2 Error Variables
```go
var (
ErrDataNodeRequired = errors.New("datanode is required")
ErrConfigIsNil = errors.New("config is nil")
ErrPasswordRequired = errors.New("password is required for encryption")
ErrInvalidStimPayload = errors.New("invalid stim payload")
ErrDecryptionFailed = errors.New("decryption failed (wrong password?)")
)
```
## 3. Public API
### 3.1 Constructors
```go
// Create empty TIM with default config
func New() (*TerminalIsolationMatrix, error)
// Wrap existing DataNode into TIM
func FromDataNode(dn *DataNode) (*TerminalIsolationMatrix, error)
// Deserialize from tar archive
func FromTar(data []byte) (*TerminalIsolationMatrix, error)
```
### 3.2 Serialization
```go
// Serialize to tar archive
func (m *TerminalIsolationMatrix) ToTar() ([]byte, error)
// Encrypt to STIM format (ChaCha20-Poly1305)
func (m *TerminalIsolationMatrix) ToSigil(password string) ([]byte, error)
```
### 3.3 Decryption
```go
// Decrypt from STIM format
func FromSigil(data []byte, password string) (*TerminalIsolationMatrix, error)
```
### 3.4 Execution
```go
// Run plain .tim file with runc
func Run(timPath string) error
// Decrypt and run .stim file
func RunEncrypted(stimPath, password string) error
```
## 4. Tar Archive Structure
### 4.1 Layout
```
config.json (root level, mode 0600)
rootfs/ (directory, mode 0755)
rootfs/bin/app (files within rootfs/)
rootfs/etc/config
...
```
### 4.2 Serialization (ToTar)
```go
// pkg/tim/tim.go:111-195
func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) {
// 1. Write config.json header (size = len(m.Config), mode 0600)
// 2. Write config.json content
// 3. Write rootfs/ directory entry (TypeDir, mode 0755)
// 4. Walk m.RootFS depth-first
// 5. For each file: tar entry with name "rootfs/" + path, mode 0600
}
```
### 4.3 Deserialization (FromTar)
```go
func FromTar(data []byte) (*TerminalIsolationMatrix, error) {
// 1. Parse tar entries
// 2. "config.json" → stored as raw bytes in Config
// 3. "rootfs/*" prefix → stripped and added to DataNode
// 4. Error if config.json missing (ErrConfigIsNil)
}
```
## 5. OCI Config
### 5.1 Default Config
The `New()` function creates a TIM with a default config from `pkg/tim/config.go`:
```go
func defaultConfig() (*trix.Trix, error) {
return &trix.Trix{Header: make(map[string]interface{})}, nil
}
```
**Note**: The default config is minimal. Applications should populate the Config field with a proper OCI runtime spec.
### 5.2 OCI Runtime Spec Example
```json
{
"ociVersion": "1.0.2",
"process": {
"terminal": false,
"user": {"uid": 0, "gid": 0},
"args": ["/bin/app"],
"env": ["PATH=/usr/bin:/bin"],
"cwd": "/"
},
"root": {
"path": "rootfs",
"readonly": true
},
"mounts": [],
"linux": {
"namespaces": [
{"type": "pid"},
{"type": "network"},
{"type": "mount"}
]
}
}
```
## 6. Execution Flow
### 6.1 Plain TIM (Run)
```go
// pkg/tim/run.go:18-74
func Run(timPath string) error {
// 1. Create temporary directory (borg-run-*)
// 2. Extract tar entry-by-entry
// - Security: Path traversal check (prevents ../)
// - Validates: target = Clean(target) within tempDir
// 3. Create directories as needed (0755)
// 4. Write files with 0600 permissions
// 5. Execute: runc run -b <tempDir> borg-container
// 6. Stream stdout/stderr directly
// 7. Return exit code
}
```
### 6.2 Encrypted TIM (RunEncrypted)
```go
// pkg/tim/run.go:79-134
func RunEncrypted(stimPath, password string) error {
// 1. Read encrypted .stim file
// 2. Decrypt using FromSigil() with password
// 3. Create temporary directory (borg-run-*)
// 4. Write config.json to tempDir
// 5. Create rootfs/ subdirectory
// 6. Walk DataNode and extract all files to rootfs/
// - Uses CopyFile() with 0600 permissions
// 7. Execute: runc run -b <tempDir> borg-container
// 8. Stream stdout/stderr
// 9. Clean up temp directory (defer os.RemoveAll)
// 10. Return exit code
}
```
### 6.3 Security Controls
| Control | Implementation |
|---------|----------------|
| Path traversal | `filepath.Clean()` + prefix validation |
| Temp cleanup | `defer os.RemoveAll(tempDir)` |
| File permissions | Hardcoded 0600 (files), 0755 (dirs) |
| Test injection | `ExecCommand` variable for mocking runc |
## 7. Cache API
### 7.1 Cache Structure
```go
// pkg/tim/cache.go
type Cache struct {
Dir string // Directory path for storage
Password string // Shared password for all TIMs
}
```
### 7.2 Cache Operations
```go
// Create cache with master password
func NewCache(dir, password string) (*Cache, error)
// Store TIM (encrypted automatically as .stim)
func (c *Cache) Store(name string, m *TerminalIsolationMatrix) error
// Load TIM (decrypted automatically)
func (c *Cache) Load(name string) (*TerminalIsolationMatrix, error)
// Delete cached TIM
func (c *Cache) Delete(name string) error
// Check if TIM exists
func (c *Cache) Exists(name string) bool
// List all cached TIM names
func (c *Cache) List() ([]string, error)
// Load and execute cached TIM
func (c *Cache) Run(name string) error
// Get file size of cached .stim
func (c *Cache) Size(name string) (int64, error)
```
### 7.3 Cache Directory Structure
```
cache/
├── mycontainer.stim (encrypted)
├── another.stim (encrypted)
└── ...
```
- All TIMs stored as `.stim` files (encrypted)
- Single password protects entire cache
- Directory created with 0700 permissions
- Files stored with 0600 permissions
## 8. CLI Usage
```bash
# Compile Borgfile to TIM
borg compile -f Borgfile -o container.tim
# Compile with encryption
borg compile -f Borgfile -e "password" -o container.stim
# Run plain TIM
borg run container.tim
# Run encrypted TIM
borg run container.stim -p "password"
# Decode (extract) to tar
borg decode container.stim -p "password" --i-am-in-isolation -o container.tar
# Inspect metadata without decrypting
borg inspect container.stim
```
## 9. Implementation Reference
- TIM core: `pkg/tim/tim.go`
- Execution: `pkg/tim/run.go`
- Cache: `pkg/tim/cache.go`
- Config: `pkg/tim/config.go`
- Tests: `pkg/tim/tim_test.go`, `pkg/tim/run_test.go`, `pkg/tim/cache_test.go`
## 10. Security Considerations
1. **Path traversal prevention**: `filepath.Clean()` + prefix validation
2. **Permission hardcoding**: 0600 files, 0755 directories
3. **Secure cleanup**: `defer os.RemoveAll()` on temp directories
4. **Command injection prevention**: `ExecCommand` variable (no shell)
5. **Config validation**: Validate OCI spec before execution
## 11. OCI Compatibility
TIM bundles are compatible with:
- runc
- crun
- youki
- Any OCI runtime-spec 1.0.2 compliant runtime
## 12. Test Coverage
| Area | Tests |
|------|-------|
| TIM creation | DataNode wrapping, default config |
| Serialization | Tar round-trips, large files (1MB+) |
| Encryption | ToSigil/FromSigil, wrong password detection |
| Caching | Store/Load/Delete, List, Size |
| Execution | ZIP slip prevention, temp cleanup |
| Error handling | Nil DataNode, nil config, invalid tar |
## 13. Future Work
- [ ] Image layer support
- [ ] Registry push/pull
- [ ] Multi-platform bundles
- [ ] Signature verification
- [ ] Full OCI config generation