Skip to main content

TazPod Rising: From DevPod Ashes to a Go-Powered Zero Trust CLI

Roberto Tazzoli
Author
Roberto Tazzoli
Passionate about declarative infrastructure, self-hosting, and Kubernetes.

Introduction: The Phoenix Moment
#

In the previous episode of this technical diary, I documented the dramatic failure of the attempt to transform DevPod into a Zero Trust enclave. The fundamental conflict between DevPod’s “Convenience-First” architecture and my security requirements led to an inevitable conclusion: I had to abandon the tool completely.

However, as every engineer knows, failure is often the mother of innovation. The ashes of DevPod became the fertile ground for something new: TazPod, a custom CLI in Go designed from scratch to address the specific security challenges that DevPod could not handle.

This is the story of how I built TazPod from v1.0 to v9.9, transforming it from the fragility of Bash scripts to the robustness of Go, from global mounts to namespace isolation, and from convenience compromises to true Zero Trust security.


Phase 1: The Foundation in Go - TazPod v1.0
#

The first technical decision was radical: abandon the idea of an environment that “magically” self-configures via SSH. I needed determinism.

The Reasoning: Why Go?
#

After the nightmare of Bash scripts in DevPod, I needed a language with:

  1. Strong typing to prevent runtime errors.
  2. Excellent integration with Docker through the SDK.
  3. Cross-platform compilation for future portability.
  4. Robust error handling without the fragility of Bash “traps”.

Go offers a critical advantage for a tool of this type: direct access to operating system syscalls and the ability to compile into a single static binary.

The Architecture: Command-First Design
#

I structured TazPod around a central set of commands, managed by a main switch in main.go. This approach transforms the container into a “Development Daemon”. It is there, waiting (sleep infinity), but inert. The magic happens when we enter it.

// cmd/tazpod/main.go (Snippet of the up function)
func up() {
    // ... loading configuration ...
    runCmd("docker", "run", "-d", 
        "--name", cfg.ContainerName, 
        "--privileged", // Necessary to mount loop devices
        "--network", "host", 
        "-e", "DISPLAY="+display, 
        "-v", cwd+":/workspace", // Mount current project
        "-w", "/workspace", 
        cfg.Image, 
        "sleep", "infinity") // The container stays alive waiting
}

The first implementation was essentially a direct translation of the Bash scripts. It worked, but it still suffered from the same global mount problem that plagued DevPod. Anyone with docker exec access could see the secrets.


Phase 2: The Security Breakthrough - TazPod v2.0 (Ghost Edition)
#

During a security review on January 17th, I identified a critical flaw: if I unlocked the vault and another user accessed the container, they could read all the secrets. The solution came from an unexpected source: Linux Mount Namespaces.

The Concept: “Ghost Mode”
#

The idea was revolutionary: instead of mounting the vault globally, create an isolated namespace where only the current session could see the mounted secrets.

In Linux, mount points are global per namespace. If I create a new mount namespace and mount a disk inside it, that disk exists only for the processes living in that namespace. For the parent process (and for the host), that mount point is simply an empty directory.

The Implementation: unshare Magic
#

The key was using unshare -m to create a new mount namespace. Here is what happens “under the hood” when a user types the vault password:

  1. Trigger: The user launches tazpod pull.
  2. Fork & Unshare: The Go binary executes itself with elevated privileges using unshare:
    sudo unshare --mount --propagation private /usr/local/bin/tazpod internal-ghost
  3. Enclave Creation: The new internal-ghost process is born in a parallel mount universe.
  4. Decryption: Inside this universe, we use cryptsetup to open the vault.img file (mounted via loop device) and mount it to /home/tazpod/secrets.
  5. Drop Privileges: Once the disk is mounted, the process “downgrades” its privileges from root to user tazpod and launches a Bash shell.

The Result:

  • You (in the ghost shell): See the secrets, use kubectl, work normally.
  • Intruders (in other shells): See an empty ~/secrets directory.
  • Exit: When you exit, the namespace disappears, taking the mount with it.

Phase 3: The IDE Revolution - TazPod v3.0
#

With DevPod gone, I lost the integrated VS Code experience. I decided to embrace a pure terminal workflow with Neovim (LazyVim configuration).

The LazyVim Integration
#

I invested significant time perfecting the Neovim setup directly in the base Docker image. I wanted the IDE to be ready immediately, without having to wait for plugin downloads on the first startup.

# LazyVim installation and headless plugin sync
RUN git clone https://github.com/LazyVim/LazyVim ~/.config/nvim && \
    nvim --headless "+Lazy! sync" +qa && \
    nvim --headless "+MasonInstall all" +qa

The Result: A complete development environment ready in seconds, with Tree-sitter, LSP, and all plugins pre-compiled.


Phase 4: The Battle for Infisical Persistence
#

Having solved the filesystem isolation, I had to tackle identity management. I use Infisical to manage centralized secrets. However, Infisical needs to save a local session token (usually in ~/.infisical).

If the container is ephemeral, I would have to log in at every restart. Unacceptable. If I save the token on a Docker volume, it is exposed in cleartext on the host. Unacceptable.

The Investigation: The “Cannibal” Bug
#

The idea was simple: move the .infisical folder inside the encrypted vault and use a bind-mount to make it appear in the user home only when the vault is open.

During the Go implementation, I encountered a critical bug that I nicknamed “The Cannibal”. The migration function, designed to move old tokens into the vault, had a logic flaw that led to the deletion of content if the paths coincided.

The Solution: The Armored Bridge
#

I rewrote the logic implementing rigorous checks:

  1. Preliminary Check: I verify if the mount is already active by reading /proc/mounts.
  2. Double Bridge: I mount both the configuration (.infisical) and the system keyring (infisical-keyring) inside the vault (.infisical-vault and .infisical-keyring).
  3. Recursive Ownership: A recurring problem was that files created during the mount (by root) were not readable by the user. I added a forced chown -R tazpod:tazpod on the entire .tazpod structure at every init or mount operation.

Now, the session survives restarts, but physically exists only inside the encrypted vault.img file.


Phase 5: From Hack to Product (TazPod v9.9)
#

At this point, I had a working but rough system. To make it a true “Zero Trust” tool usable by others, deep cleaning and standardization were needed.

Standardization and “Smart Init”
#

I introduced the tazpod init command. Instead of having to manually copy configuration files, the CLI now analyzes the current directory and generates:

  1. A hidden .tazpod/ folder.
  2. A pre-compiled config.yaml, allowing to choose the “vertical” (base, k8s, gemini) via an argument (e.g., tazpod init gemini).
  3. A secrets.yml template to map Infisical environment variables.
  4. A .gitignore that automatically excludes the vault and local AI memory (mounted in ./.gemini to persist project memories).

The Name Collision Problem
#

Launching multiple TazPod projects simultaneously, I noticed that Docker conflicted on container names (tazpod-lab). I implemented dynamic naming logic in Go in version v9.9:

cwd, _ := os.Getwd()
dirName := filepath.Base(cwd)

rng := rand.New(rand.NewSource(time.Now().UnixNano()))
containerName := fmt.Sprintf("tazpod-%s-%d", dirName, rng.Intn(9000000)+1000000)

Now every project has a unique identity, allowing work on multiple clusters or clients in parallel without overlaps.


Post-Development Reflections
#

The transition from DevPod to TazPod was an exercise in subtraction. I removed the graphical interface, removed the synchronization agent, removed the managed SSH abstraction.

In return, I gained:

  1. Verifiable Security: I know exactly where every byte of sensitive data resides (in the RAM of the Ghost process).
  2. Total Portability: The project is self-contained. Just have Docker and the TazPod binary.
  3. Speed: Without agent overhead, shell startup is instantaneous once the image is downloaded.

The Project on GitHub
#

I decided to release TazPod as an Open Source project under the MIT license. It is not just a personal script, but a complete framework for those who, like me, live in the terminal and do not want compromises on security.

Installation is now reduced to a single line:

curl -sSL https://raw.githubusercontent.com/tazzo/tazpod/master/scripts/install.sh | bash

For more technical details and to consult the complete project documentation, I invite you to visit the official repository on GitHub: https://github.com/tazzo/tazpod.

This journey confirms that in modern DevOps, building your own tools is not reinventing the wheel, but often the only way to ensure that the wheel turns exactly as required by the security constraints of critical infrastructure.

The next step? Using TazPod to complete the Terraform refactoring of the TazLab cluster, knowing that the access keys are finally safe.


Comments