feat: add Docker web UI for Hauler with security hardening#508
Open
derhornspieler wants to merge 7 commits intohauler-dev:mainfrom
Open
feat: add Docker web UI for Hauler with security hardening#508derhornspieler wants to merge 7 commits intohauler-dev:mainfrom
derhornspieler wants to merge 7 commits intohauler-dev:mainfrom
Conversation
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>
Author
|
@zackbradys feel free to take this and use as you please. |
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please check below, if the PR fulfills these requirements:
Associated Links:
feat:dockerfile-webui/feat:dockerfile-webui/README.md,feat:dockerfile-webui/docs/feat:dockerfile-webui/tests/Types of Changes:
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
HAULER_UI_API_KEYenv var to lock down the APIArchitecture
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)"]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 resultStore 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"] endBackend 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 --> WSContainer 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"] endSecurity
A security review was run against the final codebase. 8 potential issues were identified and all have been resolved:
HAULER_UI_API_KEYenv var. Bearer token on all/api/*routes.safePath()callsfilepath.Base()on every user-supplied filename beforefilepath.Join. Rejects..,., empty.escapeHTML()for innerHTML,escapeAttr()for onclick/attribute contexts. Applied everywhere user data hits the DOM.redactArgs()strips--password/-pvalues before writing to the log buffer. Registry list masks passwords as***.CheckOriginvalidates the Origin header matches the request Host.json.Decodecalls check errors and return 400. Allio.Copycalls check errors and return 500.application/jsonheader set on every JSON response.Bug fixes worth calling out
These were found during code review and fixed before this PR:
addContentHandlerpanicked on nil args if you passed something other thanchartorimage. Now returns 400.hauler store serveexited, the UI still showed "Running". The goroutine now clearsserveCmdwhen the process dies.showTab()relied on the impliciteventglobal which doesn't exist in Firefox. Now passeseventexplicitly.addFileToStore()had a variable scoped inside if/else branches but referenced outside them. Classic JS scoping bug → ReferenceError.generateYAML()output had a strayapiVersion: v1when building charts-only manifests. Refactored to build each document independently.ws://broke when served behind TLS. Now detects protocol.root:root. Added aninit-permissionsservice that fixes directory permissions before the main container starts, plusmake runpre-creates the dirs as the host user.Verification/Testing of Changes:
Things to poke at:
HAULER_UI_API_KEY=testin docker-compose, restart, confirm you get promptedAPI auth test:
Additional Context:
This is entirely additive — no existing Hauler code was modified. The only file outside
feat:dockerfile-webui/isCLAUDE.mdat 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
haulerbinary. So as hauler evolves, the UI stays in sync automatically.