# MenuKosh — Security Runbook

Operational procedures for credential rotation, incident response, and
backups. This is the "something's wrong, what do I do" document — for normal
deploys see [`DEPLOY.md`](DEPLOY.md), for the full hardening history see the
commit log (`Phase 1` through `Phase 6`).

---

## 1. Rotate `APP_KEY`

**Read [`.env.production.example`](.env.production.example)'s "APP_KEY
ROTATION" section first** — it has the full safe rollout order and explains
exactly what breaks (every session, every `encrypted` column, every signed
URL). The short version:

```bash
# On the server, during a maintenance window:
php artisan key:generate --force
php artisan config:cache
```

**What this logs everyone out of, specifically in this app:**
- Every active session, for every role (customers mid-order excepted — they
  aren't authenticated).
- Every super admin and restaurant admin's **2FA enrollment** —
  `two_factor_secret` and `two_factor_recovery_codes` are `encrypted`/
  `encrypted:array` Eloquent casts, so they become undecryptable the instant
  the key rotates. Affected users will be walked back through `/2fa/setup`
  (mandatory for super admins) the next time they log in — this is expected,
  not a bug. Warn them in advance if you can.

**When to do this:** immediately if `APP_KEY` is confirmed leaked (e.g.
committed to a public repo, shared in a zip/email/chat outside the team).
Otherwise, treat it as a "break glass" procedure, not a routine one.

## 2. Rotate VAPID keys (web push)

```bash
php artisan menukosh:generate-vapid
```

Copy the printed `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` into production's
`.env`, then `php artisan config:cache`. **Every existing push subscription
becomes invalid** — browsers will silently fail to deliver until each
user re-subscribes (they're prompted automatically next time they open a
dashboard with notifications enabled, no manual re-opt-in needed on their
end, but there's a gap until they next visit).

## 3. Rotate Reverb credentials

```bash
php -r "echo 'REVERB_APP_ID='   . random_int(100000, 999999) . PHP_EOL;
        echo 'REVERB_APP_KEY='  . bin2hex(random_bytes(20)) . PHP_EOL;
        echo 'REVERB_APP_SECRET=' . bin2hex(random_bytes(20)) . PHP_EOL;"
```

1. Set the three new values in production `.env`.
2. `php artisan config:cache`.
3. Restart the Reverb daemon: `sudo supervisorctl restart menukosh-reverb`
   (see `DEPLOY.md` Section 5).
4. Re-run `npm run build` — `VITE_REVERB_APP_KEY` is baked into the compiled
   JS at build time, not read at runtime (see `DEPLOY.md` Section 2), so the
   frontend won't pick up the new key until you rebuild and redeploy assets.
5. Every currently-open live-orders dashboard loses its WebSocket connection
   and needs a page refresh to reconnect with the new key — expected, not a
   bug.

**Why rotate:** the current dev keys were shipped inside a zip alongside
`.env` — never reuse them in production. Otherwise, rotate on the same
"break glass" basis as `APP_KEY`.

## 4. Force-logout all users (every role, immediately)

Two options depending on urgency:

**A. Nuclear — every session, right now** (requires `SESSION_DRIVER=database`,
already the production default per `.env.production.example`):

```bash
php artisan tinker --execute="DB::table('sessions')->truncate();"
```

Every logged-in user of every role is signed out on their next request.
Nothing else changes — passwords, 2FA enrollment, etc. are untouched.

**B. Targeted — one user's other sessions:** already built in. A user's own
password change automatically revokes every other session for that account
(`RestaurantAdmin\ProfileController`/`SuperAdmin\SettingController`
`updatePassword`). A super admin can also revoke their own individual
sessions one at a time from **Super Admin → Active Sessions**.

There is currently no UI for a super admin to force-logout a *different*
user's sessions directly — if you need that, option A (all sessions) or
manually deleting that user's rows from the `sessions` table
(`DB::table('sessions')->where('user_id', $id)->delete()`) both work.

## 5. Lock a tenant (suspend a restaurant)

Already built in — **Super Admin → Businesses → \[restaurant\] → toggle
status** (or `POST /super-admin/restaurants/{restaurant}/toggle-status`).
Setting status to `inactive`:

- Blocks the restaurant admin and all their staff from logging in in any
  new way, and blocks the public menu (`Restaurant::where('status', 'active')`
  is the first check in every public menu/order controller).
- Does **not** log out sessions already in progress until they next hit a
  route that re-checks restaurant status (which is effectively immediately —
  `RestaurantActiveMiddleware` runs on every authenticated restaurant-admin
  request). Public/customer routes also re-check on every request.
- Is now logged to `ActivityLog` as `restaurant_status_changed` (Phase 6b),
  visible in **Super Admin → Security Log** (toggle "Show all activity").

## 6. Daily database backup

MenuKosh doesn't ship an automated backup today. Minimum viable version —
`mysqldump` to a local file, encrypted, copied off-server, with rotation.
Adjust paths/credentials for your host; add to root's crontab (`crontab -e`)
or your host's cron-job panel:

```cron
0 3 * * * /var/www/menukosh/scripts/backup-db.sh >> /var/log/menukosh-backup.log 2>&1
```

The script itself is [`scripts/backup-db.sh`](scripts/backup-db.sh) — edit
`DB_NAME` / `DB_USER` / `OFFSITE_DIR` at the top for your environment before
first use.

Set `DB_BACKUP_PASSWORD` in root's crontab environment (`crontab -e`, add
`DB_BACKUP_PASSWORD=...` above the cron line) or a root-only-readable env
file the script sources — never hardcode it in the script itself.

**"Off-server copy" matters more than the schedule** — a backup that only
lives on the same disk as the database doesn't survive that disk failing or
the host being compromised. `OFFSITE_DIR` above should be a genuinely
different machine/provider: an rclone-mounted S3/B2 bucket, a remote synced
via `rsync` over SSH to a different VPS, or your host's managed backup
product if it offers one. Pick one and wire it in before relying on this.

**Test the restore path** at least once — a backup you've never restored
from is a hypothesis, not a backup:

```bash
gunzip -c menukosh-20260101-030000.sql.gz | mysql -u root -p menukosh_restore_test
```

---

## 7. Server-layer checklist (Ubuntu + Apache, VPS)

Documentation only — this is infrastructure configuration outside the Laravel
app itself, so review and apply it on the actual server rather than via a
code change here.

- **Firewall (UFW):** allow only what's needed.
  ```bash
  sudo ufw default deny incoming
  sudo ufw default allow outgoing
  sudo ufw allow 22/tcp     # SSH — see below, prefer a non-default port + key-only
  sudo ufw allow 80/tcp
  sudo ufw allow 443/tcp
  sudo ufw enable
  ```
- **fail2ban** on SSH and Apache auth failures:
  ```bash
  sudo apt install fail2ban
  # /etc/fail2ban/jail.local
  [sshd]
  enabled = true
  [apache-auth]
  enabled = true
  ```
  MenuKosh's own `throttle:login` limiter + progressive account lockout
  (Phase 3) handle *application*-level brute force; fail2ban is the
  *network*-level backstop for SSH and any Apache Basic Auth you might add.
- **SSH key-only login** — disable password auth entirely:
  ```
  # /etc/ssh/sshd_config
  PasswordAuthentication no
  PermitRootLogin prohibit-password   # or "no" if you always sudo from another user
  ```
  Then `sudo systemctl restart sshd` — **test a new SSH session before
  closing your current one**, so you don't lock yourself out.
- **Automatic security updates:**
  ```bash
  sudo apt install unattended-upgrades
  sudo dpkg-reconfigure --priority=low unattended-upgrades
  ```
- **certbot auto-renew** (Let's Encrypt TLS, required before enabling
  `URL::forceScheme('https')` / HSTS — see Phase 5):
  ```bash
  sudo apt install certbot python3-certbot-apache
  sudo certbot --apache -d menukosh.pinakra.com
  sudo certbot renew --dry-run    # confirm the systemd timer/cron is wired correctly
  ```
- **Keep PHP/Apache/MySQL on the distro's supported patch track** — the
  automatic-updates step above covers this for packages installed via `apt`;
  don't hand-compile these from source unless you also own patching them.
