sciagent code + Gitea Actions CI/CD
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thinh Lam
2026-06-30 09:38:30 +07:00
commit 688fac73e9
1167 changed files with 158244 additions and 0 deletions
@@ -0,0 +1,173 @@
# Security incident — rcc-ump.com (2026-05-27)
**Status:** Remediation in progress (code fixes tracked below)
**Scope:** Production exposure at `https://www.rcc-ump.com` / `https://rcc-ump.com`
**Related prior audit:** [assets/docs/2026-05-21-security-review.md](../assets/docs/2026-05-21-security-review.md)
---
## Executive summary
Public screenshots and `curl` tests show the production site was serving the **Vite development server**, not a built SPA. That exposes full TypeScript source, `import.meta.env` values (including internal Docker hostnames), stack traces, and HMR internals. Combined with backend misconfiguration (default JWT secret, unauthenticated API routes), this created a path from **reconnaissance → data theft → admin takeover → arbitrary file write**.
Treat the VPS as **compromised until forensics prove otherwise**. Rotate credentials and redeploy with the fixes in this document before bringing the site back.
---
## Evidence (what attackers saw)
| Observation | Confirms |
|---|---|
| DevTools Sources tree shows `@vite/client`, `@react-refresh`, `node_modules`, `/src/**` | Vite **dev** server on the public internet |
| `curl …/src/shared/api/client.ts` returns source with `DEV: true`, `VITE_DEV_PROXY_TARGET: http://be0:4402` | Env + internal service names leaked |
| `curl …/vite.config.ts` returns HTML error with full config + stack trace | Verbose dev error handling |
| `lovable-tagger` in plugin list | Dev-only tooling active |
**Root cause in repo:** `docker-compose.prod.yml` and `fe0/Dockerfile` run `npm run dev -- --host 0.0.0.0`.
---
## Findings → fixes (checklist)
Track implementation in git; check off after deploy to production.
### Step 0 — Incident response (ops, not code)
- [ ] Restrict public access (maintenance page / firewall) during remediation
- [ ] Rotate **Postgres**, **MinIO**, **SMTP**, and generate new **`JWT_SECRET`** (`openssl rand -base64 48`)
- [ ] Bump every user's `credential_version` in Postgres (invalidates old JWTs)
- [ ] Review `audit_events`, unknown admin users, MinIO objects, modified files under `./be0` / `./fe0`
- [ ] Bind MinIO console to localhost; do not expose `:9001` to the internet
- [ ] Purge `.env` from git history if the repo was ever shared (`git filter-repo`)
### Step 1 — JWT and production mode ✅ (code)
| ID | Finding | Fix | Status |
|---|---|---|---|
| C-1 | `JWT_SECRET` unset → dev fallback signs tokens | `JWT_SECRET` + `ENVIRONMENT=production` in `docker-compose.prod.yml`; `verify-prod-env.sh` | **Done** |
| — | `ENVIRONMENT` never set in prod | Pass `ENVIRONMENT=production` to `be0` | **Done** |
### Step 2 — Production frontend (stop source leak) ✅ (code)
| ID | Finding | Fix | Status |
|---|---|---|---|
| H-3 | Vite dev in production | `fe0/Dockerfile.prod` + `fe0/nginx/default.conf`; updated `docker-compose.prod.yml` | **Done** |
| — | Prod API URL pointed at localhost | Same-origin `/api` via nginx when `VITE_API_URL` unset | **Done** |
### Step 3 — Remove broken / dangerous endpoints ✅ (code)
| ID | Finding | Fix | Status |
|---|---|---|---|
| C-3 | `POST /upload_document` | **Removed** | **Done** |
| M-9 | `POST /get_page` | **Removed** | **Done** |
### Step 4 — Authenticate sensitive API routes ✅ (code)
| ID | Finding | Fix | Status |
|---|---|---|---|
| C-4 | `/api/v1/review-documents` CRUD | Login + owner/staff | **Done** |
| C-5 | `GET /api/applications` | Staff-only | **Done** |
| C-5 | `GET /api/applications/{id}` | Owner or staff | **Done** |
| H-1 | LLM / chat / ideas endpoints | Auth on all listed routes | **Done** |
| H-4 | `DELETE …/admin-result` auth order | Auth first | **Done** |
### Step 5 — Hardening ✅ (code)
| ID | Finding | Fix | Status |
|---|---|---|---|
| H-5 | MinIO CORS `*` default | Required `MINIO_API_CORS_ALLOW_ORIGIN`; console on localhost | **Done** |
| H-7 | No login rate limit | `allow_login()` | **Done** |
| M-2 | No security headers | Middleware + nginx | **Done** |
| M-3 | CORS `*` risk | Fail startup if `*` in origins | **Done** |
**Deploy:** Rebuild `fe0` with `Dockerfile.prod`. Confirm DevTools no longer shows `@vite/client` or `/src/`. Replace placeholder `fe0/public/logo.svg` with your institution logo if needed.
**Deploy (Steps 15):** Update `.env` (see below), run `./scripts/verify-prod-env.sh`, then:
```bash
docker compose --env-file .env -f docker-compose.prod.yml up -d --build
```
Recreate `be0` after setting `JWT_SECRET` and bump all users' `credential_version` in Postgres.
- Non-root Docker users; remove prod bind-mounts of `./be0` / `./fe0` source
- HttpOnly refresh tokens; shorten JWT TTL
- Upgrade `xlsx`; pin `pip install` at image build time
- Auth audit test: every mutating route must have auth dependency
- Add `SECURITY.md` disclosure policy
---
## Production architecture (target)
```
Browser (HTTPS, external nginx/Caddy on VPS)
├─► fe0 container (nginx :8080) ── static files from dist/
│ proxy /api/* ──► be0:4402 (Docker network)
│ proxy /submitted-initiatives/ ──► be0:4402
├─► be0 bound 127.0.0.1:4402 on host (not public)
├─► postgres bound 127.0.0.1:15432
└─► MinIO API (TLS via reverse proxy); console localhost-only
```
External TLS termination (Certbot/Caddy on the VPS) sits in front of `${FE_PORT}`. See [deploy-production-docker.md](./deploy-production-docker.md) and [minio-behind-https.md](./minio-behind-https.md).
---
## Verification after deploy
### Automated tests (run before deploy)
```bash
# Backend — includes 17 security regression tests
cd be0 && python -m pytest tests/ -q
# Frontend unit tests + env config
cd fe0 && npm test && npm run build
# Production .env validation script
./scripts/test-verify-prod-env.sh
```
### Production smoke checks
```bash
# 1. Env validation
./scripts/verify-prod-env.sh
# 2. Stack healthy
docker compose --env-file .env -f docker-compose.prod.yml ps
# 3. No Vite dev artifacts (expect 404, not TS source)
curl -sS -o /dev/null -w "%{http_code}\n" https://www.rcc-ump.com/src/main.tsx
# 4. Unauthenticated PII blocked (expect 401)
curl -sS -o /dev/null -w "%{http_code}\n" https://www.rcc-ump.com/api/applications
# 5. JWT not forgeable — login with real user; admin routes reject unsigned tokens
curl -sS -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer invalid" \
https://www.rcc-ump.com/api/v1/admin/audit-events
```
---
## `.env` additions required for production
```bash
# Generate once:
JWT_SECRET=$(openssl rand -base64 48)
# Restrict MinIO browser CORS to your SPA origin (scheme + host, no trailing slash):
MINIO_API_CORS_ALLOW_ORIGIN=https://www.rcc-ump.com
# Public app URL (emails, CORS extras):
AUTH_PUBLIC_WEB_ORIGIN=https://www.rcc-ump.com
CORS_ORIGINS_EXTRA=https://www.rcc-ump.com
```
After first deploy with `JWT_SECRET`, run SQL (or admin script) to increment `credential_version` for all users.
---
*Last updated: 2026-05-27 — update checkboxes as fixes land on `main` and production.*