Documentation
¶
Index ¶
- Constants
- Variables
- func FriendlyAddrError(field, value string) string
- func FriendlyPrefixError(field, value string) string
- func HashEnrollmentToken(raw string) string
- func HashSessionToken(raw string) string
- func HostDiff(before, after *Host) ([]byte, bool, error)
- func ValidKind(k HostKind) bool
- func ValidOperatorRole(r string) bool
- func ValidRole(r HostRole) bool
- func ValidVariant(v HostVariant) bool
- func ValidateCIDR(field, value string) (netip.Prefix, error)
- func ValidateHostAddresses(addrs []string) error
- func ValidateHostAdvanced(adv *HostAdvanced) error
- func ValidateIPAddr(field, value string) (netip.Addr, error)
- func ValidateMobileConstraints(kind HostKind, variant HostVariant, role HostRole) error
- func ValidateNetworkCIDRs(cidrs []string) error
- func ValidateRoleReachability(role HostRole, publicIP string, listenPort int) error
- type AuditEntry
- type CA
- type CAStatus
- type CertificateInfo
- type EnrollmentToken
- type Host
- type HostAdvanced
- type HostKind
- type HostRole
- type HostStatus
- type HostVariant
- type Network
- type Operator
- type OperatorAPIKey
- type OperatorAuthProvider
- type OperatorSession
- type OperatorStatus
- type SessionState
- type UnsafeRoute
- type WebhookSubscription
Constants ¶
const ( OperatorRoleAdmin = "admin" OperatorRoleUser = "user" )
Operator roles. Operator.Role is a plain string; these are the only two values the server accepts.
const MaxAddressesPerHost = 16
MaxAddressesPerHost is the maximum number of overlay addresses a host can have. This is a soft limit to prevent cert bloat; Nebula v2 certs have no hard limit but practical deployments should not exceed this without good reason.
Variables ¶
var ErrMobileRoleRestricted = errors.New("mobile hosts must have role=host (lighthouse/relay not supported)")
ErrMobileRoleRestricted is returned when a mobile host is assigned a role other than host (e.g., lighthouse or relay). Mobile Nebula clients cannot reliably listen on a public socket in the background, so these roles are not supported for mobile hosts.
var ErrMobileVariantRequired = errors.New("mobile hosts must have variant set to ios or android")
ErrMobileVariantRequired is returned when a mobile host is created without specifying a variant (ios or android). The variant field is required to distinguish mobile client types.
var ErrRoleRequiresListenPort = errors.New("listen_port is required when role is lighthouse or relay")
ErrRoleRequiresListenPort is the listen_port counterpart of ErrRoleRequiresPublicIP: peers compose static_host_map entries as "public_ip:listen_port", so a zero port produces the same silent failure.
var ErrRoleRequiresPublicIP = errors.New("public_ip is required when role is lighthouse or relay")
ErrRoleRequiresPublicIP is returned when a host with role=lighthouse or role=relay is created without a non-empty public_ip. Such a host would never be advertised to peers (see internal/api/enroll.go where static_host_map / lighthouse.hosts only include hosts whose PublicIP is set), so accept-and-silently-drop is replaced with reject-at-create.
Functions ¶
func FriendlyAddrError ¶
FriendlyAddrError returns a stable user-facing message for an IP-parse failure. The Go stdlib's netip.ParseAddr error text is intentionally dropped — strings like `ParseAddr("10.42.0.22.333"): IPv4 address too long` are diagnostic for Go authors but useless to operators typing into a form. Callers pass the form field name (or empty for unqualified messages) and the operator-supplied value so the message identifies both what is wrong and where.
func FriendlyPrefixError ¶
FriendlyPrefixError is the CIDR counterpart of FriendlyAddrError.
func HashEnrollmentToken ¶
HashEnrollmentToken produces the at-rest representation of a raw enrollment token. Closes GHSA-ghmh-jhmj-wcmf: previously the raw UUID was stored verbatim in enrollment_tokens.token, allowing anyone with read access to the DB (backup, snapshot, future SQL-injection sink) to consume pending tokens before the legitimate agent.
Symmetric: same input → same hex → constant-time DB lookup. Mirrors the operator_api_keys hashing already done in internal/api/middleware.go.
func HashSessionToken ¶ added in v0.3.8
HashSessionToken produces the at-rest representation of a raw operator session token. Closes GHSA-q4vm-pq3q-8wgq: previously the raw 32-byte hex token was stored verbatim in operator_sessions.token and sent in the session cookie, so anyone with read access to the DB (backup, snapshot, future SQL-injection sink) could hijack every active operator session.
Symmetric: same input → same hex → constant-time DB lookup. Mirrors the enrollment-token and operator_api_keys hashing already done elsewhere.
func HostDiff ¶
HostDiff computes the difference between two hosts and returns a JSON-encoded map of changed fields. Returns (nil, false, nil) if no fields differ.
For basic fields (Name, NebulaIPs, Groups, Role, PublicIP, ListenPort), the diff key is the field name in snake_case.
For Advanced sub-fields (ListenHost, MTU, TunDevice, Punchy, UnsafeRoutes), the diff key uses dot-notation: "advanced.mtu", "advanced.punchy", etc.
The JSON format for each changed field is: {"field_name": {"before": <value>, "after": <value>}}
If before is nil, zero values are used. If before.Advanced is nil, zero values are used for sub-field comparisons.
func ValidOperatorRole ¶ added in v0.4.0
ValidOperatorRole reports whether r is a known operator role.
func ValidVariant ¶
func ValidVariant(v HostVariant) bool
ValidVariant reports whether v is a known host variant (including empty).
func ValidateCIDR ¶
ValidateCIDR is a thin wrapper around netip.ParsePrefix that converts a failure into a user-facing FriendlyPrefixError.
func ValidateHostAddresses ¶
ValidateHostAddresses validates a list of overlay addresses for a host. It checks that: - The list is non-empty - Each address is parseable - There are no duplicate addresses - The list does not exceed MaxAddressesPerHost
Note: This function does not check containment in parent network CIDRs. That validation is the responsibility of the caller (API or web handler) which has the parent network context and can provide better error messages.
func ValidateHostAdvanced ¶
func ValidateHostAdvanced(adv *HostAdvanced) error
ValidateHostAdvanced rejects obviously broken advanced overrides before they reach the database. Empty / zero-value fields mean "inherit network default" and pass validation. All error messages use the user-facing friendly wrappers so the inline form (web) and the JSON API surface identical, stable strings.
func ValidateIPAddr ¶
ValidateIPAddr is a thin wrapper around netip.ParseAddr that converts a failure into a user-facing FriendlyAddrError. Returns the parsed address on success.
func ValidateMobileConstraints ¶
func ValidateMobileConstraints(kind HostKind, variant HostVariant, role HostRole) error
ValidateMobileConstraints enforces role and variant constraints for mobile hosts. For kind=mobile, role must be host (or empty) and variant must be ios or android. For kind=agent, no constraints are applied (returns nil).
func ValidateNetworkCIDRs ¶
ValidateNetworkCIDRs validates a list of CIDRs for a network. It checks that: - The list is non-empty - Each CIDR is parseable - There are no duplicate CIDRs - There are no overlapping CIDRs
func ValidateRoleReachability ¶
ValidateRoleReachability rejects role/reachability combinations that would result in a silently-useless host. role=lighthouse and role=relay must both ship with a routable public_ip and a non-zero listen_port — otherwise peer config.yml renders an empty static_host_map and the host is never dialed (issue #94).
Types ¶
type AuditEntry ¶
type CA ¶
type CA struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerOperatorID string `json:"owner_operator_id"`
CertPEM string `json:"cert_pem"`
Fingerprint string `json:"fingerprint"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
Status CAStatus `json:"status"`
PredecessorID *string `json:"predecessor_id,omitempty"`
EncryptedKeyDEK []byte `json:"-"`
NonceDEK []byte `json:"-"`
EncryptedKeyMaterial []byte `json:"-"`
NonceKey []byte `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
CA is a per-operator certificate authority. Private key material lives only inside EncryptedKeyMaterial / NonceKey, wrapped under a per-CA DEK stored in EncryptedKeyDEK / NonceDEK (envelope encryption — see ADR 0002).
type CertificateInfo ¶
type CertificateInfo struct {
ID string `json:"id"`
HostID string `json:"host_id"`
Fingerprint string `json:"fingerprint"`
PEM string `json:"pem"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
IsCurrent bool `json:"is_current"`
CreatedAt time.Time `json:"created_at"`
}
type EnrollmentToken ¶
type EnrollmentToken struct {
ID string `json:"id"`
HostID string `json:"host_id"`
TokenHash string `json:"-"` // SHA-256 hex; raw value never persisted (GHSA-ghmh-jhmj-wcmf)
Used bool `json:"used"`
ExpiresAt time.Time `json:"expires_at"`
UsedAt *time.Time `json:"used_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type Host ¶
type Host struct {
ID string `json:"id"`
NetworkID string `json:"network_id"`
CAID string `json:"ca_id,omitempty"`
Name string `json:"name"`
NebulaIPs []string `json:"nebula_ips"`
Groups []string `json:"groups"`
Role HostRole `json:"role"`
IsLighthouse bool `json:"is_lighthouse"`
IsRelay bool `json:"is_relay"`
PublicIP string `json:"public_ip,omitempty"`
ListenPort int `json:"listen_port,omitempty"`
Status HostStatus `json:"status"`
CertFingerprint string `json:"cert_fingerprint,omitempty"`
PrevCertFingerprint string `json:"prev_cert_fingerprint,omitempty"`
CertExpiresAt *time.Time `json:"cert_expires_at,omitempty"`
CertRotatedAt *time.Time `json:"cert_rotated_at,omitempty"`
PendingRekey bool `json:"pending_rekey,omitempty"`
SigningPubPEM string `json:"signing_pub_pem,omitempty"`
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
Advanced *HostAdvanced `json:"advanced,omitempty"`
Kind HostKind `json:"kind"`
Variant HostVariant `json:"variant,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type HostAdvanced ¶
type HostAdvanced struct {
Punchy *bool `json:"punchy,omitempty" yaml:"punchy,omitempty"`
ListenHost string `json:"listen_host,omitempty" yaml:"listen_host,omitempty"`
MTU int `json:"mtu,omitempty" yaml:"mtu,omitempty"`
TunDevice string `json:"tun_device,omitempty" yaml:"tun_device,omitempty"`
UnsafeRoutes []UnsafeRoute `json:"unsafe_routes,omitempty" yaml:"unsafe_routes,omitempty"`
}
HostAdvanced groups optional per-host overrides for the rendered Nebula config. All fields are optional. A field set to its zero value means "inherit network default"; a field set to a non-zero value overrides.
Punchy is a tri-state pointer so an operator can explicitly disable hole-punching for a host (false) without it being indistinguishable from "not set".
type HostStatus ¶
type HostStatus string
const ( HostStatusPending HostStatus = "pending" HostStatusEnrolled HostStatus = "enrolled" HostStatusBlocked HostStatus = "blocked" )
type HostVariant ¶
type HostVariant string
const ( HostVariantNone HostVariant = "" HostVariantIOS HostVariant = "ios" HostVariantAndroid HostVariant = "android" )
type Operator ¶
type Operator struct {
ID string `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
PasswordHash string `json:"-"`
AuthProvider OperatorAuthProvider `json:"auth_provider"`
Status OperatorStatus `json:"status"`
Role string `json:"role"`
TOTPSecret string `json:"-"`
TOTPEnabled bool `json:"totp_enabled"`
OIDCIssuer string `json:"oidc_issuer,omitempty"`
OIDCSubject string `json:"oidc_subject,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
}
Operator is an administrative user of the management server.
type OperatorAPIKey ¶
type OperatorAPIKey struct {
ID string `json:"id"`
OperatorID string `json:"operator_id"`
Name string `json:"name"`
KeyHash string `json:"-"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
}
OperatorAPIKey is a per-operator API key. Only the hash is stored.
type OperatorAuthProvider ¶
type OperatorAuthProvider string
OperatorAuthProvider identifies the authentication backend for an operator.
const ( OperatorAuthLocal OperatorAuthProvider = "local" OperatorAuthOIDC OperatorAuthProvider = "oidc" )
type OperatorSession ¶
type OperatorSession struct {
// Token is the raw session token carried in the operator's cookie. It is
// transient: the store persists only HashSessionToken(Token), never the
// raw value at rest (GHSA-q4vm-pq3q-8wgq).
Token string
OperatorID string
State SessionState
ExpiresAt time.Time
CreatedAt time.Time
}
OperatorSession represents a UI session. A session in `pending_totp` state is awaiting a second-factor verification and is not yet authenticated.
type OperatorStatus ¶
type OperatorStatus string
OperatorStatus represents the active/disabled state of an operator.
const ( OperatorStatusActive OperatorStatus = "active" OperatorStatusDisabled OperatorStatus = "disabled" )
type SessionState ¶
type SessionState string
SessionState is the lifecycle phase of an operator session.
const ( SessionStateAuthenticated SessionState = "authenticated" SessionStatePendingTOTP SessionState = "pending_totp" )
type UnsafeRoute ¶
type UnsafeRoute struct {
Route string `json:"route" yaml:"route"` // CIDR
Via string `json:"via" yaml:"via"` // Nebula IP of the gateway host
}
UnsafeRoute is a single "unsafe route" entry: traffic for `Route` is sent through the host with Nebula IP `Via`. See Nebula's tun.unsafe_routes.
type WebhookSubscription ¶ added in v0.6.0
type WebhookSubscription struct {
ID string `json:"id"`
OwnerOperatorID string `json:"owner_operator_id"`
URL string `json:"url"`
Events []string `json:"events"` // empty = all events
Active bool `json:"active"`
AllowPrivate bool `json:"allow_private"`
// Envelope-encrypted HMAC secret. All nil => unsigned deliveries.
EncryptedSecretDEK []byte `json:"-"`
NonceDEK []byte `json:"-"`
EncryptedSecret []byte `json:"-"`
NonceSecret []byte `json:"-"`
// HasSecret is a computed, response-only flag (the secret itself is never
// returned). It is not persisted as a column.
HasSecret bool `json:"has_secret"`
// Per-subscription delivery observability.
LastDeliveryAt *time.Time `json:"last_delivery_at,omitempty"`
LastStatus string `json:"last_status,omitempty"`
LastError string `json:"last_error,omitempty"`
ConsecutiveFailures int `json:"consecutive_failures"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
WebhookSubscription is an operator-owned outbound webhook target (#256 phase 2). The HMAC secret is stored envelope-encrypted (a per-row DEK wrapped under the master key, the secret sealed under the DEK), so the encrypted fields never serialize to JSON; API responses expose only HasSecret.