a73x

internal/server/config/config.go

Ref:   Size: 6.6 KiB   History

// Package config defines the eitri-server on-disk JSON configuration schema,
// loaded by the server binary (cmd/eitri-server) at startup.
package config

// Config is the eitri-server config file schema (decoded from JSON).
type Config struct {
	HTTPListen string `json:"http_listen"`
	// LogLevel sets the floor for structured logs: "debug", "info", "warn" or
	// "error". Empty means "info", which is what Go's default handler does — so
	// an unset value changes nothing.
	//
	// Without it the Debug lines throughout the server are dead code in a running
	// plane: nothing configures a handler, so the default floor of info silently
	// discards them and an operator diagnosing a live problem has no way to turn
	// detail on short of a rebuild.
	LogLevel   string `json:"log_level"`
	QUICListen string `json:"quic_listen"`
	DBPath     string `json:"db_path"`
	// AdminToken is retired: sign-in is OIDC (see OIDC below) and console
	// credentials are sessions/PATs. The field is kept only so the server can
	// detect a stale token in an old config and warn the operator to remove it.
	AdminToken string `json:"admin_token"`
	HostSecret string `json:"host_secret"`
	// KeyEncryptionKey seals every piece of key material this server holds: the
	// host CA and gate host key on disk (ssh_ca_key, ssh_host_key). 64 hex
	// characters (32 bytes, minted with `openssl rand -hex 32`).
	//
	// It lives HERE, in the config, and nowhere near the data it protects — so a
	// copied database, a nightly backup, or a lifted volume carries ciphertext
	// and no signing power. That separation is the whole mechanism: keep this
	// file's custody apart from the data's.
	//
	// Required, and never rotated in place. Losing it loses the host CA with it,
	// and with the host CA goes the plane's identity — every `@cert-authority`
	// pin and every VM host certificate names it.
	KeyEncryptionKey string `json:"key_encryption_key"`
	CIDRPool         string `json:"cidr_pool"`
	// DefaultImageURL/SHA are retired in favour of DefaultImages: one image for
	// the whole fleet is only correct while every host shares an architecture.
	// Kept so the server can spot them in an old config and say what to write.
	DefaultImageURL string `json:"default_image_url"`
	DefaultImageSHA string `json:"default_image_sha256"`
	// DefaultImages is the guest image a one-click create applies, keyed by the
	// ARCHITECTURE of the host the VM is being placed on ("amd64", "arm64" —
	// the GOARCH each agent reports at enrollment).
	//
	// Keyed by arch alone, not os/arch: the guest is Linux whichever host runs
	// it — a Mac hosts Linux arm64 guests through Virtualization.framework. The
	// host's OS picks the backend; only the CPU architecture has to match the
	// image, and handing a host an image it cannot execute is the one mistake
	// this map exists to prevent.
	DefaultImages map[string]DefaultImage `json:"default_images"`
	AdvertiseHTTP string                  `json:"advertise_http"`
	AdvertiseQUIC string                  `json:"advertise_quic"`
	// CredentialMaxAge optionally bounds host credential age (Go duration,
	// e.g. "2160h" for 90 days). Empty/zero disables — revocation via
	// POST /api/v1/hosts/{id}/revoke-credential is the primary mechanism;
	// max-age forces periodic re-enrollment and is opt-in defense-in-depth.
	//
	// READ THIS BEFORE SETTING IT. Nothing renews a host credential: one is
	// minted once, at enrollment, and never re-issued. So this is not a rotation
	// policy — it is a deadline. Every host whose credential reaches this age
	// stops syncing and stays dark until an operator re-enrolls it BY HAND, and
	// they will all reach it at whatever spread their enrollments had.
	//
	// Leaving it unset is therefore the safe default and not an oversight; the
	// hole it leaves is that a leaked credential is valid until someone notices
	// and revokes that host's generation. Closing that properly means renewal on
	// the sync channel, which does not exist yet.
	CredentialMaxAge string `json:"credential_max_age"`
	// AuditRetention bounds the audit_log age (Go duration; default "2160h" =
	// 90 days; "0" disables pruning). Pruned at startup and daily.
	AuditRetention string `json:"audit_retention"`
	// SSHCAKey is the path to the persistent SSH user CA private key
	// (auto-created 0600 if absent, handled like AdminToken — never logged).
	// The CA signs the short-lived certs the jump gate accepts.
	SSHCAKey string `json:"ssh_ca_key"`
	// SSHHostKey is the path to the gate's persistent SSH host key
	// (auto-created 0600 if absent, never regenerated on restart so users
	// don't see host-key-changed warnings).
	SSHHostKey string `json:"ssh_host_key"`
	// SSHListen is the jump-gate listen address. Empty ⇒ gate is OFF (no
	// key material is loaded and no listener is started).
	SSHListen string `json:"ssh_listen"`
	// SSHGateDomain is the hostname clients dial the gate as (the principal put
	// on the gate's signed HOST certificate). Empty ⇒ derived from SSHListen's
	// host part, which is why it is REQUIRED whenever SSHListen binds every
	// interface (":2222", "0.0.0.0:…", "[::]:…"): there is no host to derive,
	// and a gate that cannot name itself refuses to boot. It must match the
	// host in EITRI_GATE so `@cert-authority` verification accepts the
	// presented host cert.
	SSHGateDomain string `json:"ssh_gate_domain"`
	// ReleaseManifestURL is where the server discovers the latest eitri
	// release (default https://eitri.sh/dl/latest/manifest.json when the
	// field is absent — applied by cmd, not here). Empty string in an
	// explicit config disables release discovery and every upgrade surface.
	ReleaseManifestURL *string `json:"release_manifest_url"`
	// OIDC configures the console sign-in relying party (required — issuer,
	// client_id and public_url must be set; see OIDC).
	OIDC OIDC `json:"oidc"`
}

// DefaultImage is one architecture's guest image: the URL to fetch and the
// digest the agent verifies it against.
type DefaultImage struct {
	URL    string `json:"url"`
	SHA256 string `json:"sha256"`
}

// OIDC configures the server's relying-party side. The issuer is sometimes
// the bundled eitri-oidc next door and sometimes an external IdP — the
// server cannot tell the difference (spec §2).
type OIDC struct {
	Issuer            string   `json:"issuer"`
	ClientID          string   `json:"client_id"`
	ClientSecret      string   `json:"client_secret"` // external confidential clients only
	PublicURL         string   `json:"public_url"`
	AllowedDomains    []string `json:"allowed_domains"`    // optional signup gate
	AllowedIdentities []string `json:"allowed_identities"` // optional signup gate
}