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
- Variables
- type EventBus
- type HostSeenEvent
- type LifecycleEventEmitter
- type LoginResult
- type OIDC
- type SessionManager
- func (sm *SessionManager) CompleteTwoFactor(w http.ResponseWriter, r *http.Request, operatorID string) error
- func (sm *SessionManager) CurrentOperator(r *http.Request) *models.Operator
- func (sm *SessionManager) IsAuthenticated(r *http.Request) bool
- func (sm *SessionManager) Login(w http.ResponseWriter, r *http.Request, username, password string) (LoginResult, bool, error)
- func (sm *SessionManager) Logout(w http.ResponseWriter, r *http.Request)
- func (sm *SessionManager) PendingOperator(r *http.Request) *models.Operator
- func (sm *SessionManager) SetCSRFRandForTest(r io.Reader)
- func (sm *SessionManager) SetCookieSecure(secure bool)
- func (sm *SessionManager) StartAuthenticatedSession(w http.ResponseWriter, r *http.Request, op *models.Operator) error
- func (sm *SessionManager) StartCleanup(ctx context.Context, interval time.Duration)
- type Web
- func (w *Web) AllowSelfRegistration(allow bool)
- func (w *Web) ServeHTTP(rw http.ResponseWriter, r *http.Request)
- func (w *Web) Session() *SessionManager
- func (w *Web) StartSessionCleanup(ctx context.Context)
- func (w *Web) WithCAImportLimits(limits caimport.Limits)
- func (w *Web) WithCAImporter(importer caimport.Importer)
- func (w *Web) WithCAResolver(r *pki.CAResolver)
- func (w *Web) WithCookieSecure(secure bool)
- func (w *Web) WithEnrollmentTokenTTL(ttl time.Duration)
- func (w *Web) WithEventBus(bus *EventBus)
- func (w *Web) WithLifecycleEventEmitter(emitter LifecycleEventEmitter)
- func (w *Web) WithLoginRecorder(f func(result, factor string))
- func (w *Web) WithMaster(m *keystore.Master)
- func (w *Web) WithOIDC(o *OIDC)
- func (w *Web) WithPasswordPolicy(p auth.Policy)
- func (w *Web) WithRateLimiter(l *ratelimit.Limiter)
- func (w *Web) WithSecretIngressPolicy(policy secretingress.Policy)
Constants ¶
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 ¶
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.
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 (*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) 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 LoginResult ¶
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) 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 ¶
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 (*Web) AllowSelfRegistration ¶
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 ¶
StartSessionCleanup starts periodic removal of expired sessions. Stops when ctx is canceled.
func (*Web) WithCAImportLimits ¶ added in v0.8.0
WithCAImportLimits sets server-side encrypted-key resource caps.
func (*Web) WithCAImporter ¶ added in v0.8.0
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 ¶
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
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 ¶
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 ¶
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 ¶
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 ¶
WithOIDC attaches an OIDC provider and registers its login/callback routes. Must be called before ServeHTTP is invoked.
func (*Web) WithPasswordPolicy ¶
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 ¶
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.