fleetupdate

package
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package fleetupdate is the agent self-update state machine (#412, epic #405): download a control-plane-offered version, VERIFY its checksum and signature BEFORE replacing anything, install atomically, then gate on a successful health check and AUTOMATICALLY ROLL BACK if the new version does not become healthy within a bounded window.

The orchestration here is pure and fully unit-tested; the fallible/side-effecting steps (download over the mTLS channel, atomic binary swap + service-manager restart) are injected as seams so the verify-then-swap-then-health-gate contract can be exercised without touching a real host. A tampered checksum or signature is refused with NOTHING installed; a health-check failure restores the prior version and reports the version pair.

Index

Constants

View Source
const (
	ReasonNotNewer         = "target_not_newer"
	ReasonIncompletePlan   = "incomplete_plan"
	ReasonDownloadFailed   = "download_failed"
	ReasonChecksumMismatch = "checksum_mismatch"
	ReasonSignatureInvalid = "signature_invalid"
	ReasonInstallFailed    = "install_failed"
	ReasonHealthy          = "healthy"
	ReasonRolledBack       = "health_check_failed_rolled_back"
)

Reasons.

View Source
const ReleasePublicKeyOverrideEnv = "SYNAPSE_UPDATE_PUBLIC_KEY"

ReleasePublicKeyOverrideEnv lets an operator point an agent at a different release key.

It exists for two real cases — key rotation, where a fleet must accept the new key before the old one is retired, and a private build with its own signing key — and for nothing else. It is deliberately an OPERATOR-side setting on the host, not something the control plane can set, for the same reason the key is embedded.

Variables

This section is empty.

Functions

func EmbeddedReleasePublicKey

func EmbeddedReleasePublicKey() string

EmbeddedReleasePublicKey returns the compiled-in key, so a build can report what it trusts.

func SignManifest

func SignManifest(m Manifest, key ed25519.PrivateKey) (document, signature []byte, err error)

SignManifest produces the canonical bytes and their signature. It exists so the release pipeline and the tests sign exactly what the agent verifies, rather than reimplementing the canonical form.

Types

type Downloader

type Downloader interface {
	Download(ctx context.Context, url string) ([]byte, error)
}

Downloader fetches the artifact bytes for a plan over the authenticated channel.

type Ed25519Verifier

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

Ed25519Verifier verifies a detached ed25519 signature over the artifact bytes using the project's published release public key. It is a real, dependency-free Verifier; the GPG/Authenticode pipeline-signing side (packaging) is separate — this is what the AGENT uses to gate a self-update.

func DefaultVerifier

func DefaultVerifier() (*Ed25519Verifier, error)

DefaultVerifier returns the verifier an agent uses to gate a self-update.

It fails closed: a build with no embedded key, or an unusable override, returns an error rather than a permissive verifier. An agent that cannot verify an update must refuse to update, never update without verifying.

func NewEd25519Verifier

func NewEd25519Verifier(pubHex string) (*Ed25519Verifier, error)

NewEd25519Verifier builds a verifier from a hex-encoded 32-byte public key.

func (*Ed25519Verifier) Verify

func (v *Ed25519Verifier) Verify(artifact, signature []byte) error

Verify reports nil when signature is a valid ed25519 signature over artifact under the release key.

type HealthProber

type HealthProber interface {
	Healthy(ctx context.Context) (bool, error)
}

HealthProber reports whether the freshly-installed version has reported a successful heartbeat within the rollback window.

type Installer

type Installer interface {
	Install(ctx context.Context, artifact []byte, targetVersion string) error
	Rollback(ctx context.Context) error
}

Installer performs the atomic swap of the running binary (keeping a backup) and restarts under the service manager, and can restore the previous version on rollback.

type Manifest

type Manifest struct {
	// Version is the release this artifact is. It is the field an agent compares against its own.
	Version string `json:"version"`
	// SHA256 is the lowercase-hex digest of the artifact bytes.
	SHA256 string `json:"sha256"`
	// Size is the artifact length in bytes, checked before hashing so a hostile length cannot be used
	// to force unbounded work.
	Size int64 `json:"size"`
	// URL is the authenticated download location. It is inside the signature so an offer cannot point
	// a correctly-versioned agent at someone else's bytes.
	URL string `json:"url"`
	// Platform and Arch make a manifest specific to one artifact in a multi-platform release, so a
	// linux/amd64 build cannot be offered to a windows/arm64 host under the same version.
	Platform string `json:"platform"`
	Arch     string `json:"arch"`
}

Manifest is the signed statement that a specific artifact IS a specific version.

func ParseSignedManifest

func ParseSignedManifest(document, signature []byte, verifier Verifier) (Manifest, error)

ParseSignedManifest verifies a manifest document under the release key and returns it.

The order is deliberate: bound the document, parse it, validate its shape, re-canonicalise, and only then verify the signature over the canonical bytes. Verifying the RECEIVED bytes instead would let a document with duplicate or unknown members verify while parsing into something else.

func (Manifest) Canonical

func (m Manifest) Canonical() ([]byte, error)

Canonical returns the exact bytes that are signed and verified.

It is a deterministic marshal of the manifest's own fields rather than the received document, so a signature cannot be made to cover something other than what the agent will act on: a document carrying extra members, different key order or different whitespace re-serialises to the same bytes as the manifest it parsed into, and any difference in a MEANINGFUL field changes them.

func (Manifest) MatchesArtifact

func (m Manifest) MatchesArtifact(artifact []byte) error

MatchesArtifact reports whether the downloaded bytes are the artifact this manifest names.

Size is checked first: a length mismatch is a cheap, certain rejection, and it stops a hostile server from making the agent hash gigabytes to learn the same thing.

func (Manifest) Validate

func (m Manifest) Validate() error

Validate reports whether the manifest is internally usable. It is checked BEFORE the signature so a malformed document is refused cheaply, and again implicitly by the signature covering these bytes.

type Outcome

type Outcome struct {
	Attempted  bool   // a valid, newer plan was acted on
	Applied    bool   // the new version installed AND became healthy
	RolledBack bool   // installed but unhealthy → previous version restored
	From       string // current version before the attempt
	To         string // plan target version
	Reason     string // machine-readable reason
}

Outcome describes what an Apply did. Exactly one of Applied/RolledBack is true when err is nil and an update was attempted; both are false when the plan was a no-op (not newer / incomplete).

type Plan

type Plan struct {
	TargetVersion string // e.g. "1.4.0"
	URL           string // authenticated download location
	SHA256        string // expected artifact checksum, lowercase hex
	Signature     []byte // detached signature over the artifact bytes (see SECURITY NOTE)
}

Plan is an update the control plane offered (carried on the heartbeat response). Every field is required for an update to proceed; a missing field fails closed (no update).

SECURITY NOTE for whoever wires the real Downloader/Installer: Signature here is verified over the artifact BYTES. TargetVersion and SHA256 are otherwise unauthenticated labels used for the not-newer guard. To close a downgrade-via-relabel gap (pairing a validly-signed OLDER artifact with a higher TargetVersion), the release pipeline should sign a manifest that BINDS {version, sha256, url} and the Verifier should check that manifest, not just the raw bytes.

func PlanFromManifest

func PlanFromManifest(m Manifest) Plan

PlanFromManifest turns a verified manifest into an update plan.

The plan's version and checksum now come from inside the signature, so the not-newer guard in Apply is comparing a version an attacker cannot relabel.

type Updater

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

Updater orchestrates a single update attempt.

func New

func New(dl Downloader, ver Verifier, inst Installer, health HealthProber) (*Updater, error)

New constructs an Updater. All seams are required.

func (*Updater) Apply

func (u *Updater) Apply(ctx context.Context, currentVersion string, p Plan) (Outcome, error)

Apply runs one update attempt from currentVersion toward the plan. Verification happens BEFORE any swap: a checksum or signature failure returns an error with nothing installed (the running version is untouched). After a successful install, an unhealthy new version is rolled back automatically.

type Verifier

type Verifier interface {
	Verify(artifact, signature []byte) error
}

Verifier verifies a detached signature over the artifact bytes with the project's release key.

Jump to

Keyboard shortcuts

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