# NETMapper install guide (netmapper.nx1xlab.dev/docs/install)

Pick the path that matches how you want to run NETMapper. Each one below is
self-contained: you do not need to read the others. All of them end at the
same place, a running stack and a first admin user.

## Requirements

- A Linux host with Docker Engine 24 or newer and the Compose v2 plugin
  (`docker compose`, not the legacy `docker-compose` binary). Docker is the
  only requirement for the container paths.
- At least 2 GB of free RAM. The three containers (PostgreSQL, backend,
  frontend) are modest on their own but add up.
- Outbound network access so the host can pull the release images.
- SSH reachability from the host to the switches you want to scan, with
  switch credentials. NETMapper logs in to switches over SSH; password
  authentication is the supported method in the current release.
- A safe place to keep `NETMAPPER_ENCRYPTION_KEY` out of band, such as a
  password manager or a secret store. Losing that key makes every stored
  switch password and notification secret permanently unrecoverable.

NETMapper holds your switch credentials. Run it on the management network,
behind a firewall, ideally behind a zero-trust access proxy (Cloudflare
Access, Tailscale, a VPN). Do not expose it to the internet directly.

## One command: the installer

The fastest path. Docker with the Compose plugin is the only prerequisite.

```bash
curl -fsSL https://netmapper.nx1xlab.dev/install.sh | bash
```

What it does, in order:

1. Checks that `curl` and Docker with the Compose plugin are present and that
   Docker is running and reachable by your user.
2. Resolves the latest release and downloads two small files from it:
   `release.env` (version and image digests) and `SHA256SUMS`.
3. Creates `./netmapper` (or `$NETMAPPER_DIR` if you set it), downloads
   `docker-compose.yml` and `docker-compose.https.yml` for that release, and
   checks each file against `SHA256SUMS`. A mismatch deletes the file and
   aborts before anything starts.
4. Writes `.env`: the release version, the two image digests from
   `release.env`, and freshly generated secrets (`POSTGRES_PASSWORD`,
   `NETMAPPER_JWT_SECRET`, `NETMAPPER_ENCRYPTION_KEY`). It also sets
   `NETMAPPER_COOKIE_SECURE=false`, because the default is plain HTTP on port
   80 and browsers refuse Secure cookies there.
5. Starts the stack and waits until `GET /ready` answers.
6. Creates the first admin user.

What it asks you: a username and a password for the first admin. That is the
only interactive step. If there is no terminal attached (CI, a remote
command), it skips the prompt and prints the command to run yourself.

Everything it downloads is public, and it never sends anything anywhere.
Re-running it in the same directory keeps the existing `.env` and data.

### What the script does for your safety

- `https://netmapper.nx1xlab.dev/install.sh` redirects to the `stable` tag of
  the Core repository, not to `main`. Work in progress on the installer never
  reaches your host.
- The whole installer sits inside one function that is called on the last
  line. A download cut short defines the function and runs nothing, instead of
  executing half an install.
- Compose files and `.env.example` come from `raw.githubusercontent.com`;
  `SHA256SUMS` comes from the release assets on `github.com`. A tampered or
  corrupted answer on one of the two paths is caught. This does not protect
  you against someone who controls the repository itself; for that, verify the
  signature below.
- Generated secrets are written without ever appearing on a command line, so
  they do not show up in the process list of a shared host. `.env` is created
  with mode 600 under `umask 077`.

### Verify the installer before you run it

Every release publishes `install.sh`, its SHA-256 and a cosign signature. If
you would rather not pipe a script into `bash`:

```bash
TAG=v0.2.0   # the release you want
BASE=https://github.com/NX1X/NETMapper/releases/download/$TAG
curl -fsSLO $BASE/install.sh
curl -fsSL  $BASE/install.sh.sha256 | sha256sum -c -
cosign verify-blob install.sh \
  --bundle <(curl -fsSL $BASE/install.sh.cosign.bundle) \
  --certificate-identity-regexp '^https://github.com/NX1X/NETMapper/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
bash install.sh
```

The signature is keyless, through GitHub OIDC, so it proves the file was built
by the release workflow in this repository. `SHA256SUMS` is signed the same
way, as `SHA256SUMS.cosign.bundle`.

To install a specific release instead of the latest:

```bash
NETMAPPER_VERSION=0.1.0 bash -c "$(curl -fsSL https://netmapper.nx1xlab.dev/install.sh)"
```

When it finishes, open `http://<host>/` and log in.

Useful commands afterwards, run in the install directory:

```bash
docker compose logs -f backend    # follow the backend log
docker compose down               # stop, data is kept
docker compose up -d              # start again
```

## Docker Compose by hand

Prefer to read every file before it runs? This is the same stack, assembled
step by step. No source checkout is needed: the compose file runs the
published release images, pinned by version and by digest.

### 1. Get the two files

```bash
mkdir netmapper && cd netmapper
curl -fsSLO https://raw.githubusercontent.com/NX1X/NETMapper/main/docker-compose.yml
curl -fsSL  https://raw.githubusercontent.com/NX1X/NETMapper/main/.env.example -o .env
chmod 600 .env
```

### 2. Fill in the six required values

`docker compose` refuses to start while any of these is empty.

| Variable | Where it comes from |
|---|---|
| `NETMAPPER_VERSION` | The release notes of the release you want to run |
| `NETMAPPER_BACKEND_DIGEST` | The same release notes, `sha256:...` |
| `NETMAPPER_FRONTEND_DIGEST` | The same release notes, `sha256:...` |
| `POSTGRES_PASSWORD` | Generate it |
| `NETMAPPER_JWT_SECRET` | Generate it |
| `NETMAPPER_ENCRYPTION_KEY` | Generate it, then back it up out of band |

Open the [latest release](https://github.com/NX1X/NETMapper/releases/latest)
and copy its three `.env` lines verbatim:

```
NETMAPPER_VERSION=0.1.0
NETMAPPER_BACKEND_DIGEST=sha256:...
NETMAPPER_FRONTEND_DIGEST=sha256:...
```

The digest is what pins the image. A tag can be repointed upstream, a digest
cannot. Upgrading means replacing these three lines.

Generate the three secrets:

```bash
# POSTGRES_PASSWORD
python3 -c "import secrets; print(secrets.token_urlsafe(24))"

# NETMAPPER_JWT_SECRET (HS256 signing key; the production startup gate
# refuses to boot on anything shorter than 32 bytes)
python3 -c "import secrets; print(secrets.token_urlsafe(48))"

# NETMAPPER_ENCRYPTION_KEY (urlsafe-base64 Fernet key)
python3 -c "import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())"
```

Back up `NETMAPPER_ENCRYPTION_KEY` somewhere independent of the database
dump. Every switch SSH password, every notification-channel secret and every
AI provider key is encrypted with it at rest. There is no key escrow and no
recovery code: lose it and those secrets are gone for good. Never commit it
to git.

One more value matters on a plain-HTTP install: set
`NETMAPPER_COOKIE_SECURE=false` when users reach the app over `http://` with
no TLS terminator in front, otherwise the browser drops the Secure login
cookie and login fails. Leave it `true` when there is HTTPS in front.

### 3. Start the stack

```bash
docker compose up -d
```

On first boot the backend entrypoint runs `alembic upgrade heads` against the
database before starting the API, which creates the schema on a clean volume.
Later boots replay only new migrations. The Compose healthcheck calls
`GET /ready`, so the backend is marked healthy only once the database
round-trips.

### 4. Verify and create the admin user

```bash
curl -fsS http://localhost/ready
# expected: {"status":"ok","database":"ok"}

docker compose exec backend python -m netmapper.create_admin
```

If the backend is in a crash loop, read `docker compose logs --tail=50
backend`. The usual causes are a missing or malformed
`NETMAPPER_ENCRYPTION_KEY` and a database that was not reachable when the
migration ran.

The app is at `http://<host>/` (or `http://<host>:<NETMAPPER_HTTP_PORT>/`).
The API's own interactive reference is at `/docs` (Swagger UI) and `/redoc`
on the NETMapper host itself.

## Quickstart zip

Every release also ships `netmapper-quickstart-<version>.zip` on the
[releases page](https://github.com/NX1X/NETMapper/releases). It holds the
compose files, a `.env` template with the version and image digests already
filled in, and the same installer script. No source code, no build.

```bash
unzip netmapper-quickstart-<version>.zip
cd netmapper-quickstart-<version>
bash install.sh
```

Or do the same thing by hand from inside the unzipped folder:

```bash
cp .env.example .env         # then set the three secrets, see the comments
docker compose up -d
docker compose exec backend python -m netmapper.create_admin
```

Back up `.env`: it holds `NETMAPPER_ENCRYPTION_KEY`.

## HTTPS for a host reachable from outside

Read this first. NETMapper stores your switch credentials. The right answer
for almost every deployment is a firewall plus a zero-trust access proxy
(Cloudflare Access, Tailscale, a VPN) in front of plain port 80 on the
management network. Use the HTTPS overlay only when the host genuinely has to
be reachable from outside, and treat it as the floor, not the ceiling.

`docker-compose.https.yml` adds an nginx container that terminates TLS on 443
with automatic Let's Encrypt certificates, and removes the frontend's own
host port so nginx owns 80 and 443.

Prerequisites: a DNS name pointing at the host, and ports 80 and 443
reachable from the internet for the Let's Encrypt HTTP-01 challenge.

```bash
echo "DOMAIN_NAME=netmapper.example.com" >> .env

# Obtain the certificate once, while port 80 is free
docker compose -f docker-compose.yml -f docker-compose.https.yml run --rm certbot

# Then start the full stack with TLS
docker compose -f docker-compose.yml -f docker-compose.https.yml up -d
```

Keep `NETMAPPER_COOKIE_SECURE=true` here: users now reach the app over HTTPS.

Renewal is not automatic on its own. Run the certbot service from a cron job
or a systemd timer:

```bash
docker compose -f docker-compose.yml -f docker-compose.https.yml run --rm certbot renew
```

Bringing your own certificate, DNS details and renewal are covered in
[docs/operations/https.md](https://github.com/NX1X/NETMapper/blob/main/docs/operations/https.md)
in the repository.

## Kubernetes with the Helm chart (beta)

The chart at `helm/netmapper/` in the repository is **beta**: it installs, but
it is single-replica and not yet exercised in CI, and values may change before
1.0. Report problems as GitHub issues.

Prerequisites: Kubernetes 1.27 or newer, Helm 3.12 or newer, a StorageClass
that supports `ReadWriteOnce` for the PostgreSQL volume, and cluster access to
pull the images.

Four values have no safe default. Never deploy without them:
`postgres.password`, `backend.encryptionKey`, `backend.jwtSecret`, and
`ingress.host` if the ingress is enabled.

```yaml
# my-values.yaml
postgres:
  password: "a-strong-password"

backend:
  encryptionKey: "your-fernet-key-here"
  jwtSecret: "your-jwt-secret-here"
  env: "production"

ingress:
  enabled: true
  host: "netmapper.example.com"
  tls:
    enabled: true
    secretName: "netmapper-tls"
```

```bash
helm upgrade --install netmapper helm/netmapper \
  --namespace netmapper --create-namespace \
  --values my-values.yaml

kubectl rollout status deployment/netmapper-backend -n netmapper
kubectl rollout status deployment/netmapper-frontend -n netmapper
```

Create the first admin once the backend pod is running:

```bash
kubectl exec -it -n netmapper deployment/netmapper-backend -- \
  python -m netmapper.create_admin --username admin
```

Migrations run automatically before the backend process starts, so the pod
does not become Ready until they finish. Keep the backend at a single replica:
each replica runs its own scheduler and would fire scheduled scans in
parallel.

## Bare metal

For hosts that cannot run Docker, or where you would rather manage the
process with systemd. Supported distributions: Ubuntu 22.04 LTS or later,
Debian 12 or later, and Rocky Linux 9 / RHEL 9 or later (AlmaLinux 9 and
other compatible rebuilds included). Other Linux distributions work but are
untested, and macOS is not a supported production target.

You also need Python 3.13, Node 24.x for the frontend build, a PostgreSQL 16
server, and a reverse proxy such as nginx. NETMapper does not terminate TLS
itself on this path.

```bash
git clone https://github.com/NX1X/NETMapper
cd NETMapper
sudo sh scripts/install-baremetal.sh
```

The script is idempotent: re-running it skips work that is already done. It
creates the `netmapper` system user, installs the backend virtualenv, builds
the frontend static files, installs the systemd unit, and creates the config
directory at `/etc/netmapper/`. It deliberately does not start the service,
because the secrets are not in place yet.

```bash
sudo cp /etc/netmapper/.env.example /etc/netmapper/.env
sudo editor /etc/netmapper/.env
```

At minimum set `DATABASE_URL`, `NETMAPPER_ENV=production`,
`NETMAPPER_JWT_SECRET` and `NETMAPPER_ENCRYPTION_KEY`. Create the database
role and database, then start the service:

```bash
sudo -u postgres psql <<'SQL'
CREATE USER netmapper WITH PASSWORD 'a-strong-password';
CREATE DATABASE netmapper OWNER netmapper;
SQL

sudo systemctl enable --now netmapper
sudo systemctl status netmapper
sudo journalctl -u netmapper -n 50 -f
```

Then put nginx in front of it and create the first admin user. The full
walkthrough, including a working nginx server block, is in
[docs/install/baremetal.md](https://github.com/NX1X/NETMapper/blob/main/docs/install/baremetal.md).

## Build from source (contributors)

`docker-compose.dev.yml` builds the images from the checkout and bind-mounts
the backend source, so edits reload. Nothing in it is meant for production.

```bash
git clone https://github.com/NX1X/NETMapper
cd NETMapper
cp .env.example .env    # set POSTGRES_PASSWORD, NETMAPPER_JWT_SECRET,
                        # NETMAPPER_ENCRYPTION_KEY
docker compose -f docker-compose.dev.yml up -d --build
```

Add `NETMAPPER_ENV=development` (it relaxes the production secret gate) and
`NETMAPPER_COOKIE_SECURE=false` (the dev frontend runs on plain
`http://localhost:5173`) to `.env`.

The frontend is on `http://localhost:5173`, the API on
`http://localhost:8000`, and PostgreSQL on `localhost:5432`.

Running the pieces natively instead, without Docker:

```bash
# Backend (Python 3.13)
cd backend
python -m venv .venv
. .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r requirements-dev.txt
uvicorn netmapper.main:app --reload    # http://localhost:8000

# Frontend, in a second terminal (Node 24)
cd frontend
npm ci --ignore-scripts
npm run dev                   # http://localhost:5173, set VITE_API_URL
```

The quality gates a contribution has to pass:

```bash
cd backend && pytest                      # enforces at least 70% coverage
cd backend && ruff check . && black --check .
cd frontend && npm run lint && npm run build
```

See
[CONTRIBUTING.md](https://github.com/NX1X/NETMapper/blob/main/CONTRIBUTING.md)
before opening a pull request.

## First login

NETMapper ships with no users. Until an admin exists, every mutating endpoint
returns 401 and the login page has nothing to authenticate against.

```bash
# Docker Compose
docker compose exec backend python -m netmapper.create_admin

# Kubernetes
kubectl exec -it -n netmapper deployment/netmapper-backend -- \
  python -m netmapper.create_admin
```

The script prompts for a username, then for a password twice through
`getpass`, so neither appears in your shell history or in the process list.
The password is checked against the password policy (length floor and cap, a
common-password blocklist, and it may not equal the username) and against the
Have I Been Pwned range API using k-anonymity, then stored as a bcrypt hash.
The plaintext is never written anywhere. The first user is created with
`role=admin` and `is_superuser=true`.

The account is created with `must_change_password` set, so the **first login
is gated on a password change**: NETMapper refuses every other route until
the new password is set. Pick the password you actually want at that prompt.

For automation, `--username` and `--password` can be passed on the command
line, and `--no-must-change` skips the forced rotation when the password was
machine-generated. Avoid both on a shared machine, where `ps` output is
visible to other users.

Once you are in, add a switch under **Switches**, then run a scan. What to
configure next (SMTP for email alerts, scan schedules, an AI provider) is in
[docs/admin/first-time-setup.md](https://github.com/NX1X/NETMapper/blob/main/docs/admin/first-time-setup.md).
