web

package
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 53 Imported by: 0

Documentation

Overview

Package web includes CSRF protection for /ui/* mutating endpoints.

Mechanism: stateless double-submit cookie. A non-HttpOnly cookie carries a 32-byte crypto/rand token. Every mutating request (POST/PUT/PATCH/DELETE) must echo the same token either in the X-CSRF-Token header (used by htmx) or in the _csrf form field (used by HTML forms). Comparison is constant-time.

Cookie rotation happens on every privilege transition (Login, OIDC StartAuthenticatedSession, CompleteTwoFactor, Logout) — see session.go.

Known limitations:

  • A compromised same-registrable-domain origin (sibling subdomain takeover) can set parent-domain cookies and forge a match. SameSite=Lax does not prevent this. This is an inherent limitation of stateless double-submit; mitigations would require server-side token storage and are out of scope for this fix.
  • The middleware calls r.ParseForm() for URL-encoded requests. Multipart bodies are scanned without populating MultipartForm/PostForm, restored for the handler, and zeroized after the handler returns. The router's 1 MiB MaxBytesReader runs first. JSON endpoints must use the header.
  • HttpOnly is intentionally false on _csrf cookie: the layout script reads it from document.cookie / meta tag to inject into htmx hx-headers. This means an XSS bug degrades CSRF protection to XSS-level. Content-Security-Policy headers (see GHSA-w7w5 fix) mitigate the XSS surface.

Index

Constants

View Source
const (
	SettingEnforceTOTP            = "enforce_2fa"
	SettingAllowSelfRegistration  = "allow_self_registration"
	SettingPasswordMinLength      = "password_min_length"
	SettingPasswordRequireClasses = "password_require_classes"
	SettingPasswordBlockCommon    = "password_block_common"
	SettingPasswordBlockUsername  = "password_block_username"
	SettingLogLevel               = "log_level"
)

Server-wide setting keys, mirrored as exported constants so callers (other packages, tests) can use the same canonical strings.

Variables

View Source
var ErrCAMasterNotConfigured = errors.New("ca master key not configured")

ErrCAMasterNotConfigured is returned by mintCAForOperator when the master keystore is not wired. Auto-provision skips silently on this error.

View Source
var ErrQRTooLarge = errors.New("text exceeds QR Level-L byte-mode capacity (2953 bytes)")

ErrQRTooLarge is returned when the input text exceeds the QR Level-L byte-mode capacity (2953 bytes).

Functions

This section is empty.

Types

type EventBus

type EventBus struct {
	// contains filtered or unexported fields
}

EventBus is a small in-memory publish/subscribe channel for host-state events. The API server publishes; the Web UI's SSE handler subscribes. Subscribers must drain quickly — publishes drop into a buffered queue and stale subscribers get unblocked by dropping the event, never the publisher.

func NewEventBus

func NewEventBus() *EventBus

NewEventBus creates a fresh bus.

func (*EventBus) Publish

func (b *EventBus) Publish(ev HostSeenEvent)

Publish fans the event out to every subscriber. Each subscriber's queue is small (8 events) so a stuck consumer drops events rather than stalling the agent-poll path that called Publish.

func (*EventBus) Stop

func (b *EventBus) Stop(ctx context.Context)

Stop closes every active subscription. Useful in tests.

func (*EventBus) Subscribe

func (b *EventBus) Subscribe() (<-chan HostSeenEvent, func())

Subscribe registers a new listener and returns the receive channel plus a cleanup func that must be called (typically via defer) when the subscriber is done.

type HostSeenEvent

type HostSeenEvent struct {
	HostID    string    `json:"host_id"`
	LastSeen  time.Time `json:"last_seen"`
	NetworkID string    `json:"network_id,omitempty"`
}

HostSeenEvent is the wire format the SSE stream pushes to each subscriber whenever a host's last_seen_at moves forward. The shape is intentionally flat so the htmx-sse swap in the Hosts table can target a specific row without an extra fetch.

type LifecycleEventEmitter added in v0.8.0

type LifecycleEventEmitter interface {
	Emit(scope webhook.Scope, eventType string, data map[string]any)
}

type LoginResult

type LoginResult struct {
	Operator  *models.Operator
	NeedsTOTP bool
}

LoginResult is the outcome of the first authentication step.

type OIDC

type OIDC struct {
	// contains filtered or unexported fields
}

OIDC encapsulates the configured OpenID Connect identity provider and translates a successful callback into a local operator session.

func NewOIDC

func NewOIDC(ctx context.Context, cfg *config.OIDCConfig, s store.Store, sm *SessionManager, logger *slog.Logger) (*OIDC, error)

NewOIDC builds an OIDC integration from the given config. It contacts the issuer to fetch the OIDC discovery document. Returns (nil, nil) if OIDC is disabled or unconfigured.

func (*OIDC) Enabled

func (o *OIDC) Enabled() bool

Enabled reports whether OIDC is configured.

func (*OIDC) HandleCallback

func (o *OIDC) HandleCallback(rw http.ResponseWriter, r *http.Request)

HandleCallback completes the OIDC flow, upserts the local operator, and establishes a session cookie.

func (*OIDC) HandleLogin

func (o *OIDC) HandleLogin(rw http.ResponseWriter, r *http.Request)

HandleLogin starts the OIDC authorization flow.

func (*OIDC) SetCookieSecure

func (o *OIDC) SetCookieSecure(secure bool)

SetCookieSecure controls the Secure attribute on the OIDC state cookie. Called at startup from cli/serve.go with the resolved server-config value.

type SessionManager

type SessionManager struct {
	// contains filtered or unexported fields
}

SessionManager handles DB-backed cookie sessions for operator users.

func NewSessionManager

func NewSessionManager(s store.Store) *SessionManager

NewSessionManager creates a new session manager backed by the given store.

func (*SessionManager) CompleteTwoFactor

func (sm *SessionManager) CompleteTwoFactor(w http.ResponseWriter, r *http.Request, operatorID string) error

CompleteTwoFactor promotes the current pending session to fully authenticated, refreshes the cookie expiry, and updates last_login_at.

On CSRF rotation entropy failure (#144), CompleteTwoFactor returns without promoting the session — the pending_totp row stays alive for its remaining TTL so the user can retry the second factor without re-entering the password.

func (*SessionManager) CurrentOperator

func (sm *SessionManager) CurrentOperator(r *http.Request) *models.Operator

CurrentOperator returns the operator owning the request's session cookie, or nil if there is no valid session. A disabled operator's session is also treated as invalid.

func (*SessionManager) IsAuthenticated

func (sm *SessionManager) IsAuthenticated(r *http.Request) bool

IsAuthenticated reports whether the request carries a valid session.

func (*SessionManager) Login

func (sm *SessionManager) Login(w http.ResponseWriter, r *http.Request, username, password string) (LoginResult, bool, error)

Login looks up the operator by username, verifies the password (bcrypt), records a session, sets the cookie, and returns the operator. The second return value is false when the credentials were invalid. When the operator has TOTP enabled, the created session is in `pending_totp` state and the caller must complete authentication via FinishTOTP.

func (*SessionManager) Logout

func (sm *SessionManager) Logout(w http.ResponseWriter, r *http.Request)

Logout invalidates the session cookie and removes the DB record.

func (*SessionManager) PendingOperator

func (sm *SessionManager) PendingOperator(r *http.Request) *models.Operator

PendingOperator returns the operator awaiting second-factor confirmation on the current session cookie, or nil if no pending session exists.

func (*SessionManager) SetCSRFRandForTest

func (sm *SessionManager) SetCSRFRandForTest(r io.Reader)

SetCSRFRandForTest swaps the CSRF entropy source on this SessionManager instance. Tests use this to inject a failing reader and pin the fail-closed rotation behavior; production callers never need it.

func (*SessionManager) SetCookieSecure

func (sm *SessionManager) SetCookieSecure(secure bool)

SetCookieSecure controls the Secure attribute on session cookies. Called at startup from cli/serve.go with the resolved server-config value. Closes GHSA-rqfj-vv8r-xhqc.

func (*SessionManager) StartAuthenticatedSession

func (sm *SessionManager) StartAuthenticatedSession(w http.ResponseWriter, r *http.Request, op *models.Operator) error

StartAuthenticatedSession creates a fully authenticated session for the given operator and sets the session cookie. Used by external login flows (e.g. OIDC) that have already verified the operator's identity.

func (*SessionManager) StartCleanup

func (sm *SessionManager) StartCleanup(ctx context.Context, interval time.Duration)

StartCleanup runs a background goroutine that periodically deletes expired sessions from the store. It stops when ctx is canceled.

type Web

type Web struct {
	// contains filtered or unexported fields
}

Web is the web UI handler.

func New

func New(s store.Store, logger *slog.Logger) (*Web, error)

New creates a new Web UI handler.

func (*Web) AllowSelfRegistration

func (w *Web) AllowSelfRegistration(allow bool)

AllowSelfRegistration enables the public /ui/register flow. Must be set before ServeHTTP is invoked. Default is false.

func (*Web) ServeHTTP

func (w *Web) ServeHTTP(rw http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

func (*Web) Session

func (w *Web) Session() *SessionManager

Session exposes the underlying session manager so callers can wire up alternative login flows (e.g. OIDC) that need to issue sessions.

func (*Web) StartSessionCleanup

func (w *Web) StartSessionCleanup(ctx context.Context)

StartSessionCleanup starts periodic removal of expired sessions. Stops when ctx is canceled.

func (*Web) WithCAImportLimits added in v0.8.0

func (w *Web) WithCAImportLimits(limits caimport.Limits)

WithCAImportLimits sets server-side encrypted-key resource caps.

func (*Web) WithCAImporter added in v0.8.0

func (w *Web) WithCAImporter(importer caimport.Importer)

WithCAImporter installs the process-shared importer used by API and Web.

func (*Web) WithCAResolver

func (w *Web) WithCAResolver(r *pki.CAResolver)

WithCAResolver wires a CA resolver for mobile bundle generation.

func (*Web) WithCookieSecure

func (w *Web) WithCookieSecure(secure bool)

WithCookieSecure threads the resolved cookie-secure flag through to the session manager and (if attached) OIDC. Call after WithOIDC so the OIDC state cookie picks up the same flag. Closes GHSA-rqfj-vv8r-xhqc.

func (*Web) WithEnrollmentTokenTTL added in v0.5.0

func (w *Web) WithEnrollmentTokenTTL(ttl time.Duration)

WithEnrollmentTokenTTL sets the default enrollment-token TTL applied to agent hosts created through the Web UI when no per-network override exists. Mirrors the API server's setter so both host-creation paths honor the same operator-configured lifetime policy. A non-positive ttl is ignored, leaving the resolver's built-in default in place. Closes GHSA-g4x6-jcvr-9m3g.

func (*Web) WithEventBus

func (w *Web) WithEventBus(bus *EventBus)

WithEventBus wires the bus the SSE endpoint listens on. Must be set before ServeHTTP is invoked. Without a bus, /ui/events 404s.

func (*Web) WithLifecycleEventEmitter added in v0.8.0

func (w *Web) WithLifecycleEventEmitter(emitter LifecycleEventEmitter)

func (*Web) WithLoginRecorder

func (w *Web) WithLoginRecorder(f func(result, factor string))

WithLoginRecorder wires an external sink (typically the API server's Prometheus metrics) that observes the outcome of every UI login attempt. Must be set before ServeHTTP is invoked. Passing nil disables recording.

func (*Web) WithMaster

func (w *Web) WithMaster(m *keystore.Master)

WithMaster wires the keystore master the CA-create handler needs. Without it, /ui/cas/new renders an inline error pointing at the NEBULA_MGMT_MASTER_KEY docs instead of failing with a 500.

func (*Web) WithOIDC

func (w *Web) WithOIDC(o *OIDC)

WithOIDC attaches an OIDC provider and registers its login/callback routes. Must be called before ServeHTTP is invoked.

func (*Web) WithPasswordPolicy

func (w *Web) WithPasswordPolicy(p auth.Policy)

WithPasswordPolicy installs the password policy used by registration and any future self-service password change. Defaults to auth.Default() when never called.

func (*Web) WithRateLimiter

func (w *Web) WithRateLimiter(l *ratelimit.Limiter)

WithRateLimiter wires a shared rate limiter so auth + UI routes pick up the same per-IP buckets the API server uses. nil disables limiting.

func (*Web) WithSecretIngressPolicy added in v0.8.0

func (w *Web) WithSecretIngressPolicy(policy secretingress.Policy)

WithSecretIngressPolicy sets the transport guard for private-key uploads.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL