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:
parent
5781b14934
commit
4b85c24e94
@ -56,7 +56,7 @@ function generateInstallName() {
|
|||||||
// Grafana follows Prometheus because its datasource points at it.
|
// Grafana follows Prometheus because its datasource points at it.
|
||||||
const INSTALL_TIERS = [
|
const INSTALL_TIERS = [
|
||||||
['prometheus', 'grafana'],
|
['prometheus', 'grafana'],
|
||||||
['traefik', 'crowdsec']
|
['traefik', 'crowdsec', 'trivy']
|
||||||
];
|
];
|
||||||
|
|
||||||
function sortAppsByTier(apps) {
|
function sortAppsByTier(apps) {
|
||||||
|
|||||||
@ -46,7 +46,9 @@ class SetupWizard {
|
|||||||
{ slug: 'traefik', recommended: true, defaultChecked: true,
|
{ slug: 'traefik', recommended: true, defaultChecked: true,
|
||||||
fallback: { name: 'Traefik', description: 'Reverse proxy + automatic SSL via LetsEncrypt' } },
|
fallback: { name: 'Traefik', description: 'Reverse proxy + automatic SSL via LetsEncrypt' } },
|
||||||
{ slug: 'crowdsec', recommended: true, defaultChecked: true,
|
{ 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)' } }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
containers/trivy/docker-compose.yml
Normal file
43
containers/trivy/docker-compose.yml
Normal 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
|
||||||
61
containers/trivy/trivy.config
Normal file
61
containers/trivy/trivy.config
Normal 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|"
|
||||||
22
containers/trivy/trivy.svg
Normal file
22
containers/trivy/trivy.svg
Normal 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 |
@ -75,17 +75,17 @@ Three candidate shapes were on the table:
|
|||||||
|
|
||||||
| Shape | Verdict |
|
| 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. |
|
| **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:
|
**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.
|
- 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 ephemeral container (same daemon that owns the images), run as `dockerinstall` like every other docker op.
|
- 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. 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.
|
- **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`.
|
- 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.
|
- **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.
|
- **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.
|
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.
|
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.*
|
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.
|
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.
|
6. **P6 — Auto-update policy** (§6), only after P3 has soaked on a real install.
|
||||||
|
|
||||||
|
|||||||
@ -106,6 +106,7 @@ setupWizardTerminal()
|
|||||||
local crowdsec_dashboard="false"
|
local crowdsec_dashboard="false"
|
||||||
isOption " - traefik (reverse proxy, handles LetsEncrypt SSL)"
|
isOption " - traefik (reverse proxy, handles LetsEncrypt SSL)"
|
||||||
isOption " - crowdsec (host-installed intrusion prevention)"
|
isOption " - crowdsec (host-installed intrusion prevention)"
|
||||||
|
isOption " - trivy (scans your apps for known vulnerabilities)"
|
||||||
echo ""
|
echo ""
|
||||||
if [[ ${#domains[@]} -eq 0 ]]; then
|
if [[ ${#domains[@]} -eq 0 ]]; then
|
||||||
isNotice "No domains configured — Traefik has nothing to route. Skipping by default."
|
isNotice "No domains configured — Traefik has nothing to route. Skipping by default."
|
||||||
@ -125,6 +126,9 @@ setupWizardTerminal()
|
|||||||
read -p "" want_console
|
read -p "" want_console
|
||||||
[[ ! "$want_console" =~ ^[nN]$ ]] && crowdsec_dashboard="true"
|
[[ ! "$want_console" =~ ^[nN]$ ]] && crowdsec_dashboard="true"
|
||||||
fi
|
fi
|
||||||
|
isQuestion "Install Trivy (CVE scanner)? (Y/n) : "
|
||||||
|
read -p "" want_trivy
|
||||||
|
[[ ! "$want_trivy" =~ ^[nN]$ ]] && apps+=("trivy")
|
||||||
|
|
||||||
isHeader "Optional Apps"
|
isHeader "Optional Apps"
|
||||||
isQuestion "Install Wireguard (VPN — secure remote access)? (y/N) : "
|
isQuestion "Install Wireguard (VPN — secure remote access)? (y/N) : "
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user