Auto-Update
The daemon includes an automatic update service that periodically checks for new releases from the FreeSDN control plane, downloads the binary, and restarts the service. The update pipeline is designed to be fail-closed - an unsigned release, a checksum mismatch, a release manifest that does not describe this agent and this download, or a key fingerprint mismatch all abort the update.
How the update cycle works
Section titled “How the update cycle works”-
At a configurable interval (default 50 minutes, minimum 5 minutes), the daemon calls:
GET /api/v1/agents/updates/check?current_version=1.0.0&platform=linux&agent_type=daemonThe request includes
X-Agent-IDandX-Agent-Keyheaders. The endpoint returns 401 without them (information-disclosure mitigation). -
If
update_availableisfalse, the daemon logs and sleeps until the next interval. -
If an update is available, the server response includes:
{"update_available": true,"latest_version": "1.1.0","download_url": "/api/v1/agents/releases/3fa1b2c4-8d5e-4f6a-9b0c-1d2e3f4a5b6c/binary","checksum_sha256": "a3f7c891...","signature": "<base64-DER ECDSA-P256 signature over the canonical manifest bytes>","manifest": {"v": 1,"digest": "a3f7c891...","version": "1.1.0","platform": "linux","agent_type": "daemon","channel": "stable"}}signaturecoversmanifest, notchecksum_sha256.latest_version,download_urlandchecksum_sha256are unsigned envelope fields; the agent cross-checks each of them against the signed manifest before it will stage anything. A response that carries asignaturebut nomanifestis refused - see Signature verification. -
The daemon validates the download URL:
http://URLs are rejected outright- Relative paths (starting with
/) are resolved against the configured server URL - Absolute
https://URLs must match the server’s hostname - a different hostname is blocked as a potential SSRF
-
The binary is downloaded with a streaming 500 MB size cap.
-
The SHA-256 checksum is verified. A mismatch aborts the update and reports
checksum_mismatchto the control plane. -
The signed release manifest is checked against this agent and against the bytes just downloaded, and only then is the ECDSA-P256 signature over that manifest verified (see below). A missing manifest, a manifest that does not match, or a missing or invalid signature all abort the update by default.
-
The new binary is staged atomically: written to a temp file in the same directory, the current binary backed up as
.bak, thenos.replace()(atomic on POSIX and NTFS). -
A rollback marker is written recording the previous version hash so recovery is possible if the new binary fails.
-
The daemon triggers a platform-specific restart:
- Linux:
systemctl restart freesdn-agent - macOS:
launchctl kickstart -k system/com.freesdn.agent - Windows:
sc stop FreeSDNAgent && sc start FreeSDNAgent(no automatic restart recovery is configured by default; both commands must be issued explicitly)
- Linux:
Signature verification
Section titled “Signature verification”SHA-256 alone is not sufficient - a compromised or MITM’d control plane could replace both the binary and its checksum simultaneously. The ECDSA-P256 signature requires the backend’s private signing key, which is never served publicly.
What is signed: a manifest, not a bare digest
Section titled “What is signed: a manifest, not a bare digest”The signature does not cover the bare SHA-256 digest. It covers a release manifest that binds that digest to the version, platform, agent type and channel it belongs to.
Signing the digest alone was not enough, and the gap was not theoretical. A signature over sha256(binary) only asserts “the backend signed these bytes at some point”. It does not say which release they are. So a control plane that does not hold the private key could take a genuine older release - its binary, its checksum and its stored signature, all authentic - and serve it back as an update. Verification passed and the agent installed a known-vulnerable build. And because latest_version was an unsigned envelope field, the same replay could be relabelled with any version the attacker liked.
Binding the metadata into the signed object closes both holes: the agent compares the signed version against the version it is running, never the envelope field.
| Manifest field | Meaning |
|---|---|
v |
Manifest format version, currently 1. An agent refuses a format it does not fully understand rather than partially interpreting it - the field it does not know about may be the one carrying the security meaning. |
digest |
Lower-case hex SHA-256 of the release binary |
version |
The release version this digest belongs to |
platform |
windows, linux, or macos |
agent_type |
daemon or desktop |
channel |
stable, or prerelease for a pre-release row |
All six fields are required. The signed bytes are the manifest serialized canonically: those six keys only, sorted, no insignificant whitespace, UTF-8. Both ends derive the bytes the same way - signing one serialization and verifying another is how a signature scheme quietly stops meaning anything.
{"agent_type":"daemon","channel":"stable","digest":"<64 hex chars>","platform":"linux","v":1,"version":"1.1.0"}Verification flow
Section titled “Verification flow”1. Fetch the release public key: GET /api/v1/agents/releases/public-key → PEM-encoded ECDSA-P256 public key
2. Verify the public key fingerprint against the pinned value (see "Key pinning" below)
3. Check the manifest BEFORE trusting the signature at all: manifest["v"] == 1, and all six fields present manifest["digest"] == sha256(the bytes just downloaded) manifest["version"] == the version the envelope advertised manifest["platform"] == this build's platform manifest["agent_type"] == "daemon" manifest["version"] > the version this agent is running
4. Verify: ECDSA-P256.verify( public_key, signature = base64.decode(response["signature"]), data = canonical_manifest_bytes(response["manifest"]) )Step 3 carries as much weight as step 4. A valid signature only proves the backend signed that manifest once, and a replayed old release arrives with a perfectly valid signature over a perfectly self-consistent manifest. The strictly-newer comparison against the signed version is what rejects it.
A release with no manifest, or one that fails any step-3 check, is refused before the signature is checked at all, and reported to the control plane as manifest_<reason>:
| Reported status | Cause |
|---|---|
manifest_missing |
Response carried a signature but no manifest |
manifest_unsupported_format |
v is not 1 |
manifest_incomplete |
One of the six signed fields is absent |
manifest_digest_mismatch |
Manifest does not describe the bytes that were downloaded |
manifest_version_relabelled |
Unsigned latest_version disagrees with the signed version |
manifest_platform_mismatch |
Release is built for another platform |
manifest_agent_type_mismatch |
Release is not a daemon build |
manifest_unparseable_version |
Signed version does not parse as a version |
manifest_not_newer |
Signed version is not strictly newer than the running one (a rollback attempt) |
The signing algorithm is ECDSA over NIST P-256 with SHA-256.
Fail-closed by default
Section titled “Fail-closed by default”{ "daemon": { "auto_update_require_signature": true }}auto_update_require_signature defaults to true. When true:
- A release with no
signaturefield → refused (unsigned_release) - A release with no
manifest, or a manifest that does not match this agent and this download → refused (manifest_<reason>, table above) - A release with an invalid signature over the manifest → refused (
signature_mismatch) - A release where the public key fingerprint does not match the pin → refused (possible key-swap)
Set auto_update_require_signature: false only if your FreeSDN instance predates the signing infrastructure and cannot sign releases. This is explicitly not recommended, and it costs more than the signature: the manifest and anti-rollback checks live behind the same gate, so an unsigned release accepted on its checksum alone is also accepted without any proof of which version, platform or agent type those bytes are.
Public-key pinning
Section titled “Public-key pinning”Fetching the public key from the server endpoint is not enough - a compromised endpoint could serve its own key alongside its own binary and signature, and the signature would verify. So the fingerprint must be provisioned out of band, and the agent fails closed without one: with no pin it refuses to update rather than trusting a key handed to it by the same server that serves the release.
Install-time pin (recommended)
Section titled “Install-time pin (recommended)”Set release_public_key_sha256 in the daemon config to the SHA-256 fingerprint (hex) of the expected public key PEM:
{ "daemon": { "release_public_key_sha256": "a3f7c891deadbeef..." }}Get the fingerprint from your FreeSDN admin (Settings → Agent Releases → Public Key Fingerprint) and set it when deploying new agents. With this set, any key the server returns that does not match is rejected, even on the very first connection.
Registration pin (default)
Section titled “Registration pin (default)”If release_public_key_sha256 is not set, the agent uses the fingerprint it
received when it registered:
freesdn-agent registeris authenticated and the agent must be approved by an operator in the UI. The registration response carries the release-signing key fingerprint, and the agent writes it torelease_signing_key.sha256in its config directory.- Every later update check compares the fetched key against that pin. A mismatch is refused.
The agent only ever reads that file. It never adopts a key it was handed.
Configuration reference
Section titled “Configuration reference”All update settings live under the daemon key in config.json:
{ "daemon": { "auto_update_enabled": true, "auto_update_interval": 3000, "auto_update_channel": "stable", "auto_update_require_signature": true, "release_public_key_sha256": null }}| Field | Default | Description |
|---|---|---|
auto_update_enabled |
true |
Enable or disable automatic updates entirely |
auto_update_interval |
3000 |
Check interval in seconds (minimum 300, maximum 86400) |
auto_update_channel |
"stable" |
Release channel: "stable" or "beta" |
auto_update_require_signature |
true |
Reject updates with no or invalid ECDSA signature. Also gates the manifest and anti-rollback checks. |
release_public_key_sha256 |
null |
Optional hex SHA-256 of the expected signing public key (install-time pin) |
Monitoring update status
Section titled “Monitoring update status”Update results are reported to the control plane as action_result WebSocket messages with "action": "auto_update". The agent detail page in the FreeSDN UI shows the current version and the last update check result.
If an update fails, the daemon logs the reason and continues running the current version. It will retry at the next interval.
Rollback
Section titled “Rollback”Automatic rollback
Section titled “Automatic rollback”The daemon performs automatic rollback at every startup. Before applying an update the updater writes a rollback marker (.freesdn-rollback next to the binary) recording the previous version string and SHA-256 hash. On the next startup, check_rollback_needed() reads this marker. If the daemon crashed within 60 seconds of the update being applied, the .bak binary is automatically restored and the daemon continues on the previous version - no operator action required.
The rollback marker is cleared automatically once the daemon sends its first successful heartbeat, confirming the new binary is healthy. Up to 3 automatic rollback attempts are allowed. After the third failure the daemon logs a CRITICAL message and requires manual intervention.
Manual recovery (if automatic rollback fails)
Section titled “Manual recovery (if automatic rollback fails)”If all three automatic attempts are exhausted (or the .bak file is missing), restore the previous binary manually:
# Linux - restore the backup manuallycp /opt/freesdn-agent/freesdn-agent.bak /opt/freesdn-agent/freesdn-agentsudo systemctl start freesdn-agentThe rollback marker (.freesdn-rollback in the same directory as the agent binary) records the previous version string and SHA-256 hash for verification.
Disabling auto-update
Section titled “Disabling auto-update”To pin an agent to a specific version, set auto_update_enabled: false in config.json:
{ "daemon": { "auto_update_enabled": false }}You can also update manually by downloading a release binary, verifying its checksum, replacing the binary, and restarting the service.
All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.