diff --git a/AGENT.md b/AGENT.md index e7ada95..189a0a7 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,101 +1,82 @@ -# NextWorkspace — Agent Workflow Instructions +# NextWorkspace — Architecture & Workflow -## Versioning +## Deployment Model -- Format: `MILESTONE.FEATURE.PATCH.BUILD` (e.g., `0.1.0.0032`) -- Bump VERSION file on every change before commit. -- Every commit must be tagged with the version: `git tag v$(cat VERSION)` +Single unified script: **`tools/nextwks.sh`** (replaces old `deploy.sh`/`install.sh`). -## Deployment Model (Unified Script) +| Flag | When to use | +|------|------------| +| `--install` | First-time setup on a bare VM (prompts for config) | +| `--update` | Rebuild & restart with latest code (uses saved secrets) | +| `--destroy` | Full greenfield redeploy (tears down everything, rebuilds from `/opt/backup/.env`) | -The project uses a single unified script at `tools/nextwks.sh` for all operations. -The old `deploy.sh` and `install.sh` are deprecated. +**Run WITHOUT sudo.** The script invokes `sudo` only where needed (apt, writing to `/opt/`, iptables). -**Key principles:** -- Source of truth is **git remote only**. No local `/opt/NextWks` repo. -- Building happens in `/tmp/nextwks-build/` via `git clone --depth 1` (fresh every time). -- Runtime goes to `/opt/nextworkspace/` (configs, compose, binary, lng). -- Secrets persist in `/opt/backup/.env` and survive `--destroy`. -- Stack runs on a shared `nextwks-net` podman bridge network. +## Key Architecture Decisions -### Script Flags +### 1. Rootless Podman (no sudo for containers) +All `podman` / `podman-compose` commands run as the normal user. Containers are rootless. +- Caddy binds to host ports **8080** and **8443** (not 80/443 — unprivileged) +- iptables `PREROUTING` + `OUTPUT -o lo` redirect 80→8080, 443→8443 +- Firewall rules applied by `tools/firewall-routing.sh`, persisted via `netfilter-persistent` -| Flag | What it does | -|------|-------------| -| `--install` | First-time setup on a bare VM: installs deps, prompts for config, builds binary, generates configs, deploys stack | -| `--update` | Smart update: clones fresh, rebuilds binary, copies to target, bounces containers | -| `--destroy` | Full greenfield redeploy: tears down containers, wipes `/opt/nextworkspace/`, rebuilds from scratch using saved secrets from `/opt/backup/.env` | +### 2. Ephemeral Build Directory +- Fresh `git clone --depth 1` into `/tmp/nextwks-build/` every time +- No persistent `/opt/NextWks` repo (eliminates git permission issues) +- Binary + configs built as user, then `sudo cp` to `/opt/nextworkspace/` -### Workflow for Making Changes +### 3. File Ownership +All files in `/opt/nextworkspace/` and `/opt/backup/` are `chown -R` to the non-root user after every deploy. -1. Edit code in the development clone. -2. Test locally (e.g., `go build && go run .`). -3. Bump `VERSION` (increment BUILD). -4. Update `CHANGELOG.md`. -5. Commit: `git add -A && git commit -m "description"` -6. Tag: `git tag v$(cat VERSION)` -7. Push: `git push origin main --tags` -8. Deploy: `./nextwks.sh --update` (no sudo — script handles it per-command) - -> **Note:** The deploy script is downloaded by users via `curl` from the repo. For production deployment, users run: -> ```bash -> curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh | sudo bash -s -- --install -> ``` - -## Build Process - -- Static Go binary: `CGO_ENABLED=0 go build -o nextworkspace .` -- Must run on Alpine in the container (no glibc dependency). -- Binary runs as PID 1 in the `launcher` container. - -## Architecture - -``` -Caddy (:80/:443, host ports) - ├── auth.{DOMAIN} → Authelia :9091 (internal) - ├── app.{DOMAIN} → Launcher :9000 (internal) with forward auth - └── www.{DOMAIN} → static files - -All three containers on nextwks-net (podman bridge). -Only Caddy exposes ports to host. -``` - -## Config Template System - -Config files use `{PLACEHOLDER}` syntax. The script substitutes values at deploy time: +### 4. Config Templates with Placeholder Substitution +Config files live in git with `{PLACEHOLDER}` syntax. The script substitutes values at deploy time: | File | Placeholders | |------|-------------| | `config/caddy/Caddyfile` | `{DOMAIN}`, `{TLS_EMAIL}` | | `config/authelia/configuration.yml` | `{DOMAIN}`, `{JWT_SECRET}`, `{SESSION_SECRET}`, `{STORAGE_ENCRYPTION_KEY}`, `{SMTP_HOST}`, `{SMTP_PORT}`, `{SMTP_USER}`, `{SMTP_PASS}` | | `config/authelia/users_database.yml` | `{ADMIN_PASSWORD_HASH}`, `{TLS_EMAIL}` | -| `compose/stack.yaml` | `{AUTHELIA_SECRET}` (same as `SESSION_SECRET`) | +| `compose/stack.yaml` | `{AUTHELIA_SECRET}` (= `SESSION_SECRET`) | -## Env Vault (`/opt/backup/.env`) +Configs are regenerated on **every** run (install, update, destroy) from templates + saved secrets. + +### 5. Secrets Vault (`/opt/backup/.env`) +All values are single-quoted to protect `$` signs (bcrypt hashes). Survives `--destroy`. Sourced with `set +u` to avoid errors from `$` in values. + +### 6. Container Lifecycle +Containers are stopped **before** copying the new binary (avoids "Text file busy"). Restarted after deploy. + +--- + +## Network Architecture -Persisted secrets across destroys: ``` -DOMAIN=nextwks.eu -TLS_EMAIL=admin@nextwks.eu -ADMIN_USERNAME=master -ADMIN_PASSWORD= -SMTP_HOST=smtp.openxchange.eu -SMTP_PORT=587 -SMTP_USER=post@nextwks.eu -SMTP_PASS= -JWT_SECRET= -SESSION_SECRET= -STORAGE_ENCRYPTION_KEY= -ADMIN_PASSWORD_HASH= +Internet :443 → [iptables REDIRECT] → host :8443 → [Caddy container :443] +Internet :80 → [iptables REDIRECT] → host :8080 → [Caddy container :80] + +Caddy (rootless, nextwks-net) + ├── auth.{DOMAIN} → Authelia :9091 (internal, no host port) + ├── app.{DOMAIN} → Launcher :9000 (internal, forward auth via Authelia) + └── www.{DOMAIN} → static files ``` +All three containers on `nextwks-net` (rootless podman bridge). +Only Caddy has host ports (8080, 8443). Authelia and Launcher are internal-only. + +## Firewall (`tools/firewall-routing.sh`) +- NAT redirects: 80→8080, 443→8443 +- INPUT rules: allow lo, established, SSH (22), Caddy ports (8080, 8443), app ports (8000) +- Global DROP at end for all other unsolicited inbound +- Persisted: `netfilter-persistent save` (runs on every deploy mode) + +## Versioning +- Format: `MILESTONE.FEATURE.PATCH.BUILD` (e.g., `0.1.0.0032`) +- Bump VERSION, update CHANGELOG.md, `git tag v$(cat VERSION)` on every change + +## Workflow +1. Edit code in dev clone +2. `git commit -m "msg" && git tag v$(cat VERSION) && git push origin main --tags` +3. On server: `./nextwks.sh --update` + ## Health Check - -After deploy, the script polls `podman exec launcher wget -qO- http://127.0.0.1:9000/health` -up to 10 times (2s interval). Expected response: `OK`. - -## Commit Message Style - -- Imperative mood ("Add", "Fix", "Update", "Bump") -- Reference the component if relevant ("launcher: add health endpoint") -- Keep under 72 chars for the first line +The script polls `podman exec launcher curl -sf http://127.0.0.1:9000/health` up to 15×3s. Launcher container uses `alpine:latest` with `curl` installed at startup via `apk add --no-cache curl`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d85d09..973d6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.1.0.0033 — 2026-07-11 + +### Added +- `tools/firewall-routing.sh` — iptables redirects + VM firewall +- `storage.encryption_key` to Authelia config (required by v4.38+) +- Auto-detection of existing install in `--install` mode + +### Changed +- **Rootless Podman**: all container commands run without sudo +- **Ports**: Caddy binds to 8080/8443, iptables redirects 80/443 +- `.gitignore`: `/nextworkspace` (root-scoped) to track `config/nextworkspace/` +- Configs regenerated on every mode (install/update/destroy) +- `.env` values single-quoted, written via `tee -a` to preserve `$` in bcrypt hashes +- Admin password: now 24 mixed-case alphanumeric chars (base64) +- Containers stopped before binary copy to avoid "Text file busy" +- Firewall rules persisted via `netfilter-persistent save` +- Docs: AGENT.md, README.md fully updated + +### Fixed +- `SSL_ERROR_INTERNAL_ERROR_ALERT` — Authelia now starts with proper config +- Password hash corruption — `$2a$...` no longer mangled by `bash -c` +- "Text file busy" during `--update` — containers stopped before copy +- `--update` skipped config regeneration (now always regenerates) + ## 0.1.0.0032 — 2026-07-11 ### Added diff --git a/README.md b/README.md index fb20dca..6e7bce3 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,22 @@ A self-hosted productivity suite for startups. One binary + Caddy + Authelia. ## Architecture ``` -app.nextwks.eu :443 auth.nextwks.eu :443 - │ │ - Caddy (TLS + forward auth) Caddy → Authelia :9091 - │ │ - ├── /home/ → launcher page └── authelia-api :8080 - ├── /drive/* → OpenCloud :9100 - ├── /office/* → Euro Office :9200 - ├── /erp/* → ERPNext :9300 - ├── /chat/* → Matrix :9400 - ├── /meet/* → Jitsi :9500 - ├── /mail/* → Alps :9600 - ├── /ai/* → Open WebUI :9700 - └── /admin/* → Portainer :9800 +Internet :443 ──iptables──> :8443 ──> Caddy container :443 +Internet :80 ──iptables──> :8080 ──> Caddy container :80 + +Caddy (rootless podman, nextwks-net) + ├── auth.{DOMAIN} ──> Authelia :9091 (internal) + ├── app.{DOMAIN} ──> Launcher :9000 (forward auth via Authelia) + └── www.{DOMAIN} ──> static files + +Authelia :9091 ──> api :8080 (internal) +Launcher :9000 ──> /config, /people, /settings, /health ``` -- **Caddy**: Reverse proxy, TLS (auto LE), subdomain routing, forward auth to Authelia -- **Authelia**: OIDC provider, 2FA, identity store -- **Binary**: Go launcher + path-based reverse proxy to upstream apps +- **Caddy**: TLS termination (ZeroSSL/LE), subdomain routing, forward auth to Authelia +- **Authelia**: OIDC provider, 2FA, identity store, user management API +- **Launcher**: Go binary — app dashboard, people directory, admin panel, settings +- **iptables**: Redirects 80→8080 and 443→8443 so Caddy can run rootless ## Quick Start (Bare VM) diff --git a/VERSION b/VERSION index eeb7893..c6d3c54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0.0032 +0.1.0.0033