# Production environment audit

**Audited:** 2026-05-08 · **Host:** `root@152.42.223.228` (`mathapi.allgifted.com`, hostname `Math-2025`, Ubuntu 24.04 / kernel 6.8.0-71) · **Web stack:** Apache 2 + mod_php (no PHP-FPM) · **PHP:** 8.2.29 · **Laravel root:** `/var/www/html/mathapi/` · **Read-only.** Nothing on the prod server modified.

**Operational summary:** Phase 0 hardening **has not been deployed to production.** The live `.env` has `APP_ENV=local`, `APP_DEBUG=true`, `SESSION_SECURE_COOKIE=false`, no `CORS_ALLOWED_ORIGINS`, and every secret listed in the Phase 0 "rotate" list is still the leaked git-history value.

---

## §0 — File inventory at `/var/www/html/mathapi/`

| File | Size | Owner | Mode | Status |
|---|---|---|---|---|
| `.env` | 1735 B | `root:root` | `644` | **Active.** Read by mod_php on every request. World-readable — see §4. |
| `.env-20251118` | 1612 B | `www-data:www-data` | `755` | Pre-Anthropic snapshot from 2025-11-18; missing `ANTHROPIC_API_KEY`, has typo `TRIPE_PREMIUM_ANNUAL_PRICE_ID`. **Stale.** |
| `.envy` | 1173 B | `root:root` | `644` | Older snapshot from 2025-11-18; no Stripe block at all. **Stale.** |
| `.auth0.api.json` | 394 B | `root:root` | `644` | Auth0 leftover (Phase 0 deleted these locally; prod still has them). |
| `.auth0.app.json` | 5264 B | `root:root` | `644` | Auth0 leftover. |
| `auth0.exe` | 47.6 MB | `root:root` | `644` | Auth0 CLI Windows binary still on prod disk. |
| `api-backup-2025-09-30.sql` | 13.2 MB | `root:root` | `644` | Old DB dump on disk (also flagged for git history scrub). |
| `resources/views/admin/questions/mathapi11v2/` | tree | `www-data:www-data` | – | Duplicate-of-project subdirectory containing six SQL dumps (~80 MB total), `.auth0.app.json`, `.env.dev`, `.env.example`, `.env.save`, `.env.save.1`. |

The two stray env-like files in the project root (`.env-20251118`, `.envy`) and the four under `resources/views/...` are all out-of-date snapshots; they're not loaded by Laravel (only `/var/www/html/mathapi/.env` is, since `APP_ENV=local` doesn't trigger `.env.local` lookup either) but they're world-readable secrets at rest and should be removed.

---

## §1 — Active prod `.env` — every key, classified

For each key, "Status" is one of: **OK** (matches canonical, prod-appropriate value) · **Phase 0** (needs the specific Phase 0 hardening value) · **Leaked** (value matches what's in git history, rotation needed) · **Unsafe** (other issue).

Secret values are **not** echoed below per the brief — the audit confirmed each leaked secret matches the value flagged in Phase 0's CHANGES.md and `.env.production` history at commit `ff91d0a`.

| Key | In canonical? | Current value | Status | Action |
|---|---|---|---|---|
| `APP_NAME` | yes | `Laravel` | Unsafe (default scaffold value) | Set to `"AllGifted Math API"` (cosmetic, low priority) |
| `APP_ENV` | yes | `local` | **Phase 0** | Set to `production` |
| `APP_DEBUG` | yes | `true` | **Phase 0 — CRITICAL** | Set to `false`. While `true`, any unhandled exception leaks DB credentials, env values, stack traces, and request data. |
| `APP_URL` | yes | `https://mathapi.allgifted.com` | OK | – |
| `APP_KEY` | yes | (set, base64 32 bytes) | Unsafe | Not in the leaked git copies (prod's specific value differs from `.env.production` and from local dev), but the file has been world-readable on prod for the entire deployment window. **Rotate as hygiene.** Note: rotating invalidates all encrypted session cookies, queued payloads, and old API tokens — schedule during a low-traffic window. |
| `APP_CIPHER` | yes | `AES-256-CBC` | OK | – |
| `LOG_CHANNEL` | yes | `stack` | OK | – |
| `DB_CONNECTION` | yes | `mysql` | OK | – |
| `DB_HOST` | yes | `localhost` | OK | – |
| `DB_PORT` | yes | `3306` | OK | – |
| `DB_DATABASE` | yes | `api` | OK | – |
| `DB_USERNAME` | yes | `root` | Unsafe | MySQL `root` is over-privileged for app access. Phase 1.5 housekeeping: create a dedicated `mathapi` user with grants only on `api.*`. Out of this immediate rotation scope but flag. |
| `DB_PASSWORD` | yes | (currently set; matches the value leaked in git at commit `ff91d0a` — `.env.production`) | **Leaked — rotation needed** | Rotate MySQL `root` password (or migrate to a least-privilege user per the row above and rotate that). |
| `BROADCAST_DRIVER` | yes | `log` | OK | – |
| `CACHE_DRIVER` | yes | `file` | OK (Phase 1.5 wants Redis for shared OTP lockout state across nodes) | – |
| `FILESYSTEM_DRIVER` | yes | `local` | OK | – |
| `QUEUE_CONNECTION` | yes | `database` | OK | – |
| `SESSION_DRIVER` | yes | `cookie` | OK | – |
| `SESSION_LIFETIME` | yes | `120` | OK | – |
| `SANCTUM_STATEFUL_DOMAINS` | yes | `localhost,localhost:3000,localhost:4200,127.0.0.1,127.0.0.1:8000` | **Phase 0** | Set to `mathapi.allgifted.com,allgifted.com` (or whatever the Flutter Web SPA origin is). Currently no prod origins listed → SPA cookie auth would never work in prod. |
| `SANCTUM_TOKEN_PREFIX` | yes | (empty) | OK | – |
| `SESSION_SECURE_COOKIE` | yes | `false` | **Phase 0** | Set to `true`. Required for HTTPS prod; `false` allows session cookies to be sent over plain HTTP if any path bypasses TLS termination. |
| `SESSION_SAME_SITE` | yes | `lax` | OK | – |
| `REDIS_HOST` | yes | `localhost` | OK (no Redis running, but the value is fine) | – |
| `REDIS_PASSWORD` | yes | `null` | OK | – |
| `REDIS_PORT` | yes | `6379` | OK | – |
| `MAIL_MAILER` | yes | `smtp` | OK | – |
| `MAIL_HOST` | yes | `smtp.gmail.com` | OK | – |
| `MAIL_PORT` | yes | `587` | OK | – |
| `MAIL_USERNAME` | yes | `ags2025mail@gmail.com` | OK (non-secret address) | – |
| `MAIL_PASSWORD` | yes | (currently set) | Leaked-on-prod (file world-readable; not in git history because the live account differs from the one in `.env.production`'s `ags2019m@gmail.com`) | **Rotate** (Google Account → Security → App Passwords → revoke + regenerate). |
| `MAIL_ENCRYPTION` | yes | `tls` | OK | – |
| `MAIL_FROM_ADDRESS` | yes | `ags2025mail@gmail.com` | OK | – |
| `MAIL_FROM_NAME` | yes | `"AllGifted OTP"` | OK | – |
| `TWILIO_SID` | yes | `<redacted; rotated 2026-05-08; see Phase 0 rotation list>` | Leaked (account SID is identifying info; not strictly a secret) | Will rotate alongside the auth token. |
| `TWILIO_AUTH_TOKEN` | yes | (currently set; matches the value leaked in git) | **Leaked — rotation needed** | Twilio dashboard → Account → API Keys & Tokens → reset. |
| `TWILIO_FROM` | yes | `+16288886072` | OK (public phone number) | – |
| `OPENAI_API_KEY` | yes | (currently set; matches the value leaked in git) | **Leaked — rotation needed** | OpenAI dashboard → API keys → revoke. Note: codebase is mid-migration to Anthropic; consider whether OpenAI is still needed before regenerating. |
| `STRIPE_PREMIUM_ANNUAL_PRICE_ID` | yes | `price_1SUckODEJ0zFqKlFXnZXeSvz` | OK (public Stripe price ID) | – |
| `STRIPE_PREMIUM_MONTHLY_PRICE_ID` | yes | `price_1SUcjADEJ0zFqKlFCZMAnGb3` | OK (public price ID) | – |
| `STRIPE_KEY` | yes | (test publishable key, `pk_test_…`) | OK (publishable keys are designed to be public) | – |
| `STRIPE_WEBHOOK_SECRET` | yes | (currently set; matches the value leaked in git) | **Leaked — rotation needed** | Stripe dashboard → Developers → Webhooks → roll signing secret. Note: still won't matter until the BE3 webhook signature verification is re-enabled (Phase 1D), but rotate now to clear the leak. |
| `STRIPE_SECRET_KEY` | yes | (test secret key, `sk_test_…`; matches the value leaked in git AND was hardcoded in `app/Services/LivesPurchaseService.php:20` per Phase 0) | **Leaked — rotation needed (hygiene)** | Stripe dashboard → Developers → API keys → roll. Test mode keys cannot move money, but rotate to clear the leak and avoid confusion. |
| `ANTHROPIC_API_KEY` | yes | (currently set; matches the value leaked in git) | **Leaked — rotation needed** | Anthropic console → Settings → API Keys → revoke. |

**Phase 0 hardening status on prod:** **0 of 4 applied.** `APP_ENV`, `APP_DEBUG`, `SESSION_SECURE_COOKIE`, `CORS_ALLOWED_ORIGINS` all need adjusting.

---

## §2 — Keys in canonical `.env.example` MISSING from prod

| Key | Value to set on prod | Why |
|---|---|---|
| `CORS_ALLOWED_ORIGINS` | `https://allgifted.com,https://mathapi.allgifted.com` | Phase 0 changed `config/cors.php` to read this env var. With it absent, `config/cors.php` falls back to a default that includes `http://localhost:3000` — that's a hole on prod (any localhost-running attacker page on a victim's machine can hit prod with credentials). Set explicit list, **drop the `http://localhost:3000` entry from the default** for prod. |
| `SIMBA_API_BASE_URL` | (empty) | Placeholder for the eventual SIMBA telco partner integration (Phase 1E). Add empty so the var is reserved; not consumed yet. |
| `SIMBA_API_KEY` | (empty) | Same. |

Adding `CORS_ALLOWED_ORIGINS` is the only critical one of the three. The two SIMBA keys are placeholders.

---

## §3 — Items in prod NOT in canonical (deletion list)

### 3a — Stray env files in the Laravel root

Active is only `.env`. The other two are stale snapshots:

| Path | Why it's safe to remove |
|---|---|
| `/var/www/html/mathapi/.env-20251118` | 2025-11-18 backup. Missing `ANTHROPIC_API_KEY`. Has a typo `TRIPE_PREMIUM_ANNUAL_PRICE_ID` that never worked. Diff vs active confirms it predates the Anthropic migration. |
| `/var/www/html/mathapi/.envy` | 2025-11-18 root-owned snapshot. No Stripe block, no Anthropic. Even older. |

### 3b — The `resources/views/admin/questions/mathapi11v2/` duplicate tree

Mirrored from the local repo (Phase 0 flagged it for git history scrub but the prod files are independent of git scrub). Contains:
- `.auth0.api.json`, `.auth0.app.json`
- `.env.dev` (570 B — placeholder values like `AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN`)
- `.env.example` (1075 B — stale)
- `.env.save` (455 B — old DB password)
- `.env.save.1` (604 B — older Auth0 client secret)
- `Host`, `artisan` (stub copies of project files)
- `20201006.sql`, `20230608.sql`, `20240615.sql`, `20250824.sql`, `20250930.sql`, `20251002.sql` — six DB dumps totalling ~80 MB

The whole subtree should be deleted. **Caveat: the path falls inside the Laravel `resources/views/` Blade view directory.** Confirm no Blade template references this subdirectory before removal (it shouldn't — these aren't `.blade.php` files — but verify with `grep -r "questions/mathapi11v2" /var/www/html/mathapi/resources/views/ /var/www/html/mathapi/app/`).

### 3c — Auth0 dead artefacts in the Laravel root

Phase 0 confirmed these are unused: `auth0.exe` (47.6 MB), `.auth0.api.json`, `.auth0.app.json`, `populate_videos.php`, `test.php`, `gulpfile.js`, plus the bizarrely-named files `Host`, `origin)`, `files('favicons')`, `allFiles(favicons))'`, and the shell-paste-as-filename `er@DESKTOP-PNL3EFM.(none) Date Sun Aug 24 ...`. All flagged for deletion in the Phase 0 follow-ups list.

### 3d — Old DB backup at root

`/var/www/html/mathapi/api-backup-2025-09-30.sql` (13.2 MB). Move to a non-web-served location for archival, or delete after verifying the production DB is healthy.

### 3e — Permissions concern on the active `.env`

Currently `-rw-r--r-- root:root`. Anyone with shell access (or any path-traversal bug — and there's a public `/setup-storage` and `/media/{path}` route on this codebase) can read all secrets. After rotation, tighten to `-rw-r----- root:www-data` (`640`) so only the web user can read it.

---

## §4 — Concrete edit plan

> **Order matters.** Rotate the secrets in provider dashboards FIRST (the operator does this manually). Then update `.env` with the new values. Until rotation lands, the prod env still holds leaked values — do not push the env update before the new credentials exist.

### Step 0 — Backup

```bash
ssh root@152.42.223.228
cd /var/www/html/mathapi
cp .env .env.backup-$(date +%Y%m%d)
chmod 600 .env.backup-$(date +%Y%m%d)
```

The backup carries the leaked credentials, so lock it down to `600` and delete after the rotation confirms working.

### Step 1 — Rotate in provider dashboards (operator action)

| Provider | Action | Output to capture |
|---|---|---|
| Stripe (live mode if used) | Roll secret key + webhook signing secret | `sk_live_…` (or `sk_test_…` if still in test), `whsec_…` |
| Twilio | Reset auth token | new auth token |
| OpenAI | Revoke + create new key | `sk-proj-…` |
| Anthropic | Revoke + create new key | `sk-ant-api03-…` |
| Google | Revoke + create new App Password for `ags2025mail@gmail.com` | 16-char password |
| MySQL | `ALTER USER 'root'@'localhost' IDENTIFIED BY 'NEW_PASSWORD';` (run as root over local socket on prod) | new password |
| Auth0 | Decommission tenant `pamelalim.auth0.com` and the secondary `allgiftedllc.au.auth0.com` if present (per Phase 0 follow-up) | n/a |

### Step 2 — Edit `/var/www/html/mathapi/.env`

Hand-edit with `nano` is safer than `sed` for arbitrary password values (special characters in passwords break sed regex). Use `nano` for the secret rotations and `sed` only for the deterministic Phase 0 hardening flips:

```bash
# Phase 0 hardening — deterministic flips, sed-safe
sed -i 's|^APP_ENV=.*|APP_ENV=production|' /var/www/html/mathapi/.env
sed -i 's|^APP_DEBUG=.*|APP_DEBUG=false|' /var/www/html/mathapi/.env
sed -i 's|^SESSION_SECURE_COOKIE=.*|SESSION_SECURE_COOKIE=true|' /var/www/html/mathapi/.env
sed -i 's|^SANCTUM_STATEFUL_DOMAINS=.*|SANCTUM_STATEFUL_DOMAINS=mathapi.allgifted.com,allgifted.com|' /var/www/html/mathapi/.env

# Add the missing CORS key. The other two (SIMBA_*) are placeholders; add for parity.
cat >> /var/www/html/mathapi/.env <<'EOF'

CORS_ALLOWED_ORIGINS=https://allgifted.com,https://mathapi.allgifted.com

SIMBA_API_BASE_URL=
SIMBA_API_KEY=
EOF

# Cosmetic
sed -i 's|^APP_NAME=.*|APP_NAME="AllGifted Math API"|' /var/www/html/mathapi/.env

# Now hand-edit the rotated secrets:
nano /var/www/html/mathapi/.env
# Replace these eight values with the new credentials from Step 1:
#   APP_KEY=
#   DB_PASSWORD=
#   MAIL_PASSWORD=
#   TWILIO_AUTH_TOKEN=
#   OPENAI_API_KEY=
#   ANTHROPIC_API_KEY=
#   STRIPE_WEBHOOK_SECRET=
#   STRIPE_SECRET_KEY=
```

For `APP_KEY` rotation specifically, generate the new key on prod with:
```bash
php /var/www/html/mathapi/artisan key:generate --show
```
Copy the output into the `APP_KEY=` line. Note: rotating `APP_KEY` invalidates all encrypted cookies and queued jobs. Schedule during a low-traffic window; flush the queue first if any jobs are encrypted-payload sensitive.

### Step 3 — Tighten permissions

```bash
chown root:www-data /var/www/html/mathapi/.env
chmod 640 /var/www/html/mathapi/.env
```

Web requests run under `www-data` via mod_php; `640` lets Apache read while blocking everyone else. Verify the web app can still load right after with the curl in Step 5.

### Step 4 — Apply

```bash
cd /var/www/html/mathapi

# Drop any cached config so the new values are picked up.
php artisan config:clear

# Restart Apache so mod_php workers re-read /var/www/html/mathapi/.env.
systemctl restart apache2

# Optional after smoke-test passes: re-cache for performance.
# php artisan config:cache
```

Note: this stack uses Apache + mod_php. There is **no PHP-FPM** running (`systemctl list-unit-files | grep -i fpm` returns nothing; `apache2ctl -M` shows `php_module (shared)`). Do **not** run `systemctl restart phpX-fpm` — it doesn't exist here.

### Step 5 — Verify

```bash
# 1. Health endpoint returns 200 plain (not Whoops HTML).
curl -s -o /dev/null -w "%{http_code}\n" https://mathapi.allgifted.com/up
# expected: 200

# 2. APP_DEBUG is off — trigger a 404 and check we get a short JSON, not a stack trace.
curl -s https://mathapi.allgifted.com/api/this-route-does-not-exist | head -c 500
# expected: small JSON like {"message":"Not Found"} — NOT an HTML page with file paths.

# 3. CORS lockdown — request from a disallowed Origin should not echo it back.
curl -sI -X OPTIONS \
  -H "Origin: https://evil.example.com" \
  -H "Access-Control-Request-Method: POST" \
  https://mathapi.allgifted.com/api/auth/request-otp \
  | grep -i "access-control-allow-origin"
# expected: header absent OR set to one of the configured origins, NEVER "https://evil.example.com".

# 4. CORS allowed origin works.
curl -sI -X OPTIONS \
  -H "Origin: https://mathapi.allgifted.com" \
  -H "Access-Control-Request-Method: POST" \
  https://mathapi.allgifted.com/api/auth/request-otp \
  | grep -i "access-control-allow-origin"
# expected: Access-Control-Allow-Origin: https://mathapi.allgifted.com

# 5. Real OTP flow (smoke).
curl -s -X POST https://mathapi.allgifted.com/api/auth/request-otp \
  -H "Content-Type: application/json" \
  -d '{"contact":"+6500000000"}'
# expected: 422 (validation reject — no real number) or 200 with masked phone hint.
# NOT a stack trace, NOT 500.
```

### Step 6 — Cleanup (after verification proves healthy)

```bash
# Stale env snapshots
rm /var/www/html/mathapi/.env-20251118
rm /var/www/html/mathapi/.envy

# Auth0 leftovers
rm /var/www/html/mathapi/.auth0.api.json
rm /var/www/html/mathapi/.auth0.app.json
rm /var/www/html/mathapi/auth0.exe

# Old DB dump (move first if archival desired)
rm /var/www/html/mathapi/api-backup-2025-09-30.sql

# Duplicate tree — confirm no Blade refs first
grep -r "questions/mathapi11v2" /var/www/html/mathapi/resources/views/ /var/www/html/mathapi/app/ 2>/dev/null
# If empty, safe:
rm -rf /var/www/html/mathapi/resources/views/admin/questions/mathapi11v2/

# Other Phase 0 root-level junk
rm /var/www/html/mathapi/Host
rm "/var/www/html/mathapi/origin)"
rm "/var/www/html/mathapi/files('favicons')"
rm "/var/www/html/mathapi/allFiles(favicons))'"
rm "/var/www/html/mathapi/files(favicons))'"
rm "/var/www/html/mathapi/er@DESKTOP-PNL3EFM.(none) Date   Sun Aug 24 185503 2025 +1000      update all to server"

# After everything verified working for 24h:
rm /var/www/html/mathapi/.env.backup-*  # backups still carry leaked values
```

### Step 7 — Lock the door behind us

After successful rotation:
- Confirm the rotated values in providers cannot be looked up from the old leak (verify by attempting a Stripe API call with the OLD `sk_test_…` — should 401).
- Schedule the `git filter-repo` history scrub from Phase 0's operational follow-ups (separate task; affects all clones).
- Roll out Phase 0/1A/1B code to prod (the new `.env.example` and `.gitignore` are already on prod's filesystem if rsync'd; the rotation here covers the *runtime* env that Apache reads).

---

## §5 — What this audit does NOT cover

- Database `users.auth0` column drop (Phase 1 housekeeping migration).
- Git history scrub (`git filter-repo` for `.env.production` at commit `ff91d0a`, `auth0.exe`, the SQL dumps, and the duplicate tree).
- Hardcoded Stripe key in `app/Services/LivesPurchaseService.php:20` — the local Phase 0 patch fixed this in source but the prod copy of the file may still have it; verify after deploying Phase 0 code to prod.
- Stripe webhook signature verification re-enable (Phase 1D).
- Apache vhost config / TLS cert audit (separate scope).

The above are **not** preconditions for the rotation itself — start the rotation; the rest follows.
