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 trỏ vào path này. Các deploy cũ vốn probe POST /api/captcha như một tín hiệu health thì nên chuyển sang đây, vì mỗi lần probe sẽ tạo một KV entry và làm đầ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, một Docker container đơn lẻ | Không: file-lock chỉ có phạm vi trong một process. Startup log cảnh báo khi WEB_CONCURRENCY > 1. |
upstash |
Mọi nơi (khuyến nghị cho production) | Có: kv.consume() được hậu thuẫn bởi lệnh atomic GETDEL của Redis. |
vercel-kv |
Deploy Vercel | Có: tự 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. |
Các lỗi đã được xử lý (429 rate-limit, CAPTCHA bị từ chối, 404 search) không ghi log, vì đó 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 ở dạng thô. |
ip |
Client IP (tôn trọng TRUST_PROXY_HEADERS). |
Nếu GSHEET_WEBHOOK_URL được set (và là https://), mỗi entry sẽ được POST theo kiểu fire-and-forget lên sheet thông qua một ThreadPoolExecutor(4) có giới hạn. Bản ghi SQLite mới là chính thức, nên webhook lỗi không bao giờ làm hỏng 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: kiểm soát
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: kiểm soát
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 hoàn toàn mới: cả hai công tắc đều bật (giữ nguyên hành vi như trước khi có gate). KV chỉ được ghi khi super-admin lưu lần đầu, nên một deploy chưa đụng tới sẽ đọc giá trị mặc định được nhúng sẵn (baked-in).
Update dependency¶
Dependabot quét hàng tuần (pip) và hàng tháng (github-actions, docker) rồi tự mở PR. Hãy review và merge sớm, vì reportlab, pypdf, cryptography là những mục tiêu của tấn công chuỗi cung ứng (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; các entry KV (counter rate-limit, CAPTCHA challenge) chỉ là tạm thời.
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 bị lộ qua cùng một kênh:
private_key.pemlà artifact riêng nhưng thường nằm chung chỗ với secret.