88cf6458d0
Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker. Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
7.7 KiB
Markdown
125 lines
7.7 KiB
Markdown
# Security Decisions
|
|
|
|
Decisions, trade-offs, and documented limitations for Stupid Simple Network Inventory.
|
|
|
|
---
|
|
|
|
## JWT in localStorage vs HttpOnly cookie
|
|
|
|
**Decision**: keep JWT in `localStorage` + Bearer token.
|
|
|
|
**Rationale**: migrating to HttpOnly cookies requires changes to the Nginx config (cookie proxying, SameSite handling), the FastAPI auth flow, and all frontend API calls. The added complexity is disproportionate for a self-hosted LAN tool. The risk is mitigated by:
|
|
- 24-hour token expiry (reduced from 7 days)
|
|
- `token_version` invalidation: password change immediately revokes all prior tokens
|
|
- The application is designed for trusted LAN use, not public internet exposure
|
|
|
|
**Assumption**: the deployment is behind a trusted proxy or on a private network where XSS is the primary concern and is partially mitigated by the LAN context.
|
|
|
|
**Future**: if the app ever needs public internet exposure, migrating to `HttpOnly; Secure; SameSite=Strict` cookies should be the first priority.
|
|
|
|
---
|
|
|
|
## Token versioning instead of a session store
|
|
|
|
**Decision**: `token_version` integer column on `User`, incremented on password change.
|
|
|
|
**Rationale**: no Redis, no external session store. A single integer per user provides immediate token invalidation with zero dependencies. The version is included in the JWT payload (`ver` field) and validated on every request.
|
|
|
|
**Trade-off**: revocation is per-user, not per-token. Logging out one device still lets other devices use their tokens until the common password is changed. This is acceptable for a single-user or small-team tool.
|
|
|
|
**Limitation**: `ver` absent from old tokens is treated as `ver=1` for backward compatibility. Existing valid tokens (before `token_version` was added) will therefore continue to work until the user changes their password.
|
|
|
|
---
|
|
|
|
## no-new-privileges absent from backend container
|
|
|
|
**Decision**: omit `security_opt: no-new-privileges:true` for the backend service.
|
|
|
|
**Rationale**: the discovery feature uses `subprocess.run(["ping", ...])`. In `iputils-ping` on Debian, the ping binary has the file capability `cap_net_raw=ep`. For a non-root process to execute ping successfully, the `execve` call must be allowed to promote capabilities from the file's permitted set. `no-new-privileges` blocks this promotion, breaking the ping feature.
|
|
|
|
**Alternatives considered**:
|
|
- Ambient capabilities (Linux 4.3+): would require modifying the entrypoint to `prctl(PR_CAP_AMBIENT_RAISE, ...)` before exec. Adds complexity with no meaningful security gain for this threat model.
|
|
- SUID ping: `no-new-privileges` also blocks SUID.
|
|
- Replace subprocess ping with a Python ICMP implementation: significant refactor out of scope.
|
|
|
|
**Mitigations in place**: `cap_drop: ALL` + `cap_add: NET_RAW DAC_OVERRIDE` limits the container to only the capabilities needed. `DAC_OVERRIDE` is required because the `db_data/` bind-mount is owned by the host user; root without it cannot create SQLite journal files in that directory. Both capabilities are in Docker's default set, so this is a net reduction from a standard container.
|
|
|
|
---
|
|
|
|
## CORS default to `*`
|
|
|
|
**Decision**: `ALLOWED_ORIGINS` defaults to `"*"` for backward compatibility.
|
|
|
|
**Rationale**: the application is designed for same-origin access via Nginx (browser → Nginx → FastAPI), so CORS headers are not required for normal operation. However, changing the default to empty (no CORS) would silently break integrations where users access the API from a different origin. The default `"*"` maintains existing behavior while making it configurable.
|
|
|
|
**Production recommendation**: set `ALLOWED_ORIGINS=` (empty) if the backend is only accessed through the Nginx proxy, or set it to the specific domain if cross-origin access is needed.
|
|
|
|
---
|
|
|
|
## In-memory rate limiting
|
|
|
|
**Decision**: Python `dict[str, list[float]]` protected by `threading.Lock`, no Redis.
|
|
|
|
**Rationale**: Redis adds operational complexity (another service, volume, potential failure mode). For a single-process Uvicorn deployment behind Nginx, in-memory rate limiting is sufficient. The Nginx `limit_req` module provides the primary protection at the network edge; the Python layer adds defense-in-depth.
|
|
|
|
**Limitations**:
|
|
- State is lost on container restart (acceptable: a restart already breaks all active sessions).
|
|
- Does not work across multiple workers. Uvicorn defaults to a single worker; if scaled, the Nginx layer remains effective.
|
|
- IP tracking sees the Nginx container's IP (Docker internal), not the real client IP, unless `X-Real-IP` is forwarded to the backend. The Nginx `limit_req` uses the real client IP correctly.
|
|
|
|
---
|
|
|
|
## APP_UID / APP_GID build args
|
|
|
|
**Decision**: configurable container UID/GID via build args, defaulting to 1000.
|
|
|
|
**Rationale**: the backend bind-mounts `./db_data:/app/data`. For the non-root container user to write to this directory, the container UID must match the host directory owner. On most Linux systems, the first non-root user is UID 1000. The build args allow customisation without Dockerfile changes.
|
|
|
|
**Alternative**: use an entrypoint that `chown`s the directory at runtime (current implementation via `entrypoint.sh`). This ensures compatibility regardless of UID mismatch, at the cost of a brief root execution before dropping to `appuser`.
|
|
|
|
---
|
|
|
|
## nginx-unprivileged for frontend container
|
|
|
|
**Decision**: use `nginxinc/nginx-unprivileged:alpine` as the frontend base image.
|
|
|
|
**Rationale**: the standard `nginx:alpine` image requires `CAP_CHOWN` and `CAP_SETUID` at startup — the master process (root) chowns temp directories and forks workers as `nginx` user (UID 101). With `cap_drop: ALL`, both syscalls fail. Two alternative approaches were tried and abandoned:
|
|
- `USER nginx` in the Dockerfile: BusyBox `sed` does not support `\s` in basic regex mode, so the PID path substitution silently failed; the entrypoint scripts also need root.
|
|
- `gosu appuser` in the backend: `CAP_SETUID` is dropped, so `gosu` cannot switch UIDs.
|
|
|
|
`nginxinc/nginx-unprivileged:alpine` is maintained by nginx Inc. and pre-configures nginx to run entirely as UID 101 without needing `CAP_CHOWN` or `CAP_SETUID`. The entire process tree (master + workers) runs as nginx, making `cap_drop: ALL` compatible with zero extra capabilities on the frontend.
|
|
|
|
**Trade-off**: adds a dependency on a third-party image (`nginxinc/nginx-unprivileged`) instead of the official `nginx` image. The image is maintained by nginx Inc. itself, so the trust model is equivalent.
|
|
|
|
---
|
|
|
|
## ping and NET_RAW capability
|
|
|
|
**Decision**: retain `cap_add: NET_RAW` on the backend container.
|
|
|
|
**Rationale**: the discovery feature (`/api/discovery/ping`, `/api/discovery/scan`) uses ICMP ping, which requires `CAP_NET_RAW`. This is the minimum capability needed and is explicitly documented. The capability is dropped from the frontend container which has no need for it.
|
|
|
|
---
|
|
|
|
## SQLite without foreign key enforcement
|
|
|
|
**Status**: known limitation, not addressed in this phase.
|
|
|
|
SQLite does not enforce foreign keys by default. Enabling `PRAGMA foreign_keys=ON` per connection via a SQLAlchemy event was out of scope for this phase. The application code manually handles cascades (explicit DELETE before device removal). This is documented in SEC-FIX-013 as a future fix.
|
|
|
|
---
|
|
|
|
## Future security improvements (not scheduled)
|
|
|
|
| ID | Topic |
|
|
|----|-------|
|
|
| SEC-FIX-006 | Scope discovery scan to inventory CIDRs, validate IPs |
|
|
| SEC-FIX-007 | Pydantic field constraints on business models |
|
|
| SEC-FIX-008 | HTTP security headers (CSP, X-Content-Type-Options, etc.) |
|
|
| SEC-FIX-009 | Document TLS / reverse-proxy contract more explicitly |
|
|
| SEC-FIX-012 | Structured audit logging |
|
|
| SEC-FIX-013 | SQLite foreign key enforcement |
|
|
| SEC-FIX-014 | `npm audit` / `pip-audit` in CI |
|
|
| SEC-FIX-015 | Import JSON schema validation and size limits |
|
|
| SEC-FIX-016 | Remove `v-html` from App.vue |
|