diff --git a/containers/libreportal/backend/routes/setup-routes.js b/containers/libreportal/backend/routes/setup-routes.js index 90ea102..d245e98 100644 --- a/containers/libreportal/backend/routes/setup-routes.js +++ b/containers/libreportal/backend/routes/setup-routes.js @@ -56,7 +56,7 @@ function generateInstallName() { // Grafana follows Prometheus because its datasource points at it. const INSTALL_TIERS = [ ['prometheus', 'grafana'], - ['traefik', 'crowdsec'] + ['traefik', 'crowdsec', 'trivy'] ]; function sortAppsByTier(apps) { diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js index 6e1e75d..a49b063 100755 --- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js +++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js @@ -46,7 +46,9 @@ class SetupWizard { { slug: 'traefik', recommended: true, defaultChecked: true, fallback: { name: 'Traefik', description: 'Reverse proxy + automatic SSL via LetsEncrypt' } }, { slug: 'crowdsec', recommended: true, defaultChecked: true, - fallback: { name: 'CrowdSec', description: 'Host-installed intrusion prevention' } } + fallback: { name: 'CrowdSec', description: 'Host-installed intrusion prevention' } }, + { slug: 'trivy', recommended: true, defaultChecked: true, + fallback: { name: 'Trivy', description: 'Scans your apps for known vulnerabilities (CVEs)' } } ]; } diff --git a/containers/trivy/docker-compose.yml b/containers/trivy/docker-compose.yml new file mode 100644 index 0000000..b275847 --- /dev/null +++ b/containers/trivy/docker-compose.yml @@ -0,0 +1,43 @@ +networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + external: true + +services: + trivy-service: #LIBREPORTAL|SERVICE_TAG_1|trivy-service + container_name: trivy-service + image: aquasec/trivy:latest + restart: unless-stopped + command: server --listen 0.0.0.0:4954 + # GLUETUN_OFF_BEGIN + ports: + - "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1 + # GLUETUN_OFF_END + environment: + - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA + - TRIVY_CACHE_DIR=/cache + labels: + libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA + libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA + traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN + traefik.http.routers.trivy-service.entrypoints: web,websecure + traefik.http.routers.trivy-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 + traefik.http.routers.trivy-service.tls: true + traefik.http.routers.trivy-service.tls.certresolver: production + traefik.http.services.trivy-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 + traefik.http.routers.trivy-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END + traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + healthcheck: + disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA + volumes: + - SOCKET_DATA #LIBREPORTAL|SOCKET_TAG|SOCKET_DATA + - ./trivy-cache:/cache + # GLUETUN_OFF_BEGIN + networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1 + # GLUETUN_OFF_END + # GLUETUN_ON_BEGIN + # network_mode: "container:gluetun-service" + # GLUETUN_ON_END diff --git a/containers/trivy/trivy.config b/containers/trivy/trivy.config new file mode 100644 index 0000000..39de117 --- /dev/null +++ b/containers/trivy/trivy.config @@ -0,0 +1,61 @@ +# +# ============================================================================= +# GENERAL CONFIGURATION +# ============================================================================= +# APP_NAME = name of application for use in scripts +# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is +# BACKUP = if true, include this application in backup operations +# HEALTHCHECK = if true, default docker health checks for that container will be enabled +# AUTHELIA = if true, use Authelia authentication, if false turned off. +# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote +# +CFG_TRIVY_APP_NAME=trivy +CFG_TRIVY_BACKUP=false +CFG_TRIVY_BACKUP_STRATEGY=auto +CFG_TRIVY_COMPOSE_FILE=default +CFG_TRIVY_HEALTHCHECK=true +CFG_TRIVY_AUTHELIA=false +CFG_TRIVY_HEADSCALE=false +# +# ============================================================================= +# METADATA +# ============================================================================= +# CATEGORY = application category for grouping +# TITLE = display name for the application +# DESCRIPTION = short description of the application +# LONG_DESCRIPTION = detailed description of the application +# URL = source repository or documentation URL +# ACTIONS = available actions for this application +# +CFG_TRIVY_CATEGORY="security,recommended" +CFG_TRIVY_TITLE="Trivy" +CFG_TRIVY_DESCRIPTION="Vulnerability scanner" +CFG_TRIVY_LONG_DESCRIPTION="Trivy checks the container images behind your installed apps for known vulnerabilities (CVEs) and powers the Security view in the App Center Overview. Scanning happens entirely on your box against a locally cached database — nothing about your apps is sent anywhere" +CFG_TRIVY_URL="https://github.com/aquasecurity/trivy" +CFG_TRIVY_ACTIONS="configure|install|restart|shutdown|uninstall" +# +# ============================================================================= +# NETWORK CONFIGURATION +# ============================================================================= +# DOMAIN = number of domain from the general config, useful when using multiple domains +# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all +# +CFG_TRIVY_DOMAIN=1 +CFG_TRIVY_WHITELIST=false +CFG_TRIVY_NETWORK=default +# +# ============================================================================= +# PORT CONFIGURATION +# ============================================================================= +# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description +# - app: application name +# - name: service identifier (webui, dns, ssh, etc.) +# - external:internal: port mapping (external can be 'random' for auto-allocation) +# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running) +# - protocol: 'tcp' or 'udp' +# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true) +# - traefik: if true, Traefik handles this port (reverse proxy) +# - webui: if true, this port serves the main web interface +# - description: human-readable description of the service +# +CFG_TRIVY_PORT_1="trivy-service|api|random:4954|private|tcp|false|false|false|Scanner API|" diff --git a/containers/trivy/trivy.svg b/containers/trivy/trivy.svg new file mode 100644 index 0000000..5336737 --- /dev/null +++ b/containers/trivy/trivy.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/roadmap/app-version-updater-and-cve.md b/docs/roadmap/app-version-updater-and-cve.md index c872d5f..1b52634 100644 --- a/docs/roadmap/app-version-updater-and-cve.md +++ b/docs/roadmap/app-version-updater-and-cve.md @@ -75,17 +75,17 @@ Three candidate shapes were on the table: | Shape | Verdict | |---|---| -| **A. Always-on scanner app container** (trivy server / a "security app" in the catalog) | ❌ Resident RAM for a periodic job; permanent docker-socket exposure; duplicates our UI with its own. | +| **A. Scanner as a catalog app** (trivy in the App Center, recommended alongside CrowdSec) | ✅ **Chosen** (2026-07-14). Installing/removing the scanner is the opt-in switch, visible and revertible like every other app; fits "everything is an app"; sits next to CrowdSec as the second security recommendation at first install. | | **B. Scanner binary on the host** | ❌ Grows the host footprint we've deliberately kept lean (rootless + de-sudo); another thing to install/update outside the app model. | -| **C. LibrePortal-orchestrated, ephemeral scanner container** — the scan task does `docker run --rm aquasec/trivy: image --format json ` per installed image | ✅ **Chosen.** No resident process, no host binary, socket exposed only for the seconds a scan runs, rides the existing task/throttle machinery, and the scanner itself is version-pinned like any other image. | +| **C. Invisible ephemeral container** — the scan task does `docker run --rm` per scan, no catalog presence | ❌ Zero idle cost, but the scanner is invisible: no App Center card, no obvious opt-in/uninstall, and users can't see what has socket access. Visibility won. | -So: **system-managed orchestration, containerized execution.** Not its own catalog app, not a host package. +So: **trivy ships as a normal catalog app** (`containers/trivy/`), marked **recommended** and listed in the first-install recommended apps directly under CrowdSec. The updater's CVE step is gated on the app being installed — no trivy app, no scan, honest empty state pointing at the App Center ("Install the security scanner to enable CVE reports"). **Scanner: Trivy** (over grype — both fine, trivy has the larger ecosystem, single pinned OCI image, clean JSON, Apache-2.0). Details: -- Vulnerability DB cached in a named volume (`libreportal-trivy-cache`, ~600 MB on disk); the DB refresh is the only network traffic. -- Image access via the rootless docker socket mounted read-only into the ephemeral container (same daemon that owns the images), run as `dockerinstall` like every other docker op. -- **Privacy posture (worth stating in user-facing copy):** nothing about your images or apps ever leaves the box — Trivy matches locally against a downloaded DB; the only outbound call is the DB fetch from ghcr. Default **on** (it's a security feature and the traffic is one public DB pull), with a single honest switch: `CFG_UPDATER_CVE_SCAN=on|off` (`off` for air-gapped boxes) and `CFG_UPDATER_CVE_INTERVAL` (minutes, **default 1440** — daily; scans are the expensive step, results don't change hourly). Both live in `configs/webui/webui_updater` next to the existing knobs. +- The app runs `trivy server` (its long-running mode): one modest container that owns the vulnerability DB (~600 MB volume) and keeps it fresh. The updater scan executes scans **through the app's container** (`docker exec trivy image --format json `) — no per-scan container spawn, no host binary, and the DB downloads once instead of per run. +- Image access via the rootless docker socket mounted read-only into the trivy container (same daemon that owns the images) — the same trusted position CrowdSec-style security tooling already occupies, but here it's a visible, uninstallable app. +- **Privacy posture (worth stating in user-facing copy):** nothing about your images or apps ever leaves the box — Trivy matches locally against a downloaded DB; the only outbound call is the DB fetch from ghcr. The opt-in is installing the app (recommended, pre-ticked at first install like the other recommendations — skipping it = air-gap-friendly); `CFG_UPDATER_CVE_INTERVAL` (minutes, **default 1440** — daily; scans are the expensive step, results don't change hourly) lives in `configs/webui/webui_updater` next to the existing knobs. - Output maps straight onto the schema the frontend already renders: per app `cves[] { id, severity, package, fixed_in, url }` + global `totals`. Dedupe per image (shared base layers repeat findings), scan each distinct image once per run. **Drop the `[ ! -f ]` guard** so re-scans overwrite `cves.json`. - **Not alarmist** (house rule): the Security tab lists everything, but the per-app chip/badge only fires for **critical/high with a fix available** — "your box has 400 unfixable medium CVEs" red badges are noise, not signal. - **Tie-in with updates:** a CVE whose `fixed_in` is satisfied by the available build marks that update as a *security update* — the Security filter chip and severity sort already exist in the UI, they just start meaning something. @@ -99,7 +99,7 @@ Once detection + pinned apply are trustworthy, add `CFG_UPDATER_AUTO=off|securit 1. **P1 — Pin foundation.** Factor the compose image-line rewrite into a shared helper (artifact op + updater both use it, all `image:` lines not just the first). Pin-on-install + reconcile-on-first-scan. No UI change yet. 2. **P2 — Real detection.** Registry digest compare in `webuiUpdaterScan` behind the new interval knob; `updates.json` gains services/digests; fleet + per-app tabs light up with truthful data. Emit recovery fields while in there. 3. **P3 — Pinned apply/revert.** `updaterApplyApp`/`updaterRollbackApp` rewrite pins as in §4; history carries digest refs; failure path restores pins. *After this, the Update and Roll back buttons are honest.* -4. **P4 — CVE scanner.** Ephemeral trivy runs, `cves.json` for real, guard dropped, totals + severity chips live, security-update tie-in. +4. **P4 — CVE scanner.** The trivy app (catalog + recommended + first-install list) plus the updater's scan step through it; `cves.json` for real, guard dropped, totals + severity chips live, security-update tie-in. The app itself can ship ahead of the rest of P4. 5. **P5 — Polish.** Per-app critical-CVE / update chip on the app header (pattern exists for improvements), remote version-label enrichment, "N services" aggregation in rows. 6. **P6 — Auto-update policy** (§6), only after P3 has soaked on a real install. diff --git a/scripts/checks/first_install.sh b/scripts/checks/first_install.sh index a7503a4..db19b1e 100755 --- a/scripts/checks/first_install.sh +++ b/scripts/checks/first_install.sh @@ -106,6 +106,7 @@ setupWizardTerminal() local crowdsec_dashboard="false" isOption " - traefik (reverse proxy, handles LetsEncrypt SSL)" isOption " - crowdsec (host-installed intrusion prevention)" + isOption " - trivy (scans your apps for known vulnerabilities)" echo "" if [[ ${#domains[@]} -eq 0 ]]; then isNotice "No domains configured — Traefik has nothing to route. Skipping by default." @@ -125,6 +126,9 @@ setupWizardTerminal() read -p "" want_console [[ ! "$want_console" =~ ^[nN]$ ]] && crowdsec_dashboard="true" fi + isQuestion "Install Trivy (CVE scanner)? (Y/n) : " + read -p "" want_trivy + [[ ! "$want_trivy" =~ ^[nN]$ ]] && apps+=("trivy") isHeader "Optional Apps" isQuestion "Install Wireguard (VPN — secure remote access)? (y/N) : "