package main import ( "context" "encoding/json" "fmt" "io" "log" "net" "net/http" "net/http/httputil" "strings" ) func main() { tr := &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return net.Dial("unix", "/var/run/docker.sock") }, DisableKeepAlives: true, } proxy := &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = "http" req.URL.Host = "docker" // dummy host, won't be used }, Transport: tr, ModifyResponse: redactSensitiveFields, } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("➡️ %s %s", r.Method, r.URL.Path) if !isAllowed(r.Method, r.URL.Path) { log.Printf("⛔ Blocked: %s %s", r.Method, r.URL.Path) http.Error(w, "Forbidden", http.StatusForbidden) return } proxy.ServeHTTP(w, r) }) log.Println("Unix socket proxy running on :80") log.Fatal(http.ListenAndServe(":80", nil)) } func isAllowed(method, path string) bool { if method == "GET" { if strings.HasSuffix(path, "/containers/json") || strings.HasSuffix(path, "/networks") || strings.HasSuffix(path, "/services") || strings.HasSuffix(path, "/version") || strings.HasSuffix(path, "/info") || strings.HasSuffix(path, "/events") || strings.HasSuffix(path, "/tasks") { log.Printf("➡️ isAllowed %s", path) return true } } if method == "POST" { if strings.HasSuffix(path, "/start") || strings.HasSuffix(path, "/stop") { return true } } return allowPath(path) } func redactSensitiveFields(resp *http.Response) error { data, err := io.ReadAll(resp.Body) if err != nil { log.Printf("❌ Error reading body: %v", err) return err } // Try to parse as a JSON object first var obj map[string]interface{} if err := json.Unmarshal(data, &obj); err == nil { log.Printf("➡️ Body (object): %+v", obj) // Redact sensitive fields from single object redactObject(obj) redacted, _ := json.Marshal(obj) resp.Body = io.NopCloser(strings.NewReader(string(redacted))) resp.ContentLength = int64(len(redacted)) resp.Header.Set("Content-Length", fmt.Sprint(len(redacted))) return nil } // Try to parse as a JSON array var arr []map[string]interface{} if err := json.Unmarshal(data, &arr); err == nil { log.Printf("➡️ Body (array): %d items", len(arr)) // Redact sensitive fields from each item in the array for _, item := range arr { redactObject(item) } redacted, _ := json.Marshal(arr) resp.Body = io.NopCloser(strings.NewReader(string(redacted))) resp.ContentLength = int64(len(redacted)) resp.Header.Set("Content-Length", fmt.Sprint(len(redacted))) return nil } // Fallback if it's neither an object nor an array log.Printf("⚠️ Failed to parse response body as JSON object or array") resp.Body = io.NopCloser(strings.NewReader(string(data))) resp.ContentLength = int64(len(data)) resp.Header.Set("Content-Length", fmt.Sprint(len(data))) return nil } // Only allow specific Docker API paths (e.g., container inspect, used by Traefik) func allowPath(path string) bool { log.Printf("Inspecting %s", path) // Example: /v1.24/containers//json parts := strings.Split(path, "/") log.Printf("Parts %d parts[0] %s", len(parts), parts) if len(parts) == 5 && parts[1] == "v1.24" && parts[2] == "containers" && parts[4] == "json" { log.Printf("Passes") return true } return false } func redactObject(obj map[string]interface{}) { // Top-level redactions delete(obj, "HostConfig") delete(obj, "GraphDriver") delete(obj, "Mounts") // Nested redactions if config, ok := obj["Config"].(map[string]interface{}); ok { delete(config, "Env") delete(config, "Volumes") delete(config, "User") } if netSettings, ok := obj["NetworkSettings"].(map[string]interface{}); ok { delete(netSettings, "SandboxID") delete(netSettings, "SandboxKey") } }