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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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.