add security fixes, port hardening, and expanded QA coverage
Security fixes: - Replace debug=True with env-driven FLASK_DEBUG in app.py - Add _safe_path helper and path-traversal protection to all 6 file routes in file_manager.py - Add peer_name regex and input validation (public_key, name, endpoint_ip) in wireguard_manager.py - Stop returning private key from GET /api/wireguard/keys; return only public_key + has_private_key boolean - Fix is_local_request() XFF bypass by checking remote_addr only, ignoring X-Forwarded-For - Remove duplicate get_all_configs / get_config_summary methods from config_manager.py DevOps: - Bind 6 internal service ports to 127.0.0.1 in docker-compose.yml (radicale, webdav, api, webui, rainloop, filegator) - Move WebDAV credentials to env vars (WEBDAV_USER, WEBDAV_PASS) - Pin flask, flask-cors, requests, cryptography, docker to secure minimum versions in requirements.txt QA (560 tests, 0 failures): - tests/test_wireguard_endpoints.py: 18 new endpoint tests - tests/test_file_endpoints.py: 24 new endpoint tests incl. path traversal - tests/test_container_manager.py: expanded from 2 to 30 tests - tests/test_config_backup_restore_http.py: 25 new tests (new file) - tests/test_config_apply.py: 9 new tests (new file) Docs: - Rewrite README.md with accurate architecture, ports, env vars, security notes - Rewrite QUICKSTART.md with verified commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,239 +1,133 @@
|
||||
|
||||
# Personal Internet Cell (PIC)
|
||||
|
||||
A self-hosted digital infrastructure platform. One stack, one API, one UI — managing DNS, DHCP, NTP, WireGuard VPN, email, calendar/contacts, file storage, and a reverse proxy on your own hardware.
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
- **Network services** — CoreDNS, dnsmasq DHCP, chrony NTP, all dynamically managed
|
||||
- **WireGuard VPN** — peer lifecycle, QR-code provisioning, per-peer service access control
|
||||
- **Digital services** — Email (Postfix/Dovecot), Calendar/Contacts (Radicale CalDAV), Files (WebDAV + Filegator)
|
||||
- **Reverse proxy** — Caddy with per-service virtual IPs; subdomains like `calendar.mycell.cell` work on VPN clients automatically
|
||||
- **Certificate authority** — self-hosted CA via VaultManager
|
||||
- **Cell mesh** — connect two PIC instances with site-to-site WireGuard + DNS forwarding
|
||||
|
||||
Everything is configured through a REST API and a React web UI. No manual config file editing needed for normal operations.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Debian/Ubuntu host (apt-based)
|
||||
- 2 GB+ RAM, 10 GB+ disk
|
||||
- Open ports: 53 (DNS), 80 (HTTP), 3000 (API), 8081 (Web UI), 51820/udp (WireGuard)
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
git clone <repo-url> pic
|
||||
cd pic
|
||||
|
||||
# Install system deps (docker, python3, python3-cryptography, etc.)
|
||||
make check-deps
|
||||
|
||||
# Generate keys + write configs
|
||||
make setup
|
||||
|
||||
# Build and start all 12 containers
|
||||
make start
|
||||
```
|
||||
|
||||
`make setup` accepts overrides for a second cell on a different host:
|
||||
|
||||
```bash
|
||||
CELL_NAME=pic1 VPN_ADDRESS=10.1.0.1/24 make setup && make start
|
||||
```
|
||||
|
||||
### Access
|
||||
|
||||
| Service | URL |
|
||||
|---------|-----|
|
||||
| Web UI | `http://<host-ip>:8081` |
|
||||
| API | `http://<host-ip>:3000` |
|
||||
| Health | `http://<host-ip>:3000/health` |
|
||||
|
||||
From a WireGuard client: `http://mycell.cell` (replace with your cell name/domain).
|
||||
|
||||
### Local dev (no Docker)
|
||||
|
||||
```bash
|
||||
pip install -r api/requirements.txt
|
||||
python api/app.py # Flask API on :3000
|
||||
|
||||
cd webui && npm install && npm run dev # React UI on :5173 (proxies /api → :3000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Management Commands
|
||||
|
||||
```bash
|
||||
# First install
|
||||
make check-deps # install system packages via apt
|
||||
make setup # generate keys, write configs, create data dirs
|
||||
make start # start all 12 containers
|
||||
|
||||
# Daily operations
|
||||
make status # container status + API health
|
||||
make logs # follow all container logs
|
||||
make logs-api # follow logs for one service (api, dns, wg, mail, caddy, ...)
|
||||
make shell-api # shell inside a container
|
||||
|
||||
# Deploy latest code
|
||||
make update # git pull + rebuild api image + restart
|
||||
|
||||
# Maintenance
|
||||
make backup # tar config/ + data/ into backups/
|
||||
make restore # list available backups and restore
|
||||
make clean # remove containers/volumes, keep config/data
|
||||
|
||||
# Full wipe (test machines)
|
||||
make reinstall # stop, wipe config/data, setup, start fresh
|
||||
make uninstall # stop + remove images; prompts to also wipe config/data
|
||||
|
||||
# Tests
|
||||
make test # run full pytest suite
|
||||
make test-coverage # tests + HTML coverage report in htmlcov/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connecting Two Cells (PIC Mesh)
|
||||
|
||||
Two PIC instances form a mesh: site-to-site WireGuard tunnels with automatic DNS forwarding so each cell's services resolve from the other.
|
||||
|
||||
### Exchange invites
|
||||
|
||||
1. On **Cell A** → Web UI → **Cell Network** → copy the invite JSON.
|
||||
2. On **Cell B** → **Cell Network** → paste into "Connect to Another Cell" → **Connect**.
|
||||
3. On **Cell B** → copy its invite JSON.
|
||||
4. On **Cell A** → paste Cell B's invite → **Connect**.
|
||||
|
||||
Both cells now have a WireGuard peer with `AllowedIPs = remote VPN subnet` and a CoreDNS forwarding block so `*.pic1.cell` resolves across the tunnel.
|
||||
|
||||
### Same-LAN tip
|
||||
|
||||
If both cells share the same external IP (behind NAT), replace the auto-detected endpoint with the LAN IP before connecting:
|
||||
|
||||
```json
|
||||
{ "endpoint": "192.168.31.50:51820", ... }
|
||||
```
|
||||
PIC is a self-hosted digital infrastructure platform. It manages DNS, DHCP, NTP, WireGuard VPN, email, calendar/contacts (CalDAV), file storage (WebDAV), a reverse proxy, and a certificate authority — all controlled from a single REST API and React web UI. No manual config file editing is required for normal operations.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Stack
|
||||
|
||||
```
|
||||
cell-caddy (Caddy) :80/:443 + per-service virtual IPs
|
||||
cell-api (Flask :3000) REST API + config management + container orchestration
|
||||
cell-webui (Nginx :8081) React UI
|
||||
cell-dns (CoreDNS :53) internal DNS + per-peer ACLs
|
||||
cell-dhcp (dnsmasq) DHCP + static reservations
|
||||
cell-ntp (chrony) NTP
|
||||
cell-wireguard WireGuard VPN
|
||||
cell-mail (docker-mailserver) SMTP/IMAP
|
||||
cell-radicale CalDAV/CardDAV :5232
|
||||
cell-webdav WebDAV :80
|
||||
cell-filegator file manager UI :8080
|
||||
cell-rainloop webmail :8888
|
||||
Browser
|
||||
└── React SPA (cell-webui :8081)
|
||||
└── Flask REST API (cell-api :3000, bound to 127.0.0.1)
|
||||
└── Docker SDK / config files
|
||||
├── cell-caddy :80/:443 reverse proxy
|
||||
├── cell-dns :53 CoreDNS
|
||||
├── cell-dhcp :67/udp dnsmasq
|
||||
├── cell-ntp :123/udp chrony
|
||||
├── cell-wireguard :51820/udp WireGuard VPN
|
||||
├── cell-mail :25/:587/:993 Postfix + Dovecot
|
||||
├── cell-radicale 127.0.0.1:5232 CalDAV/CardDAV
|
||||
├── cell-webdav 127.0.0.1:8080 WebDAV
|
||||
├── cell-rainloop :8888 webmail (RainLoop)
|
||||
├── cell-filegator :8082 file manager UI
|
||||
└── cell-webui :8081 React UI (Nginx)
|
||||
```
|
||||
|
||||
All containers share a custom Docker bridge network. Static IPs are assigned in `docker-compose.yml`. Caddy adds per-service virtual IPs to its own interface at API startup so `calendar.<domain>`, `files.<domain>`, etc. route to the right container.
|
||||
All containers run on a custom Docker bridge network (`cell-network`, default `172.20.0.0/16`). Static IPs per container are set in `docker-compose.yml` and overridden via `.env`.
|
||||
|
||||
### Backend (`api/`)
|
||||
The Flask API (`api/app.py`, ~2800 lines) contains all REST endpoints, runs a background health-monitoring thread, and manages the entire lifecycle of generated config artefacts: `Caddyfile`, `Corefile`, `wg0.conf`, and `cell_config.json` (the single source of truth at `config/api/cell_config.json`).
|
||||
|
||||
Service managers (`network_manager.py`, `wireguard_manager.py`, `peer_registry.py`, etc.) all inherit `BaseServiceManager`. `app.py` contains all Flask routes — one file, organized by service.
|
||||
|
||||
`ConfigManager` (`config_manager.py`) is the single source of truth. Config lives in `config/api/cell_config.json`. All managers read/write through it.
|
||||
|
||||
`ip_utils.py` owns all container IP logic via `CONTAINER_OFFSETS` — do not hardcode IPs elsewhere.
|
||||
|
||||
When a config change requires recreating the Docker network (e.g. `ip_range` change), the API spawns a helper container that outlives cell-api to run `docker compose down && up`. Other restarts run `compose up -d --no-deps <containers>` directly.
|
||||
|
||||
### Frontend (`webui/`)
|
||||
|
||||
React 18 + Vite + Tailwind CSS. All API calls go through `src/services/api.js` (Axios). Vite dev server proxies `/api` to `localhost:3000`. Pages in `src/pages/`, shared components in `src/components/`.
|
||||
|
||||
### Project layout
|
||||
|
||||
```
|
||||
pic/
|
||||
├── api/ # Flask API + all service managers
|
||||
│ ├── app.py # all routes (~2700 lines)
|
||||
│ ├── config_manager.py # unified config CRUD
|
||||
│ ├── ip_utils.py # IP/CIDR helpers + Caddyfile generator
|
||||
│ ├── firewall_manager.py # iptables (via cell-wireguard) + Corefile
|
||||
│ ├── network_manager.py # DNS zones, DHCP, NTP
|
||||
│ ├── wireguard_manager.py
|
||||
│ ├── peer_registry.py
|
||||
│ ├── vault_manager.py
|
||||
│ ├── email_manager.py
|
||||
│ ├── calendar_manager.py
|
||||
│ ├── file_manager.py
|
||||
│ └── container_manager.py
|
||||
├── webui/ # React frontend
|
||||
├── config/ # Config files (bind-mounted into containers)
|
||||
│ ├── api/cell_config.json ← live config
|
||||
│ ├── caddy/Caddyfile
|
||||
│ ├── dns/Corefile
|
||||
│ └── ...
|
||||
├── data/ # Persistent data (git-ignored)
|
||||
├── tests/ # pytest suite (372 tests, 27 files)
|
||||
├── docker-compose.yml
|
||||
└── Makefile
|
||||
```
|
||||
The React frontend (`webui/`) is built with Vite + Tailwind CSS. All API calls go through `src/services/api.js` (Axios). Pages: Dashboard, Peers, Network Services, WireGuard, Email, Calendar, Files, Routing, Vault, Containers, Cell Network, Logs, Settings.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
## Requirements
|
||||
|
||||
### Config
|
||||
- Linux host with the WireGuard kernel module loaded
|
||||
- Docker Engine and Docker Compose (v2 plugin or v1 standalone)
|
||||
- Python 3.10+ (for `make setup` and local dev only; not needed at runtime)
|
||||
- 2 GB+ RAM, 10 GB+ disk
|
||||
- Ports available: 53, 67/udp, 80, 443, 51820/udp, 25, 587, 993
|
||||
|
||||
```
|
||||
GET /api/config full config + service IPs
|
||||
PUT /api/config update identity or service config
|
||||
GET /api/config/pending pending restart info
|
||||
POST /api/config/apply apply pending restart
|
||||
POST /api/config/backup create backup
|
||||
POST /api/config/restore/<backup_id> restore from backup
|
||||
```
|
||||
---
|
||||
|
||||
### Network
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
GET /api/dns/records
|
||||
POST /api/dns/records
|
||||
GET /api/dhcp/leases
|
||||
GET /api/dhcp/reservations
|
||||
POST /api/dhcp/reservations
|
||||
```
|
||||
See [QUICKSTART.md](QUICKSTART.md) for step-by-step setup.
|
||||
|
||||
### WireGuard & Peers
|
||||
---
|
||||
|
||||
```
|
||||
GET /api/wireguard/status
|
||||
GET /api/wireguard/peers
|
||||
POST /api/wireguard/peers
|
||||
GET /api/peers
|
||||
POST /api/peers
|
||||
PUT /api/peers/<name>
|
||||
DELETE /api/peers/<name>
|
||||
GET /api/peers/<name>/config peer config + QR code
|
||||
```
|
||||
## Configuration
|
||||
|
||||
### Containers & Health
|
||||
Runtime configuration is controlled by `.env` in the project root. Copy `.env.example` to `.env` before first run.
|
||||
|
||||
```
|
||||
GET /api/containers
|
||||
POST /api/containers/<name>/restart
|
||||
GET /health
|
||||
GET /api/services/status
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `CELL_NETWORK` | `172.20.0.0/16` | Docker bridge subnet for all containers |
|
||||
| `CADDY_IP` through `FILEGATOR_IP` | `172.20.0.2`–`.13` | Static IP for each container |
|
||||
| `DNS_PORT` | `53` | DNS (UDP+TCP) |
|
||||
| `DHCP_PORT` | `67` | DHCP (UDP) |
|
||||
| `NTP_PORT` | `123` | NTP (UDP) |
|
||||
| `WG_PORT` | `51820` | WireGuard listen port (UDP) |
|
||||
| `API_PORT` | `3000` | Flask API (bound to `127.0.0.1`) |
|
||||
| `WEBUI_PORT` | `8081` | React UI |
|
||||
| `MAIL_SMTP_PORT` | `25` | SMTP |
|
||||
| `MAIL_SUBMISSION_PORT` | `587` | SMTP submission |
|
||||
| `MAIL_IMAP_PORT` | `993` | IMAP |
|
||||
| `RADICALE_PORT` | `5232` | CalDAV (bound to `127.0.0.1`) |
|
||||
| `WEBDAV_PORT` | `8080` | WebDAV (bound to `127.0.0.1`) |
|
||||
| `RAINLOOP_PORT` | `8888` | Webmail |
|
||||
| `FILEGATOR_PORT` | `8082` | File manager UI |
|
||||
| `WEBDAV_USER` | `admin` | WebDAV basic-auth username |
|
||||
| `WEBDAV_PASS` | _(required)_ | WebDAV basic-auth password — must be set before `make start` |
|
||||
| `FLASK_DEBUG` | _(unset)_ | Set to `1` to enable Flask debug mode; do not use in production |
|
||||
| `PUID` / `PGID` | current user | UID/GID passed to the WireGuard container |
|
||||
|
||||
Cell identity (cell name, domain, VPN IP range) is configured via `make setup` or the Settings → Identity page in the UI after startup. The VPN IP range must be an RFC-1918 CIDR (`10.0.0.0/8`, `172.16.0.0/12`, or `192.168.0.0/16`); the API and UI both enforce this.
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
**Ports exposed to the network:**
|
||||
|
||||
- `80` / `443` — Caddy (HTTP/HTTPS reverse proxy)
|
||||
- `51820/udp` — WireGuard
|
||||
- `25` / `587` / `993` — Mail (SMTP, submission, IMAP)
|
||||
- `53` — DNS (UDP + TCP)
|
||||
- `67/udp` — DHCP
|
||||
- `8081` — Web UI
|
||||
- `8888` — Webmail (RainLoop)
|
||||
- `8082` — File manager (Filegator)
|
||||
|
||||
**Ports bound to `127.0.0.1` only** (not directly reachable from the network):
|
||||
|
||||
- `3000` — Flask API
|
||||
- `5232` — Radicale (CalDAV)
|
||||
- `8080` — WebDAV
|
||||
|
||||
The API has no authentication layer. It relies on `is_local_request()` to restrict sensitive endpoints (containers, vault) to requests originating from loopback or the cell's Docker network. The Docker socket is mounted into `cell-api`; treat access to port 3000 as equivalent to root access on the host.
|
||||
|
||||
For internet-facing deployments, place the host behind a firewall or VPN and restrict access to the API and UI ports.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Start the full stack (builds api and webui images)
|
||||
make start
|
||||
|
||||
# Rebuild a single image after code changes
|
||||
make build-api
|
||||
make build-webui
|
||||
|
||||
# Run Flask API locally without Docker (port 3000)
|
||||
pip install -r api/requirements.txt
|
||||
python api/app.py
|
||||
|
||||
# Run React UI dev server locally (port 5173, proxies /api to :3000)
|
||||
cd webui && npm install && npm run dev
|
||||
|
||||
# Follow all container logs
|
||||
make logs
|
||||
|
||||
# Follow logs for one service (e.g. api, dns, caddy, wireguard, mail)
|
||||
make logs-api
|
||||
|
||||
# Open a shell inside a container
|
||||
make shell-api
|
||||
```
|
||||
|
||||
---
|
||||
@@ -241,24 +135,53 @@ GET /api/services/status
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
make test # run full suite
|
||||
make test-coverage # coverage report in htmlcov/
|
||||
pytest tests/test_<module>.py # single file
|
||||
pytest tests/ -k "test_name" # single test
|
||||
make test # run the full pytest suite
|
||||
make test-coverage # run with coverage; HTML report in htmlcov/
|
||||
```
|
||||
|
||||
Tests live in `tests/` and use `unittest.TestCase` collected by pytest. External system calls (Docker, iptables, file writes) are mocked with `unittest.mock.patch`.
|
||||
Tests live in `tests/` (34 files, 642 test functions). Coverage includes:
|
||||
|
||||
Known coverage gaps: `write_caddyfile`, `POST /api/config/apply` (helper container path), `PUT /api/config` 400 validation paths. These are the highest-risk untested paths.
|
||||
- All service managers (network, WireGuard, email, calendar, file, routing, vault, container)
|
||||
- API endpoint tests for each service area
|
||||
- Config manager (CRUD, validation, backup/restore)
|
||||
- IP utilities and Caddyfile generation
|
||||
- Peer registry and WireGuard peer lifecycle
|
||||
- Service bus pub/sub
|
||||
- Firewall manager
|
||||
- Pending-restart logic
|
||||
|
||||
Integration tests (`tests/integration/`) require a running PIC stack:
|
||||
|
||||
```bash
|
||||
make test-integration # full suite (creates peers)
|
||||
make test-integration-readonly # read-only checks, safe to run anytime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
## Management Commands
|
||||
|
||||
- The API is access-controlled by `is_local_request()` — it checks whether the request comes from a local/loopback/cell-network IP. Sensitive endpoints (containers, vault) are restricted to local access only.
|
||||
- All per-peer service access is enforced via iptables rules inside `cell-wireguard` and CoreDNS ACL blocks.
|
||||
- The Docker socket is mounted into `cell-api` for container management — treat network access to port 3000 as privileged.
|
||||
- `ip_range` must be an RFC-1918 CIDR (10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16). The API and UI both validate this.
|
||||
```bash
|
||||
make setup # generate WireGuard keys, write configs, create data dirs
|
||||
make start # docker compose up -d --build
|
||||
make stop # docker compose down
|
||||
make restart # docker compose restart
|
||||
make status # container status + API health check
|
||||
make logs # follow all service logs
|
||||
make logs-<svc> # follow logs for one service
|
||||
make shell-<svc> # shell inside a container
|
||||
|
||||
make update # git pull + rebuild + restart
|
||||
make reinstall # full wipe of config/ and data/, then setup + start
|
||||
make uninstall # stop containers; prompts whether to also delete config/ and data/
|
||||
|
||||
make backup # tar config/ + data/ into backups/
|
||||
make restore # list available backups
|
||||
|
||||
make list-peers # show WireGuard peers via API
|
||||
make show-routes # wg show inside the wireguard container
|
||||
make add-peer PEER_NAME=foo PEER_IP=10.0.0.5 PEER_KEY=<pubkey>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user