Dự án secret-letter (tiền thân là one-time-link) nghe tên thì đơn giản lắm: nhập secret, tạo link, người nhận mở một lần rồi thôi. Mình cũng nghĩ vậy lúc đầu. Rồi mình bắt đầu ngồi nghĩ kỹ hơn và nhận ra cái “đơn giản” đó ẩn khá nhiều thứ thú vị bên dưới.
Bài viết này mình tổng hợp lại toàn bộ quá trình thiết kế, giải quyết các thách thức kỹ thuật và hoàn thiện hệ thống — từ việc chống bot, bảo mật mã hóa client-side, quản lý Rate Limiting cho đến rà soát an ninh mã nguồn và đóng gói hạ tầng triển khai thực tế.
Những bài toán thực tế buộc phải nghĩ kỹ
Nếu chỉ cần làm cho hệ thống chạy được thì đúng là xong trong 5 phút. Nhưng để nó hoạt động đúng và an toàn như kỳ vọng, mình đã phải giải quyết các bài toán lớn sau:
1. Tránh preview bot nuốt mất secret
Khi bạn gửi một link qua Telegram, Slack, hay Facebook Messenger, gần như chắc chắn sẽ có bot tự động truy cập link đó để lấy preview (title, description). Nếu thiết kế theo kiểu cứ truy cập link là reveal và xóa, thì con bot đó sẽ tiêu thụ secret trước khi người nhận kịp bấm vào. Người nhận mở ra chỉ thấy báo “Liên kết đã được sử dụng”.
Cách xử lý: Tách biệt hành động xem trạng thái và consume secret. Khi truy cập link, frontend chỉ gọi API kiểm tra trạng thái (pending, expired). Trang web sẽ hiển thị một “Reveal Gate” (cổng xác nhận). Chỉ khi người dùng thật sự chủ động nhấn nút “Xem bí mật”, hệ thống mới gọi API lấy dữ liệu và xóa secret đó đi.
2. Tránh Race Condition khi đọc và xóa
Giả sử hai request gần như đồng thời cùng cố truy cập để xem một secret. Nếu hệ thống backend làm theo kiểu tuần tự: đọc dữ liệu từ database → gửi về cho client → ra lệnh xóa, thì có một khoảng trống thời gian (race window) nhỏ để request thứ hai cũng đọc được dữ liệu trước khi lệnh xóa kịp thực hiện.
Cách xử lý: Sử dụng lệnh GETDEL của Redis và thao tác Atomic. Đây là một thao tác nguyên tử (atomic) duy nhất: vừa lấy dữ liệu vừa xóa key khỏi RAM ngay lập tức. Không có bất kỳ khoảng trống nào để request thứ hai chen vào giữa.
3. Server không được biết nội dung secret (Client-side Encryption)
Nếu server nhận plaintext, người dùng bắt buộc phải đặt niềm tin tuyệt đối rằng hệ thống không lưu log, không bị hack database, không bị rò rỉ dữ liệu.
Cách xử lý: Áp dụng Trust Model phi tập trung thông qua mã hóa phía Client:
- Bí mật được mã hóa bằng thuật toán AES-GCM 256-bit ngay trên trình duyệt bằng Web Crypto API trước khi gửi đi.
- Khóa giải mã (decryption key) nằm ở phần Fragment của URL (phần sau dấu
#, ví dụ:https://domain.com/reveal/id#key). - Theo tiêu chuẩn trình duyệt, phần fragment sau dấu
#không bao giờ được gửi lên server trong HTTP request. Do đó, server chỉ nhận và lưu trữ ciphertext (bản mã). Kể cả khi hacker hack được toàn bộ database Redis của server, họ cũng chỉ thấy những chuỗi ký tự vô nghĩa mà không có cách nào giải mã được.
4. Giới hạn tốc độ và chống lạm dụng (Rate Limiting)
Một API công khai nếu không được bảo vệ sẽ rất dễ bị lạm dụng hoặc tấn công DDoS (tạo hàng vạn secret rác làm đầy Redis).
Cách xử lý: Cài đặt Rate limiter bằng Redis dựa trên IP:
- Giới hạn
120 request/giờcho việc tạo mới. - Giới hạn
240 request/giờcho việc xem. - Áp dụng Rate limit riêng lẻ cho các tính năng để ngăn chặn Brute-force nhưng không làm phiền người dùng hợp lệ.
5. Rà soát an ninh mã nguồn nghiêm ngặt (Security Auditing)
Mã hóa phía client rất tốt, nhưng nếu mã nguồn backend của server bị dính lỗi bảo mật cơ bản thì hệ thống vẫn có thể bị sập. Để đạt chuẩn Production-Ready cho Secret Letter, mình đã chạy công cụ quét an ninh chuyên sâu của Go:
- Khắc phục cấu hình
http.Servermặc định để tránh tấn công Slowloris/DoS bằng các cài đặtReadTimeout,WriteTimeoutchặt chẽ. - Cập nhật và rà quét bằng
govulncheckđể đảm bảo thư viện tiêu chuẩn luôn an toàn ở Go 1.26.
Các quyết định kỹ thuật đã chốt
Việc chốt sớm các tham số kỹ thuật giúp hệ thống hoạt động vô cùng nhất quán và gọn gàng:
| Thành phần | Cấu hình kỹ thuật | Mục tiêu |
|---|---|---|
| Mã hóa | AES-GCM 256-bit, nonce 12 bytes, base64url | Tối ưu độ bảo mật, tương thích tốt với Web Crypto API gốc của trình duyệt. |
| Giới hạn dữ liệu | Plaintext tối đa 10 KB, request body tối đa 15 KB | Đủ cho mật khẩu, token, SSH key — tránh việc lạm dụng hệ thống để lưu file. |
| Thời gian sống (TTL) | 1 giờ, 24 giờ hoặc 7 ngày | Tự động dọn dẹp bộ nhớ RAM nhờ tính năng native TTL của Redis. |
| Bảo mật Header | HSTS, CSP, X-Frame-Options (DENY) | Ngăn chặn các cuộc tấn công Clickjacking, XSS và giả mạo giao diện. |
Công nghệ được lựa chọn và Lý do
- Frontend: React 19 + TypeScript + Vite. Sử dụng Web Crypto API có sẵn trên trình duyệt, không cần ôm thêm thư viện mã hóa dư thừa.
- Backend: Go 1.26 (Standard library). Ngôn ngữ biên dịch ra file binary siêu gọn nhẹ, khởi động tức thì và tiết kiệm tài nguyên bộ nhớ.
- Database: Redis 7. Hoàn hảo cho bài toán lưu trữ tạm thời nhờ cơ chế tự hủy dữ liệu bằng TTL và hỗ trợ đọc-xóa nguyên tử.
Hạ tầng triển khai
Dự án được cấu trúc dạng Monorepo và sẵn sàng chạy trên VPS kết hợp Vercel:
- Frontend (Vercel): Phân phối giao diện React tĩnh qua mạng lưới CDN toàn cầu.
- Backend + Database (Docker Compose): Toàn bộ Go API, cơ sở dữ liệu Redis và máy chủ Web Caddy được đóng gói trong container an toàn, có health check và sẵn sàng restart tự động.
Trạng thái hiện tại của dự án
Hệ thống Secret Letter đã hoàn thành Milestone 4 (Production Readiness) và đang chờ triển khai:
- Backend có test coverage cao (~82%).
- Sạch lỗi bảo mật khi quét với
gosec&govulncheck. - Có đầy đủ Rate limiting, Structured errors và Metrics.
Dự án này tuy không lớn, nhưng nó bắt buộc mình phải đi sâu giải quyết từng khía cạnh thực tế: bảo mật dữ liệu, tối ưu hóa băng thông, kiểm thử tự động, và vận hành DevOps chuyên nghiệp. Đó là cách để hoàn thiện tư duy xây dựng nền tảng vững chắc!