Skip to content

feat: add Docker web UI for Hauler with security hardening#508

Open
derhornspieler wants to merge 7 commits intohauler-dev:mainfrom
derhornspieler:feat/dockerfile-webui
Open

feat: add Docker web UI for Hauler with security hardening#508
derhornspieler wants to merge 7 commits intohauler-dev:mainfrom
derhornspieler:feat/dockerfile-webui

Conversation

@derhornspieler
Copy link

@derhornspieler derhornspieler commented Feb 10, 2026

Built with Claude Code
Security Review - Passed
Docker Hardened Images


Please check below, if the PR fulfills these requirements:

  • Commit(s) and code follow the repositories guidelines.
  • Test(s) have been added or updated to support these change(s).
  • Doc(s) have been added or updated to support these change(s).

Associated Links:

  • Feature directory: feat:dockerfile-webui/
  • Docs: feat:dockerfile-webui/README.md, feat:dockerfile-webui/docs/
  • Tests: feat:dockerfile-webui/tests/

Types of Changes:

  • New Feature (Docker-based web UI for Hauler)

Proposed Changes:

What is this?

A web UI for Hauler that runs in a Docker container. It wraps the hauler CLI behind a Go HTTP server and gives you a browser-based interface for all the core workflows — syncing manifests, saving/loading hauls, pushing to registries, serving content, managing artifacts, etc. No CLI access needed.

Everything lives in feat:dockerfile-webui/ and doesn't touch any existing Hauler source.

Why?

In airgapped environments, having a visual interface for managing your Hauler store makes things significantly easier — especially for teams where not everyone is comfortable on the command line. The UI bundles all its assets locally (Tailwind, FontAwesome) so it works fully offline with zero external dependencies.

What's included

  • Go backend (gorilla/mux) — 37 REST endpoints + WebSocket for live command logs
  • Vanilla JS frontend — no build step, no node_modules, just works
  • Hardened Dockerfile — multi-stage build on Docker Hardened Images, runs as non-root
  • Optional API key auth — set HAULER_UI_API_KEY env var to lock down the API
  • Init container — fixes bind-mount permissions so the non-root container can write to data volumes
  • MCP server — Python-based, for AI assistant integration

Architecture

Network Flow

How the components talk to each other:

graph LR
    Browser["Browser<br/>:8080"] -->|HTTP/WS| Backend["Go Backend<br/>(gorilla/mux)"]
    Backend -->|exec.Command| Hauler["hauler CLI"]
    Backend -->|HTTP GET| HelmRepos["Helm Repos<br/>(index.yaml)"]
    Hauler -->|OCI pull/push| ExtReg["External Registries<br/>(docker.io, ghcr.io, etc.)"]
    Hauler -->|Read/Write| Store[("/data/store<br/>OCI Layout")]
    Hauler -->|Serve :5000| Registry["OCI Registry"]
    Hauler -->|Serve :8081| FileServer["File Server"]
    Backend -->|Static files| Frontend["/app/frontend<br/>(HTML/JS/CSS)"]
Loading

Request Data Flow

Every user action follows this path through the system. Input validation happens at the backend boundary before anything touches the filesystem or CLI:

sequenceDiagram
    participant U as Browser
    participant F as Frontend (app.js)
    participant M as Auth Middleware
    participant H as Go Handler
    participant V as safePath()
    participant E as executeHauler()
    participant C as hauler CLI

    U->>F: User clicks action
    F->>F: Collect form inputs
    F->>M: fetch(/api/...) + Bearer token
    M->>M: Validate API key
    alt Invalid key
        M-->>F: 401 Unauthorized
        F->>U: Prompt for API key
    end
    M->>H: Route to handler
    H->>H: json.Decode (check error → 400)
    H->>V: safePath(baseDir, userInput)
    V->>V: filepath.Base() + reject ".."
    alt Path traversal
        V-->>H: error
        H-->>F: 400 Invalid filename
    end
    H->>E: executeHauler(cmd, args...)
    E->>E: redactArgs (strip passwords)
    E->>E: Log to memory (redacted)
    E->>C: exec.Command("hauler", args)
    C-->>E: stdout + stderr
    E-->>H: (output, error)
    H-->>F: JSON {success, output, error}
    F->>U: Update DOM with result
Loading

Store Operation Flows

The main workflows — sync, save, load, push — and how they move data:

flowchart TB
    subgraph Input["Content Input"]
        M["Manifest YAML"] -->|sync| STORE
        IMG["Docker Image"] -->|add image| STORE
        CHT["Helm Chart"] -->|add chart| STORE
        FIL["Local/Remote File"] -->|add file| STORE
        HAUL_IN["Haul Archive"] -->|load| STORE
    end

    STORE[("Hauler Store<br/>/data/store")]

    subgraph Output["Content Output"]
        STORE -->|save| HAUL_OUT["haul.tar.zst<br/>/data/hauls/"]
        STORE -->|copy| REG["OCI Registry<br/>registry://host"]
        STORE -->|extract| DISK["Extracted Files<br/>/data/extracted/"]
        STORE -->|serve registry| OCI[":5000 OCI Registry"]
        STORE -->|serve fileserver| FS[":8081 File Server"]
    end
Loading

Backend Code Flow

How the Go backend is organized — middleware wraps the router, handlers delegate to executeHauler(), and shared state is mutex-protected:

flowchart TB
    REQ["Incoming Request"] --> AUTH["authMiddleware"]
    AUTH -->|Static files| STATIC["http.FileServer"]
    AUTH -->|/api/health| HEALTH["healthHandler"]
    AUTH -->|/api routes| ROUTER["mux.Router<br/>37 endpoints"]

    ROUTER --> STORE_OPS["Store Handlers<br/>sync, save, load,<br/>extract, clear, info"]
    ROUTER --> CONTENT["Content Handlers<br/>add-content, add-file,<br/>artifacts, remove"]
    ROUTER --> FILE_OPS["File Handlers<br/>upload, download,<br/>list, delete"]
    ROUTER --> REG_OPS["Registry Handlers<br/>configure, push,<br/>login, logout, test"]
    ROUTER --> SERVE["Serve Handlers<br/>start, stop, status"]
    ROUTER --> WS["logsHandler<br/>WebSocket"]

    STORE_OPS --> SAFE["safePath"]
    CONTENT --> SAFE
    FILE_OPS --> SAFE
    REG_OPS --> SAFE
    SERVE --> SAFE

    STORE_OPS --> EXEC["executeHauler"]
    CONTENT --> EXEC
    REG_OPS --> EXEC
    SERVE -->|long-running| CHILD["serveCmd<br/>child process"]

    EXEC --> REDACT["redactArgs"]
    REDACT --> LOG["logLines<br/>mutex-protected"]
    LOG --> WS
Loading

Container Architecture

What the Docker image looks like and where data lives:

graph TB
    subgraph Compose["Docker Compose"]
        INIT["init-permissions<br/>(alpine:3.21)<br/>chmod -R 777 /data"] -->|runs first| UI
        subgraph UI["hauler-ui Container (non-root)"]
            subgraph App["/app"]
                BE["backend binary"]
                FE["/app/frontend<br/>index.html, app.js,<br/>tailwind, fontawesome"]
            end
            BE -->|"serves"| FE
            BE -->|"shells out to"| HAULER["/usr/local/bin/hauler"]
        end
    end

    subgraph Volumes["Mounted Volumes"]
        V1["/data/store"] --- UI
        V2["/data/manifests"] --- UI
        V3["/data/hauls"] --- UI
        V4["/data/config<br/>(keys, certs, values,<br/>registries.json)"] --- UI
        V5["/data/extracted"] --- UI
    end

    subgraph Ports["Exposed Ports"]
        P1["8080 → Web UI"]
        P2["5000 → OCI Registry"]
        P3["8081 → File Server"]
    end
Loading

Security

A security review was run against the final codebase. 8 potential issues were identified and all have been resolved:

What How it's handled
API Authentication Optional API key via HAULER_UI_API_KEY env var. Bearer token on all /api/* routes.
Path Traversal safePath() calls filepath.Base() on every user-supplied filename before filepath.Join. Rejects .., ., empty.
XSS escapeHTML() for innerHTML, escapeAttr() for onclick/attribute contexts. Applied everywhere user data hits the DOM.
Credential Leaking redactArgs() strips --password/-p values before writing to the log buffer. Registry list masks passwords as ***.
WebSocket Hijacking CheckOrigin validates the Origin header matches the request Host.
Malformed Input All json.Decode calls check errors and return 400. All io.Copy calls check errors and return 500.
Content-Type application/json header set on every JSON response.
Cert Validation CA cert uploads are validated as proper PEM with x509 parsing before being stored.

Bug fixes worth calling out

These were found during code review and fixed before this PR:

  • Crash on unknown content typeaddContentHandler panicked on nil args if you passed something other than chart or image. Now returns 400.
  • Stale server status — after hauler store serve exited, the UI still showed "Running". The goroutine now clears serveCmd when the process dies.
  • Firefox tab switchingshowTab() relied on the implicit event global which doesn't exist in Firefox. Now passes event explicitly.
  • Broken file addaddFileToStore() had a variable scoped inside if/else branches but referenced outside them. Classic JS scoping bug → ReferenceError.
  • Duplicate YAMLgenerateYAML() output had a stray apiVersion: v1 when building charts-only manifests. Refactored to build each document independently.
  • WebSocket over HTTPS — hardcoded ws:// broke when served behind TLS. Now detects protocol.
  • Bind-mount permissions — DHI runtime image runs as non-root, but Docker creates host bind-mount dirs as root:root. Added an init-permissions service that fixes directory permissions before the main container starts, plus make run pre-creates the dirs as the host user.

Verification/Testing of Changes:

cd feat:dockerfile-webui
docker compose build
docker compose up -d
# Open http://localhost:8080

Things to poke at:

  • Switch through all tabs (especially in Firefox)
  • Add a chart and an image, check the store info updates
  • Upload/download/delete a manifest file
  • Try the manifest builder tab
  • Start and stop a registry serve, confirm status tracks correctly
  • Check the logs tab shows live output
  • Set HAULER_UI_API_KEY=test in docker-compose, restart, confirm you get prompted

API auth test:

# With HAULER_UI_API_KEY=secret
curl http://localhost:8080/api/store/info                              # 401
curl -H "Authorization: Bearer secret" http://localhost:8080/api/store/info  # 200
curl http://localhost:8080/api/health                                  # 200 (always open)

Additional Context:

This is entirely additive — no existing Hauler code was modified. The only file outside feat:dockerfile-webui/ is CLAUDE.md at the repo root (dev guidance for AI-assisted workflows, can be dropped if preferred).

The backend doesn't reimplement any Hauler logic — it just shells out to the hauler binary. So as hauler evolves, the UI stays in sync automatically.

derhornspieler and others added 3 commits February 10, 2026 00:11
Remove unrestricted /api/command endpoint that allowed arbitrary CLI execution,
add path traversal protection (safePath helper using filepath.Base) to all file
upload/download/delete handlers, add PEM certificate validation before CA trust
store installation, and add HTML escaping to all innerHTML interpolations to
prevent stored XSS. Also adds CLAUDE.md project guide.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace standard base images with DHI equivalents for zero-CVE runtime:
- Builder: golang:1.21-alpine → dhi.io/golang:1-alpine3.21-dev
- Obfuscator: node:18-alpine → dhi.io/node:23-alpine3.21-dev
- Runtime: alpine:latest → dhi.io/golang:1-alpine3.21 (no shell, no pkg mgr)

Move hauler install to a dedicated alpine:3.21 builder stage since the
runtime image has no shell. Use npx for javascript-obfuscator to handle
DHI node PATH differences. Remove update-ca-certificates call from
certUploadHandler as it's unavailable in the hardened runtime (SSL_CERT_FILE
env var already handles CA cert usage).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address remaining issues found during post-security-patch code review:
- Fix nil args panic in addContentHandler for unrecognized content types
- Fix variable scoping bug in addFileToStore causing ReferenceError
- Clear stale serveCmd after process exits to fix status reporting
- Apply safePath() to 10 remaining filepath.Join calls using user input
- Add escapeAttr() helper and apply to all onclick interpolations (XSS)
- Add error handling to 8 unchecked json.Decode calls (return 400)
- Check io.Copy errors in 5 upload handlers
- Set Content-Type: application/json on 12 direct json.NewEncoder calls
- Fix showTab implicit event global (Firefox compat) across 14 nav buttons
- Add missing manifest-builder tab section with nav button
- Fix generateYAML duplicate apiVersion when only charts present
- Use wss:// for WebSocket when served over HTTPS
- Add try/catch to apiCall for network error resilience
- Fix WebSocket ticker loop to break on write error
- Add fileserver port 8081 and extracted volume to docker-compose

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@derhornspieler
Copy link
Author

@zackbradys feel free to take this and use as you please.

derhornspieler and others added 4 commits February 10, 2026 00:26
…in check

Security fixes identified by Claude Code security review:

- Add optional API key authentication via HAULER_UI_API_KEY env var
  - When set, all /api/* endpoints require Authorization: Bearer <key>
  - Health endpoint exempt for monitoring
  - WebSocket supports api_key query parameter
  - Frontend prompts for key on 401 and stores in sessionStorage
- Redact --password and -p flag values from command log output
  to prevent credential exposure via WebSocket log stream
- Validate WebSocket Origin header against request Host to
  prevent Cross-Site WebSocket Hijacking (CSWSH)
- Add authFetch() wrapper for all direct fetch calls in frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The DHI runtime image runs as non-root, but Docker creates host bind-mount
directories as root:root — blocking writes from the container. Added an
init-permissions service that fixes directory permissions before hauler-ui
starts, and pre-creates data dirs in the Makefile run target.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed the stale Docker-WebUI-Feature/ subdirectory (84 files) that
duplicated the parent without any of the security fixes or DHI changes.

Updated README, SECURITY.md, and DEPLOYMENT_CHECKLIST to reflect current
state: DHI base images, init-permissions container, API key auth, credential
redaction, path traversal protection, and Mermaid architecture diagrams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To Triage

Development

Successfully merging this pull request may close these issues.

1 participant