# What the installer refuses

The objects install will not build on, the values it will not put in a RouterOS command, and how uninstall proves it left nothing behind.

Source: https://jmrplens.github.io/mikroscope/security/installer/

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. This page lists them: 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.

## Nothing is written before it is listed

`install` builds the image, prints every command it would run with its exact RouterOS text, and
then, in this order:

1. runs `doctor`, the read-only preflight, unless `--no-doctor` is given; a missing prerequisite
   stops it with `N prerequisite(s) missing; nothing was written`;
2. asks `write the objects above to the router? [y/N]`, unless `--yes` is given; anything but `y` or
   `Y` stops it with `not confirmed; nothing written`;
3. only then writes.

`plan`, and `install --dry-run`, stop after the listing. The listing masks the token as
`value="(token)"`.

`upgrade` does not get this guarantee. It builds the image, refuses a router where any step of the
plan built from its own flags is missing (`nothing to upgrade: run install first`), and asks the
same `write the objects above to the router? [y/N]` — but it prints no listing and runs no `doctor`
first, so there are no objects above. On `y` it removes the container, the envlist and the image
and writes them again, the envlist from the flags given to `upgrade`; [what --expose
opens](/mikroscope/security/expose/) has what that means for the token.

**What `install` writes to your router**

- a veth
- one address
- one interface-list membership
- one address-list entry
- an envlist
- the image tar, unless `--remote-image` has the router pull the image
- the container

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.

## Objects it does not own

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:

```text
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
```

What counts as "the same effect" is the object's identity, not its name alone:

| Step                      | Collides with any existing                                                                        | Ours when it carries                                    |
| ------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| 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 using the same image file, an envlist named `<name>-env`, or a file at the image path | a container with the tag; an envlist holding the marker |

A step that is already ours is skipped, so running `install` twice creates nothing the second time.

The refusal happens when `install` reaches the colliding step. Steps before it that were absent
have already been created; they carry the tag, and `uninstall` removes them. `uninstall` never
touches the foreign object.

### The envlist and the image carry no comment

Neither `/container/envs` nor `/file` has a comment field, so the container step signs them another
way: 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.

## Selectors are exact

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. 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 address and port attributes. Unquoted, RouterOS parses them as typed values and
the comparison with the stored one comes back empty; verified for both on RB5009UG+S+, RouterOS 7.24.2, 2026-09-11.

## A write that prints is a failure

A RouterOS write prints nothing on success, and over ssh it reports errors as text with exit status
0, 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.

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.

`uninstall` reads output the same way in the other direction: a removal that printed something is
reported as `skip`, not `gone`.

## Values it will not put in a command

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_.-]{0,31}$`                                                               |
| `--veth`, `--iface-list`, `--addr-list` | `^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$`                                                               |
| `--disk`                                | `^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$`, or empty for the internal flash; `--ephemeral` forces `tmpfs` |
| `--arch`                                | `^[a-z0-9]{1,16}$`                                                                                |
| `--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                                                                                             |
| `--expose`                              | needs `--lan-address` as an IPv4 address, and 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                    |

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 may retry it up to five times. No run
with a bad `TRIGGERS` on the router is recorded.

## What it refuses to send to the router

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 `--arch`; otherwise the verb stops, and
  for a mismatch it names the asset to download instead
  (`--agent-tar … is a linux/arm64 image and --arch says arm: download the mikroscope-agent-arm.tar
  asset instead`). 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 against `checksums.txt` from 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. The router then needs to reach that registry over its own network, and it takes
  the registry host from `/container/config registry-url`, a setting global to the device and shared
  with every other container on it and ships as `https://registry-1.docker.io`. **mikroscope never
  writes that setting.** `doctor` reads it, and when the reference names a host the setting does not
  match it prints the one command to run
  (`/container/config/set registry-url=https://ghcr.io`, for the GHCR copy of the image) or says to
  use `--agent-tar` instead. The Docker Hub reference `jmrplens/mikroscope-agent:1.0.0` carries no
  host and leaves the setting as the router has it. Trust in the image is trust in that registry:
  nothing in the CLI verifies what the router pulls.

## A generated .rsc script is a credential

`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,
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.

`plan` and `install --dry-run` mask the token in what they print to the terminal
(`value="(token)"`); `--rsc` cannot, since the script has to run.

## How uninstall proves it is done

`uninstall` runs every removal newest first, ignoring what is already gone. Then it asks the router,
in one connect, how many objects each step created are still there, prints one line per step with
the count, and fails naming every step whose count is not zero
(`uninstall left objects behind: …`). A removal that printed nothing is not evidence; the count is.
`status` runs the same count on its own.

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 removed;
- `/container/remove` returns before the container is gone, and a `/file/remove` of the image issued
  meanwhile did nothing, silently (RB5009UG+S+, RouterOS 7.24.2, 2026-09-11) — 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.

A doctor → install → status → upgrade → uninstall round trip (`make roundtrip`) left the router's `/export` byte-identical, compared by hash (verified on RB5009UG+S+, RouterOS 7.24.2, 2026-09-12).

> **True of this device, not of yours**
>
> The byte-identical `/export` after that one round trip, and every RouterOS behaviour quoted on
> this page — the silent `/file/remove`, the quoted `find` selectors, errors printed with exit
> status 0 — were observed on one RB5009 running RouterOS 7.24.2. The refusal logic itself is
> covered by tests against a fake router, not by a run on another board or RouterOS version.

## See also

- [Installing the agent](/mikroscope/install/): the commands, and what `doctor` checks before any
  of this.
- [Where things go](/mikroscope/install/layout/): every object `install` creates, and the flag that
  moves it.
- [What --expose opens](/mikroscope/security/expose/): the two optional firewall rules and their
  selectors.
- [Commands and flags](/mikroscope/reference/cli/): every flag with its default and variable.
