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
}