diff --git a/.env.example b/.env.example index bf96141..701ecbb 100644 --- a/.env.example +++ b/.env.example @@ -14,15 +14,16 @@ INSTANCE_NAME=My Stoat Instance # Admin API listen port and the HTTPS browser origin allowed by CORS. ADMIN_API_PORT=5181 -ADMIN_WEB_ORIGIN=https://admin.example.com +ADMIN_WEB_ORIGIN=https://localhost:9443 -# Admin hostname terminated by the dedicated Caddy proxy. This hostname only -# needs to resolve on your WireGuard/private network. -ADMIN_HOSTNAME=admin.example.com +# Admin hostname terminated by the dedicated Caddy proxy. Use localhost for +# local compose usage and replace it with a real name for deployment. +ADMIN_HOSTNAME=localhost -# Compose-level convenience variables for the dedicated admin proxy. +# Compose-level convenience variables for the dedicated admin proxy. These +# defaults avoid privileged ports locally; set 80/443 in deployment if needed. ADMIN_BIND_IP=127.0.0.1 -ADMIN_HTTP_PORT=80 -ADMIN_HTTPS_PORT=443 -# Leave blank to use same-origin /api requests through admin-proxy. +ADMIN_HTTP_PORT=9080 +ADMIN_HTTPS_PORT=9443 +# Optional frontend API base URL override for standalone web builds. ADMIN_WEB_API_URL= diff --git a/README.md b/README.md index e4f90f5..b1a1218 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Stoat Admin is a lightweight self-hosted moderation and invite dashboard for a S ```text . ├── api/ # Express + TypeScript backend -├── web/ # Vite + React + TypeScript frontend +├── proxy/ # Caddy + Coraza reverse proxy image +├── web/ # Vite + React frontend and static image build ├── deploy/s6/ # Example s6 service directories and systemd unit ├── compose.yml # Production-oriented compose file ├── compose.override.example.yml @@ -93,19 +94,22 @@ pnpm dev:web | `ADMIN_API_PORT` | Listen port for `admin-api` | | `ADMIN_WEB_ORIGIN` | Exact HTTPS browser origin allowed by CORS | | `ADMIN_HOSTNAME` | Hostname served by the dedicated Caddy proxy | -| `ADMIN_BIND_IP` | Compose bind IP for the proxy's `80`/`443` ports | -| `ADMIN_HTTP_PORT` | Host port for ACME HTTP and redirect handling | -| `ADMIN_HTTPS_PORT` | Host port for HTTPS | -| `ADMIN_WEB_API_URL` | Optional frontend API base URL override | +| `ADMIN_BIND_IP` | Compose bind IP for the proxy's published ports | +| `ADMIN_HTTP_PORT` | Published HTTP port for redirect handling | +| `ADMIN_HTTPS_PORT` | Published HTTPS port for the admin dashboard | +| `ADMIN_WEB_API_URL` | Optional frontend API override outside compose | ## Deployment The repo ships with a standalone `compose.yml` that expects an external `stoat_default` network. Update the network name if your Stoat stack uses a different one. +For local compose use, the proxy defaults to `https://localhost:9443` and `http://localhost:9080`. For deployed hosts, set `ADMIN_HOSTNAME` to the real admin name and switch `ADMIN_HTTP_PORT`/`ADMIN_HTTPS_PORT` to `80`/`443` or use [compose.override.example.yml](/home/jasonross/workspace/stoat-admin/compose.override.example.yml) as a starting point. + The deployment topology is: -- `admin-proxy` publishes ports `80` and `443`, issues a private certificate from Caddy's internal CA, and reverse-proxies `/api/*` to `admin-api` and everything else to `admin-web`. +- `admin-proxy` is built from [proxy/Dockerfile](/home/jasonross/workspace/stoat-admin/proxy/Dockerfile), publishes the configured HTTP and HTTPS ports, issues a private certificate from Caddy's internal CA, applies Coraza, and reverse-proxies `/api/*` to `admin-api` and everything else to `admin-web`. - `admin-web` and `admin-api` are no longer published directly on the host. +- `admin-web` serves the built Vite bundle privately on the admin network. - `admin-api` stays attached to the shared Stoat network for MongoDB access and also joins a private admin network used by the proxy. Before starting the stack, point `ADMIN_HOSTNAME` at the host running `admin-proxy` on your WireGuard/private network and set `ADMIN_WEB_ORIGIN` to `https://`. @@ -144,7 +148,7 @@ tail -f /var/log/s6/stoat-admin/current | s6-tai64nlocal ## Development Notes - The backend uses SQLite for admin credentials, audit logs, and invite metadata, and MongoDB for Stoat state. -- In the composed deployment, the frontend uses same-origin `/api` requests through `admin-proxy`, while local development can still override `VITE_API_URL`. +- In the composed deployment, the frontend always uses same-origin `/api` requests through `admin-proxy`; `VITE_API_URL` is only useful outside compose. - Root task orchestration is handled by Turborepo through [turbo.json](/home/jasonross/workspace/stoat-admin/turbo.json). - The current repo state is a first implementation slice based on the design docs in [docs/stoat-admin-design.md](/home/jasonross/workspace/stoat-admin/docs/stoat-admin-design.md) and [docs/stoat-admin-tasks.md](/home/jasonross/workspace/stoat-admin/docs/stoat-admin-tasks.md). diff --git a/compose.override.example.yml b/compose.override.example.yml index 0f0c4d1..898a754 100644 --- a/compose.override.example.yml +++ b/compose.override.example.yml @@ -1,10 +1,12 @@ services: admin-proxy: + environment: + ADMIN_HOSTNAME: admin.example.com + ADMIN_HTTP_PORT: 80 + ADMIN_HTTPS_PORT: 443 ports: - "10.0.0.1:80:80" - "10.0.0.1:443:443" - environment: - ADMIN_HOSTNAME: admin.example.com admin-api: environment: @@ -14,5 +16,6 @@ services: # Common customizations: # - Change the external network name in compose.yml if your Stoat stack uses a different name. # - Bind admin-proxy to the interface IP that should answer ports 80/443. +# - Use ADMIN_HTTP_PORT/ADMIN_HTTPS_PORT=9080/9443 for local compose HTTPS without privileged ports. # - Ensure ADMIN_HOSTNAME resolves to that IP on your WireGuard/private network. # - Add resource limits or alternate image tags per deployment. diff --git a/compose.yml b/compose.yml index f071a87..dd02a31 100644 --- a/compose.yml +++ b/compose.yml @@ -15,7 +15,7 @@ services: image: ghcr.io/owner/stoat-admin-api:latest restart: unless-stopped expose: - - "5181" + - "${ADMIN_API_PORT:-5181}" volumes: - ./data:/data - ./.env:/app/.env:ro @@ -26,8 +26,6 @@ services: admin-web: build: context: ./web - args: - VITE_API_URL: ${ADMIN_WEB_API_URL:-} image: ghcr.io/owner/stoat-admin-web:latest restart: unless-stopped expose: @@ -36,18 +34,22 @@ services: - admin admin-proxy: - image: caddy:latest + build: + context: ./proxy + image: ghcr.io/owner/stoat-admin-proxy:latest restart: unless-stopped depends_on: - admin-api - admin-web environment: - ADMIN_HOSTNAME: ${ADMIN_HOSTNAME} + ADMIN_API_PORT: ${ADMIN_API_PORT:-5181} + ADMIN_HOSTNAME: ${ADMIN_HOSTNAME:-localhost} + ADMIN_HTTP_PORT: ${ADMIN_HTTP_PORT:-9080} + ADMIN_HTTPS_PORT: ${ADMIN_HTTPS_PORT:-9443} ports: - - "${ADMIN_BIND_IP:-127.0.0.1}:${ADMIN_HTTP_PORT:-80}:80" - - "${ADMIN_BIND_IP:-127.0.0.1}:${ADMIN_HTTPS_PORT:-443}:443" + - "${ADMIN_BIND_IP:-127.0.0.1}:${ADMIN_HTTP_PORT:-9080}:${ADMIN_HTTP_PORT:-9080}" + - "${ADMIN_BIND_IP:-127.0.0.1}:${ADMIN_HTTPS_PORT:-9443}:${ADMIN_HTTPS_PORT:-9443}" volumes: - - ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro - admin_proxy_data:/data - admin_proxy_config:/config networks: diff --git a/proxy/Caddyfile b/proxy/Caddyfile index ca2a94d..fcb12a1 100644 --- a/proxy/Caddyfile +++ b/proxy/Caddyfile @@ -1,14 +1,28 @@ -{$ADMIN_HOSTNAME} { +{ + http_port {$ADMIN_HTTP_PORT:9080} + https_port {$ADMIN_HTTPS_PORT:9443} + order coraza_waf first +} + +{$ADMIN_HOSTNAME:localhost} { tls internal encode zstd gzip + coraza_waf { + load_owasp_crs + directives ` + Include /etc/caddy/coraza.conf + SecRuleEngine On + ` + } + @api path /api/* handle @api { - reverse_proxy admin-api:5181 + reverse_proxy admin-api:{$ADMIN_API_PORT:5181} } handle { - reverse_proxy admin-web:80 + reverse_proxy admin-web:80 admin-web:443 admin-web:9080 admin-web:9443 } } diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..ba6573d --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,9 @@ +FROM caddy:2-builder-alpine AS build-caddy +RUN xcaddy build \ + --with github.com/corazawaf/coraza-caddy/v2 + +FROM caddy:2-alpine +COPY --from=build-caddy /usr/bin/caddy /usr/bin/caddy +COPY Caddyfile /etc/caddy/Caddyfile +COPY coraza.conf /etc/caddy/coraza.conf +EXPOSE 80 443 9080 9443 \ No newline at end of file diff --git a/web/coraza.conf b/proxy/coraza.conf similarity index 84% rename from web/coraza.conf rename to proxy/coraza.conf index d1a2df6..3848bc5 100644 --- a/web/coraza.conf +++ b/proxy/coraza.conf @@ -1,5 +1,4 @@ # Coraza configuration overrides # Tune false positives here as they arise. -SecAction "id:900000, phase:1, pass, t:none, nolog, setvar:tx.blocking_paranoia_level=1" - +SecAction "id:900000, phase:1, pass, t:none, nolog, setvar:tx.blocking_paranoia_level=1" \ No newline at end of file diff --git a/web/Caddyfile b/web/Caddyfile deleted file mode 100644 index e91d4d1..0000000 --- a/web/Caddyfile +++ /dev/null @@ -1,14 +0,0 @@ -:80 { - coraza_waf { - load_owasp_crs - directives ` - Include /etc/caddy/coraza.conf - SecRuleEngine On - ` - } - - root * /srv - file_server - try_files {path} /index.html -} - diff --git a/web/Dockerfile b/web/Dockerfile index d465d3f..d026b6d 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -5,15 +5,4 @@ RUN corepack enable && pnpm install --frozen-lockfile=false COPY . . ARG VITE_API_URL ENV VITE_API_URL=${VITE_API_URL} -RUN pnpm build - -FROM caddy:builder AS build-caddy -RUN xcaddy build \ - --with github.com/corazawaf/coraza-caddy/v2 - -FROM caddy:latest -COPY --from=build-caddy /usr/bin/caddy /usr/bin/caddy -COPY --from=build-app /app/dist /srv -COPY Caddyfile /etc/caddy/Caddyfile -COPY coraza.conf /etc/caddy/coraza.conf -EXPOSE 80 +EXPOSE 9080 9443 \ No newline at end of file