feat(trivy): add Trivy as a recommended security app

New containers/trivy app (aquasec/trivy in server mode, private API port,
docker socket + cache volume) in the security,recommended categories, plus
placement in the setup wizard recommended step, the server-side install
tier after crowdsec, and the CLI first-install prompt. The updater's CVE
scan (design doc P4) will gate on this app being installed; §5 of the
design doc updated to record the app-based decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-07-14 21:46:37 +01:00
parent 5781b14934
commit 4b85c24e94
7 changed files with 141 additions and 9 deletions

View File

@ -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) {

View File

@ -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)' } }
];
}

View File

@ -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

View File

@ -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|"

View File

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
<!-- Shield silhouette -->
<path d="M64 8 L112 24 V60 C112 86 92 110 64 120 C36 110 16 86 16 60 V24 Z"
fill="url(#tv-grad)" stroke="#5dd3ff" stroke-width="2.5" stroke-linejoin="round"/>
<defs>
<linearGradient id="tv-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#1a2b3d"/>
<stop offset="1" stop-color="#0a1420"/>
</linearGradient>
<radialGradient id="tv-lens" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#0f2334"/>
<stop offset="1" stop-color="#12314a"/>
</radialGradient>
</defs>
<!-- Magnifier lens over the shield -->
<circle cx="56" cy="54" r="22" fill="url(#tv-lens)" stroke="#5dd3ff" stroke-width="5"/>
<!-- Handle -->
<path d="M72 70 L90 88" stroke="#5dd3ff" stroke-width="8" stroke-linecap="round"/>
<!-- Bug dot found under the lens -->
<circle cx="56" cy="54" r="6" fill="#ff5d7a"/>
<path d="M56 44 V38 M56 64 V70 M46 54 H40 M66 54 H72" stroke="#ff5d7a" stroke-width="2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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:<pinned> image --format json <image>` 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> trivy image --format json <image>`) — 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.

View File

@ -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) : "