Docs

Admin guide

Patch management for MSPs and IT teams: Windows Server and Linux servers (all editions), plus Windows 10/11 endpoints with Pro. Machines download updates straight from the vendor; this app decides what gets installed and when, restarts servers in their maintenance windows, and checks they come back healthy. A small agent on each machine (a Windows service or a Linux systemd service) checks in with the console over HTTPS and carries out what the console decides. There’s no WSUS sync and no local update storage. Marshal does patching only.

Design and decisions: Design. Editions and licenses: Licensing. Security review and hardening roadmap: Security review.

What it does

AreaWhat you get
PatchingUpdate catalogue with MSRC severity and CVEs, per-customer approvals, maintenance windows and blackouts, install and reboot runs (tag order, limits, health checks, halt on failure), manual install/reboot, abort, live progress
MachinesWindows Server, Linux servers (Ubuntu, Debian, RHEL family; apt/dnf, security-only or all, Linux servers), Windows 10/11 endpoints (Pro)
AgentsGo agent with one-line install and enrollment tokens, Marshal CA and mutual TLS, approval, self-update rings, remote uninstall
NetworksManaged Linux proxies (forwarding + cache, routes with failover) for networks that can’t reach the console
OperationsNotifications (Microsoft 365 mail, SMTP, Teams, Slack, webhook), compliance, reports and CSV exports, REST API (Pro), users limited to customers, audit log, Microsoft (Entra ID) sign-in
SecuritySigned releases, TLS 1.3, separated keys and database roles: Security review
LicensingFree and Pro (self-hosted or Marshal Cloud), delivered online: Licensing

To try everything on a Windows server in one go, follow docs/TESTING.md.

Licensing

Free patches one organisation with all the core patching. Pro removes the limits and adds what a growing business needs:

FreePro
Customers1unlimited
Machines25unlimited
Users3 localunlimited, plus Microsoft (Entra ID) sign-in, users per customer, audit log
Notificationsemail+ Teams, Slack, webhooks
ExtrasWindows 10/11 endpoints, reports, compliance CSV exports, REST API
Hostingself-hostedself-hosted, or Marshal Cloud
Price€0€35 a month or €300 a year (Pro Cloud: €50 / €500)

Licenses are delivered online to a console bound to an organisation (Administration > License). The full plan (editions, pricing, expiry, the vendor tool, and what’s still open): Licensing.

Running it

cp .env.example .env            # set POSTGRES_PASSWORD, AGENT_DB_PASSWORD, PROXY_SECRET (openssl rand -hex 32) and AGENT_URL
# The release-signing key, once, on the machine that builds the image (not in ./data, not on a shared drive):
docker run --rm -v "$PWD/agent":/src -v ~/marshal-keys:/keys -w /src golang:1.26.9-bookworm \
    go run ./tools/sign keygen -out /keys/agent-signing.key
echo "SIGNING_KEY_FILE=$HOME/marshal-keys/agent-signing.key" >> .env
docker compose up -d --build
docker compose logs api | grep "Setup token"

Then open the web address (through nginx, below), enter the setup token and create the first admin. Signing in for the first time sets up an authenticator app (TOTP). MFA is mandatory for every local account.

The signing key signs the agent and proxy builds while the image is built (a BuildKit secret: it never goes into the image or ./data). Agents only install updates signed with it, so a compromised console can’t push code to your servers. Keep a copy of it offline. Without it the console runs normally but offers no agent updates.

ServiceWhat it does
apiFastAPI + the built React UI on HTTP_PORT (default 8081). Runs DB migrations on start.
agent-apiWhere agents connect: mutual TLS on AGENT_PORT (default 8443), terminated by the app itself.
workerRuns MSRC syncs and patch runs, queues the daily scans (agents carry them out), and marks servers unreachable when their agent stops checking in.
dbPostgreSQL 17.
backupNightly pg_dump to ./backups, encrypted with BACKUP_AGE_RECIPIENT (an age public key; keep the private key elsewhere), keeps BACKUP_KEEP_DAYS.

Each container gets only what it needs: the agent API (the part on the internet) has the CA and its own TLS key but no vault key and no signing key, and its own database role (marshal_agent) that can’t read users, sessions or notification secrets. The worker has only the vault key.

Put nginx in front of the UI. deploy/nginx-marshal.conf is a sample site to start from: TLS with HSTS, and the X-Marshal-Proxy-Secret header set to PROXY_SECRET, without which the app ignores X-Forwarded-For/-Proto (so nobody can fake their address in the audit log). The UI port is published on BIND_ADDRESS (127.0.0.1 by default). The agent port must reach the app untouched: expose it directly, or pass it through with an nginx stream block (deploy/nginx-marshal-agents.conf, a sample). Never terminate its TLS in nginx, or agents can’t present their certificates. Agents and proxies use TLS 1.3; TLS 1.2 (forward-secret AEAD ciphers only) is left for the PowerShell install one-liner on Windows versions without TLS 1.3.

Back up ./data/keys (and the signing key)

None of it is in the database backups. Copy it somewhere safe, once. The checklist is in Security review.

  • vault/master.key encrypts MFA and notification secrets. Without it you re-enter them and re-enrol MFA.
  • agent-api/ca.key / ca.crt are the Marshal CA every agent pins. Without them, every agent must be installed again. agent-api/agent-signing.pub is the update-signing public key the console pinned.
  • The update-signing private key lives on the build machine (SIGNING_KEY_FILE), not here. Back it up offline: without it no agent can be updated again until it is reinstalled.

Settings shows the fingerprints so you can check that a copy matches.

Restore: put the keys back in ./data/keys/, start db, then docker compose exec -T db pg_restore -U marshal -d marshal --clean < backups/marshal-YYYYMMDD-HHMM.dump (for an encrypted dump: age -d -i key.txt marshal-….dump.age | docker compose exec -T db pg_restore …).

Upgrading from 0.5 (security hardening)

  1. Add AGENT_DB_PASSWORD and PROXY_SECRET to .env (see .env.example), and the X-Marshal-Proxy-Secret line to your nginx site. BIND_ADDRESS now defaults to 127.0.0.1.
  2. At its first start the api moves the keys into data/keys/{vault,agent-api,signing}. The old signing key ends up in data/keys/signing/: copy it to the build machine, set SIGNING_KEY_FILE to it, rebuild, then delete data/keys/signing/ (Settings warns while it is there). Agents keep trusting updates, as the key stays the same.

Adding servers

Servers join by installing the agent; nothing is added by hand and the console never logs in to them.

  1. Customers: create the customer. If its servers reach the console at another address than AGENT_URL, set Agent URL; if they can’t reach it at all, give them a proxy first (below) and put it into the customer’s route.
  2. Add servers (customer page or Servers): choose the tags, how long the link stays valid (1 day by default), optionally how many servers may use it, and (admins) whether new servers are approved automatically. The script is a credential: anyone holding it can add a server to that customer. You get a one-liner (plus a powershell -Command form for cmd/RMM), and the msiexec line for GPO, Intune or an RMM tool.
  3. Paste the one-liner into an elevated PowerShell on each server. It accepts only the agent API’s own key (pinned in the line), downloads the install script from the agent port, which downloads MarshalAgent.msi, checks its hash and installs the MarshalAgent service. If servers can reach the web address and it has a publicly trusted certificate, turn that on in Settings and the one-liner becomes the short irm <web address>/install/<token> | iex.
  4. The server shows up within a minute, awaiting approval unless the script approves automatically. Approve it: only approved agents are scanned and patched, and the first scan starts right away. Installing the agent again on a server that is already in the console puts it back to awaiting approval (unless the script approves automatically) and raises an enrolled again notification, because a reinstall can’t be told apart from someone posing as the server.

What the server needs: outbound HTTPS to AGENT_URL, or to its proxy. No inbound ports, no WinRM, no domain trust; workgroup and domain servers work the same.

Proxies: networks that can’t reach the console

A proxy is a small Linux service (any systemd distribution, x86-64 or ARM64, or Docker) in the customer’s network that can reach the console. Servers there connect to the proxy instead; it forwards them to the console without seeing inside their encrypted connection (it can’t read or forge anything), and caches Marshal’s files (the MSI and agent updates) so each is pulled over the WAN once.

  1. Proxies → Add proxy (or a customer’s Proxies tab): for one customer, or shared (admins; usable by every customer, e.g. one proxy in a datacenter). Pick the ports endpoints use (8443 forwarding, 8444 cache by default) and, if that network only goes out through a corporate HTTP proxy, give it.
  2. Run the command on the Linux box: the curl … | sudo sh one-liner (installs the marshal-proxy systemd service, logs in /opt/marshal-proxy/logs), or docker run … / docker compose (the image comes from the console itself). Open the two ports in its firewall.
  3. Approve the proxy (or let the link approve it) and check Address endpoints connect to on its page: it is taken from the proxy’s own IPs; type a DNS name or the NAT address if endpoints reach it another way.
  4. Put it into a route: on the customer (default for its servers), a tag (shared proxies only) or a single server, as an ordered list, for example proxy A → proxy B → direct. Agents use the first route that answers and come back to the first one when it recovers.

Installed agents move to a new route at their next check-in, without a reinstall, but only once the new route answers; otherwise they stay and the server page shows why. Install links made for a customer with a route go through its first proxy. Agents always check the console’s own certificate name, so a proxy can never pose as the console. A proxy that stops checking in raises a proxy unreachable notification listing the servers behind it. Remove on a proxy uninstalls it at its next check-in (systemd: everything is removed; Docker: it stops, then delete the container).

Agent updates

Each console upgrade brings the agent built with it. Under Agents, point the Pilot ring (customers set to Pilot, e.g. your test customer) at the new version, then Broad (everyone else). Customers on Manual only get the version pinned on them. Agents update while idle: they download the new exe from the console, check its SHA-256 and the release signature (made at build time; it covers the product, platform and version, and agents refuse an older version unless it was signed as a rollback), swap it in and restart. If the new version doesn’t check in within 10 minutes, a scheduled task puts the previous exe back; the failure shows on the Agents page and that version isn’t offered to the server again (Retry offers it once more).

A hotfix build: build it, sign it on the build machine and register it (see backend/app/tools/add_release.py): go run ./tools/sign release -key <key> -product agent -platform windows-amd64 -version 0.6.1 marshal-agent.exe writes marshal-agent.exe.sig.json; copy both into the api container and run python -m app.tools.add_release marshal-agent.exe 0.6.1. Add -allow-downgrade to sign a deliberate rollback.

Agents from before 0.3.0 can’t update themselves: install the current MarshalAgent.msi over them once, msiexec /i MarshalAgent.msi /qn, without properties, and the agent keeps its enrollment. (Running an install script again also works; the server then re-enrolls onto its existing record.)

Removing servers

Remove on a server: at its next check-in the agent uninstalls itself (Windows Installer) and its certificate is revoked. The server is archived: it leaves the lists and counts, and its run history stays (filter Servers by Removed to see it). Installing the agent on that machine again brings the record back. Delete instead drops the server and its history at once, but leaves the agent installed (it is locked out).

The agent keeps its state and certificate in C:\ProgramData\Marshal (SYSTEM and Administrators only). Its logs are next to the agent, in C:\Program Files\Marshal Agent\logs: one agent-YYYY-MM-DD.log per day with everything that happened that day, each line stamped with the local time (check-ins, status changes, every step of each scan, install or reboot tagged with its job number, Windows Update result codes, updates, errors). Logs older than 30 days are deleted; the folder stays under 200 MB and is readable only by SYSTEM and Administrators. "C:\Program Files\Marshal Agent\marshal-agent.exe" run runs it in the foreground for troubleshooting (stop the service first). Revoke a script under the customer’s Install scripts tab; Reject or Delete on a server locks its agent out.

Notifications

Integrations › Notifications (admin) holds the channels. Each one chooses its events and, optionally, which customers it covers. Send test checks a channel.

  • Email (Microsoft 365): needs an Entra app registration with the Microsoft Graph application permission Mail.Send (admin consent) and a client secret. Limit it to the sender mailbox with an Exchange application access policy (New-ApplicationAccessPolicy). Mail goes out through the Graph API over HTTPS.
  • Email (SMTP): any mail server: host, port, STARTTLS / TLS / none, and an optional sign-in. A mail channel with no events and no recipients can be used for reports only (Pro).
  • Teams: in the channel, Workflows → Post to a channel when a webhook request is received; paste the URL.
  • Slack: an incoming webhook URL.
  • Webhook: a JSON POST to any HTTPS URL, optionally signed: X-Marshal-Timestamp: <unix time> and X-Marshal-Signature-V2: sha256=<HMAC of "<timestamp>.<body>"> (refuse old timestamps against replays), and for older receivers X-Marshal-Signature: sha256=<HMAC of the body>.

Set Settings → Web address so notifications link back into the app.

Microsoft sign-in (Entra ID)

  1. Entra ID → App registrations → New: single tenant, platform Web, redirect URI as shown under Settings → Sign in with Microsoft. Create a client secret.
  2. Enter the tenant ID, client ID and secret in Settings, and enable it.
  3. Under Users, add each person as a Microsoft account, using their sign-in name. Only those users can get in. Entra enforces their MFA, so use Conditional Access or Security Defaults.

Users can be limited to certain customers (Users → Limit to customers). They then only see those customers, and cannot create customers or change tags.

Moving to the production server

  1. Copy the project (without data/, backups/ and .env), create .env, and run docker compose up -d --build.
  2. Stop the app, copy data/keys/ from the old server, restore the latest dump (see above), and start it again.
  3. Copy all of data/keys/ too: agents pin the CA, so with the same CA they carry on. Agents call the address they were installed with, so keep AGENT_URL’s host name (point its DNS at the new server); a new name means reinstalling the agents.
  4. Set Settings → Web address to the new address, and update the Entra redirect URI if it changed.

Without the original master.key, the app still starts and you can sign in with a Microsoft account. Notification secrets and local users’ MFA must then be re-entered.

Leftover WSUS policy

Scans query Windows Update directly, ignoring any WSUS policy, but Windows itself still follows it. Servers still pointed at WSUS by GPO are flagged on the dashboard and server pages. Remove the WSUS GPO for those servers, and set Configure Automatic Updates to 2 - Notify for download and auto install so that Windows doesn’t install updates or reboot on its own.

Patching in short

  1. Approve. In the update catalogue, pick a customer (or none, for “every customer that needs it”) and approve or decline updates, one at a time or in bulk. Nothing installs without an approval. Approving an update declines the updates it supersedes (the update’s page lists them). Decline everywhere blocks a KB for good. The catalogue is shared, so what servers report only becomes everyone’s once servers of two customers report the same; until then an update’s supersedence only declines updates for the customer that reported it, and an update whose details customers disagree on is marked details differ.
  2. Windows. On a customer’s Maintenance windows tab, add an install window and a reboot window, for example install on Patch Tuesday + 4 days at 01:00 for 4 h, and reboot every Sunday at 03:00 for 2 h. Tags (the calendar icon on Tags) and single servers can override them. Next windows per server shows what applies, and blackout dates pause everything.
  3. Runs start by themselves when a window opens:
    • Install: each server with approved, missing updates installs exactly those updates through its agent. If a reboot it didn’t cause is already pending, the server is skipped.
    • Reboot: only servers whose install left a reboot owed. They go in tag order (lowest first, the next tag after the previous is healthy), within the customer’s and each tag’s limits. Each server’s agent reboots it, comes back, and checks its tag’s required services. A failure halts the rest of that run.
    • Agents hear about new work within about a second (also through proxies): between check-ins they keep one request open that the console answers the moment something is queued for them (re-opened every 2 minutes). They also report changes on the server (pending reboot, boot, low disk…) the moment they notice them (checked every 30 s).
    • No new server starts after the window ends; leftovers wait for the next window.
  4. Admins can Install now, Reboot now or Abort from a customer, server or run page.

Every server rescans after its install or reboot, so the catalogue stays current.