# Installer safeguards

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://docs.jmrp.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: 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

`install` writes nothing until it has printed every command it would run, with its exact RouterOS
text. In this order, it:

1. runs `doctor`, the read-only preflight, in one ssh connect, unless `--no-doctor` is given; a
   missing prerequisite stops it with `N prerequisite(s) missing; nothing was written`. A `WARN`
   line is printed and does not stop it. With `--no-doctor`, no `--arch` and no `--remote-image`,
   one connect of a single read takes the router's architecture instead, and the output says so;
2. builds or loads the image for the architecture the router reported (`--agent-tar` must match
   it; `--remote-image` leaves the choice to RouterOS);
3. prints the listing, with the token masked as `value="(token)"`;
4. 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`;
5. 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/<name>.manifest.txt` on the install's disk that lists the options and every object below
- 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-image` has 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

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

```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; nothing was written
```

What 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/<name>.manifest.txt` 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 |

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

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

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

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

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` | `^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$` |
| `--iface-list`, `--addr-list` | `^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$`, 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_.-]{0,31}$`, or empty for the name RouterOS gives |
| `--disk`                                | `^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$`, 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_./~+:-]{1,256}$` |

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

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 by `doctor`) 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`, with `armv7` for `--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 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. It goes into `remote-image=` whole, registry host included: a reference with no
  host, or with a Docker Hub alias, becomes `registry-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's `registry-url` and its one
  registry username and password, and needs nothing in it: the host inside `remote-image=` overrides
`registry-url` ([Registry
settings](https://docs.jmrp.io/mikroscope/install/routes/#nothing-to-set-on-the-router)). 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 registry `registry-url` names, and a credential meant for another
registry can end the pull of a public image in `auth error`. `doctor` warns when a
  username is set and `registry-url` is empty or names a host other than the one the image comes
  from (`WARN no registry credential meant for another registry`), and `upgrade` prints the same
  check before it removes anything; both read `registry-url` and only whether a username is set,
  never the name. The password cannot be read back at all.
  `--agent-tar` pulls from no registry, so no registry credential is sent.

## 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](https://docs.jmrp.io/mikroscope/install/script/) and the [Script generator](https://docs.jmrp.io/mikroscope/install/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

`uninstall` removes everything the install created, and nothing else.

1. It reads the install manifest, `mikroscope/<name>.manifest.txt`, 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.
2. 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.
3. 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.
4. 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 `mikroscope` directory when
   nothing else is in it. A `/file/remove` of 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.
5. 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
  `uninstall` waits, 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/remove` returns before the container is gone, and a `/file/remove` of 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](https://docs.jmrp.io/mikroscope/reference/testing/#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.

## See also

- [Install with the CLI](https://docs.jmrp.io/mikroscope/install/): the commands, and what `doctor` checks before any
  of this.
- [Storage and container settings](https://docs.jmrp.io/mikroscope/install/layout/): every object `install` creates,
  and the flag that moves it.
- [Expose on the LAN](https://docs.jmrp.io/mikroscope/security/expose/): the two optional firewall rules and their
  selectors.
- [CLI](https://docs.jmrp.io/mikroscope/reference/cli/): every flag with its default and variable.
