Skip to content

feat(security): Ed25519 signing verification for agent updates#758

Open
matztam wants to merge 5 commits into
PatchMon:mainfrom
matztam:feat/agent-signing-verification
Open

feat(security): Ed25519 signing verification for agent updates#758
matztam wants to merge 5 commits into
PatchMon:mainfrom
matztam:feat/agent-signing-verification

Conversation

@matztam

@matztam matztam commented May 6, 2026

Copy link
Copy Markdown

Closes #754.

The agent auto-update mechanism previously verified downloaded binaries via SHA256, but both the binary and the hash were served by the same server. A compromised server could deliver a malicious binary alongside a matching hash — the check would pass.

This PR adds asymmetric Ed25519 signing via minisign as a second, independent verification layer:

  • Developer signs agent binaries with a private key at build/release time
  • Agent verifies the signature using a public key embedded at build time via -ldflags
  • A compromised server cannot forge a valid signature without the private key

Changes

  • internal/pkgversion/signing_keys.go — new file; SigningPublicKey variable embedded at build time via -ldflags="-X ...SigningPublicKey=RW...". Build without key set aborts.
  • cmd/patchmon-agent/commands/version_update.go — after SHA256 check: fetch .minisig from server, verify Ed25519 signature, enforce downgrade protection (version in trusted comment must be strictly greater than current)
  • cmd/patchmon-agent/commands/version_update_test.go — unit tests for signature verification, key ID mismatch, downgrade protection, and version comparison using real Ed25519 keypairs (no external dependencies)
  • server-source-code/internal/handler/install.go — new ServeAgentSignature handler serving patchmon-agent-{os}-{arch}.minisig
  • server-source-code/internal/server/router.go — route GET /api/v1/hosts/agent/signature
  • Makefilecheck-signing-key guard, sign-all target, PATCHMON_SIGNING_PUBLIC_KEY / PATCHMON_SIGNING_PRIVATE_KEY env vars; local dev build (make build) does not require a key
  • .github/workflows/agent-release.yml — minisign install, sign step, .minisig upload to release artifacts
  • docker/server.Dockerfile — fix nodenodejs (correct Alpine package name), remove silent frontend build fallback

Security model

Scenario Key source
Official build Developer public key embedded via CI (vars.PATCHMON_SIGNING_PUBLIC_KEY)
Self-hosted Operator builds agent from source with own key via -ldflags

First installation (bootstrap) is not covered by signature verification — known, accepted limitation (chicken-and-egg). All subsequent updates are verified.

Test plan

  • Unit tests pass (go test ./...)
  • Valid signature accepted, tampered binary rejected, wrong key rejected, key ID mismatch rejected, downgrade rejected
  • End-to-end update tested locally: agent 2.0.4 → 2.0.5 via update-agent command with real minisign keypair

Matthias added 5 commits May 6, 2026 13:34
Closes PatchMon#754. Implements asymmetric cryptographic signing for the agent
auto-update mechanism to close the trust gap where both the binary and
its SHA256 hash were served by the same server.

Changes:
- Agent: verify Ed25519 minisign signature before installing any update,
  using a public key embedded at build time via -ldflags. Refuses update
  if no key is compiled in, if the signature is invalid, or if the signed
  version is not newer than the current version (downgrade protection).
- Agent: new fetchAgentSignature() fetches .minisig file from server;
  verifyMinisignSignature() implements minisign wire format using pure
  stdlib (crypto/ed25519 + golang.org/x/crypto/blake2b), no new deps.
- Server: new ServeAgentSignature() handler serves .minisig files from
  the agents directory; registered at GET /api/v1/hosts/agent/signature.
- Build: PATCHMON_SIGNING_PUBLIC_KEY (env var) is required for all
  release builds (build-linux/freebsd/windows/all); build fails fast
  if unset. AGENT_SIGNING_PRIVATE_KEY (GitHub secret) is used to sign
  each binary with minisign in the release workflow.
- Makefile: new sign-all target; build-all and build-all-for-docker
  copy .minisig files alongside binaries.
…rfile

- Makefile: skip .minisig files in sign-all loop to prevent signing signatures
- Makefile: read signing password once via MINISIGN_PASSWORD env var
- Dockerfile: replace 'node' with 'nodejs' (correct Alpine package name)
- Dockerfile: remove silent frontend build fallback to surface errors
…tion

Public key uses 'Ed' (0x45 0x64) but minisign signature files use 'ED'
(0x45 0x44) — the byte check was wrong, causing all updates to be rejected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: agent binary updates have no cryptographic signing - compromised server can push arbitrary root-level code

1 participant