# Security model

Which piece of mikroscope runs on the MikroTik router and which on your host, what each reaches, where every credential lives, and how to report a flaw.

Source: https://docs.jmrp.io/mikroscope/security/

The router holds an agent that listens and never connects out, and every credential that opens
something other than the agent stays on your host. The table below is the whole map: what runs
where, what it reaches, and which credential it holds.

## What runs where

| Piece                                                            | Runs                                 | Reaches                                                                                                               | Credentials                                                     |
| ---------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `mikroscope-agent`                                               | in a scratch container on the router | serves HTTP on the veth address only; **no outbound connection**                                                      | none it presents; an optional bearer token it _requires_        |
| `mikroscope doctor`, `install`, `upgrade`, `uninstall`, `status` | your host, when you run them         | the router over your own admin ssh; `install`, `upgrade` and `status` also probe the agent's `/healthz`, and `doctor` reads `/healthz` and then the agent's ring, the newest 10 000 samples at most; `uninstall --targets dashboard` or `data` also reaches your Grafana and the stores | your ssh key; `doctor` also presents the agent's token if one is set; `GRAFANA_TOKEN` and the sink credentials for those targets |
| `mikroscope plan`, `install --dry-run`, `upgrade --dry-run`      | your host                            | nothing: they build the image and print the listing without connecting to the router                                  | none, unless `plan --rsc` is given a token (below)              |
| `mikroscope record`, `forward`                                   | your host                            | the agent over HTTP; the router's binary API for `forward`'s API tier, the relay transport and `record --log-markers` | the agent's token if one is set; a dedicated read-only API user; `GRAFANA_TOKEN` with `--grafana` |
| `mikroscope mark`                                                | your host                            | the recording's local files; the router's binary API only with `--log-markers`; never the agent                       | the API user, only with `--log-markers`                         |
| sinks                                                            | your host, inside `forward`          | serve `/metrics` for your Prometheus; push to InfluxDB and the other destinations you name                            | their tokens, in your environment                               |
| `mikroscope dashboards publish`, `import`, `check`               | your host                            | your Grafana; `publish` creates or corrects the datasources and the folder there                                      | `GRAFANA_TOKEN`, in your environment; `publish` also the sink credentials it copies into the datasources |

`plan`, `install --dry-run` and `upgrade --dry-run` never reach the router, so they do not run the
ownership checks either; those run only on a real `install`.

Nothing about the tool is reported anywhere. There is no update check. In the code, the agent's
only network code is its HTTP server. The outbound connections all live in the CLI — the sinks
(HTTP, TCP or UDP), the Grafana client, the RouterOS API client, the direct transport, the
`/healthz` probe and `doctor`'s read of the ring, the last three over plain HTTP — and each goes only to
an address you gave it or, for the agent, to the `.2` of `--subnet` (`172.30.10.2` unless you
change it).

The collector's own Prometheus exposition, `forward --prom <address>`, listens on the address you
pass and serves `/metrics` with no authentication. Bind it to an address only your Prometheus
reaches.

## Credentials on the router

**Over the binary API, `/container/print` returns every property of every container, `cmd` and
`envlist` included, to a user with only `read,api`** ([verified](https://docs.jmrp.io/mikroscope/about/status/#verified-read-user-envlist)).
Whether such a user can also read the envlist entries' values, in `/container/envs`, is [not tested](https://docs.jmrp.io/mikroscope/about/status/#not-tested).
The design assumes it can: whatever is in an envlist is treated as readable by every `read` user
on that router, not only by administrators.

That is why the agent has no push sink: a sink token on the router would be readable by every
`read` user. If push ever comes, it will be agent → collector with the same NDJSON, never
agent → InfluxDB.

What `install` does put in the envlist `<name>-env` is configuration, and nothing that opens
anything else:

The entries install writes into the agent's envlist:

| Key | Written | From | Holds |
| --- | --- | --- | --- |
| `MIKROSCOPE_TAG` | always | `--name` | the ownership marker `mikroscope:<name> (managed by mikroscope)`, written first and removed last; the agent ignores it |
| `RATE_HZ` | always | `--rate`, default `10`, 1–100 | the sampler rate, in Hz |
| `BUFFER_S` | always | `--buffer`, default `60`, 10–3600 | the ring's length, in seconds |
| `PORT` | always | `--port`, default `9123`, 1–65535 | the agent's HTTP port |
| `ADDR` | always | `--subnet` | the agent's address, the `.2` of the /30; the agent binds only there |
| `MEM_LIMIT_MB` | always | `--mem-limit-mb`, 8–1024 | the agent's Go soft memory limit, in MiB; derived from the ring (rate × buffer × line, × 2.5, at least 16 MiB, at most three quarters of `--memory-max` while that still holds the ring) unless `--mem-limit-mb` gives it |
| `FLOOR_HZ` | only when above 0 | `--floor-hz`, default `0`, 0–1000 | one cadence for every level source, in Hz |
| `CAPTURE_MB` | always | `--capture-mb`, default `4`, 0–256 | the triggered-capture budget, in MiB; `0` turns captures off |
| `TRIGGERS` | only when set | `--triggers` | the trigger conditions; unset, the agent uses its default set |
| `TOKEN` | only when set | `--token` | the bearer token the agent requires, from `--token` or `MIKROSCOPE_TOKEN`, with or without `--expose` |

The last row is the one secret that does live on the router. It is written whenever `--token` or
`MIKROSCOPE_TOKEN` is set, with or without `--expose`, and like the rest of the envlist it is
treated as readable by every `read` user. It opens the agent's own HTTP paths and nothing else. `plan` and
`--dry-run` print it as `value="(token)"`, the options line as `token=(set)`, and the agent's start
line in the router log as `token=true`.

`plan --rsc` is the exception to that masking. It writes the install as a RouterOS script to run on
the router itself, so the envlist line has to carry the real token; the script says so in its own
header. A generated `.rsc` with a token in it is a credential — [Installer
safeguards](https://docs.jmrp.io/mikroscope/security/installer/) has how to handle it.

## Collector credentials

On your host, the credentials that open something other than the agent are read from the
environment only, never from a flag, with one exception: the PostgreSQL DSN, below. The code gives
the reason: a flag is visible in `ps` and in a shell history.

- The API user's password: `MIKROSCOPE_API_PASSWORD`. The address and user come from `--api` and
  `--api-user`, or `MIKROSCOPE_API_ADDR` and `MIKROSCOPE_API_USER`.
- Sink credentials: `MIKROSCOPE_INFLUX_TOKEN`, `MIKROSCOPE_LOKI_TOKEN`, `MIKROSCOPE_OTLP_TOKEN`,
  `MIKROSCOPE_ELASTIC_AUTH`, `MIKROSCOPE_TELEGRAF_TOKEN`.
- PostgreSQL (`--postgres`, or `MIKROSCOPE_POSTGRES_DSN`): the DSN is a flag, so keep the password
  out of it and let the driver read `PGPASSWORD` or `~/.pgpass`, as psql does. A password written
  in a DSN passed as the flag is visible in `ps`, and wherever the DSN comes from,
  `forward --grafana` and `dashboards publish` copy a password written in it into the Grafana
  datasource they create. A password found only in the environment or the password file is not
  copied.
- `forward --grafana` and `dashboards publish` also copy the InfluxDB token
  (`MIKROSCOPE_INFLUX_TOKEN`, which can write) and `MIKROSCOPE_ELASTIC_AUTH` into the datasources
  they create. To give Grafana a read-only credential, create the datasource yourself and pass
  `--grafana-datasource-uid`.
- Grafana: `GRAFANA_TOKEN`.

The agent's token is the exception: it has a `--token` flag as well as `MIKROSCOPE_TOKEN`. The
same reasoning applies to it, so prefer the variable.

The CLI does not read `.env` itself. Export the variables into the shell that runs it, for example
with `set -a; . ./.env; set +a`.

## Agent image source

The container runs an image, and which route puts it there decides what you are trusting.

- `install` with a Go toolchain and a checkout builds the image on your host from the source in
  front of you and uploads it over your own ssh session. You trust your own tree.
- `install --agent-tar <file>` uploads the tar the release publishes, over the same ssh session. The
  CLI checks that the tar is a mikroscope agent image of the architecture `--arch` names before it
  sends it; checking that it is the file the release published is yours to do, against
  `checksums.txt`.
- `install --remote-image <reference>` uploads nothing: the router itself fetches the image from the
  registry the reference names, which mikroscope writes into `remote-image=` with its host,
  `registry-1.docker.io` for Docker Hub; the global `/container/config registry-url` is neither
  needed nor written. You trust that registry and the router's path to it, and mikroscope verifies
  nothing about what arrives. `/container/config` also holds one registry username and password for
  the whole device, and a Docker Hub login sent to GHCR fails the pull with `auth error` ([tested](https://docs.jmrp.io/mikroscope/about/status/#install-routes-tested));
  [Installer safeguards](https://docs.jmrp.io/mikroscope/security/installer/) has what that means and what `doctor`
  checks.
- `plan --rsc` writes the same commands as a RouterOS script for you to paste or `/import`; the
  image still has to come from one of the two routes above that need no upload from the CLI.

## Container settings

`install` creates one container with these settings, all printed by `plan` before anything is
written ([where verified](https://docs.jmrp.io/mikroscope/about/status/#one-device-two-routeros-versions)):

- **`privileged=yes` by default**, `--privileged=false` to opt out. The setting needs RouterOS 7.24 or later, whose [changelog](https://download.mikrotik.com/routeros/7.24/CHANGELOG) added privileged mode for containers. It drops the container's user namespace ([verified](https://docs.jmrp.io/mikroscope/about/status/#verified-privileged-namespaces)), which is what makes the kernel log, `/proc/slabinfo`,
  `/proc/pagetypeinfo` and the MTD ECC counters readable. It does **not** drop the network or PID
  namespace: no interface counters, no router conntrack table, no view of RouterOS's processes.
  [Privileged mode](https://docs.jmrp.io/mikroscope/limits/privileged/) has what it makes readable.
- **`memory-max=64M`** (`--memory-max`), enforced as the container's cgroup limit, with the agent's
  Go soft limit (`--mem-limit-mb`) inside it. Unless you pass a number, that limit is derived from
  the ring (rate × buffer × line, × 2.5, at least 16 MiB, at most three quarters of `memory-max`
  while that still holds the ring): 16 MiB at the defaults.
- **`restart-policy=on-failure`**, bounded to five retries ten seconds apart, so a broken image
  cannot loop at boot.
- **`start-on-boot=yes`**, or `no` with `--ephemeral`, whose root lives on the tmpfs disk and does
  not survive a reboot. Surviving a reboot with `yes` is [not tested](https://docs.jmrp.io/mikroscope/about/status/#not-tested).
- **`logging=yes`**, so the agent's lifecycle lines reach the router log.
- **`ignore-remote-image-change=yes`**: with the default, RouterOS watches the image and, once the
  tar is removed, stops and removes the container and re-extracts it minutes later ([verified](https://docs.jmrp.io/mikroscope/about/status/#verified-remote-image-change)).
  `install` removes the tar right after extraction, which is why it sets this.
- Root and image on the disk you chose with `--disk`: the internal flash by default.
- **No bind mount.** `install` mounts no host path into the container.

The agent reads `/proc`, `/sys` (`/sys/fs/cgroup` for its own accounting, `/sys/class/thermal` and
`/sys/class/mtd` for the device), and — privileged — `/dev/kmsg` and the hardware performance
counters. It writes nothing to its root at runtime: the ring and the triggered captures are held
in memory. It catches SIGTERM, because RouterOS kills a container that does not, at once.

The last setting in the list is deliberate. Host `/proc` and `/sys` bind-mounted into a privileged
container add nothing, because the namespaces hold, but host `/` exposes the RouterOS flash
filesystem, configuration and secrets included ([Host
mounts](https://docs.jmrp.io/mikroscope/limits/namespaces/#mounting-the-hosts-paths-does-not-cross-the-boundary)).
mikroscope does not do this, and a container you build yourself should not either.

## Report a vulnerability

Report a vulnerability privately, never with its details in a public issue or discussion: open the
repository's [Security tab](https://github.com/jmrplens/mikroscope/security) and choose **Report a
vulnerability**. GitHub then opens a private advisory that only the maintainer can see. If that
button is not there, start a discussion in
[General](https://github.com/jmrplens/mikroscope/discussions/categories/general) that asks for a
private channel and says nothing else about the problem, and wait there for a reply that names one;
issues go through forms that ask for the details, so an issue cannot stay that empty. Include the
version (`mikroscope version`, and the agent's from the `agent:` line of `mikroscope status` if it
differs), the RouterOS version and board, what an attacker gains, and the smallest way to reproduce
it. If a proof of concept needs a credential, a router address or an export, do not send it:
describe it.

The policy itself is [`SECURITY.md`](https://github.com/jmrplens/mikroscope/blob/main/SECURITY.md). It promises an acknowledgement within a week and,
once a report is confirmed, a fix released before the advisory is published; a reporter who wants
credit gets it in the advisory, and one who does not is not named. Only the latest release is supported, because a fix ships as a new
tag rather than as a patch to an older one. It also sets out what is worth reporting: anything that
breaks the separation this page describes, such as a way for something other than the configured
collector to reach the agent, or a credential appearing where it does not belong. The prerequisites
RouterOS imposes, and the agent's token being readable by every `read` user, are documented choices,
not vulnerabilities.

## See also

- [API user](https://docs.jmrp.io/mikroscope/security/api-user/): the RouterOS user the collector needs, and the
  policy it gets.
- [Expose on the LAN](https://docs.jmrp.io/mikroscope/security/expose/): the two firewall rules, and why the token
  becomes mandatory.
- [Installer safeguards](https://docs.jmrp.io/mikroscope/security/installer/): the objects it will not build on,
  and the values it will not put in a command.
- [Privileged mode](https://docs.jmrp.io/mikroscope/limits/privileged/): what the default privilege grant reads,
  and what it does not.
