Đi tới nội dung chính
Bài viết tổng quan Bài viết chính Milestone 4 Complete Security Audited VPS Ready Security

Một Link, Một Lần Đọc, và Rất Nhiều Thứ Để Nghĩ

Bài viết tổng quan về dự án `secret-letter`: từ thiết kế bảo mật, chống preview bot, race condition đến rà soát an ninh mã nguồn và quy trình đóng gói Docker lên VPS.

5 tháng 5, 20267 phút1473 từMạnh Cường

Dự án secret-letter (tiền thân là one-time-link) nghe qua thì có vẻ rất đơn giản: nhập nội dung cần bảo mật, tạo liên kết, người nhận mở một lần rồi hệ thống tự hủy. Bản thân mình ban đầu cũng nghĩ thế. Nhưng chỉ khi bắt tay vào tìm hiểu sâu và triển khai thực tế, mình mới phát hiện ra đằng sau sự đơn giản đó là cả một chuỗi bài toán kỹ thuật thú vị.

Toàn bộ mã nguồn dự án được công khai tại: github.com/vmcchooky/secret-letter.

Trong bài viết này, mình sẽ 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, mã hóa bảo mật phía máy khách (client-side encryption), giới hạn tần suất yêu cầu (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 thách thức kỹ thuật phát sinh trong thực tế

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 việc preview bot vô tình kích hoạt và xóa mất nội dung

Khi bạn gửi một liên kết qua Telegram, Slack, hay Facebook Messenger, gần như chắc chắn các ứng dụng này sẽ tự động cho bot cào link để lấy thông tin hiển thị trước (preview gồm tiêu đề, mô tả). Nếu thiết kế theo kiểu cứ truy cập đường dẫn là hiển thị và xóa dữ liệu, bot sẽ vô tình kích hoạt liên kết trước khi người nhận kịp mở ra. Hậu quả là người dùng khi click vào link chỉ còn nhận được thông 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áitruy xuất nội dung (consume). Khi truy cập link, frontend chỉ gọi API để kiểm tra trạng thái của thư (pending, expired). Trang web sau đó hiển thị một “Reveal Gate” (cổng xác nhận). Chỉ khi người nhận 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 thực tế và xóa vĩnh viễn dữ liệu đó khỏi bộ nhớ.

2. Tránh hiện tượng tranh chấp dữ liệu (Race Condition) khi đọc và xóa

Giả sử có hai yêu cầu (request) gửi lên gần như đồng thời để xem cùng một mật thư. Nếu backend xử lý tuần tự theo kiểu: đọc dữ liệu từ database → trả về cho client → gửi lệnh xóa, thì sẽ có một kẽ hở thời gian ngắn (race window) đủ để request thứ hai cũng đọc được dữ liệu trước khi lệnh xóa được thực thi.

Cách xử lý: Sử dụng lệnh GETDEL của Redis để đảm bảo thao tác mang tính nguyên tử (atomic). Đây là một thao tác duy nhất được thực thi trọn vẹn trong một bước xử lý của Redis: vừa đọc dữ liệu vừa xóa khóa (key) khỏi bộ nhớ RAM ngay lập tức. Nhờ đó, không có bất kỳ kẽ hở nào để request thứ hai có thể chen vào giữa.

3. Máy chủ hoàn toàn không biết nội dung bảo mật (Client-side Encryption)

Nếu máy chủ tiếp nhận dữ liệu dưới dạng văn bản thô (plaintext), người dùng bắt buộc phải đặt niềm tin tuyệt đối vào nhà vận hành hệ thống — tin rằng họ không lưu log, cơ sở dữ liệu không bị rò rỉ và máy chủ không bao giờ bị tấn công.

Cách xử lý: Áp dụng mô hình tin cậy (Trust Model) không phụ thuộc máy chủ thông qua mã hóa phía máy khách (Client-side Encryption):

  • Nội dung thư được mã hóa bằng thuật toán AES-GCM 256-bit ngay trên trình duyệt của người gửi thông qua Web Crypto API trước khi truyền đ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 các yêu cầu HTTP. Do đó, máy chủ chỉ nhận và lưu trữ bản mã (ciphertext). Kể cả trong trường hợp xấu nhất là cơ sở dữ liệu Redis của máy chủ bị hack hay rò rỉ, kẻ tấn công 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ần suất 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 spam (ví dụ: liên tục tạo hàng vạn nội dung rác làm tràn bộ nhớ Redis).

Cách xử lý: Cấu hình bộ giới hạn tần suất (Rate limiter) bằng Redis dựa trên địa chỉ IP của client:

  • Giới hạn 120 request/giờ cho hành động tạo mới.
  • Giới hạn 240 request/giờ cho hành động xem.
  • Áp dụng giới hạn tần suất riêng biệt cho từng tính năng để ngăn chặn tấn công dò quét (brute-force) mà không gây ảnh hưởng đến trải nghiệm của người dùng bình thường.

5. Rà soát an ninh mã nguồn nghiêm ngặt (Security Auditing)

Mã hóa phía client rất tối ưu, nhưng nếu phần backend trên máy chủ mắc các lỗi cấu hình cơ bản, hệ thống vẫn có nguy cơ bị tấn công từ chối dịch vụ (DoS). Để đảm bảo hệ thống sẵn sàng vận hành thực tế (Production-Ready), mình đã áp dụng các công cụ rà quét an ninh chuyên dụng cho Go và tinh chỉnh hệ thống:

  • Cấu hình lại http.Server mặc định để phòng chống tấn công Slowloris (một dạng tấn công DoS) bằng cách thiết lập các tham số ReadTimeoutWriteTimeout chặt chẽ.
  • Cập nhật và rà quét bằng govulncheck để phát hiện và loại bỏ sớm các lỗ hổng bảo mật đã biết trong các dependency.

Các thông số kỹ thuật cốt lõi

Các lựa chọn cấu hình này giúp hệ thống cân bằng giữa bảo mật và hiệu năng:

Thành phầnCấu hình kỹ thuậtMục tiêu
Mã hóaAES-GCM 256-bit, nonce 12 bytes, base64urlTố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ệuVăn bản thô (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 trữ file.
Thời gian sống (TTL)1 giờ, 24 giờ hoặc 7 ngàyTự động dọn dẹp bộ nhớ RAM nhờ tính năng TTL sẵn có của Redis.
Bảo mật HeaderHSTS, 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.

Lựa chọn công nghệ và lý do cụ thể

  • Frontend: React 19 + TypeScript + Vite. Tận dụng Web Crypto API tích hợp sẵn trên các trình duyệt hiện đại, tránh việc nhúng thêm các thư viện mã hóa bên thứ ba cồng kềnh.
  • Backend: Go 1.26 (Standard library). Biên dịch ra file nhị phân (binary) duy nhất, chạy 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. Phù hợp 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 theo thời gian (TTL) và hỗ trợ các thao tác đọc-xóa nguyên tử.

Hạ tầng triển khai

Dự án được cấu trúc dưới dạng Monorepo và vận hành trên mô hình VPS kết hợp Vercel:

  • Frontend (Vercel): Phân phối giao diện React tĩnh thông qua mạng lưới phân phối nội dung (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 các container an toàn, có cơ chế kiểm tra sức khỏe (health check) và tự động khởi động lại khi gặp sự cố.

Trạng thái dự án

Hệ thống Secret Letter đã hoàn thành chặng phát triển thứ tư (Milestone 4 - Production Readiness) và sẵn sàng triển khai thực tế:

  • Backend đạt tỷ lệ bao phủ kiểm thử (test coverage) cao (~82%).
  • Không phát hiện lỗi bảo mật khi kiểm tra bằng gosecgovulncheck.
  • Tích hợp đầy đủ giới hạn tần suất (Rate limiting), chuẩn hóa thông báo lỗi (Structured errors) và thu thập chỉ số vận hành (Metrics).

Dự án này tuy quy mô không lớn, nhưng mang lại những bài học quý giá khi phải giải quyết triệt để từng khía cạnh thực tế: từ bảo mật dữ liệu, tối ưu băng thông, kiểm thử tự động, cho tới thiết lập hạ tầng CI/CD và DevOps. Đó chính là cách để rèn luyện tư duy thiết kế hệ thống vững chắc!

Chủ đề của bài viết

Chia sẻ bài viết

Gửi bài này cho người đang cần đúng chủ đề, hoặc lưu lại để quay về sau.

Đọc tiếp

Tất cả bài viết

Tiếp theo

Gợi ý bài tiếp theo

Về Quorix Việt Nam

Quorix Việt Nam là không gian kỹ thuật chia sẻ và hệ thống hóa kiến thức về mạng máy tính, tư duy hệ thống và xây dựng các dự án, cung cấp một số dịch vụ hỗ …

Mở bài tiếp theo

Thảo luận