# Stupid Simple Network Inventory Self-hosted web application for manual network inventory and logical network topology visualisation. ## Stack | Layer | Technology | |-------|-----------| | Backend | FastAPI + SQLAlchemy + SQLite (Python 3.11) | | Frontend | Vue 3 + Vite, served by Nginx | | Auth | JWT HS256, 24-hour expiry | | Runtime | Docker Compose | --- ## Quick start ```bash # 1. Clone and enter the project git clone && cd topologie # 2. Create the data directory owned by the current user mkdir -p db_data # 3. Configure environment (required for correct bind-mount ownership) cp .env.example .env # Set DOCKER_UID / DOCKER_GID to match your host user: # id -u && id -g # Set INITIAL_ADMIN_PASSWORD to avoid the admin/admin bootstrap. # 4. Build and start docker compose --env-file .env up --build -d # 5. Open the app open http://localhost:8080 ``` ### First login | Case | Credentials | Behaviour | |------|------------|-----------| | `INITIAL_ADMIN_PASSWORD` set | `admin` / `` | Normal login | | `INITIAL_ADMIN_PASSWORD` unset | `admin` / `admin` | Forced password change before accessing the app | --- ## Configuration All configuration is via environment variables. See `.env.example` for the full list with descriptions. | Variable | Default | Description | |----------|---------|-------------| | `SECRET_KEY` | auto-generated | JWT signing key. Set explicitly in production. | | `INITIAL_ADMIN_PASSWORD` | _(empty)_ | Bootstrap admin password. If unset, `admin/admin` is used with forced change. | | `ALLOWED_ORIGINS` | `*` | CORS allowed origins (comma-separated). Set to your domain in production. | | `DOCKER_UID` / `DOCKER_GID` | `1000` | UID/GID for the backend process. Must match the host user owning `./db_data/`. | ### Using .env with Docker Compose ```bash cp .env.example .env # Edit .env — at minimum set DOCKER_UID, DOCKER_GID, INITIAL_ADMIN_PASSWORD docker compose --env-file .env up --build -d ``` --- ## Security ### Secret management Two options depending on your security requirements. #### Option A — Auto-generated secret (recommended for single-node) Leave `SECRET_KEY` unset (or empty) in `.env`. On first start the backend generates a random 64-character hex key, writes it to `db_data/secret_key.txt` with permissions **0600**, and reuses it on every subsequent restart. The secret never appears in an environment variable, a compose file, or a log. ```bash # .env — leave the line empty or remove it SECRET_KEY= ``` The only requirement is that `db_data/` is backed up (it already contains the database). #### Option B — Docker Compose file secret Stores the secret in a file on the host, outside version control, and mounts it into the container. The value never appears in an environment variable. ```bash # Generate and store the secret outside the project directory mkdir -p ~/.secrets python3 -c "import secrets; print(secrets.token_hex(32))" > ~/.secrets/topologie_secret_key chmod 600 ~/.secrets/topologie_secret_key ``` Then uncomment the `secrets:` blocks in `docker-compose.yml` (see comments in that file) and remove `SECRET_KEY` from `.env`. Docker Compose merges the override automatically: ```bash docker compose up -d ``` --- ### Key rotation To rotate the JWT secret (invalidates all active sessions): ```bash # Option A — environment variable (recommended) # Set a new SECRET_KEY in your deployment config and restart # Option B — file rotation docker compose stop backend rm db_data/secret_key.txt docker compose start backend # All users will need to log in again ``` ### HTTPS This application does not terminate TLS. For production use, place it behind a reverse proxy that handles HTTPS: ```nginx # Example nginx reverse-proxy (external, on the host or a dedicated container) server { listen 443 ssl; server_name inventory.example.com; ssl_certificate /etc/letsencrypt/live/inventory.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/inventory.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` For local-only use, bind to loopback to prevent accidental LAN exposure: ```yaml # docker-compose.override.yml services: frontend: ports: - "127.0.0.1:8080:8080" ``` ### Container hardening The containers run with reduced privileges: | Measure | Backend | Frontend | |---------|---------|----------| | Non-root user | `DOCKER_UID:DOCKER_GID` (host user) | `nginx` (UID 101) | | `cap_drop: ALL` | ✓ | ✓ | | `cap_add: NET_RAW` | ✓ (ping) | — | | `no-new-privileges` | — ¹ | ✓ | | Healthcheck | ✓ | ✓ | ¹ Omitted on the backend: ping uses file capabilities (`cap_net_raw=ep`); `no-new-privileges` suppresses the file effective bit and would prevent the subprocess from acquiring `CAP_NET_RAW` in its effective set even though the parent holds it in its permitted set. --- ## Data persistence All data is stored in `./db_data/`: | File | Description | |------|-------------| | `topology.db` | SQLite database | | `secret_key.txt` | Auto-generated JWT secret (0600 permissions) | **Backup**: `cp -r db_data/ db_data.bak/` **Restore**: stop the stack, replace `db_data/`, restart. --- ## Development ### Backend tests ```bash cd backend pip install -r requirements.txt -r requirements-test.txt pytest tests/ -v ``` ### Local dev (without Docker) ```bash # Backend cd backend pip install -r requirements.txt uvicorn main:app --reload # Frontend (separate terminal) cd frontend npm install npm run dev # Vite dev server on :5173, proxies /api/ to :8000 ``` --- ## Architecture See [`docs/architecture.md`](docs/architecture.md) for the detailed request flow, Docker setup, and authentication model.