Commit Graph

26 Commits

Author SHA1 Message Date
roof 0a21f22076 Phase 4: service store — manifest validation, install/remove, Store UI
- ServiceStoreManager: manifest allowlist (git.pic.ngo/roof/*), volume
  denylist, ACCEPT-only iptables rules, ${SERVICE_IP}-only dest_ip
- IP allocator: pool 172.20.0.20-254, skips CONTAINER_OFFSETS VIPs
- Compose overlay: docker-compose.services.yml auto-included via DCF
- Flask blueprint at /api/store: list, install, remove, refresh
- Store.jsx: full install/remove UI with spinners and toast notifications
- 95 new unit tests for ServiceStoreManager (all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 10:19:39 -04:00
roof 7d290c12c4 Phase 2: caddy_manager — Caddyfile generation, health monitor, DNS-01 support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:04:11 -04:00
roof cf1b9672f4 Phase 1: first-run setup wizard, bash installer, Docker profiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 08:05:38 -04:00
roof 56d677e925 fix: copy button HTTP fallback, reset-admin-password in Docker, scripts volume
- CellNetwork.jsx CopyButton: use execCommand fallback when clipboard API
  is unavailable (HTTP non-localhost context)
- Makefile reset-admin-password: run inside cell-api container via docker exec
  so bcrypt and all deps are available without host installation
- docker-compose.yml: mount ./scripts:/app/scripts:ro in cell-api so the
  reset script is accessible inside the container
- scripts/reset_admin_password.py: auto-detect API module path and data dir
  so the script works in both host (api/ sibling) and container (/app) layouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:34:38 -04:00
roof a43f9fbf0d fix: full security audit remediation — P0/P1/P2/P3 fixes + 1020 passing tests
P0 — Broken functionality:
- Fix 12+ endpoints with wrong manager method signatures (email/calendar/file/routing)
- Fix email_manager.delete_email_user() missing domain arg
- Fix cell-link DNS forwarding wiped on every peer change (generate_corefile now
  accepts cell_links param; add/remove_cell_dns_forward no longer clobber the file)
- Fix Flask SECRET_KEY regenerating on every restart (persisted to DATA_DIR)
- Fix _next_peer_ip exhaustion returning 500 instead of 409
- Fix ConfigManager Caddyfile path (/app/config-caddy/)
- Fix UI double-add and wrong-key peer bugs in Peers.jsx / WireGuard.jsx
- Remove hardcoded credentials from Dashboard.jsx

P1 — Security:
- CSRF token validation on all POST/PUT/DELETE/PATCH to /api/* (double-submit pattern)
- enforce_auth: 503 only when users file readable but empty; never bypass on IOError
- WireGuard add_cell_peer: validate pubkey, name, endpoint against strict regexes
- DNS add_cell_dns_forward: validate IP and domain; reject injection chars
- DNS zone write: realpath containment + record content validation
- iptables comment /32 suffix prevents substring match deleting wrong peer rules
- is_local_request() trusts only loopback + 172.16.0.0/12 (Docker bridge)
- POST /api/containers: volume allow-list prevents arbitrary host mounts
- file_manager: bcrypt ($2b→$2y) for WebDAV; realpath containment in delete_user
- email/calendar: stop persisting plaintext passwords in user records
- routing_manager: validate IPs, networks, and interface names
- peer_registry: write peers.json at mode 0o600
- vault_manager: Fernet key file at mode 0o600
- CORS: lock down to explicit origin list
- domain/cell_name validation: reject newline, brace, semicolon injection chars

P2 — Architecture:
- Peer add: rollback registry entry if firewall rules fail post-add
- restart_service(): base class now calls _restart_container(); email and calendar
  managers call cell-mail / cell-radicale respectively
- email/calendar managers sync user list (no passwords) to cell_config.json
- Pending-restart flag cleared only after helper subprocess exits with code 0
- docker-compose.yml: add config-caddy volume to API container

P3 — Tests (854 → 1020):
- Fill test_email_endpoints.py, test_calendar_endpoints.py,
  test_network_endpoints.py, test_routing_endpoints.py
- New: test_peer_management_update.py, test_peer_management_edge_cases.py,
  test_input_validation.py, test_enforce_auth_configured.py,
  test_cell_link_dns.py, test_logs_endpoints.py, test_cells_endpoints.py,
  test_is_local_request_per_endpoint.py, test_caddy_routing.py
- E2E conftest: skip WireGuard suite when wg-quick absent
- Update existing tests to match fixed signatures and comment formats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 11:30:21 -04:00
roof 420dced9ff fix: WireGuard peer sync, privileged mode, E2E and integration test correctness
- api/app.py: sync WireGuard server config on peer add/remove (non-fatal)
- docker-compose.yml: add privileged:true to wireguard service
- E2E tests: fix logout selector, DNS IP lookup, wg config DNS line, VIP skip guards,
  badge text selectors, heading .first, async logout wait
- Integration tests: fix 4 tests that sent unauthenticated requests expecting 400
  (now use authenticated session helpers); accept 401 as valid in webui proxy test;
  add password field to service_access validation test
- Remove stale tracked config templates (config/api/api/*, config/api/cell.env, etc.)
  that no longer exist on disk after config layout was reorganised

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 06:04:40 -04:00
roof 1e81b3b618 Fix webui port binding: restore public access on 8081
The devops security pass incorrectly bound the webui to 127.0.0.1,
making it unreachable from the network. The webui is the user-facing
interface and must be publicly accessible. Internal-only services
(api :3000, radicale :5232, webdav :8080, rainloop :8888,
filegator :8082) retain their loopback bindings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:10:49 -04:00
roof a338836bb8 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>
2026-04-25 13:08:24 -04:00
roof eb817ffdc5 fix: WireGuard sysctl || true, port check on page load, add peer status tests
Root cause: sysctl -q net.ipv4.conf.all.rp_filter=0 in PostUp exited non-zero
inside the linuxserver/wireguard container (no permission), causing wg-quick to
tear down the wg0 interface — breaking peer status, port check, and internet
access through full tunnel.

- wireguard_manager.py: add || true to both sysctl PostUp/PostDown lines
- docker-compose.yml: add net.ipv4.conf.all.rp_filter=0 to wireguard sysctls
- WireGuard.jsx: kick off port check asynchronously on page load (was refresh-only)
- tests: add TestWireGuardSysctlAndPortCheck — 14 new tests covering sysctl
  content, check_port_open (interface up / down / fallback-to-handshake),
  get_peer_status (online / offline / not-found / no-handshake), and
  get_all_peer_statuses (multi-peer / empty / skips interface line)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 10:31:57 -04:00
roof 7a273ad43e fix: consolidate WireGuard port config and propagate port changes to UI
- docker-compose: fix WireGuard port mapping to ${WG_PORT}:${WG_PORT} so
  the daemon ListenPort matches the Docker host-to-container binding
- app.py: sync wireguard.port ↔ identity.wireguard_port in both directions
  so changing either keeps them consistent; identity path now also updates
  wg0.conf via wireguard_manager.update_config
- Settings.jsx: remove duplicate wireguard_port from Cell Identity section
  (port is configurable under WireGuard VPN service config); add
  refreshConfig() after saveService so other pages see new values immediately
- WireGuard.jsx: import useConfig() and use service_configs.wireguard.port
  as the reactive port source for endpoint display and port-open warnings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 13:27:35 -04:00
roof 673fe04164 feat(service-ports): remove hardcoded ports from docker-compose, make all service ports configurable
All host port bindings in docker-compose.yml now use \${VAR:-default} substitution,
driven by the .env file generated by ip_utils.write_env_file(). Changing a port in
Settings triggers a per-container pending-restart banner so only the affected container
is restarted on Apply.

- ip_utils: add PORT_DEFAULTS, PORT_ENV_VAR_NAMES, PORT_TO_CONTAINERS; extend
  write_env_file() to accept optional ports dict and write all port env vars
- docker-compose: convert all hardcoded port bindings to \${VAR:-default} form
- app.py: add _collect_service_ports helper; detect port changes in update_config,
  write updated .env and call _set_pending_restart with specific container list;
  update _set_pending_restart to merge/accumulate pending state with containers list;
  update apply_pending_config to use --no-deps <service> for targeted restarts
- config_manager: add submission_port, webmail_port to email schema; add manager_port
  to files schema
- Settings.jsx: make all email/files ports editable, add submission_port, webmail_port,
  manager_port fields; update stale identity note
- tests: 8 new tests for PORT_DEFAULTS, PORT_ENV_VAR_NAMES, and port override in write_env_file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:51:10 -04:00
roof c3b2c8d8e5 feat: pending-restart banner + Apply button for config changes
When ip_range changes, a persistent amber banner appears at the top of
every page showing what changed and a "Apply Now" button.  Clicking it
shows a confirmation modal ("containers will restart briefly"), then
calls POST /api/config/apply which runs docker compose up -d from inside
the API container — no manual make start needed.

Backend:
- _set_pending_restart() / _clear_pending_restart() helpers track state
  in config_manager so it survives page refresh
- GET /api/config/pending returns { needs_restart, changed_at, changes }
- POST /api/config/apply runs docker compose up -d via the mounted
  docker.sock, using the project working_dir label to resolve host paths
- docker-compose.yml mounts docker-compose.yml itself read-only into
  the API container so docker compose can read it from inside

Frontend (App.jsx):
- Polls /api/config/pending every 5 s alongside the health check
- PendingRestartBanner component with confirmation modal
- Optimistically clears banner on Apply click; API and containers
  restart in the background

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:29:26 -04:00
roof 1c939249e4 feat: replace hardcoded docker-compose IPs with .env-based substitution
docker-compose.yml now uses ${VAR:-default} for every container IP and
the network subnet, so there are no hardcoded addresses in the YAML.

How it works:
- setup_cell.py generates .env at project root from ip_range (gitignored).
- docker-compose reads .env automatically at startup.
- When ip_range changes in Settings, the API writes a new .env via
  ip_utils.write_env_file(); DNS/firewall/vIPs update immediately.
- User runs `make start` to recreate containers with the new IPs.

api/ip_utils.py gains ENV_VAR_NAMES dict and write_env_file(ip_range, path).
The old update_docker_compose_ips() direct-patch approach is removed from app.py.
3 new tests added (TestWriteEnvFile); total 324 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 10:43:33 -04:00
roof 1e43f408bc fix: make setup auto-installs deps and handles container-owned dirs
- Add check-deps target: auto-installs python3, pip3, cryptography via
  apt (with pip fallback) before running setup_cell.py
- Add sudo chown in setup to reclaim config/data dirs if containers have
  taken ownership (e.g. re-running setup after make start)
- Pass PUID/PGID=$(id -u/g) to docker-compose so linuxserver/wireguard
  chowns its config dir to the host user instead of hardcoded UID 911

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 09:19:44 -04:00
roof f848a1d056 feat: proper logging architecture — Docker rotation, persisted service logs, verbosity config
docker-compose.yml:
- Add json-file logging driver (max-size: 10m, max-file: 5) to all 13 containers
- Docker now owns container stdout/stderr rotation automatically
- Add ./data/logs:/app/api/data/logs volume to API — service logs now persist across restarts

log_manager.py:
- Remove container log collection hack (Docker handles it natively)
- Add set_service_level(service, level) — change log level at runtime without restart
- Add get_service_levels() — return current per-service levels
- Simplify get_all_log_file_infos to return only service log files

app.py:
- Add GET /api/logs/verbosity — return current per-service log levels
- Add PUT /api/logs/verbosity — update levels at runtime, persist to config/log_levels.json
- Load persisted log level overrides at startup from log_levels.json
- Simplify rotate endpoint (service logs only, container logs owned by Docker)

wireguard_manager.py:
- get_keys(): return empty strings if key files don't exist (prevents get_status crash
  when wg0.conf is missing at startup and falls through to generate_config)

Logs page (4 tabs):
- API Service Logs: structured JSON logs from Python managers, with search/filter/rotate panel
- Container Logs: live docker logs (read via existing /api/containers/<name>/logs endpoint)
- Verbosity Config: per-service level dropdowns, apply immediately + persist
- Health History: existing health poll table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 02:07:57 -04:00
roof 53c7661812 feat: per-peer access enforcement, live peer status, auto IP assignment
Server-side access control:
- firewall_manager.py: per-peer iptables FORWARD rules in WireGuard container;
  virtual IPs on Caddy (172.20.0.21-24) for per-service DROP/ACCEPT targeting
- CoreDNS Corefile regenerated with ACL blocks for blocked services per peer
- POST /api/wireguard/apply-enforcement re-applies rules after WireGuard restart;
  wg0.conf PostUp calls it via curl so rules restore automatically on container start

WireGuard fixes:
- _syncconf uses `wg set peer` instead of `wg syncconf` to avoid resetting ListenPort
- add_peer validates AllowedIPs must be /32 — rejects full/split tunnel CIDRs that
  would route internet or LAN traffic to that peer
- _config_file() checks for linuxserver wg_confs/ subdirectory first

UI:
- Peers page fetches /api/wireguard/peers/statuses for live handshake data;
  status badge now shows real Online/Offline + seconds since last handshake
- IP field removed from Add Peer form (auto-assigned from 10.0.0.0/24)

Tests (246 pass):
- test_firewall_manager.py: 22 tests for ACL generation, iptables rule correctness,
  comment tagging, clear_peer_rules filter logic
- test_peer_wg_integration.py: 10 tests for /32 enforcement, IP auto-assignment,
  syncconf called on add/remove
- test_wireguard_manager.py: updated to reflect correct IPs and /32 requirement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:01:07 -04:00
roof 9d7d74f3f4 fix: full-tunnel default, real host routing table, peer config tunnel mode
- WireGuard default changed to full tunnel (0.0.0.0/0) — all peer traffic
  routes through PIC server so internet latency matches server's clean 41ms
- UI tunnel toggle now defaults to Full tunnel
- API /peers/config accepts allowed_ips param so UI toggle wires through
- Routing page reads real host routes via /proc/1/net/route (pid: host)
  instead of mock data; shows ens18/192.168.31.1 correctly
- Add iproute2 + util-linux to API Dockerfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:20:55 -04:00
roof 03d100b776 fix: cell-ntp restart loop — add SYS_TIME cap, clear stale PID, remove log perms
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:37:56 -04:00
roof cbdefbd110 fix: static IPs for all containers, radicale config, DNS zone, cleanup
- Assign static IPs to all 13 containers (172.20.0.2–13) so DNS zone
  records match actual container IPs regardless of start order.
- Update cell.zone: all .cell domains now point to cell-caddy (172.20.0.2)
  which is the correct single entry point via Caddy reverse proxy.
- Create config/radicale/config so the calendar container actually starts.
- Fix webdav: replace empty users.passwd with USERNAME/PASSWORD env vars.
- Fix DNS fallback IP in wireguard_manager: 172.20.0.2→172.20.0.3 (cell-dns).
- Remove duplicate http://ui.cell from Caddyfile.
- Add persistent data volumes for rainloop and filegator.
- Fix mail domainname placeholder (yourdomain.com→cell.local).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:31:38 -04:00
roof e79ee08c63 fix: WireGuard routing, DNS, service access, and UI improvements
- Fix CoreDNS not loading .cell zones (wrong Corefile path, now uses -conf flag)
- Fix WireGuard server address conflict (172.20.0.1/16 overlapped with Docker
  network; changed to 10.0.0.1/24 to eliminate duplicate routes)
- Add SERVERMODE=true and sysctls to WireGuard docker-compose for server mode
- Fix DNS zone file parser to handle 4-field records (name IN type value)
- Add get_dns_records() to NetworkManager; mount data/dns into API container
- Fix peer config endpoint: look up IP/key from registry, use real endpoint
- Add bulk peer statuses endpoint keyed by public_key
- Normalize snake_case API fields to camelCase in WireGuard UI
- Add port check endpoint (checks via live handshake, not unreliable TCP probe)
- Add Caddy virtual hosts for ui/calendar/files/mail .cell domains (HTTP only)
- Fix cell config domain default from cell.local to cell
- Fix Routing Network Config tab (was calling hardcoded localhost:3000)
- Fix DNS records display (record.value not record.ip)
- Move service access guide to top of Dashboard with login hints
- Add /api/routing/setup endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:43:23 -04:00
Cloud bb6ccfe023 wip: wireguard 2025-09-14 03:31:14 -05:00
Cloud 5bd7443681 wip: peer make work with qr code 2025-09-13 12:08:28 -05:00
Constantin 4f65f95ac9 wip: peers 2025-09-13 18:56:00 +03:00
Constantin 3e8a1bd530 exclude platform: linux/amd64 2025-09-13 16:14:51 +03:00
Constantin 4052e95e2e docker compose version: '3.3' 2025-09-13 16:12:24 +03:00
Constantin 2277b11563 init 2025-09-12 23:04:52 +03:00