This document provides a comprehensive security analysis of the dual chroot isolation system, examining the implementation to ensure complete compartmentalization between production jobs and runtime builds with no security leaks.
The system uses automatic service-based job type detection to route jobs to appropriate isolation levels:
JobService API RuntimeService API
│ │
▼ ▼
JobType: "standard" JobType: "runtime-build"
│ │
▼ ▼
Production Isolation Builder Isolation
(Minimal Chroot) (Builder Chroot)
Key Implementation:
/home/jay/joblet/internal/modes/isolation/isolation.go:119-136
jobType := i.platform.Getenv("JOB_TYPE")
isBuilderJob := jobType == "runtime-build"
standard
jobs)Isolation Method: Minimal chroot with restricted filesystem access
Implementation: jobFS.Setup()
in filesystem/isolator.go
Security Boundaries:
Production Chroot: /opt/joblet/jobs/{JOB_ID}/
├── bin/ # Minimal binaries (read-only bind mount from host)
├── lib/ # Required libraries (read-only bind mount from host)
├── usr/ # Selected /usr contents (read-only bind mount from host)
├── etc/ # Basic config files (isolated copies)
├── work/ # Job workspace (writable, isolated)
├── tmp/ # Isolated tmp: /tmp/job-{JOB_ID}/ (writable, isolated)
└── [runtime]/ # Optional runtime mount (read-only bind mount)
Key Security Features:
/tmp/job-{JOB_ID}/
runtime-build
jobs)Isolation Method: Builder chroot with controlled host filesystem access
Implementation: jobFS.SetupBuilder()
in filesystem/isolator.go
Security Boundaries:
Builder Chroot: /opt/joblet/jobs/{BUILD_ID}/
├── bin/ # Full /bin from host (read-only bind mount)
├── lib/ # Full /lib from host (read-only bind mount)
├── usr/ # Full /usr from host (read-only bind mount)
├── etc/ # Full /etc from host (read-only bind mount)
├── var/ # Full /var from host (read-only bind mount)
├── home/ # Full /home from host (read-only bind mount)
├── root/ # Full /root from host (read-only bind mount)
├── work/ # Build workspace (writable, isolated)
├── tmp/ # Isolated tmp: /tmp/job-{BUILD_ID}/ (writable, isolated)
└── opt/
├── [other]/ # Other /opt contents (read-only bind mount)
└── joblet/
└── runtimes/ # ONLY runtimes dir (read-write bind mount)
Key Security Features:
/opt/joblet/
completely excluded except runtimes/
/opt/joblet/runtimes/
is writable/tmp/job-{BUILD_ID}/
Problem: Builder chroot could mount /opt/joblet/jobs/
creating infinite recursion
Solution: Complete exclusion of /opt/joblet/
except controlled runtimes directory
Implementation Analysis:
// Source: filesystem/isolator.go:mountOptDirectory()
func (f *JobFilesystem) mountOptDirectory(hostOptPath, targetOptPath string) error {
for _, entry := range optEntries {
dirName := entry.Name()
// Skip joblet directory to prevent recursion
if dirName == "joblet" {
log.Debug("skipping /opt/joblet to prevent recursion")
continue // - CRITICAL SECURITY CHECK
}
// ... mount other /opt contents
}
}
// Source: filesystem/isolator.go:mountRuntimesDirectory()
func (f *JobFilesystem) mountRuntimesDirectory() error {
hostRuntimesPath := "/opt/joblet/runtimes"
targetRuntimesPath := filepath.Join(f.RootDir, "opt", "joblet", "runtimes")
// Only mount runtimes directory - NOT the entire /opt/joblet
if err := f.platform.Mount(hostRuntimesPath, targetRuntimesPath, "", uintptr(syscall.MS_BIND), ""); err != nil {
return fmt.Errorf("failed to bind mount runtimes directory: %w", err)
}
// - PRECISE MOUNTING - only runtimes directory accessible
}
Security Guarantee: Builder jobs cannot access:
/opt/joblet/jobs/
(where other jobs run)/opt/joblet/config/
(system configuration)/opt/joblet/logs/
(system logs)/opt/joblet/
subdirectoriesConcern: Builder jobs could contaminate production job filesystems
Analysis:
/opt/joblet/runtimes/
and /tmp/job-{BUILD_ID}/
/opt/joblet/jobs/{JOB_ID}/
Implementation Verification:
// Builder chroot creation
func (i *Isolator) CreateBuilderFilesystem(jobID string) (*JobFilesystem, error) {
// Same directory structure as production jobs
jobRootDir := filepath.Join(i.config.BaseDir, jobID) // /opt/joblet/jobs/{BUILD_ID}/
jobTmpDir := strings.Replace(i.config.TmpDir, "{JOB_ID}", jobID, -1) // /tmp/job-{BUILD_ID}/
// - BUILD_ID != JOB_ID - complete separation
}
Concern: Jobs could be misrouted between isolation levels
Analysis of Detection Chain:
RuntimeService
vs JobService
) sets JobType
JobType
to environment: JOB_TYPE=runtime-build
jobType := i.platform.Getenv("JOB_TYPE")
isBuilderJob := jobType == "runtime-build"
Security Properties:
RuntimeService
can set runtime-build
typernx job run
Both job types use identical process isolation:
Shared Security Features:
Implementation Verification:
// Source: filesystem/isolator.go:validateJobContext()
func (i *Isolator) validateJobContext() error {
// Both job types must pass same safety checks
jobID := i.platform.Getenv("JOB_ID")
if jobID == "" {
return fmt.Errorf("not in job context - JOB_ID not set") // - SAFETY CHECK
}
if i.platform.Getpid() != 1 {
return fmt.Errorf("not in isolated PID namespace - refusing filesystem isolation") // - NAMESPACE VERIFICATION
}
return nil
}
Concern: Shared /tmp
could leak data between job types
Analysis:
/tmp/job-{JOB_ID}/
/tmp/job-{BUILD_ID}/
Attack: Malicious production job attempts to access builder chroot
Mitigation:
- - Read-only host mounts: Cannot modify system binaries - - No package managers: Cannot install escape tools - - Minimal attack surface: Limited binaries available - - Process isolation: Cannot see other namespaces
Attack: Builder job attempts to gain root access
Mitigation:
- - Same user context: Runs as same unprivileged user as production jobs - - No setuid binaries: Host mounts are read-only - - Cgroup limits: Resource restrictions prevent DoS attacks - - Network isolation: Cannot access privileged network services
Attack: Production job attempts to read builder artifacts
Mitigation:
- - Separate filesystem trees: No shared writable space - - Isolated temp directories: Different UUID-based paths - - Read-only runtime access: Production jobs can only read completed runtimes - - No job directory access: Cannot access other jobs’ workspaces
Attack: One job type consumes resources affecting the other
Mitigation:
- - Same cgroup limits: Both job types subject to identical resource controls - - Independent quotas: Each job gets separate CPU/memory/I/O allocation - - Process limits: Same process count restrictions - - Cleanup guarantees: Failed jobs are cleaned up automatically
While the current implementation is secure, consider these enhancements:
# Example SELinux policy for additional MAC
type joblet_production_t;
type joblet_builder_t;
type_transition initrc_t joblet_exec_t:process joblet_production_t;
// Different syscall profiles for each job type
productionSeccompProfile := &seccomp.Profile{
AllowedSyscalls: []string{"read", "write", "open", "close"}, // minimal set
}
builderSeccompProfile := &seccomp.Profile{
AllowedSyscalls: append(productionSeccompProfile.AllowedSyscalls,
"mount", "umount2"), // additional for compilation
}
// Verify runtime integrity before production use
func (r *RuntimeManager) VerifyRuntime(runtimePath string) error {
// Check signatures, validate structure, scan for malware
}
Previous Security Gap: Runtime installations performed in builder chroot would mount host OS paths directly to production jobs, potentially exposing the entire host filesystem.
Example of Previous Risk:
# Previous runtime.yml (INSECURE)
mounts:
- source: "usr/lib/jvm/java-21-openjdk-amd64" # Points to HOST filesystem
target: "/usr/lib/jvm/java-21-openjdk-amd64"
readonly: true
Runtime Cleanup Process: After successful runtime installation, a cleanup phase creates an isolated, self-contained runtime structure:
# In builder chroot during setup:
/opt/joblet/runtimes/java/openjdk-21/
├── isolated/ # NEW: Isolated runtime files
│ ├── usr/lib/jvm/ # Copied from host, not mounted
│ ├── usr/bin/ # Java binaries (isolated copies)
│ └── etc/ssl/certs/ # CA certificates (isolated copies)
├── runtime.yml # Updated with isolated paths
└── runtime.yml.original # Backup of original config
Updated Runtime Configuration:
# New runtime.yml (SECURE)
mounts:
- source: "isolated/usr/lib/jvm/java-21-openjdk-amd64" # Points to ISOLATED copy
target: "/usr/lib/jvm/java-21-openjdk-amd64"
readonly: true
-
- Runtime Cleanup System: Implemented in internal/joblet/core/runtime_cleanup.go
-
- Setup Script Integration: Updated runtimes/openjdk-21/setup-ubuntu-amd64.sh
-
- Isolated Structure: Creates isolated/
directory with runtime files
-
- Configuration Update: Rewrites runtime.yml
with isolated paths
VERDICT: - SECURE - No isolation leaks detected + Enhanced runtime isolation
The service-based dual chroot implementation provides:
The architecture successfully separates production workload execution from administrative runtime building while maintaining security isolation equivalent to running completely separate container environments.