Installer safeguards
The installer writes to a router it did not configure, over the operator’s own admin ssh session,
so there is no privilege boundary between a mistake and the router. What stands in for one is a
set of refusals: what install stops on before it writes, what it will not touch, what it treats as
a failure, and how uninstall shows that it is done rather than saying so.
Listing before writing
Section titled “Listing before writing”install writes nothing until it has printed every command it would run, with its exact RouterOS
text. In this order, it:
- runs
doctor, the read-only preflight, in one ssh connect, unless--no-doctoris given; a missing prerequisite stops it withN prerequisite(s) missing; nothing was written. AWARNline is printed and does not stop it. With--no-doctor, no--archand no--remote-image, one connect of a single read takes the router’s architecture instead, and the output says so; - builds or loads the image for the architecture the router reported (
--agent-tarmust match it;--remote-imageleaves the choice to RouterOS); - prints the listing, with the token masked as
value="(token)"; - asks
write the objects above to the router? [y/N], unless--yesis given; anything butyorYstops it withnot confirmed; nothing written; - only then writes, the install manifest first.
plan, install --dry-run and upgrade --dry-run stop after the listing and open no connection.
upgrade runs no doctor. In one connect it reads the shape the install was made with (its
manifest, or its tagged objects for an install that has none), whether every step is there, the
router’s architecture and, with --remote-image, doctor’s registry-credential check, which it
prints; when the shape it reads changes the plan the flags gave, a second connect asks about the
steps of that plan. It refuses a router with no install (nothing to upgrade: run install first), and an
install whose envlist holds a TOKEN when no --token is given. It lists the install manifest,
which it writes first every time, and the container step; any other step of the install the router
no longer holds is listed after them and created before the container. Then it asks the same
question. On y it removes the container, the envlist and the image and writes them
again, the envlist from the flags given to upgrade.
What install writes to your router
- the install manifest, a file
mikroscope/on the install's disk that lists the options and every object below<name>. manifest.txt - a veth
- one address
- one interface-list membership, unless
--iface-list none - one address-list entry, unless
--addr-list none - an envlist
- the image tar, deleted once the container is extracted, unless
--remote-imagehas the router pull the image - the container, and its root
mikroscope/<name>on the same disk
Every object carries the comment mikroscope:<name> (managed by mikroscope)
mikroscope plan prints every command before anything is written.
uninstall removes by exact tag plus identity, never by pattern, and fails naming the step if anything remains.
Foreign objects
Section titled “Foreign objects”doctor names the objects an install would collide with before anything is written: a veth, an
envlist or a container name that is not this install’s, a file at the install manifest’s path that
is not its manifest, and a route that overlaps the /30. Then, before writing, install asks the
router three questions about every step in one ssh connect: is our object there, does something
with the same effect exist, and does it carry our tag. An object that exists but does not carry
mikroscope’s tag stops install, naming the step:
veth interface veth-mikroscope exists on the router and was not created by mikroscope (no ownership tag); pick another --name/--veth/--subnet, or remove it by hand if it is yours; nothing was writtenWhat counts as “the same effect” is the object’s identity, not its name alone:
| Step | Collides with any existing | Ours when it carries |
|---|---|---|
| install manifest | a file at mikroscope/ on the install’s disk |
its tag= line holding the tag |
| veth | /interface/veth with the same name |
the tag in comment |
| router address | /ip/address on that veth |
the tag in comment |
| interface-list membership | member with that interface in that list | the tag in comment |
| address-list membership | entry with the /30 in that list | the tag in comment |
| expose dst-nat | dstnat rule with that destination address, port and protocol |
the tag in comment |
| expose forward accept | forward rule with the container’s address, that port and protocol |
the tag in comment |
| container | a container on the same veth, an envlist named <name>-env, a file at the image path, or a container named --container-name |
a container with the tag; an envlist holding the marker |
Scroll sideways to see every column
A step that is already ours is skipped, so running install twice creates nothing the second time.
install has every answer before its first write, so a foreign object at any step stops it with
nothing written, with --no-doctor too. uninstall never touches the foreign object.
Untagged objects
Section titled “Untagged objects”Neither /container/envs nor /file has a comment field, so the install signs them another way.
The manifest carries the tag on a tag= line and is this install’s only while it does. In the
container step, the first entry written to the envlist is MIKROSCOPE_TAG holding the exact tag, and the
image file counts as ours only while that marker exists. A foreign envlist with the same name, a
foreign file at the image path, or another container’s envlist is therefore foreign, and stops
install. Leftovers of an earlier mikroscope install — an envlist under our marker with no
container — are ours to replace, and install clears them before writing new ones.
Exact selectors
Section titled “Exact selectors”Every object install creates carries the comment mikroscope:<name> (managed by mikroscope),
verbatim. Every network object is removed by that exact comment together with the identity its check used —
comment="…", never a pattern match. The container is removed by the comment alone, and the
envlist and the image, which carry no comment, by list="<name>-env" and the exact image file name,
only while the marker entry holding the exact tag exists; the manifest, the container root and the
mikroscope directory by their exact paths, which --name and --disk derive. So uninstall cannot reach a hand-made
setup, or anything else whose comment or name shares a substring with the container name.
Every find quotes every non-numeric value except the chain and action enums. Unquoted, an
address or a port is parsed as a typed value and the comparison with the stored one comes back
empty. A bare word such as tcp is read as a variable name, whose unset value is empty, so
find chain=dstnat protocol=tcp matches no rule where protocol="tcp" matches every TCP one.
Write errors
Section titled “Write errors”A RouterOS write prints nothing on success, and over ssh it reports errors as text, with exit
status 0 or 1, abandoning the rest of a ;-joined line. So install treats any output from a
write as a failure (create <step>: router said "…") and stops there. When ssh exits non-zero, the
error starts with what RouterOS printed, so the words survive where only an error’s first line is
shown, as in uninstall’s skip lines.
The image tar is uploaded with scp before the container step runs, and before the marker exists.
If that step then fails, install removes the uploaded file (undo removed the uploaded …);
otherwise it would count as a foreign file on every later attempt. The tar stays when a container
of this install already holds it — the step stopped at its wait for the extraction, and RouterOS may
still be extracting it (keep …) — and uninstall removes the two together. The ssh deadline
for one command is 3 minutes, or the --extract-timeout and one minute more when that is longer,
so the extraction wait ends with its own message rather than with ssh killed.
uninstall reads output the same way in the other direction: a removal that printed something is
reported as skip, not gone.
Input bounds
Section titled “Input bounds”Every flag that reaches a RouterOS command is interpolated into it verbatim. There is no privilege to escalate — the command runs as your admin — but a quote or a semicolon would turn a clear error into a confusing RouterOS syntax error, or a selector into something wider than intended. So every value is checked before the first connect, and one outside these bounds stops the command with the rule it broke:
| Flag | Accepted |
|---|---|
--name |
^[A-Za-z0-9][A-Za-z0-9_. |
--veth |
^[A-Za-z0-9][A-Za-z0-9_. |
--iface-list, --addr-list |
^[A-Za-z0-9][A-Za-z0-9_., or none for no membership; --iface-list refuses RouterOS’s built-in lists all, dynamic and static, which take no member |
--container-name |
^[A-Za-z0-9][A-Za-z0-9_., or empty for the name RouterOS gives |
--disk |
^[A-Za-z0-9][A-Za-z0-9_, or empty for the internal flash; --ephemeral forces tmpfs |
--arch |
arm64, arm, amd64 or auto |
--token |
^[A-Za-z0-9_.-]{0,128}$ |
--subnet |
an IPv4 /30, written as its network address |
--port |
1–65535 |
--rate |
1–100 Hz |
--buffer |
10–3600 s |
--memory-max |
^\d{1,6}[KMG]?$ |
--mem-limit-mb |
8–1024 |
--floor-hz |
0–1000 |
--capture-mb |
0–256 |
--restart-max-count |
0–100 |
--restart-interval |
^\d{1,4}[smh]$ |
--start-on-boot |
auto, yes or no |
--extract-timeout |
^\d{1,4}[smh]$, 10–600 s |
--expose |
needs --lan-address as an IPv4 address; install, upgrade and plan also need a non-empty token |
--triggers |
the agent’s own condition list, parsed by agent.ParseTriggers |
--remote-image |
a registry reference: owner/name:1.0.0, with or without a host, no quote, space or semicolon |
--ssh-option |
a key from StrictHostKeyChecking, UserKnownHostsFile, ConnectTimeout, HostKeyAlgorithms, PubkeyAcceptedAlgorithms, IdentitiesOnly and ServerAliveInterval, and a value matching ^[A-Za-z0-9_./ |
Scroll sideways to see every column
--ssh-option does not reach RouterOS but the ssh and scp command lines, and the same rule applies:
no key that runs a command or reads a file as configuration (ProxyCommand, LocalCommand,
Include), so a value in an env file cannot become command execution. Its options go before the
CLI’s own -o BatchMode=yes -o ConnectTimeout=15, because ssh keeps the first value it reads for
an option.
There is no exception. --triggers is checked by the same gate as the rest: Finish hands it to
agent.ParseTriggers, the agent’s own parser, which is the authority on what a condition means.
An unknown condition, a malformed threshold, a quote or a semicolon fails the verb with exit
status 2 before the first connect, and nothing is written.
The agent parses TRIGGERS again when it starts, because the envlist can be edited on the router
by hand. On a value it cannot parse it exits non-zero with a mikroscope-agent: bad configuration: …
line in the router log; under the on-failure policy RouterOS retries it up to
--restart-max-count times.
Image checks
Section titled “Image checks”Two of the four install routes hand the router something the CLI did not build, and each gets a check of its own before anything is written.
--agent-tar <file>, the image tar the release publishes, is read and inspected on your host first. It has to be a docker-save tar of exactly one image with one layer whose entrypoint is/mikroscope-agent, and its architecture has to match the router’s (read bydoctor) or an explicit--arch; otherwise the verb stops, and for a mismatch it names the asset to download instead (--agent-tar … is a linux/arm64 image and the router is arm: download the mikroscope-agent-armv5.tar asset instead, witharmv7for--goarm 7). That check says the tar is a mikroscope agent image of the right architecture. It does not say the tar is the one the release published — verify it againstchecksums.txtfrom the release, and its cosign signature if you use one, before you pass it.--remote-image <reference>makes the router pull the image itself, so nothing is uploaded and no tar lands on the device. The reference is matched against a registry-reference pattern before it reaches the command line, because RouterOS takes it inside a quoted string on a;-joined line. It goes intoremote-image=whole, registry host included: a reference with no host, or with a Docker Hub alias, becomesregistry-1.docker.io/…, and any other host is kept. The router then needs to reach that registry over its own network. mikroscope never writes/container/config, the global setting that holds the device’sregistry-urland its one registry username and password, and needs nothing in it: the host insideremote-image=overridesregistry-url(Registry settings). Trust in the image is trust in that registry: nothing in the CLI verifies what the router pulls. When a username is set, RouterOS presents it to the registryregistry-urlnames, and a credential meant for another registry can end the pull of a public image inauth error.doctorwarns when a username is set andregistry-urlis empty or names a host other than the one the image comes from (WARN no registry credential meant for another registry), andupgradeprints the same check before it removes anything; both readregistry-urland only whether a username is set, never the name. The password cannot be read back at all.--agent-tarpulls from no registry, so no registry credential is sent.
Scripts and tokens
Section titled “Scripts and tokens”plan --rsc writes the install as a RouterOS script for a router you reach only through WinBox or
WebFig. It carries the same commands install runs, in the same order, with the same tags and the
same install manifest, so uninstall removes a scripted install as completely as its own. It runs
as one block whose guards stop it before its first write on a router that cannot take it: below
RouterOS 7.24, without the container package or device-mode, with the tar missing on the tar route,
or with a veth, an envlist, a container of --container-name or a file at the manifest’s path of
the install’s names that is not mikroscope’s. An envlist without mikroscope’s marker would
otherwise take the marker, and uninstall would then remove the entries it holds. And,
when --token or MIKROSCOPE_TOKEN is set, the envlist line carries the token in clear, because
the router needs it. The script says so in its own header. Treat the file the way you treat the
token: do not commit it, do not paste it where it is logged, and delete it from the router’s Files
after /import. Without a token it holds no secret, only the plan. Paste it only at the ] >
prompt: a script pasted while RouterOS is asking about the licence loses its first lines.
RouterOS script and the Script generator
show both ways to run it.
plan, install --dry-run and upgrade --dry-run mask the token in what they print to the
terminal (value="(token)"); --rsc cannot, since the script has to run.
The CLI’s own commands never put the token on a command line of this machine: the RouterOS command
that writes it reaches ssh on its standard input, so it does not show in the process table while
ssh runs. The CLI reads it from --token or MIKROSCOPE_TOKEN; the variable keeps it off the
CLI’s own command line too.
Uninstall verification
Section titled “Uninstall verification”uninstall removes everything the install created, and nothing else.
- It reads the install manifest,
mikroscope/, in the connect that reads the install’s shape, and takes from it the shape the install was made with; a flag that contradicts it is refused, naming both values. An install made before the manifest existed has none, and its shape is read from its tagged objects.<name>. manifest.txt - It runs every removal newest first, ignoring what is already gone. The container step, once it
has removed this install’s container, removes the container root
mikroscope/<name>if RouterOS left it. - It removes, by the exact tag and nothing wider, any other object that carries the install’s tag in the menus an install writes to, and in any other menu the manifest lists.
- It removes the manifest last: first the container root, when one is left, no container holds it
and the manifest is this install’s, then the manifest, then the
mikroscopedirectory when nothing else is in it. A/file/removeof a directory takes everything under it, so a directory with anything else in it stays. A path is removed only on the word of the tagged container or of the manifest; without either, a root no container holds is reported, not removed. - It asks the router, in one connect, how many objects of each step are still there, how many
carry the tag in each menu beyond what those steps counted, and whether any path the manifest
lists is there, prints one line per step with the count, and fails naming every step, menu or
path that remains (
uninstall left objects behind: …). A removal that printed nothing is not evidence; the count is.
status runs the same count on its own, in the connect that reads the install’s shape.
What uninstall never touches: device-mode, the container package, /container/config, and any
list, disk, rule or file the router had before the install, a mikroscope directory with other
files in it included. An empty mikroscope directory on the install’s disk is removed even when it
was there before the install: nothing in it says whose it is, and removing it takes nothing with
it.
The container step is the slow one, and the order inside it is what keeps the count honest:
- the container is stopped (guarded, because stopping a stopped container is an error), and
uninstallwaits, up to 30 s, until RouterOS reports it neither running nor stopping: RouterOS refuses to remove a container while it is still stopping (cannot remove running), and clears the running flag before it has stopped; /container/removereturns before the container is gone, and a/file/removeof the image issued meanwhile does nothing, silently, so it waits up to 20 s for the container to vanish, then retries the file removal for up to 15 s;- the rest of the envlist goes, and the marker goes last, only once the file is gone.
If the file removal does not take, the marker stays, the count keeps including the envlist and the
file, and uninstall says so instead of reporting clean.
The virtual RouterOS lab checks this
for every install route, and for an install without a manifest: after uninstall, the router’s
/export equals the one taken before the install, and /file lists no path of mikroscope’s.