From 42da48864f07dbeb138d2531edb7db3217d04bea Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 17 Apr 2026 22:31:49 -0400 Subject: [PATCH] =?UTF-8?q?Remove=20operator=20auth=20=E2=80=94=20trust=20?= =?UTF-8?q?the=20LAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Can't log in from a fresh LXC deploy, and the service is LAN-only by design. Rip out the whole bcrypt-password / signed-cookie session layer: internal/auth, login templates, gen-admin-password binary + Makefile targets, auth config block, login/logout routes and the RequireSession middleware wrap. Agent bearer-token auth on /api/v1/runs/{id}/* is untouched. Operators who want a password can front the service with a reverse proxy — noted in README and docs/operations.md. --- Makefile | 10 +-- README.md | 18 ++--- cmd/vetting/main.go | 32 -------- deploy/install.sh | 29 +++---- deploy/proxmox-install.sh | 4 +- deploy/vetting.example.yaml | 9 --- deploy/vetting.production.yaml | 9 --- docs/architecture.md | 1 - docs/operations.md | 37 ++++----- internal/api/ui_handlers.go | 34 --------- internal/auth/middleware.go | 64 ---------------- internal/auth/session.go | 100 ------------------------- internal/config/config.go | 22 ------ internal/httpserver/router.go | 33 +++----- internal/web/templates/layout.templ | 3 - internal/web/templates/layout_templ.go | 4 +- internal/web/templates/login.templ | 20 ----- internal/web/templates/login_templ.go | 94 ----------------------- tools/gen-admin-password/main.go | 21 ------ 19 files changed, 52 insertions(+), 492 deletions(-) delete mode 100644 internal/auth/middleware.go delete mode 100644 internal/auth/session.go delete mode 100644 internal/web/templates/login.templ delete mode 100644 internal/web/templates/login_templ.go delete mode 100644 tools/gen-admin-password/main.go diff --git a/Makefile b/Makefile index d5377e0..2544194 100644 --- a/Makefile +++ b/Makefile @@ -28,14 +28,6 @@ agent: ## Build agent for host OS (handy for unit testing only — real agent ru agent-linux: ## Cross-build agent for linux-amd64 (consumed by live-image build) $(GOOS_LINUX) go build -ldflags="$(LDFLAGS)" -o bin/vetting-agent.linux-amd64 ./cmd/vetting-agent -.PHONY: gen-admin-password -gen-admin-password: ## Build the bcrypt password generator - go build -o bin/gen-admin-password$(if $(filter Windows%,$(UNAME_S)),.exe,) ./tools/gen-admin-password - -.PHONY: gen-admin-password-linux -gen-admin-password-linux: ## Cross-build the bcrypt password generator for linux-amd64 - $(GOOS_LINUX) go build -ldflags="$(LDFLAGS)" -o bin/gen-admin-password-linux-amd64 ./tools/gen-admin-password - .PHONY: tidy tidy: ## go mod tidy go mod tidy @@ -68,7 +60,7 @@ endif $(MAKE) -C live-image all .PHONY: all -all: orchestrator agent gen-admin-password ## Build everything buildable on host OS +all: orchestrator agent ## Build everything buildable on host OS .PHONY: run run: orchestrator ## Build and run orchestrator with example config diff --git a/README.md b/README.md index 3b0d2b0..a831a49 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,16 @@ notifications. ## Quick start (local, against QEMU) ```bash -# 1. Build make all - -# 2. Generate an admin password hash and paste it into the config. -./bin/gen-admin-password 'your-password' -# Edit deploy/vetting.example.yaml: -# auth.admin_password_bcrypt = -# auth.session_secret_hex = $(openssl rand -hex 32) - -# 3. Run ./bin/vetting --config deploy/vetting.example.yaml # → http://localhost:8080 ``` +The UI has no built-in auth — bind to loopback or LAN only, or front +the service with a reverse proxy (Caddy/nginx basic-auth) if you +want a password. The agent↔orchestrator channel keeps its own +bearer-token auth and is unaffected. + For a full end-to-end QEMU walk-through (bridge setup, host registration, PXE boot), see [docs/operations.md § First vetting run](docs/operations.md#first-vetting-run). @@ -53,7 +49,7 @@ which lays down the binary, systemd unit, example config, and `vetting` service user. Then: ```bash -# Edit /etc/vetting/vetting.yaml (bcrypt password, session secret, public URL) +# Edit /etc/vetting/vetting.yaml (server.bind + server.public_url) sudo systemctl enable --now vetting journalctl -fu vetting ``` @@ -80,7 +76,7 @@ live-image/ mkosi config for the PXE-bootable Debian live image deploy/ systemd unit + install.sh + example config docs/ operator + developer docs test/e2e/ build-tag-gated QEMU + PXE full-stack test -tools/ small CLI helpers (e.g. gen-admin-password) +tools/ small CLI helpers ``` ## Development diff --git a/cmd/vetting/main.go b/cmd/vetting/main.go index 9684211..54e997a 100644 --- a/cmd/vetting/main.go +++ b/cmd/vetting/main.go @@ -14,7 +14,6 @@ import ( "time" "vetting/internal/api" - "vetting/internal/auth" "vetting/internal/config" "vetting/internal/db" "vetting/internal/events" @@ -54,19 +53,6 @@ func main() { } defer func() { _ = conn.Close() }() - secret, err := cfg.Auth.SessionSecret() - if err != nil { - log.Fatalf("auth: %v", err) - } - authMgr := &auth.Manager{ - PasswordHash: cfg.Auth.AdminPasswordBcrypt, - Secret: secret, - TTL: time.Duration(cfg.Auth.SessionTTLHours) * time.Hour, - } - if err := validateAuth(cfg, authMgr); err != nil { - log.Fatalf("auth: %v", err) - } - hostStore := &store.Hosts{DB: conn} runStore := &store.Runs{DB: conn} stageStore := &store.Stages{DB: conn} @@ -113,7 +99,6 @@ func main() { Hosts: hostStore, Runs: runStore, Artifacts: artifactStore, - Auth: authMgr, EventHub: hub, Runner: runner, Tiles: tiles, @@ -163,7 +148,6 @@ func main() { } router := httpserver.NewRouter(httpserver.Deps{ - Auth: authMgr, UI: ui, Agent: agentAPI, LiveDir: cfg.PXE.LiveDir, @@ -231,19 +215,3 @@ func main() { } _ = hub.Shutdown(ctx) } - -func validateAuth(cfg *config.Config, _ *auth.Manager) error { - if cfg.Auth.AdminPasswordBcrypt == "" || cfg.Auth.AdminPasswordBcrypt == "$2a$10$REPLACE_ME_WITH_A_REAL_BCRYPT_HASH_0123456789abcdefABCDEFxx" { - return errPlaceholderPassword - } - if len(cfg.Auth.AdminPasswordBcrypt) < 4 || cfg.Auth.AdminPasswordBcrypt[0] != '$' { - return errPlaceholderPassword - } - return nil -} - -var errPlaceholderPassword = plainErr("auth.admin_password_bcrypt is the placeholder; run bin/gen-admin-password and paste the hash into your config") - -type plainErr string - -func (e plainErr) Error() string { return string(e) } diff --git a/deploy/install.sh b/deploy/install.sh index 3084cab..09e3360 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -7,9 +7,9 @@ # 2. Creates the `vetting` system user with /var/lib/vetting homedir. # 3. Copies the pre-built `vetting` binary into /usr/local/bin. # 4. Drops the systemd unit and example config into /etc/vetting. -# 5. Reminds the operator to edit the config and set a bcrypt -# password before enabling the service — we don't auto-start -# because a placeholder password would just refuse to boot. +# 5. Reminds the operator to edit the config before enabling +# the service — we don't auto-start because the default bind +# is loopback-only and needs at least a tweak to be useful. # # What it deliberately does NOT do: # - Build the orchestrator (this script assumes you ran @@ -95,20 +95,6 @@ install -d -m 0755 "${CONFIG_DIR}" echo "==> installing binary" install -m 0755 "${BINARY}" /usr/local/bin/vetting -# Install the bcrypt password generator too if we can find it — the -# operator needs it to fill in auth.admin_password_bcrypt. -GEN_PW="" -for cand in \ - "${REPO_ROOT}/bin/gen-admin-password-linux-amd64" \ - "${REPO_ROOT}/bin/gen-admin-password" \ - "${SCRIPT_DIR}/gen-admin-password"; do - if [[ -x "${cand}" ]]; then GEN_PW="${cand}"; break; fi -done -if [[ -n "${GEN_PW}" ]]; then - echo "==> installing gen-admin-password" - install -m 0755 "${GEN_PW}" /usr/local/bin/gen-admin-password -fi - echo "==> installing config and systemd unit" # vetting.production.yaml uses absolute /var/lib/vetting + /var/log/vetting # paths that match the systemd unit's ReadWritePaths. vetting.example.yaml @@ -140,8 +126,9 @@ vetting is installed but not yet enabled. Next steps: 1. Edit ${CONFIG_DIR}/vetting.yaml and set: - - auth.admin_password_bcrypt (run: gen-admin-password 'YOURPW') - - auth.session_secret_hex (run: openssl rand -hex 32) + - server.bind (127.0.0.1:8080 by default; switch to + 0.0.0.0:8080 once you're ready to expose + it on the LAN) - server.public_url (the URL you'll browse to) - pxe.* if you want PXE boot support - notifiers + routes (optional) @@ -150,4 +137,8 @@ Next steps: 3. Watch the logs: journalctl -fu vetting +The UI has no built-in auth — it trusts the LAN. If you need a +password, front the service with a reverse proxy (Caddy/nginx +basic-auth) instead. + EOF diff --git a/deploy/proxmox-install.sh b/deploy/proxmox-install.sh index 9de6534..bfcf3a1 100644 --- a/deploy/proxmox-install.sh +++ b/deploy/proxmox-install.sh @@ -65,9 +65,9 @@ fi echo "==> installing templ ${TEMPL_VERSION}" GOBIN=/usr/local/bin go install "github.com/a-h/templ/cmd/templ@${TEMPL_VERSION}" -echo "==> building orchestrator + gen-admin-password" +echo "==> building orchestrator (make orchestrator-linux)" cd "${SRC_DIR}" -make orchestrator-linux gen-admin-password-linux +make orchestrator-linux echo "==> running deploy/install.sh" bash deploy/install.sh --binary "bin/vetting-linux-amd64" diff --git a/deploy/vetting.example.yaml b/deploy/vetting.example.yaml index 823e9ee..f1d6670 100644 --- a/deploy/vetting.example.yaml +++ b/deploy/vetting.example.yaml @@ -28,15 +28,6 @@ janitor: # Interval between cleanup sweeps. 0 defaults to 60. interval_minutes: 60 -auth: - # bcrypt hash of your admin password. - # Generate via: ./bin/gen-admin-password "your-password" - admin_password_bcrypt: "$2a$10$REPLACE_ME_WITH_A_REAL_BCRYPT_HASH_0123456789abcdefABCDEFxx" - # Random 32-byte hex string used to sign session cookies. - # Generate via: openssl rand -hex 32 (or use PowerShell equivalent) - session_secret_hex: "0000000000000000000000000000000000000000000000000000000000000000" - session_ttl_hours: 24 - dispatcher: max_concurrent_runs: 3 diff --git a/deploy/vetting.production.yaml b/deploy/vetting.production.yaml index 1da1190..6f584a2 100644 --- a/deploy/vetting.production.yaml +++ b/deploy/vetting.production.yaml @@ -28,15 +28,6 @@ janitor: # Interval between cleanup sweeps. 0 defaults to 60. interval_minutes: 60 -auth: - # bcrypt hash of your admin password. - # Generate via: gen-admin-password 'your-password' - admin_password_bcrypt: "$2a$10$REPLACE_ME_WITH_A_REAL_BCRYPT_HASH_0123456789abcdefABCDEFxx" - # Random 32-byte hex string used to sign session cookies. - # Generate via: openssl rand -hex 32 - session_secret_hex: "0000000000000000000000000000000000000000000000000000000000000000" - session_ttl_hours: 24 - dispatcher: max_concurrent_runs: 3 diff --git a/docs/architecture.md b/docs/architecture.md index 7960ec9..9a0443f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,7 +45,6 @@ Operator browser (HTMX + SSE, admin login) | `internal/api` | HTTP handlers: `agent_handlers.go` (the agent-facing API) and `ui_handlers.go` (HTMX fragments + SSE). | | `internal/httpserver` | chi router assembly — lives here to avoid `api ↔ orchestrator` cyclic imports. | | `internal/web` | Embedded static assets + compiled Templ templates. | -| `internal/auth` | Single-admin bcrypt + signed-cookie sessions. | | `internal/pxe` | dnsmasq subprocess supervisor + per-MAC iPXE script generator. | | `internal/events` | In-process SSE hub (fan-out to live browser clients). | | `internal/logs` | Per-run flat-file writer + SSE fan-out of live log tail. | diff --git a/docs/operations.md b/docs/operations.md index 6501788..0dc01c8 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -37,25 +37,18 @@ repaired nodes so DHCP and WoL work. - disables the distro-default dnsmasq (the orchestrator supervises its own) - The installer does **not** enable the service, because the default - config has a placeholder bcrypt password that the binary refuses to - start with. + The installer does **not** enable the service. You'll want to edit + the config first. -3. Generate an admin password hash and a session secret, then edit - `/etc/vetting/vetting.yaml`: +3. Edit `/etc/vetting/vetting.yaml`: - ``` - ./bin/gen-admin-password 'your-password-here' # prints a bcrypt hash - openssl rand -hex 32 # prints a 64-char hex string - ``` - - Required fields: - - `auth.admin_password_bcrypt` — the bcrypt hash - - `auth.session_secret_hex` — the 32-byte hex string + - `server.bind` — defaults to `127.0.0.1:8080`. Switch to + `0.0.0.0:8080` (or bind to a specific LAN IP) once you're ready + to expose it. There is no built-in auth — see *Exposing outside + the LAN* below. - `server.public_url` — the URL your browser hits the LXC on - (e.g. `https://vetting.lan:8443`). This is used as the - click-through link in notifications, so it must be the *external* - URL, not the bind address. + (e.g. `http://vetting.lan:8080`). Used as the click-through link + in notifications. 4. (Optional) Configure notifiers in the same file — see the commented-out example block for ntfy / Discord / SMTP. @@ -79,7 +72,7 @@ Against a QEMU VM first, before you point it at real hardware: sudo ip link set br-vetting up ``` -2. In the UI at `https://:8443`, log in and register a host: +2. In the UI at `http://:8080`, register a host: - Name: `qemu-test` - MAC: `52:54:00:12:34:56` - WoL broadcast IP: `10.77.0.255` @@ -145,11 +138,19 @@ Retention is governed by the `artifacts.retention_days` and `logs.retention_days` settings. DB rows (run history) are preserved indefinitely; only on-disk files get pruned. +## Exposing outside the LAN + +The orchestrator UI has no built-in auth. It's designed to live on a +trusted home LAN and trust whatever reaches it. If you want to reach +it from outside that LAN, don't expose the bind port directly — put +it behind a reverse proxy (Caddy, nginx, Traefik) that terminates TLS +and adds basic-auth or OIDC. The agent↔orchestrator bearer token +auth is independent and keeps working either way. + ## Troubleshooting | Symptom | First check | |---|---| -| Service refuses to start with `auth.admin_password_bcrypt is the placeholder` | You didn't replace the bcrypt hash in the config. Run `gen-admin-password`. | | PXE client gets no DHCP offer | `journalctl -u vetting` for dnsmasq errors; confirm the LXC has `CAP_NET_ADMIN` (the shipped systemd unit does); confirm the host MAC is actually registered (`sqlite3 /var/lib/vetting/vetting.db 'SELECT name, mac FROM hosts;'`). | | Agent `/hello` never fires | Check the live image is actually loading the agent binary — SSH into the live env (use the hold key path), `systemctl status vetting-agent`. | | Tile stuck on `Booting` | Most likely the live image booted but the agent can't reach the orchestrator. Verify `vetting.orchestrator=` in the kernel cmdline resolves from the host's network. | diff --git a/internal/api/ui_handlers.go b/internal/api/ui_handlers.go index 1f39ef1..0ba0591 100644 --- a/internal/api/ui_handlers.go +++ b/internal/api/ui_handlers.go @@ -11,7 +11,6 @@ import ( "github.com/go-chi/chi/v5" "gopkg.in/yaml.v3" - "vetting/internal/auth" "vetting/internal/events" "vetting/internal/model" "vetting/internal/orchestrator" @@ -23,7 +22,6 @@ type UI struct { Hosts *store.Hosts Runs *store.Runs Artifacts *store.Artifacts - Auth *auth.Manager EventHub *events.Hub Runner *orchestrator.Runner Tiles *TileEnricher @@ -93,38 +91,6 @@ func (u *UI) StartRun(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusSeeOther) } -func (u *UI) LoginForm(w http.ResponseWriter, r *http.Request) { - next := r.URL.Query().Get("next") - if next == "" { - next = "/" - } - _ = templates.Login("", next).Render(r.Context(), w) -} - -func (u *UI) LoginSubmit(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, "bad form", http.StatusBadRequest) - return - } - password := r.PostForm.Get("password") - next := r.PostForm.Get("next") - if next == "" || !strings.HasPrefix(next, "/") { - next = "/" - } - if !u.Auth.VerifyPassword(password) { - w.WriteHeader(http.StatusUnauthorized) - _ = templates.Login("Invalid password.", next).Render(r.Context(), w) - return - } - u.Auth.Issue(w, r) - http.Redirect(w, r, next, http.StatusSeeOther) -} - -func (u *UI) Logout(w http.ResponseWriter, r *http.Request) { - u.Auth.Clear(w) - http.Redirect(w, r, "/login", http.StatusSeeOther) -} - func (u *UI) NewHostForm(w http.ResponseWriter, r *http.Request) { _ = templates.Registration(templates.RegistrationForm{}).Render(r.Context(), w) } diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go deleted file mode 100644 index 3798de9..0000000 --- a/internal/auth/middleware.go +++ /dev/null @@ -1,64 +0,0 @@ -package auth - -import ( - "net/http" -) - -// RequireSession redirects unauthenticated requests to /login. -func (m *Manager) RequireSession(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if err := m.Validate(r); err != nil { - if acceptsHTML(r) { - http.Redirect(w, r, "/login?next="+r.URL.RequestURI(), http.StatusSeeOther) - return - } - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - next.ServeHTTP(w, r) - }) -} - -func acceptsHTML(r *http.Request) bool { - accept := r.Header.Get("Accept") - if accept == "" { - return true - } - for _, part := range splitComma(accept) { - if part == "text/html" || part == "*/*" { - return true - } - } - return false -} - -func splitComma(s string) []string { - var out []string - start := 0 - for i := 0; i < len(s); i++ { - if s[i] == ',' { - out = append(out, trimSpace(s[start:i])) - start = i + 1 - } else if s[i] == ';' { - out = append(out, trimSpace(s[start:i])) - for i < len(s) && s[i] != ',' { - i++ - } - start = i + 1 - } - } - if start < len(s) { - out = append(out, trimSpace(s[start:])) - } - return out -} - -func trimSpace(s string) string { - for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') { - s = s[1:] - } - for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') { - s = s[:len(s)-1] - } - return s -} diff --git a/internal/auth/session.go b/internal/auth/session.go deleted file mode 100644 index a0fb363..0000000 --- a/internal/auth/session.go +++ /dev/null @@ -1,100 +0,0 @@ -package auth - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "errors" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "golang.org/x/crypto/bcrypt" -) - -const cookieName = "vetting_session" - -type Manager struct { - PasswordHash string - Secret []byte - TTL time.Duration -} - -func (m *Manager) VerifyPassword(password string) bool { - if m.PasswordHash == "" { - return false - } - return bcrypt.CompareHashAndPassword([]byte(m.PasswordHash), []byte(password)) == nil -} - -// Issue writes a signed session cookie valid for m.TTL. -func (m *Manager) Issue(w http.ResponseWriter, r *http.Request) { - expiry := time.Now().Add(m.TTL).Unix() - payload := strconv.FormatInt(expiry, 10) - sig := m.sign(payload) - value := payload + "." + sig - - http.SetCookie(w, &http.Cookie{ - Name: cookieName, - Value: value, - Path: "/", - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - Expires: time.Unix(expiry, 0), - }) -} - -func (m *Manager) Clear(w http.ResponseWriter) { - http.SetCookie(w, &http.Cookie{ - Name: cookieName, - Value: "", - Path: "/", - HttpOnly: true, - MaxAge: -1, - }) -} - -var errInvalidSession = errors.New("invalid session") - -// Validate returns nil if the request's cookie is present, signed, and not expired. -func (m *Manager) Validate(r *http.Request) error { - c, err := r.Cookie(cookieName) - if err != nil { - return errInvalidSession - } - parts := strings.SplitN(c.Value, ".", 2) - if len(parts) != 2 { - return errInvalidSession - } - payload, sig := parts[0], parts[1] - expected := m.sign(payload) - if !hmac.Equal([]byte(sig), []byte(expected)) { - return errInvalidSession - } - expiry, err := strconv.ParseInt(payload, 10, 64) - if err != nil { - return errInvalidSession - } - if time.Now().Unix() >= expiry { - return errInvalidSession - } - return nil -} - -func (m *Manager) sign(payload string) string { - mac := hmac.New(sha256.New, m.Secret) - _, _ = mac.Write([]byte(payload)) - return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) -} - -// BcryptHash is a helper used by the gen-admin-password tool. -func BcryptHash(password string) (string, error) { - b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", fmt.Errorf("bcrypt: %w", err) - } - return string(b), nil -} diff --git a/internal/config/config.go b/internal/config/config.go index 0675980..9fe2acf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,6 @@ package config import ( - "encoding/hex" "fmt" "os" @@ -13,7 +12,6 @@ type Config struct { Database Database `yaml:"database"` Artifacts Artifacts `yaml:"artifacts"` Logs Logs `yaml:"logs"` - Auth Auth `yaml:"auth"` Dispatcher Dispatcher `yaml:"dispatcher"` Janitor Janitor `yaml:"janitor"` PXE PXE `yaml:"pxe"` @@ -52,23 +50,6 @@ type Janitor struct { IntervalMinutes int `yaml:"interval_minutes"` // 0 = 60 } -type Auth struct { - AdminPasswordBcrypt string `yaml:"admin_password_bcrypt"` - SessionSecretHex string `yaml:"session_secret_hex"` - SessionTTLHours int `yaml:"session_ttl_hours"` -} - -func (a Auth) SessionSecret() ([]byte, error) { - b, err := hex.DecodeString(a.SessionSecretHex) - if err != nil { - return nil, fmt.Errorf("session_secret_hex: %w", err) - } - if len(b) < 32 { - return nil, fmt.Errorf("session_secret_hex must decode to at least 32 bytes, got %d", len(b)) - } - return b, nil -} - type Dispatcher struct { MaxConcurrentRuns int `yaml:"max_concurrent_runs"` } @@ -132,9 +113,6 @@ func Load(path string) (*Config, error) { if c.Logs.Dir == "" { c.Logs.Dir = "./var/logs" } - if c.Auth.SessionTTLHours == 0 { - c.Auth.SessionTTLHours = 24 - } if c.Dispatcher.MaxConcurrentRuns == 0 { c.Dispatcher.MaxConcurrentRuns = 3 } diff --git a/internal/httpserver/router.go b/internal/httpserver/router.go index ab02b71..bd9b9b5 100644 --- a/internal/httpserver/router.go +++ b/internal/httpserver/router.go @@ -11,12 +11,10 @@ import ( "github.com/go-chi/chi/v5/middleware" "vetting/internal/api" - "vetting/internal/auth" "vetting/internal/web" ) type Deps struct { - Auth *auth.Manager UI *api.UI Agent *api.Agent LiveDir string // directory containing vmlinuz + initrd.img; "" disables /live @@ -38,13 +36,8 @@ func NewRouter(d Deps) http.Handler { r.Handle("/live/*", http.StripPrefix("/live/", http.FileServer(http.Dir(d.LiveDir)))) } - // Public (no session required) endpoints. - r.Get("/login", d.UI.LoginForm) - r.Post("/login", d.UI.LoginSubmit) - r.Post("/logout", d.UI.Logout) - // Agent / PXE endpoints — authenticated per-request by bearer token - // or by the unforgeable MAC path parameter, never by the UI session. + // or by the unforgeable MAC path parameter. r.Get("/ipxe/{mac}", d.Agent.IPXEScript) r.Route("/api/v1/runs/{id}", func(r chi.Router) { r.Post("/hello", d.Agent.Hello) @@ -56,20 +49,16 @@ func NewRouter(d Deps) http.Handler { r.Post("/sensor", d.Agent.Sensor) }) - // Session-gated browser UI. - r.Group(func(r chi.Router) { - r.Use(d.Auth.RequireSession) - - r.Get("/", d.UI.Dashboard) - r.Get("/hosts/new", d.UI.NewHostForm) - r.Post("/hosts", d.UI.CreateHost) - r.Post("/hosts/{id}/delete", d.UI.DeleteHost) - r.Post("/hosts/{id}/start", d.UI.StartRun) - r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage) - r.Get("/reports/{runID}", d.UI.Report) - - r.Get("/events", d.UI.SSE) - }) + // Browser UI — no auth; bind to loopback or LAN only, or front + // with a reverse proxy if you need a password. + r.Get("/", d.UI.Dashboard) + r.Get("/hosts/new", d.UI.NewHostForm) + r.Post("/hosts", d.UI.CreateHost) + r.Post("/hosts/{id}/delete", d.UI.DeleteHost) + r.Post("/hosts/{id}/start", d.UI.StartRun) + r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage) + r.Get("/reports/{runID}", d.UI.Report) + r.Get("/events", d.UI.SSE) return r } diff --git a/internal/web/templates/layout.templ b/internal/web/templates/layout.templ index aa36f7e..1641f51 100644 --- a/internal/web/templates/layout.templ +++ b/internal/web/templates/layout.templ @@ -20,9 +20,6 @@ templ Layout(title string) {
· -
- -
diff --git a/internal/web/templates/layout_templ.go b/internal/web/templates/layout_templ.go index bf4ac34..1dfc9aa 100644 --- a/internal/web/templates/layout_templ.go +++ b/internal/web/templates/layout_templ.go @@ -42,7 +42,7 @@ func Layout(title string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting
Vetting
·
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting
Vetting
·
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -86,7 +86,7 @@ func BareLayout(title string) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 41, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 38, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { diff --git a/internal/web/templates/login.templ b/internal/web/templates/login.templ deleted file mode 100644 index 8dbd3d4..0000000 --- a/internal/web/templates/login.templ +++ /dev/null @@ -1,20 +0,0 @@ -package templates - -templ Login(errMsg, next string) { - @BareLayout("Sign in") { - - } -} diff --git a/internal/web/templates/login_templ.go b/internal/web/templates/login_templ.go deleted file mode 100644 index 046d1eb..0000000 --- a/internal/web/templates/login_templ.go +++ /dev/null @@ -1,94 +0,0 @@ -// Code generated by templ - DO NOT EDIT. - -// templ: version: v0.3.1001 -package templates - -//lint:file-ignore SA4006 This context is only used if a nested component is present. - -import "github.com/a-h/templ" -import templruntime "github.com/a-h/templ/runtime" - -func Login(errMsg, next string) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var1 := templ.GetChildren(ctx) - if templ_7745c5c3_Var1 == nil { - templ_7745c5c3_Var1 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Vetting

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if errMsg != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/login.templ`, Line: 8, Col: 31} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) - templ_7745c5c3_Err = BareLayout("Sign in").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -var _ = templruntime.GeneratedTemplate diff --git a/tools/gen-admin-password/main.go b/tools/gen-admin-password/main.go deleted file mode 100644 index d4f3f3e..0000000 --- a/tools/gen-admin-password/main.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "vetting/internal/auth" -) - -func main() { - if len(os.Args) != 2 { - fmt.Fprintln(os.Stderr, "usage: gen-admin-password ") - os.Exit(2) - } - hash, err := auth.BcryptHash(os.Args[1]) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - fmt.Println(hash) -}