Skip to content

Repository files navigation

Mandatum

Self-hosted remote task execution for Windows fleets, in two small Rust binaries. A central server publishes a per-host task list and hosts the scripts / installers; every machine runs the client from a scheduled task, which asks "what should I run?", downloads and verifies the files, runs them, and reports back. A minimal, dependency-free alternative to a configuration-management agent for pushing PowerShell scripts and .exe / .msi installers.

mandatum

Mandatum executes whatever the server hands out. Since 1.0 every manifest is signed, every file is hash-verified and every request is authenticated with a shared token, and the server can speak HTTPS — but whoever controls the server (or its scripts/ folder) still controls every client. Treat the server like a domain controller. See Security model.

                ┌──────────────────────────────────────────────────────┐
                │                        SERVER                         │
                │  master_task_config.json  (hot-reloaded on change)    │
                │  GET  /api/tasks/{host}  → signed manifest + SHA-256s  │
                │  GET  /api/files/...     → scripts / installers        │
                │  POST /api/status        → signed result reports       │
                │  GET  /api/hosts  /api/reports  /api/health            │
                └──────────────────────────────────────────────────────┘
                     ▲ Bearer token          │ files            ▲ HMAC-signed
                     │                       ▼                  │ report
                     │                ┌─────────────┐           │
                     └────────────────│   CLIENT    │───────────┘
                                      │ (per host,  │
                                      │  sched task)│
                                      └─────────────┘
                                             │ verify SHA-256 → run with timeout
                                             ▼ powershell.exe -NoProfile -ExecutionPolicy Bypass -File …

Quick start

  1. Download mandatum-<version>-windows-x64.zip (or the Linux / macOS tarball for the server) from the latest release and unpack it on the server.
  2. Generate a token: server --gen-token. Put it into server_config.json and client_config.json.
  3. Put your scripts into scripts/ and describe who runs what in master_task_config.json (or open gui.html in a browser — it edits and downloads that file).
  4. server --check, then server. It logs to logs/server.log and appends every report to reports/status.jsonl.
  5. On each machine: copy client.exe, client_config.json, log4rs-client.yaml into one folder, set server_base_url, run client --check, then import Mandatum.xml into Task Scheduler (runs daily as SYSTEM).

Configuration

server_config.json

key
listen_address, listen_port bind address (0.0.0.0 to serve the network)
master_config_path the fleet manifest (watched, hot-reloaded when it changes and still parses)
auth_token shared secret; leave empty only on a fully trusted network (the server warns loudly)
tls_cert, tls_key PEM chain + key → HTTPS (rustls). Self-signed is fine, see below
reports_path JSON-lines file receiving every status report (default reports/status.jsonl)
max_output_bytes script output kept per report (default 64 KiB)

master_task_config.json — host groups, each with a list of scripts. hostnames are case-insensitive; "*" matches everybody. Host-specific tasks come first, then wildcard tasks, each sorted by executionOrder.

{
  "configVersionID": "2026-08-21",
  "scriptBaseDirectory": "./scripts",
  "globalForceExecution": false,
  "hostConfigurations": [
    { "description": "Agent rollout", "hostnames": ["HOST01", "HOST02"],
      "scriptsToExecute": [
        { "name": "Install agent", "scriptPath": "installers/agent-setup.exe", "arguments": "/S",
          "executionOrder": 10, "timeoutSeconds": 900, "forceExecution": false } ] },
    { "description": "Everyone", "hostnames": ["*"],
      "scriptsToExecute": [
        { "name": "Remove language packs", "scriptPath": "remove_languages.ps1", "executionOrder": 10 } ] }
  ]
}

Per script: scriptPath (relative to scriptBaseDirectory, no ..), arguments (double quotes group words), executionOrder, timeoutSeconds (default 3600 — the process is killed after that), forceExecution (run even if this host already completed it). Supported types: .ps1, .exe, .msi (msiexec /i … /qn), .bat / .cmd on Windows; .ps1 (via pwsh) and .sh on Linux / macOS.

client_config.json

key
server_base_url http:// or https:// URL of the server
auth_token must equal the server's token
server_ca_cert PEM of your CA or the server's self-signed certificate (needed for HTTPS with a private CA)
log_and_state_base_dir where <host>/ExecutedScriptsState.json (completed tasks) and temp files live, relative to the exe
hostname_override report as a different hostname (testing)
request_timeout_seconds HTTP timeout (default 60)

All paths resolve relative to the executable's folder, so the client works from Task Scheduler (whose working directory is System32). Client logs go to client_logs/client.log (rolling).

HTTPS with a self-signed certificate

openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -keyout key.pem -out cert.pem -subj "/CN=mandatum" \
   -addext "subjectAltName=DNS:mandatum.example.internal,IP:10.0.0.5"

Server: "tls_cert": "cert.pem", "tls_key": "key.pem". Clients: "server_ca_cert": "cert.pem" and an https:// URL. The name / IP in the URL must match the SAN.

CLI

server                server --check        server --gen-token
client                client --check        client --dry-run

client exits 0 when everything succeeded, 1 when a task failed, 2 when the server could not be reached or rejected the token / signature.

API

All routes require Authorization: Bearer <auth_token> when a token is configured.

route
GET /api/tasks/{hostname} {hostname, configVersionId, issuedAt, tasks[]} — header X-Mandatum-Signature = hex HMAC-SHA256(token, body); every task carries sha256
GET /api/files/<scriptPath> the file
POST /api/status a TaskStatusReport; must carry X-Mandatum-Signature over the body
GET /api/hosts last report per host
GET /api/reports?host=NAME last 50 reports (per host) kept in memory; the full history is in reports/status.jsonl
GET /api/health version, config version, uptime, whether auth is on

Security model

What 1.0 gives you:

  • Authentication — nobody without the token can list tasks, fetch scripts or post reports.
  • Manifest integrity — the client verifies the HMAC over the exact bytes it received and refuses anything unsigned, mis-signed or older than 10 minutes (replay of an old manifest).
  • File integrity — every download is compared to the SHA-256 in the signed manifest; a tampered or swapped file is reported as HashMismatch and never executed.
  • Report integrity — forged status reports are rejected.
  • Transport confidentiality — optional; enable TLS if the token or script contents must not be readable on the wire.
  • No path traversalscriptPath cannot escape scriptBaseDirectory; /api/files serves only that folder.
  • Bounded execution — per-task timeouts, output size limits, one client instance at a time (Task Scheduler IgnoreNew).

What it does not give you: the server is the root of trust (protect its files and the token like credentials — rotating the token means updating every client), there is no per-host key, no audit of who changed the manifest, and a host that runs the client as SYSTEM will execute whatever an attacker with server access publishes. Keep the server on a management network and review scripts before publishing.

Build from source

cargo build --release      # target/release/server, target/release/client
cargo test && cargo clippy --all-targets -- -D warnings

The GitHub workflow builds Windows (x64), Linux (x64) and macOS (arm64) on every tag, runs the tests and a server ↔ client round trip, and attaches the archives to the release.

Changes in 1.0

  • shared-token authentication on every route, HMAC-SHA256 signed manifests and reports, SHA-256 verified downloads, manifest freshness check, optional HTTPS (rustls) with private-CA support on the client
  • per-task timeoutSeconds (processes are killed), quoted arguments, .msi / .bat / .cmd / .sh support, -NoProfile for PowerShell, scripts run from their temp folder
  • status reports carry task name, path, duration and timestamp; persisted to reports/status.jsonl; new /api/hosts, /api/reports, /api/health
  • hot reload watches the directory (survives editors that replace the file), debounced; bad JSON keeps the old config
  • server --gen-token / --check, client --check / --dry-run, meaningful exit codes, no panics on I/O or network errors, atomic state-file writes, stable task ids (derived from the path when not set)
  • gui.html edits timeoutSeconds; example configs include the new keys; release builds for all three platforms

Upgrading from 0.1: the manifest changed from a bare array to an object — 0.1 clients cannot talk to a 1.0 server and vice versa; upgrade both. Config files are backwards compatible (new keys are optional), but set auth_token.

About

Self-hosted remote task-execution system for Windows fleets (Rust client/server).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages