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
| Area | What you get |
|---|---|
| Patching | Update 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 |
| Machines | Windows Server, Linux servers (Ubuntu, Debian, RHEL family; apt/dnf, security-only or all, Linux servers), Windows 10/11 endpoints (Pro) |
| Agents | Go agent with one-line install and enrollment tokens, Marshal CA and mutual TLS, approval, self-update rings, remote uninstall |
| Networks | Managed Linux proxies (forwarding + cache, routes with failover) for networks that can’t reach the console |
| Operations | Notifications (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 |
| Security | Signed releases, TLS 1.3, separated keys and database roles: Security review |
| Licensing | Free 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:
| Free | Pro | |
|---|---|---|
| Customers | 1 | unlimited |
| Machines | 25 | unlimited |
| Users | 3 local | unlimited, plus Microsoft (Entra ID) sign-in, users per customer, audit log |
| Notifications | + Teams, Slack, webhooks | |
| Extras | Windows 10/11 endpoints, reports, compliance CSV exports, REST API | |
| Hosting | self-hosted | self-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.
| Service | What it does |
|---|---|
api | FastAPI + the built React UI on HTTP_PORT (default 8081). Runs DB migrations on start. |
agent-api | Where agents connect: mutual TLS on AGENT_PORT (default 8443), terminated by the app itself. |
worker | Runs MSRC syncs and patch runs, queues the daily scans (agents carry them out), and marks servers unreachable when their agent stops checking in. |
db | PostgreSQL 17. |
backup | Nightly 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.keyencrypts MFA and notification secrets. Without it you re-enter them and re-enrol MFA.agent-api/ca.key/ca.crtare the Marshal CA every agent pins. Without them, every agent must be installed again.agent-api/agent-signing.pubis 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)
- Add
AGENT_DB_PASSWORDandPROXY_SECRETto.env(see.env.example), and theX-Marshal-Proxy-Secretline to your nginx site.BIND_ADDRESSnow defaults to 127.0.0.1. - At its first start the api moves the keys into
data/keys/{vault,agent-api,signing}. The old signing key ends up indata/keys/signing/: copy it to the build machine, setSIGNING_KEY_FILEto it, rebuild, then deletedata/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.
- 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. - 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 -Commandform for cmd/RMM), and themsiexecline for GPO, Intune or an RMM tool. - 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 shortirm <web address>/install/<token> | iex. - 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.
- 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.
- Run the command on the Linux box: the
curl … | sudo shone-liner (installs themarshal-proxysystemd service, logs in/opt/marshal-proxy/logs), ordocker run …/ docker compose (the image comes from the console itself). Open the two ports in its firewall. - 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.
- 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>andX-Marshal-Signature-V2: sha256=<HMAC of "<timestamp>.<body>">(refuse old timestamps against replays), and for older receiversX-Marshal-Signature: sha256=<HMAC of the body>.
Set Settings → Web address so notifications link back into the app.
Microsoft sign-in (Entra ID)
- Entra ID → App registrations → New: single tenant, platform Web, redirect URI as shown under Settings → Sign in with Microsoft. Create a client secret.
- Enter the tenant ID, client ID and secret in Settings, and enable it.
- 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
- Copy the project (without
data/,backups/and.env), create.env, and rundocker compose up -d --build. - Stop the app, copy
data/keys/from the old server, restore the latest dump (see above), and start it again. - 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 keepAGENT_URL’s host name (point its DNS at the new server); a new name means reinstalling the agents. - 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
- 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.
- 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.
- 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.
- 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.