Vận hành¶
Cách chạy một deployment LUONVUITUOI-CERT hàng ngày: health probe, log surface, chọn KV backend, thu hồi session, checklist ứng cứu sự cố.
Health probe¶
Trả về {"ok": true} với HTTP 200. Endpoint này:
- Không đọc database.
- Không ghi KV.
- Không chạm rate limiter.
- Không yêu cầu auth.
HEALTHCHECK của Docker, liveness probe của Kubernetes, và load balancer probe đều nên target path này. Các deploy cũ probe POST /api/captcha như health signal nên chuyển — mỗi probe mint một KV entry và đẩy rate-limit bucket.
KV backend¶
KV_BACKEND quyết định state ephemeral sống ở đâu: CAPTCHA challenge, counter rate-limit, mã OTP, hash magic-link, JWT denylist. Chọn một:
| Backend | Khi nào dùng | Multi-worker safe? |
|---|---|---|
local |
Dev local, Docker container đơn | Không — file-lock chỉ scope trong một process. Startup log warning khi WEB_CONCURRENCY > 1. |
upstash |
Bất cứ đâu — khuyến nghị cho production | Có — Redis atomic GETDEL back kv.consume(). |
vercel-kv |
Deploy Vercel | Có — auto-inject qua Vercel KV integration. |
Chạy gunicorn nhiều worker với local, theo dõi log startup:
Hoặc drop về 1 worker (WEB_CONCURRENCY=1), hoặc chuyển sang upstash.
Logs¶
Mọi thứ flow qua module stdlib logging ở WARNING trở lên. Trong Docker, log vào stdout và capture bởi docker logs. Trên Vercel, vercel logs --follow stream.
Message loud đáng alert:
| Logger | Message | Ý nghĩa |
|---|---|---|
luonvuitoi_cert_cli.server.app |
RESEND_API_KEY not set |
Email OTP/magic-link đang bị drop âm thầm. |
luonvuitoi_cert.storage.kv.factory |
KV_BACKEND=local with N workers is unsafe |
Worker concurrent race trên single file KV. |
luonvuitoi_cert.auth.activity_log |
must be https:// |
Ai đó set GSHEET_WEBHOOK_URL sang target không HTTPS; forward disable. |
luonvuitoi_cert.auth.activity_log |
activity log webhook POST failed |
Endpoint GSheet down. Record SQLite local vẫn là chính thức. |
Lỗi đã handle (429 rate-limit, CAPTCHA từ chối, 404 search) không log — là traffic bình thường.
Audit log¶
Action admin lưu trong bảng SQLite admin_activity:
| Cột | Ghi chú |
|---|---|
id |
UUID4 per entry. |
timestamp |
ISO-8601 UTC (YYYY-MM-DDTHH:MM:SSZ). |
user_id / user_email |
Claim JWT sub + email. |
action |
admin.login.success, admin.login.failure, student.update, shipment.upsert, v.v. |
target_id |
Chủ thể thay đổi (ví dụ students:12345). |
metadata |
JSON blob — không bao giờ chứa PII; student.update ghi {column, changed, value_length_delta}, không ghi giá trị cũ/mới thô. |
ip |
Client IP (tôn trọng TRUST_PROXY_HEADERS). |
Nếu GSHEET_WEBHOOK_URL set (và là https://), mỗi entry được POST fire-and-forget lên sheet trên ThreadPoolExecutor(4) bị giới hạn. SQLite write là chính thức — webhook fail không bao giờ break flow admin.
Recipe truy vấn¶
sqlite3 data/portal.db "SELECT timestamp, user_email, action, target_id FROM admin_activity ORDER BY timestamp DESC LIMIT 20;"
# Login fail trong 1 giờ gần nhất:
sqlite3 data/portal.db "SELECT timestamp, user_email, metadata FROM admin_activity WHERE action = 'admin.login.failure' AND timestamp > datetime('now', '-1 hour');"
Thu hồi session¶
Xem admin-auth.md cho flow đầy đủ. Tóm tắt:
POST /api/admin/logoutvới JWT hiện tại của user →jtithêm vào KV denylist với TTL khớp remaining-life.- Bất kỳ endpoint nào truyền
kv=vàoverify_admin_tokensẽ từ chối token. - Denylist tự hết hạn — không cần cron.
Dùng cái này thay vì xoay JWT_SECRET (vô hiệu mọi session cùng lúc và mọi admin phải login lại).
Feature gate cho public surface¶
Hai công tắc super-admin-only điều khiển xem surface công khai có sống hay không. State lưu trong KV nên bật/tắt có hiệu lực ngay, không cần redeploy.
- Công tắc tra cứu — gate
POST /api/search(mode student). Khi tắt, endpoint trả503với{"error": "public lookup is currently disabled by the operator"}. Mode admin (mode=admin, cần JWT) không bị ảnh hưởng để operator vẫn làm việc được trong thời gian đóng băng. - Công tắc tải chứng nhận — gate
POST /api/download(mode student). Khi tắt, endpoint trả503. Mode admin không bị ảnh hưởng.
Ràng buộc: download phụ thuộc lookup. Lookup tắt ⇒ download bị ép tắt theo. Được enforce cả khi ghi (API toggle tự clamp) và khi đọc (gate check cả hai flag).
Cách bật/tắt¶
Login với quyền super-admin → thẻ Public surface hiện ra phía trên form tra cứu → tick checkbox → Save. Checkbox download tự disable khi lookup tắt.
REST tương đương:
curl -X POST https://your.host/api/admin/features/update \
-H 'Content-Type: application/json' \
-d '{"token": "<super-admin JWT>", "lookup_enabled": false, "download_enabled": false}'
Chỉ role super-admin được chấp nhận; admin / viewer trả 403. Mỗi lần bật/tắt đều ghi vào audit log với action admin.features.update và state mới trong metadata.
Mặc định¶
Deploy mới toanh: cả hai đều bật (hành vi như trước khi có gate). KV chỉ được ghi khi super-admin lưu lần đầu — deploy chưa động tới sẽ đọc default baked-in.
Update dependency¶
Dependabot scan hàng tuần (pip) và hàng tháng (github-actions, docker) và mở PR. Review và merge nhanh — reportlab, pypdf, cryptography là target supply-chain.
Backup¶
# Dừng container để snapshot consistent:
docker compose stop
tar czf "backup-$(date +%Y%m%d).tar.gz" data/
docker compose start
Hoặc với container đang chạy, dùng SQLite online backup:
DB là artifact stateful duy nhất — entry KV (counter rate-limit, CAPTCHA challenge) là ephemeral.
Checklist ứng cứu sự cố¶
Session một admin có vẻ bị compromise:
POST /api/admin/logoutvới token của họ (thu hồi JTI).UPDATE admin_users SET is_active = 0 WHERE email = '…';(đai và nịt).- Xoay mật khẩu admin (hoặc reset qua OTP).
- Grep
admin_activitychotarget_idhọ chạm trong window gần đây.
JWT_SECRET leak:
- Tạo secret mới.
- Redeploy với giá trị mới (vô hiệu mọi session).
- Xoay khóa ký QR nếu secret leak qua cùng channel —
private_key.pemlà artifact riêng nhưng thường co-located.