Skip to content
Mon–Fri · 09:00 — 12:00 New YorkReply within 1 business daycontact@ferwyn.com
FerwynContact
FerwynToggle navigation

Declare maintenance jobs as data, not as code

A maintenance tool that can run any command is one nobody should trust with admin rights. What changes when the job is declarative data instead of code.

4 min readWindows operationsInternal systems

Most fleet maintenance tooling ends up in the same shape. There is a runner, and the runner executes whatever the config file tells it to — a shell line, a PowerShell block, a path to another script. It is the obvious design, it takes an afternoon, and it works.

It also means the tool can do anything, which is a problem you only meet later, in a room with somebody from security asking what exactly you are installing on four hundred machines running as SYSTEM.

What “reviewable” actually requires

The usual answer to that question is “the script is right here, you can read it”. That answer sounds reasonable and does not survive contact.

If the runner executes arbitrary commands, reading the runner tells you nothing about what will happen. The behaviour is not in the code being reviewed — it is in a config file that will be edited next week by somebody else, possibly generated, possibly fetched. You have asked the reviewer to approve a capability, not an action, and no honest reviewer can approve “runs anything as SYSTEM”.

The change that fixes this is not more documentation. It is removing the capability.

Four actions instead of one

Our maintenance tool takes a job file that looks like this:

{
  "description": "Remove a stale application folder",
  "engine": "safemode",
  "steps": [
    { "action": "delete-paths", "volumeLabel": "Local Disk",
      "paths": ["Program Files\\VendorName"] },
    { "action": "remove-services", "names": ["VendorNameService"] },
    { "action": "delete-registry", "keys": ["HKLM\\SOFTWARE\\Vendor\\Product"] },
    { "action": "uninstall-product", "productCode": "{GUID}" }
  ]
}

Four permitted actions. Not “four convenient shortcuts plus an escape hatch” — four, and no way to express a fifth. There is no run-command, no script key, no field that takes a string and hands it to an interpreter.

This is a real loss. Everyone who uses the tool eventually wants a fifth action, and the honest answer is that they cannot have one without a change to the tool itself, reviewed and released. That friction is the feature. It is what makes the sentence “this tool physically cannot execute attacker-supplied commands” true rather than aspirational, and that sentence is the entire argument in any security review.

Guards belong in validation, not in execution

A fixed action list is necessary and not sufficient. delete-paths with no constraints is still a way to delete C:\Windows.

So each action carries guards, and every guard runs at validation time — before anything is armed, scheduled or rebooted into:

  • No drive letters. Paths resolve against a volume found by label. Drive letters differ between machines, and a job that cleans D:\VendorApp on your test box finds something else entirely on a machine where D: is the data volume.
  • No ... Obvious in hindsight, and the thing that turns a constrained path into an unconstrained one.
  • System directories are never deletable. Not “warn”, not “require a flag”. Refused.
  • Critical services and protected registry subtrees are refused against a blocklist, for the same reason.

Validating early matters more than the individual rules. A bad job is refused whole, and nothing is staged. The alternative — discovering the problem in step three of five, on a machine that has already rebooted into a maintenance context with nobody watching — is the failure mode this whole design exists to avoid.

Destructive by default is a choice, and it is the wrong one

With guards in place there is still the case where the job is valid, permitted and simply wrong: somebody typed the name of a folder that turned out to matter.

The cheap insurance is to not delete on the first pass. Folders on the same volume get moved to a timestamped quarantine directory — instant, and it costs no additional space, because a move within a volume is a metadata operation. Registry keys and service registrations get exported to .reg files before removal. A restore script puts any of it back.

The cost is a quarantine directory somebody has to clear eventually. The benefit is that the one run that goes wrong is recoverable, and you cannot know in advance which run that is.

What you get back

Making the job declarative buys three things that are hard to get any other way.

The job file is the entire contract. What you read is what runs. A reviewer approving a job is approving an action, not a capability.

Reports become structured for free. When the runner knows the shape of every step, it can emit a machine-readable result per step — outcome, target, backup location — alongside the human report. That is what turns a maintenance tool into something a fleet system can consume, and it falls out of the design rather than being bolted on.

Jobs can be built rather than written. A generator can walk somebody through a valid job with live validation, because “valid” is a property the format can check. Nothing equivalent exists for a config file whose contents are a script.

The whole thing is on GitHub as OfflineTasks, MIT licensed, if you want the working version rather than the argument. The argument transfers to anything that runs with privileges on machines you cannot watch: decide what the tool is not allowed to do, and make that decision structural rather than documented.

Does your version have a constraint this guide does not?

That constraint is usually the whole problem. Describe it and we will tell you whether it is the kind of thing we take on.

Start a project

We reply within 1 business day. No sales calls unless you ask for one.