# Shorefront — Shorewall Configuration Manager: Design Document **Date:** 2026-02-28 **Status:** Approved --- ## Overview Shorefront is a production-ready web application for managing Shorewall firewall configurations. Users manage zones, interfaces, policies, rules, and masquerade/NAT entries through a browser UI backed by a PostgreSQL database, and can generate Shorewall config files on demand. --- ## Architecture **Option A (selected):** Monorepo with separate Docker services. Frontend and backend are independently containerized. In production, Nginx serves the React bundle and reverse-proxies `/api` to the FastAPI backend. In development, the Vite dev server proxies `/api`. Clean separation between frontend, backend API, and the Shorewall generator module. **Deployment targets:** - Local dev: Docker Compose - Production: Kubernetes (Traefik ingress) with Helm charts --- ## Project Structure ``` shorefront/ ├── backend/ │ ├── Dockerfile │ ├── requirements.txt │ ├── alembic/ │ │ ├── env.py │ │ └── versions/ │ │ └── 0001_initial.py # schema + seed data │ └── app/ │ ├── main.py # FastAPI app, CORS, lifespan │ ├── database.py # SQLAlchemy engine + session │ ├── models.py # ORM models │ ├── schemas.py # Pydantic schemas │ ├── auth.py # JWT logic, password hashing │ ├── shorewall_generator.py # ShorewallGenerator class │ └── api/ │ ├── auth.py # /auth/* │ ├── configs.py # CRUD + /generate │ ├── zones.py │ ├── interfaces.py │ ├── policies.py │ ├── rules.py │ └── masq.py ├── frontend/ │ ├── Dockerfile │ ├── nginx.conf # prod: serve static + proxy /api │ ├── vite.config.ts # dev: proxy /api → backend:8000 │ ├── package.json │ └── src/ │ ├── main.tsx │ ├── App.tsx │ ├── api.ts # axios wrapper, withCredentials, 401 redirect │ ├── routes/ │ │ ├── Login.tsx │ │ ├── ConfigList.tsx │ │ └── ConfigDetail.tsx # tabbed: zones/interfaces/policies/rules/masq │ └── components/ │ ├── Layout.tsx # dark sidebar + top bar │ ├── DataTable.tsx # reusable table w/ edit/delete │ ├── EntityForm.tsx # reusable MUI Dialog form │ └── GenerateModal.tsx # file preview tabs + ZIP download ├── helm/ │ └── shorefront/ │ ├── Chart.yaml │ ├── values.yaml │ ├── values-prod.yaml │ └── templates/ │ ├── _helpers.tpl │ ├── namespace.yaml │ ├── secret.yaml │ ├── configmap.yaml │ ├── pv.yaml # NFS PersistentVolume │ ├── pvc.yaml │ ├── postgres-deployment.yaml │ ├── postgres-service.yaml │ ├── backend-deployment.yaml │ ├── backend-service.yaml │ ├── frontend-deployment.yaml │ ├── frontend-service.yaml │ └── ingress.yaml # Traefik, networking.k8s.io/v1 ├── docker-compose.yml └── README.md ``` --- ## Data Model | Model | Key Fields | |-------|-----------| | `User` | `id`, `username`, `email`, `hashed_password`, `is_active` | | `Config` | `id`, `name`, `description`, `is_active`, `created_at`, `updated_at`, `owner_id FK→User` | | `Zone` | `id`, `config_id FK`, `name` (unique per config), `type` (ipv4/ipv6/firewall), `options` | | `Interface` | `id`, `config_id FK`, `name`, `zone_id FK→Zone`, `options` | | `Policy` | `id`, `config_id FK`, `src_zone_id FK`, `dst_zone_id FK`, `policy` (ACCEPT/DROP/REJECT/CONTINUE), `log_level`, `comment`, `position` | | `Rule` | `id`, `config_id FK`, `action`, `src_zone_id FK`, `dst_zone_id FK`, `src_ip`, `dst_ip`, `proto`, `dport`, `sport`, `comment`, `position` | | `Masq` | `id`, `config_id FK`, `source_network`, `out_interface`, `to_address`, `comment` | **Constraints:** Foreign keys enforced. Zone name unique per config. Position/order fields for policies and rules to preserve Shorewall file ordering semantics. --- ## Seed Data (Initial Alembic Migration) - User: `admin` / `admin` (must change on first login) - Config: `homelab` - Zones: `fw` (firewall), `net` (ipv4), `loc` (ipv4) - Interface: `eth0` → zone `net` - Policies: `loc→net ACCEPT`, `net→all DROP info`, `all→all REJECT info` - Masq: `192.168.1.0/24` via `eth0` --- ## API Surface ``` POST /auth/register POST /auth/login → sets httpOnly JWT cookie POST /auth/logout → clears cookie GET /auth/me GET /configs POST /configs GET /configs/{id} PUT /configs/{id} DELETE /configs/{id} POST /configs/{id}/generate # ?format=zip for ZIP, JSON default GET /configs/{id}/zones POST /configs/{id}/zones GET /configs/{id}/zones/{zone_id} PUT /configs/{id}/zones/{zone_id} DELETE /configs/{id}/zones/{zone_id} # same pattern for interfaces, policies, rules, masq ``` **Auth:** JWT in `httpOnly` cookie. `passlib` + `bcrypt` for password hashing. All `/configs/*` routes require valid JWT. --- ## Shorewall Generator `app/shorewall_generator.py` — `ShorewallGenerator` class: ```python class ShorewallGenerator: def __init__(self, config: Config): ... def zones(self) -> str: ... def interfaces(self) -> str: ... def policy(self) -> str: ... def rules(self) -> str: ... def masq(self) -> str: ... def as_json(self) -> dict: ... # { zones, interfaces, policy, rules, masq } def as_zip(self) -> bytes: ... # in-memory ZIP ``` Each method produces a Shorewall-formatted string with a comment header: ``` # zones — generated by shorefront | config: homelab | 2026-02-28T18:00:00Z #ZONE TYPE OPTIONS fw firewall net ipv4 ``` The `/generate` endpoint returns JSON by default; appending `?format=zip` streams a ZIP archive. The frontend uses JSON for the in-browser preview modal and triggers a separate `?format=zip` request for the download button. --- ## Frontend Design **Theme:** MUI with custom dark sidebar (`#1a1f2e`), light main content (`#f5f7fa`), white cards. **Pages:** - **Login** — centered card, username/password - **Config List** — data table with name, description, active badge, created date, edit/delete actions. FAB to create. - **Config Detail** — breadcrumb, 5 tabs (Zones / Interfaces / Policies / Rules / Masq/NAT). Each tab: `DataTable` + "Add" button → `EntityForm` dialog. Top-right: "Generate Config" button → `GenerateModal`. - **Generate Modal** — 5 tabs with monospace code blocks, copy-to-clipboard per file, Download ZIP button. **Key implementation:** - `api.ts`: Axios instance, `baseURL=/api`, `withCredentials: true`, 401 interceptor → redirect to `/login` - `DataTable.tsx`: Generic, accepts column defs + row data, reused across all tabs - `EntityForm.tsx`: Generic MUI Dialog, field config passed per entity type - React Router v6 with `` checking `GET /auth/me` --- ## Infrastructure ### Docker Compose (local dev) - Services: `postgres`, `backend`, `frontend` - Backend runs `alembic upgrade head` on startup - Local named volume for postgres data - Backend on port 8000, frontend (Nginx) on port 80 ### Kubernetes / Helm - **Namespace:** `shorefront` - **Ingress:** Traefik (`ingressClassName: traefik`), standard `networking.k8s.io/v1` Ingress - `shorefront.example.com/api/*` → backend service - `shorefront.example.com/*` → frontend service - Hostname configurable in `values.yaml` - **Storage:** Static NFS PersistentVolume at `192.168.17.199:/mnt/user/kubernetesdata/shorefront`, `ReadWriteOnce`, bound via PVC to Postgres deployment - **Secrets:** DB password + JWT secret key in `secret.yaml`, set via `values.yaml` (override at deploy time) - **Migrations:** `alembic upgrade head` runs as an `initContainer` in the backend Deployment - **values.yaml** configures: image tags, replicas, hostname, resource requests/limits, NFS server/path