- Make OWASP, Gitleaks, pnpm audit blocking (remove || true fallbacks) - Add Maven -T 1C for parallel reactor threads - Fix parallel Docker build race condition (PID tracking + set -euo pipefail) - Externalize JWT/NextAuth secrets via env vars with dev-only defaults - Add .env.example with generation instructions - Add CI/CD infrastructure review document
10 KiB
CI/CD Infrastructure Review — CannaManage
Date: 2026-06-19
Reviewer: Lumen (Code + Security)
Scope: .gitea/workflows/ci.yml, .gitea/workflows/deploy.yml, Dockerfile.backend, docker-compose*.yml, .snyk
Verdict: ⚠️ Approved with findings — 2 High, 4 Medium, 3 Low
1. Architecture Overview
Push to main
├── CI Pipeline (ci.yml)
│ ├── backend (compile + test + OWASP SCA) ─┐
│ ├── frontend (lint + type-check + pnpm audit) ─┼── parallel
│ ├── secrets-scan (Gitleaks) ─┘
│ └── image-scan (Trivy) ── needs: [backend, frontend]
│
└── Deploy Pipeline (deploy.yml)
└── docker compose build + up + healthcheck
Good: CI jobs are already parallelized. concurrency prevents redundant runs. cancel-in-progress: true on CI is correct (abort stale runs). cancel-in-progress: false on deploy prevents interrupted deployments.
2. Security Findings
❌ HIGH — S-01: Hardcoded JWT Secret in docker-compose.yml
File: docker-compose.yml:34
CANNAMANAGE_SECURITY_JWT_SECRET: hmSULRhmFYcOXDwYxb7bGXp7Bovh+hXgua/VqF44Ts/N+8YELWpWiqQ+aLrymCuM
Risk: This base64-encoded HMAC key is committed to git. Anyone with repo access can forge valid JWTs. If this key is the same as production, it's a full auth bypass.
Mitigation:
- Replace with
${JWT_SECRET}and use.envfile (gitignored) for local dev - Alternatively, generate a random key at container startup for dev-only:
CANNAMANAGE_SECURITY_JWT_SECRET: ${JWT_SECRET:-$(openssl rand -base64 48)} - Verify that production (
docker-compose.prod.yml) uses env vars ✅ — it does use${JWT_SECRET}
Accepted Risk? If this is intentionally a dev-only secret different from production, document it in .snyk and add a comment. Gitleaks should flag this — check if it's being suppressed.
❌ HIGH — S-02: Hardcoded NextAuth Secret in docker-compose.yml
File: docker-compose.yml:54 and docker-compose.truenas.yml:19
NEXTAUTH_SECRET: docker-dev-nextauth-secret-minimum-32chars
AUTH_SECRET: docker-dev-nextauth-secret-minimum-32chars
Risk: Same as S-01. If this secret is reused on the TrueNAS deployment (which is internet-accessible via cannamanage.plate-software.de), session cookies can be forged.
Mitigation:
docker-compose.truenas.ymlshould reference${AUTH_SECRET}from an env file- The word "dev" in the value suggests it's dev-only, but TrueNAS is the production host per
deploy.yml
⚠️ MEDIUM — S-03: OWASP Dependency-Check Always Passes (|| true)
File: ci.yml:44
run: |
./mvnw org.owasp:dependency-check-maven:check \
... || true
Risk: The || true means the step NEVER fails the build, regardless of CVSS score. The failBuildOnCVSS=7 flag is effectively useless.
Mitigation: Remove || true. If you need to allow known issues, use the suppression file that's already configured (-DsuppressionFile=.snyk-maven-suppressions.xml).
⚠️ MEDIUM — S-04: Gitleaks Non-Blocking (echo instead of exit)
File: ci.yml:168
gitleaks detect ... --exit-code 1 \
|| echo "::error::Gitleaks found potential secrets in the repository"
Risk: --exit-code 1 should fail the step, but the || fallback to echo means the step always succeeds. Secrets can be pushed without blocking the pipeline.
Mitigation: Remove the || echo fallback. Let Gitleaks fail the build:
run: gitleaks detect --source . --report-format json --report-path gitleaks-report.json --exit-code 1
⚠️ MEDIUM — S-05: pnpm audit Non-Blocking
File: ci.yml:83
pnpm audit --audit-level=high || echo "::warning::pnpm audit found vulnerabilities"
Risk: High/Critical frontend CVEs produce a warning but don't fail the build.
Mitigation: Remove || echo ... to make High/Critical CVEs block the pipeline. Use .snyk or pnpm audit --ignore-path for documented exceptions.
⚠️ MEDIUM — S-06: Parallel Docker Build Race Condition
File: ci.yml:96-99
docker build -t cannamanage-backend:scan -f Dockerfile.backend . &
docker build -t cannamanage-frontend:scan -f cannamanage-frontend/Dockerfile cannamanage-frontend/ &
wait
Risk: If either docker build fails, wait returns the exit code of the last process that exited, not the first failure. One image could fail silently while the other succeeds.
Mitigation: Use set -euo pipefail and capture PIDs:
run: |
set -euo pipefail
docker build -t cannamanage-backend:scan -f Dockerfile.backend . &
PID1=$!
docker build -t cannamanage-frontend:scan -f cannamanage-frontend/Dockerfile cannamanage-frontend/ &
PID2=$!
wait $PID1 $PID2
With wait $PID1 $PID2, if either fails, the step fails.
ℹ️ LOW — S-07: Trivy Uses curl|sh Install Pattern
File: ci.yml:103
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
Risk: Supply-chain risk — if the upstream script is compromised, arbitrary code runs in CI. Using main branch means no version pinning.
Mitigation: Pin to a specific version tag or commit hash, or use the official Trivy GitHub Action:
- uses: aquasecurity/trivy-action@v0.28.0
with:
image-ref: cannamanage-backend:scan
severity: HIGH,CRITICAL
exit-code: 1
ℹ️ LOW — S-08: Deploy Workflow Uses Hardcoded IP
File: deploy.yml:60, 74
wget -q -O /dev/null http://192.168.188.119:8081/actuator/health
Risk: Low — private IP only reachable on LAN. But if the TrueNAS IP changes (DHCP), deploy silently breaks.
Mitigation: Use localhost or a DNS name (e.g., truenas.local) instead. Or use container-internal checks via docker compose exec.
ℹ️ LOW — S-09: No Image Signing or SBOM
Risk: Docker images are built and deployed without signatures or Software Bill of Materials. Can't verify image integrity post-deployment.
Mitigation (future): Add docker sbom or syft for SBOM generation. Consider cosign for image signing when maturing to multi-env deployments.
3. Code Quality Findings
CI Workflow (ci.yml)
| # | Check | Status | Notes |
|---|---|---|---|
| 1 | Jobs parallelized | ✅ | backend, frontend, secrets-scan run concurrently |
| 2 | Concurrency control | ✅ | Group + cancel-in-progress |
| 3 | Maven caching | ✅ | cache: maven in setup-java |
| 4 | Maven -T 1C |
✅ | Multi-threaded reactor (just added) |
| 5 | Surefire forkCount | ✅ | forkCount=2 in parent POM |
| 6 | Artifact upload | ✅ | Reports preserved on failure (if: always()) |
| 7 | Frontend lockfile | ✅ | --frozen-lockfile prevents silent dep changes |
| 8 | Trivy ignore-unfixed | ✅ | Only actionable vulns fail the build |
| 9 | Security gates blocking | ❌ | All 3 security tools use ` |
| 10 | Docker parallel build | ⚠️ | Race condition on failure (see S-06) |
Deploy Workflow (deploy.yml)
| # | Check | Status | Notes |
|---|---|---|---|
| 1 | No test step | ⚠️ | Comment says "tests remain local-only gate" — acceptable for self-hosted |
| 2 | Health checks | ✅ | 20 retries × 6s for backend, 10 × 5s for frontend |
| 3 | Image pruning | ✅ | Prevents disk exhaustion |
| 4 | set -euo pipefail |
✅ | Fail-fast on errors |
| 5 | Concurrency non-cancelling | ✅ | Prevents interrupted deploys |
Dockerfile.backend
| # | Check | Status | Notes |
|---|---|---|---|
| 1 | Multi-stage build | ✅ | Builder + runtime stages |
| 2 | Non-root user | ✅ | appuser in runtime |
| 3 | Alpine base | ✅ | Minimal attack surface |
| 4 | Layer caching | ✅ | POMs copied before source |
| 5 | No HEALTHCHECK | ⚠️ | Compose defines it, but image itself has no HEALTHCHECK |
| 6 | dependency:go-offline |
✅ | Pre-downloads deps for cache |
| 7 | No COPY of secrets | ✅ | No .env or keys copied |
Docker Compose Files
| # | Check | Status | Notes |
|---|---|---|---|
| 1 | Production uses env vars | ✅ | docker-compose.prod.yml uses ${...} |
| 2 | Resource limits (prod) | ✅ | Memory/CPU limits set |
| 3 | Log rotation (prod) | ✅ | json-file driver with max-size |
| 4 | Dev compose has secrets | ❌ | S-01, S-02 — hardcoded in plain text |
| 5 | Postgres healthcheck | ✅ | pg_isready |
| 6 | Service dependencies | ✅ | condition: service_healthy |
| 7 | Prod binds 127.0.0.1 only | ✅ | Prevents external access without reverse proxy |
| 8 | Dev binds 0.0.0.0 | ⚠️ | ports: "8080:8080" is all-interfaces |
4. Snyk Policy (.snyk)
✅ Well-structured. CSRF ignore is correctly scoped to the JWT API filter chain with clear rationale and expiry date. No concerns.
5. Recommendations (Priority Order)
| Priority | Finding | Action |
|---|---|---|
| 🔴 High | S-01, S-02 | Move secrets to .env (gitignored) or generate at runtime |
| 🟡 Medium | S-03, S-04, S-05 | Remove ` |
| 🟡 Medium | S-06 | Fix parallel Docker build error propagation |
| 🟢 Low | S-07 | Pin Trivy version or use official action |
| 🟢 Low | S-08 | Replace hardcoded IP with DNS/localhost |
| 🟢 Low | S-09 | Add SBOM generation (future) |
6. Summary
The CI/CD setup is architecturally sound — parallel jobs, proper caching, multi-stage Docker builds, non-root containers, and comprehensive security scanning (OWASP, Trivy, Gitleaks, pnpm audit).
The main weakness is that all security gates are non-blocking (|| true everywhere), which means the scanning tools generate reports but never actually prevent a vulnerable build from deploying. This is the single most impactful fix: remove the fallbacks and let CVEs/secrets block the pipeline.
The hardcoded secrets in docker-compose.yml are a moderate risk — they're clearly dev-only values different from production, but the TrueNAS deployment (which is internet-facing) reuses the same NEXTAUTH_SECRET, which should be fixed.