Skip to content

About

A daemon running on edge devices to establish a connection to the Reswarm router and manage apps and containers

Topics

Resources

Stars

3 stars

Watchers

2 watching

Forks

Repository files navigation

IronFlock Agent

Coverage

The IronFlock Agent is a (lightweight) daemon running on IoT devices that provides an interface to manage containers on the device and to collect/request app logs. In particular, the daemon enables the IronFlock IoT Development Studio to authenticate and securely connect to an IoT device in order to control apps running in containers on the device and retrieve their result and logs.

Overview

Introduction

Usage

The IronFlock Agent is provided as a statically linked binary and accepts various CLI parameters to configure it when launched. To show a list of the parameters it accepts apply the help parameter ./reagent -help

Usage of ./reagent:
  -agentDir string
    	default location of the agent binary (default "/opt/reagent", (linux), "$HOME/reagent", (other))
  -appsDir string
       default path for apps and app-data (default (default agentDir) + "/apps")
  -arch
       displays the architecture for which the binary was built
  -compressedBuildExtension string
       sets the extension in which the compressed build files will be provided (default "tgz")
  -config string
       reswarm configuration file
  -connTimeout uint
       Sets the connection timeout for the socket connection in milliseconds (0 means no timeout) (default 1250)
  -dbFileName string
       defines the name used to persist the database file (default "reagent.db")
  -debug
       sets the log level to debug (default true)
  -debugMessaging
    	enables debug logs for messenging layer
  -env string
    	determines in which environment the agent will operate. Possible values: (production, test, local) (default "production")
  -logFile string
       log file used by the reagent (default "/var/log/reagent.log" (linux), "$HOME/reagent/reagent.log" (other))
  -nmw
    	enables the agent to use the NetworkManager API on Linux machines (default true)
  -offline
       starts the agent without establishing a socket connection. meant for debugging (default=false)
  -ppTimeout uint
       Sets the ping pong timeout of the client in milliseconds (0 means no timeout)
  -prettyLogging
       enables the pretty console writing, intended for debugging
  -profiling
       sets up a pprof webserver on the defined port
  -profilingPort uint
       port of the profiling service (default 80)
  -remoteUpdateURL string
    	bucket to be used to download updates (default "https://storage.googleapis.com")
  -respTimeout uint
       Sets the response timeout of the client in milliseconds (default 5000)
  -update
       determines if the agent should update on start (default true)
  -version
       displays the current version of the agent

The config parameter needs to be populated with the path to a local .flock file. This .flock file contains all the neccessary device configuration and authentication data required to run the agent.

Read more on .flock files and how they work here: https://ironflock.com/en/docs/device-management/flockflasher

Example Usage

./reagent -config path/to/config.flock -prettyLogging

Running as a Windows service

On Windows the agent should be installed as a service instead of being started manually — the service starts at boot (no logged-in user required), restarts on failure via SCM recovery actions, and activates self-updates (a console agent only downloads them). From an elevated (Administrator) prompt:

reagent.exe service install -config path\to\config.flock
reagent.exe service status|start|stop|uninstall

service install starts the service when it finishes — a device that was just registered should come online without a second command. Pass -noStart to install without starting. (-start is still accepted for compatibility with older docs and provisioning scripts, but it no longer does anything.)

What service install sets up:

  • Copies the exe and the .flock (as device.flock) into the agent dir (default %ProgramData%\IronFlock\Reagent, override with -agentDir) and bakes -config/-agentDir/-appsDir/-dbFileName/-logFile into the service ImagePath, so no path ever falls back to os.UserHomeDir (meaningless under LocalSystem).
  • Restricts the agent dir ACLs to SYSTEM + Administrators (the .flock and generated .env-compose files contain the device secret; the self-update executes binaries from this dir as SYSTEM), with a Users modify-grant on the apps dir for Docker Desktop bind mounts.
  • Recovery actions: restart after 5s/30s/120s, repeating forever (reset period 1 day). Deliberate restarts (update activation, system_restart_agent) exit without reporting SERVICE_STOPPED so they always trigger recovery.
  • A boot-time repair Scheduled Task ("IronFlock Agent Repair") that restores reagent.exe from reagent-prev.exe if an interrupted update swap left the ImagePath vacant — SCM recovery cannot fix a service that fails to start.
  • Optional -proxy http://host:port writes HTTP(S)_PROXY into the service environment (a LocalSystem service only sees machine-wide env).

Update flow (mirrors the Linux reagent-manager.sh semantics in-process, see src/selfupdate): download reagent-v<ver>.exe (SHA-256-verified against the published manifest) → validate it executes → rename the running exe to reagent-prev.exe, rename the update into place → exit for a supervised restart → probation: if the new version fails 3 consecutive starts within its first 2 minutes, it is rolled back and blacklisted until a newer release supersedes it.

Docker note: with Docker Desktop the engine only starts at user sign-in. The service tolerates that (it waits patiently and reports the device as CONFIGURING), but for unattended devices enable Docker Desktop autostart plus Windows auto sign-in.

The device terminal works on Windows 10 1809 (build 17763) and newer: the agent opens a Windows pseudoconsole (ConPTY) running PowerShell 7 where it is installed and Windows PowerShell otherwise, in place of the bash pty it uses on Linux. Because the agent runs as LocalSystem, that terminal is a SYSTEM shell — the same posture as the root shell the Linux terminal gives. On older builds the terminal reports that the pseudoconsole API is missing rather than failing obscurely.

Development

The agent embeds the frp client into the binary. For that to work locally you first have to download the frp client.

just download-frpc

Once Go has been downloaded and installed, users can run the project locally by running the following command :

just run

# or on a mac
just run_mac

During development the /opt/reagent folder is used as the agent directory. There you can find additional config like the frpc.ini for the tunnel configuration. To test with a local test device use the test-config.flock file when connecting to the local dev environment. A few things might need to be adjusted according to your environment. The secret must be the one from the device's database record. Also use the insecure (ws instead of wss) endpoint and make sure the swarm_key and device_key are set.

If you encounter any privilege issues, please try removing the agent home directory beforehand (by default found in ${HOME}/reagent) or try running go as root.

Build, Publish and Release

Building and publishing is done by CI (GitHub Actions). The pipeline is deliberately split into a CI-automated build + publish step and a manual release (promotion) step, so a version can be staged and verified before any device picks it up.

Release via CI (tag push)

  1. just release-patch (or release-minor / release-major) — the whole local side in one command: refuses a dirty tree, bumps src/release/version.txt + availableVersions.json, commits just those two files as release reagent vX.Y.Z, runs the lint gate, then tags vX.Y.Z and pushes.

    Commit your own work first, with your own message — the release commit carries the version bump alone. This is the shared IronFlock release contract; see REDeployments/docs/RELEASE.md.

    For a hand-managed version, just bump-patch (or editing version.txt yourself; see Versioning) + your own commit + just release still works — release tags and pushes an already-committed version and requires a clean working tree.

  2. .github/workflows/release.yml then, for every entry in the targets file: builds the binary, generates a CycloneDX SBOM, records a sigstore-signed SBOM attestation bound to the binary's digest, publishes the binary to gs://re-agent/<os>/<arch>/<version>/, and pushes the multi-arch agent images to Artifact Registry. It can also be run manually from the Actions tab (workflow_dispatch).

This stages the version — the binaries and images now exist, but agents will not update to it yet. The agent decides what to install from availableVersions.json (see the next step).

Verify a published binary's provenance any time with:

gh attestation verify reagent-linux-amd64 --repo IronFlock/device-agent
# releases up to v1.2.8 were attested before the repo moved from RecordEvolution:
gh attestation verify reagent-linux-amd64 --owner IronFlock --signer-repo RecordEvolution/DeviceManagementAgent

Promote a version (manual release)

Making a staged version live is a deliberate manual step. Update availableVersions.json for the target environment(s), then:

just promote   # signs through OpenBao (sign-release), republishes the `latest` alias, then src/release/version.txt + availableVersions.json to gs://re-agent

Agents read availableVersions.json to determine the latest version, so this publish is the release gate. (To promote only the available-versions list: just publish-latestVersions.)

The latest alias

just promote first runs just publish-latest-alias, which server-side-copies every binary staged under the production version to a latest version segment:

gs://re-agent/windows/amd64/<version>/reagent.exe  ->  gs://re-agent/windows/amd64/latest/reagent.exe
gs://re-agent/linux/arm64/<version>/reagent        ->  gs://re-agent/linux/arm64/latest/reagent
...  (every os/arch in `targets`, plus the .sha256 and .sig sidecars for each)

This exists for the manual install surface only. Linux devices resolve the version themselves through availableVersions.json (reswarmify), but Windows has no such installer — the user downloads reagent.exe from a link in the docs and the platform UI, so that link must not carry a version. Those links point at:

https://instance-registry.ironflock.com/dl/re-agent/windows/amd64/latest/reagent.exe
https://instance-registry.ironflock.com/dl/re-agent/linux/<arch>/latest/reagent

(/dl/re-agent/* is the RESWARM registry-proxy route; it forwards verbatim to https://storage.googleapis.com/re-agent/*, so the raw GCS URL works identically.)

The copy is byte-for-byte, so the Authenticode signature applied by scripts/sign-windows.sh, the .sha256 sidecar and the .sig update signature published by scripts/publish.sh all stay valid. The alias objects are written with Cache-Control: public, max-age=0 so they are always revalidated.

Because it runs before publish-latestVersions, it also acts as a preflight guard: promoting a version whose binaries the release CI never published, or published without a .sig, fails here, leaving the live availableVersions.json untouched.

Never put "latest" into availableVersions.json. The agent's OTA path feeds that value to compareVersion() (src/system/system.go) as a semver constraint, which would fail to parse. The alias is for humans; agents always resolve a concrete version.

Update signatures (Ed25519)

The agent installs an OTA update, and on Windows the frpc it downloads, only if <binary>.sig, published next to the binary and its .sha256, verifies against a public key compiled into the running agent (AGENT_UPDATE_PUBLIC_KEYS in src/updatesig/updatesig.go). The .sig holds the standard base64 of the raw 64-byte Ed25519 signature over

reagent-update:v1            (frpc: frpc-download:v1)
<os>/<arch>/<version>/<file> e.g. linux/armv7/1.2.6/reagent, windows/amd64/0.71.0/frpc.exe
<sha256 of the binary, lowercase hex>

joined by \n, no trailing newline. The agent builds the middle line from what it is downloading (the version it resolved, its own platform, the file name), so a server cannot pass an old signed release off as a new one, or one platform's binary as another's. Every server that hands out agent or frpc downloads, including an appliance's agent mirror, must serve the .sig at the same path as the .sha256.

Where the key lives. The signing key is the OpenBao transit key agent-update (ed25519, exportable=false): no private key exists in GitHub or on a laptop. CI stages binaries unsigned; just promote signs them before anything goes live. The maintainer reaches OpenBao through kubectl only; scripts/openbao-admin.sh runs bao in openbao-0 as role admin (context OPENBAO_KUBE_CONTEXT, default gke_record-1283_europe-west1-b_ironflock-prod).

One-time setup (fresh installs of the ironflock chart create the mount and key themselves):

scripts/openbao-admin.sh 'bao secrets enable transit'
scripts/openbao-admin.sh 'bao write -f transit/keys/agent-update type=ed25519 exportable=false allow_plaintext_backup=false'
just agent-update-pubkeys    # paste the printed line(s) into AGENT_UPDATE_PUBLIC_KEYS

Signing happens in just promote (also in just publish-version / just publish-latestVersions, so the manifest cannot flip unsigned), via just sign-release (scripts/sign-release.sh): for every version availableVersions.json names and for the pinned frpc it downloads each binary from GCS, hashes it locally, builds the message (sign-update message), signs all messages in one kubectl exec session (transit/sign/agent-update), converts each vault:vN:… answer into a .sig, verifies every .sig against the keys compiled into this tree, and only then uploads them. A published .sig that already verifies is kept. A staged version that is already signed is never restaged by publish.sh; a signed frpc is never overwritten by publish-frpc.sh (bump FRP_VERSION instead).

Rotation: scripts/openbao-admin.sh 'bao write -f transit/keys/agent-update/rotate', then keep signing with the old version (OPENBAO_TRANSIT_KEY_VERSION=<old> just promote) while a release that lists both lines from just agent-update-pubkeys reaches the fleet; then sign with the new version (no variable) and drop the old line in a later release. A new key version's public key only exists after the rotate, which is why signing stays pinned to the old one meanwhile.

Backup: the key is in OpenBao's Raft storage, so the OpenBao snapshots plus the seal key cover it; there is nothing else to back up.

AGENT_UPDATE_PUBLIC_KEYS holds the production key's version 1 (key ID 2b040fcf). Should it ever hold the TODO-AGENT-UPDATE-PUBLIC-KEY placeholder, TestProductionUpdateKeysConfigured fails and blocks the release gate (locally, AGENT_SIGNING_KEY_PLACEHOLDER_OK=1 skips it). To check a published binary by hand: cd src && go run ./cmd/sign-update verify -version <version> [-kind reagent|frpc -os <os> -arch <arch>] <file> (with <file>.sig next to it). sign-update keygen / sign work with a local key file for development and tests only.

Emergency only (broken signing key): AGENT_UPDATE_ALLOW_UNSIGNED=true in the agent's service environment installs agent updates and frpc downloads whose signature is missing or invalid, with a warning each time. Releases published before signing existed have no .sig, so a verifying agent never installs them again.

One-time CI setup (Workload Identity Federation)

The release workflow authenticates to Google Cloud keylessly via Workload Identity Federation — no long-lived key in GitHub. Set it up once:

PROJECT=record-1283
POOL=github-pool
PROVIDER=github-provider
REPO=IronFlock/device-agent
REPO_ID=$(gh api repos/${REPO} --jq .id)   # 314514906 — immutable, survives renames and transfers
SA=reagent-release@${PROJECT}.iam.gserviceaccount.com

# 1. Workload Identity Pool + GitHub OIDC provider (locked to this repo by its numeric id)
gcloud iam workload-identity-pools create $POOL --project=$PROJECT --location=global --display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc $PROVIDER \
  --project=$PROJECT --location=global --workload-identity-pool=$POOL \
  --display-name="GitHub" --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_id=assertion.repository_id" \
  --attribute-condition="assertion.repository_id=='${REPO_ID}'"

# 2. Service account with publish permissions
gcloud iam service-accounts create reagent-release --project=$PROJECT --display-name="reagent release"
gsutil iam ch "serviceAccount:${SA}:roles/storage.objectAdmin" gs://re-agent
gcloud projects add-iam-policy-binding $PROJECT --member="serviceAccount:${SA}" --role="roles/artifactregistry.writer"

# 3. Let the repo impersonate the SA via the pool
PROJNUM=$(gcloud projects describe $PROJECT --format='value(projectNumber)')
gcloud iam service-accounts add-iam-policy-binding $SA --project=$PROJECT \
  --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/${PROJNUM}/locations/global/workloadIdentityPools/${POOL}/attribute.repository_id/${REPO_ID}"

Then add two organization variables in IronFlock (Settings → Secrets and variables → Actions → Variables; the provider condition limits them to this repo anyway):

  • GCP_WORKLOAD_IDENTITY_PROVIDER = projects/<projNum>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
  • GCP_SERVICE_ACCOUNT = reagent-release@record-1283.iam.gserviceaccount.com

Targets

The targets file, which can be found in the root of this project, determines which platforms and architectures the binary should be (cross-)compiled into.

The following platforms are set by default:

linux/amd64
linux/arm64
linux/arm/7
linux/arm/6
linux/arm/5
windows/amd64
darwin/amd64

Run go tool dist list to see all possible combinations supported by go in case you wish to add your own.

NOTE: The IronFlock Agent only supports a limited amount of targets, please read more here

Target platform and architecture limitations

Due to this project using a CGo-free port of SQLite/SQLite3, the IronFlock Agent can only be built into a limited amount of target platforms and architectures:

linux/386
linux/amd64
linux/arm
linux/arm64
linux/riscv64
windows/amd64
windows/arm64
darwin/amd64
darwin/arm64
freebsd/amd64

Versioning

The version that is baked into the binary on build is determined by the string provided in the src/release/version.txt file. Adjust this file accordingly before making a build.

Once built the version of a binary can be verified with:

./reagent -version

Local build/publish (fallback)

CI (above) is the primary path. The original local recipes still work for ad-hoc builds and manual recovery, but require local gcloud/docker auth:

  • just build-all-docker — cross-compile every target inside the builder image into build/.
  • just publish — stage the build/ binaries with their .sha256 in the re-agent bucket (unsigned; just promote signs them).
  • just push-docker-image — build + push the multi-arch agent images.
  • just rollout — !!! USE WITH CAUTION !!! build + publish + promote (which signs) (incl. availableVersions.json) in one local step; make sure src/release/version.txt and availableVersions.json are correct first.

Implementation

The Reagent makes use of WAMP and Docker as its two key technologies.

WAMP

References

Docker

In order to implement a daemon running on an IoT device that is able to manage, create, stop and remove Docker containers we use the officially supported Go SDK.

Please remember that on a default docker setup on a Linux platform we always have to launch the daemon with root permissions, since the docker daemon is accessed via the socket ///var/run/docker.sock which is only readable with root permissions by default.

About

A daemon running on edge devices to establish a connection to the Reswarm router and manage apps and containers

Topics

Resources

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages