// SPDX-License-Identifier: AGPL-3.0-or-later package main import ( "encoding/json" "net/http" "os" ) // devStatusResponse is the JSON shape returned by /admin/dev/web-status. // Reports the wiring state of each cmd/web dep without exposing the // underlying secret values. Used by operators curling the endpoint from // a tailnet host to debug wiring regressions when SSH into the staging // microvm isn't available (e.g., internal ref — Phase 5c-1 broke // /enroll on staging because the adminClient was unwired and there was // no in-VM way to confirm). type devStatusResponse struct { // BinaryPath is the running cmd/web binary's absolute path // (resolved via /proc/self/exe). Confirms the running closure // matches the latest deploy. BinaryPath string `json:"binaryPath"` // Wired reports the boolean status of each dep. Mirrors the // log line emitted at startup ("web.deps.5c-1_wired: …") so the // state is visible to a running diagnostic, not just journalctl. Wired struct { Funnel bool `json:"funnel"` DIDResolver bool `json:"didResolver"` LabelChecker bool `json:"labelChecker"` AdminClient bool `json:"adminClient"` SessionVerifier bool `json:"sessionVerifier"` } `json:"wired"` EnrollMounted bool `json:"enrollMounted"` AccountMounted bool `json:"accountMounted"` // SecretsCheck reports the on-disk state of each secret file that // cmd/web's wireFoundationDeps tries to read at startup. Whenever // a wired:false above is paired with the corresponding path being // missing/empty/unreadable, the diagnosis is at the deploy layer // (sops template not rendered, wrong path, wrong owner). When the // file looks fine but the dep is still nil, the issue is in the // loader (e.g., session secret < 32 bytes, base64 decode failed). // // Never includes the secret content — only path + existence + size // + (if any) the load error message. SecretsCheck struct { AdminTokenFile secretFileStatus `json:"adminTokenFile"` SessionSecretFile secretFileStatus `json:"sessionSecretFile"` } `json:"secretsCheck"` } // secretFileStatus is the per-secret-file diagnostic block. Path is // always the configured value (or empty); Exists/Size/LoadError are // only populated when Path is non-empty. type secretFileStatus struct { Path string `json:"path"` Exists bool `json:"exists"` Size int64 `json:"size"` LoadError string `json:"loadError,omitempty"` } // inspectSecretFile probes one secret file path: stats it, runs the // same loader cmd/web uses at startup, and reports any error string. // Never returns the loaded value — only the metadata that distinguishes // "file missing" from "file empty" from "loader rejected the format". func inspectSecretFile(path string, loader func(string) error) secretFileStatus { out := secretFileStatus{Path: path} if path == "" { return out } if info, err := os.Stat(path); err == nil { out.Exists = true out.Size = info.Size() } if err := loader(path); err != nil { out.LoadError = err.Error() } return out } // registerDevEndpoints attaches staging-only debug routes when // cfg.EnableDevEndpoints is true. atmos-staging.nix sets this to true; // prod leaves it false so this handler is never registered in // production binaries. // // The endpoint is a JSON status surface, not a control surface — it // only reflects state, never mutates. Even so, gate it explicitly to // avoid information leak about the cmd/web internals on prod. func registerDevEndpoints(mux *http.ServeMux, cfg *WebConfig, deps *foundationDeps, enrollMounted, accountMounted bool) { if cfg == nil || !cfg.EnableDevEndpoints { return } mux.HandleFunc("/admin/dev/web-status", func(w http.ResponseWriter, r *http.Request) { resp := devStatusResponse{ BinaryPath: binaryPath(), EnrollMounted: enrollMounted, AccountMounted: accountMounted, } if deps != nil { resp.Wired.Funnel = deps.funnel != nil resp.Wired.DIDResolver = deps.didResolver != nil resp.Wired.LabelChecker = deps.labelChecker != nil resp.Wired.AdminClient = deps.adminClient != nil resp.Wired.SessionVerifier = deps.sessionVerifier != nil } // Probe the configured secret-file paths fresh on every request // (they're tiny stat + read calls; this is staging-only). This // is what distinguishes "wired:false because path empty" from // "wired:false because file missing" from "wired:false because // loader rejected the contents". resp.SecretsCheck.AdminTokenFile = inspectSecretFile(cfg.AdminTokenFile, func(p string) error { _, err := loadAdminToken(p) return err }) resp.SecretsCheck.SessionSecretFile = inspectSecretFile(cfg.SessionSecretFile, func(p string) error { _, err := loadSessionSecret(p) return err }) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) }) } // binaryPath returns the absolute path of the running cmd/web binary // (resolved through /proc/self/exe on Linux). Lets the operator // confirm the running closure matches the latest deploy by comparing // against /var/lib/microvms/atmos-staging/booted's atmos-web ExecStart. // Falls back to os.Args[0] on platforms where /proc/self/exe doesn't // exist (macOS dev builds), which is fine — the binary path on dev is // less load-bearing than on staging. func binaryPath() string { if p, err := os.Readlink("/proc/self/exe"); err == nil { return p } if len(os.Args) > 0 { return os.Args[0] } return "" }