← All posts

Ba kiếp sống của khóa ký JWT của tôi

Authagonal·July 17, 2026
authjwtkey-managementkmsvaultecdsaoidc

Một khóa riêng tư duy nhất ký mọi token mà máy chủ xác thực của chúng tôi phát hành. Đánh mất nó và kẻ tấn công có thể tạo ra một token hợp lệ cho bất kỳ người dùng nào, không cần mật khẩu, còn nhật ký của bạn thì hiển thị một lượt đăng nhập sạch sẽ. Đó là bí mật giá trị nhất trong toàn hệ thống, và trong suốt đời mình nó đã di chuyển hai lần: nó được sinh ra bên trong tiến trình ứng dụng, rồi bị lưu đày vào một két an toàn mà nó không còn có thể rời khỏi, rồi tái sinh thành một loại khóa hoàn toàn khác. Mỗi lần di chuyển đều diễn ra trên một issuer đang chạy, với các token đã đang lưu hành và các bộ xác thực đang cache trạng thái mà chúng tôi không kiểm soát. Đây là những gì các lần di chuyển đó thực sự đòi hỏi, và lần dạy cho chúng tôi nhiều nhất lại chính là lần chúng tôi không hề công bố.

Kiếp một: chiếc khóa mà máy chủ giữ trong túi

Thuở ban đầu, khóa sống ngay tại nơi nó được dùng. Máy chủ tạo ra một khóa RSA-2048, giữ nó trong bộ nhớ, và ký các token của mình bằng RS256. Đây là mặc định ở gần như mọi nơi, và nó vẫn ổn cho tới đúng lúc bạn nói to lên từ quyền giám hộ. Khóa riêng tư có thể bị chạm tới bởi bất cứ thứ gì đọc được bộ nhớ của tiến trình, cấu hình của nó, hay một bản sao lưu của một trong hai. Chúng tôi muốn có thể nói với khách hàng rằng một cơ sở dữ liệu bị rò rỉ chẳng để lộ điều gì hữu ích, thế mà một khóa ký nằm cách cơ sở dữ liệu ấy đúng một tiến trình lại biến lời khẳng định đó thành lời nói dối. Khóa buộc phải rời khỏi tòa nhà.

Kiếp hai: chiếc khóa dọn vào một két an toàn mà nó không thể rời khỏi

Chúng tôi tạo ra một điểm nối — một ISignatureProvider mà máy chủ gọi mỗi khi cần một chữ ký — và đặt một KMS phía sau nó. Giờ đây khóa được tạo ra bên trong transit engine của Vault và không bao giờ được xuất ra ngoài. Ứng dụng không giữ khóa; nó gửi các byte cần ký đến két an toàn và nhận lại một chữ ký. Nó có thể dùng khóa nhưng không thể tuồn nó ra ngoài.

Sự phân biệt đó chính là toàn bộ vấn đề. Một bản dump bộ nhớ, một tệp cấu hình bị lộ, một bản sao lưu bị rò rỉ — không cái nào còn chứa khóa nữa, bởi vì khóa chưa từng nằm ở bất kỳ nơi nào trong số đó. Việc xâm phạm ứng dụng giờ chỉ mua cho kẻ tấn công khả năng nhờ két an toàn ký, điều mà chúng tôi có thể giới hạn tần suất, ghi nhật ký và thu hồi. Nó không còn mua cho họ chính chiếc khóa nữa — thứ mà một khi đã mất thì chúng tôi chẳng thể làm gì được.

Đây là phần chúng tôi đã không ghi vào ghi chú phát hành. Chính cái commit đưa khóa vào két an toàn cũng lặng lẽ cắt đôi số vòng lặp PBKDF2 của chúng tôi, từ 100.000 xuống còn 50.000. Một diff, hai thay đổi bảo mật chỉ về hai hướng ngược nhau: tiêu đề là “ký trong KMS”, còn núp bên dưới nó là một hệ số công việc băm mật khẩu bị cắt đi một nửa, nằm ở một dòng chẳng ai để mắt tới vì câu chuyện đang xoay quanh một thứ khác. Cả hai đều là mã bảo mật, nên cả hai được xem xét chung như một thay đổi “an toàn hơn” duy nhất, và được cho qua nhờ sức nặng của nửa tốt đẹp kia.

Bản sửa lỗi chỉ một dòng. Bài học thì không. Một diff được dán nhãn an toàn hơn thì vẫn là một diff, và những hằng số nhạy cảm về bảo mật bên trong nó — số vòng lặp, kích thước khóa, thời gian chờ, tên thuật toán — không thừa hưởng vầng hào quang nào từ thông điệp commit. Giờ chúng tôi đối xử với một con số hệ số công việc y như cách đối xử với một lựa chọn thuật toán: một điều được khẳng định và kiểm chứng, chứ không phải điều bạn tin tưởng chỉ vì thay đổi bao quanh nó là một ý tưởng hay.

Kiếp ba: chiếc khóa nhỏ đi để nhanh hơn

Với việc ký nằm sau một KMS, mỗi chữ ký là một vòng khứ hồi qua mạng, mà ký RSA lại thuộc loại tốn kém. Vậy nên khóa đã đổi giống loài: RS256 trên RSA-2048 trở thành ES256 trên đường cong P-256. Ký bằng đường cong elliptic rẻ hơn RSA khoảng một bậc độ lớn, một chữ ký P-256 nặng 64 byte trong khi của RSA-2048 là 256, và tập khóa công khai mà các bộ xác thực tải về cũng co lại tương ứng. Khóa nhỏ hơn, chữ ký nhỏ hơn, JWKS nhỏ hơn, token nhanh hơn — tất cả chỉ nhờ chọn một đường cong tốt hơn.

Cái bẫy nằm ở chỗ bạn không thể tráo phắt thuật toán ký trên một issuer đang chạy. Mọi token đã phát hành đều được ký bằng RS256 và phải tiếp tục xác thực được cho tới khi hết hạn. Mọi bộ xác thực đều đang cache khóa công khai cũ của bạn. Đổi thuật toán trong một nước đi duy nhất và bạn sẽ vô hiệu hóa mọi token đang lưu hành cùng mọi tập khóa đã được cache ngay trong cùng một khoảnh khắc — một sự cố tự chuốc lấy, khoác lên mình lớp áo nâng cấp.

Cách hiệu quả là thôi giả vờ rằng xưa nay chỉ có một khóa. Kho khóa trở nên bất khả tri về thuật toán: nó giữ khóa RSA và khóa EC cùng một lúc, và công bố cả hai trong JWKS, mỗi khóa dưới key id và thuật toán riêng của nó. Một bộ chọn khóa đang hoạt động riêng biệt quyết định khóa nào ký các token mới, và nó tự động chuyển khỏi khóa RSA cũ ngay khoảnh khắc có một khóa EC — không cần cờ cấu hình, không cần một lần triển khai canh giờ trùng với một đợt xoay khóa. Trong suốt quãng chồng lấn, discovery quảng bá cả hai khóa công khai, nên các token được ký bằng bất kỳ khóa nào cũng vẫn xác thực được. Một khi token RS256 cuối cùng đã hết hạn, discovery chỉ còn quảng bá ES256 và việc xác thực ghim chặt ValidAlgorithms = ES256, điều này cũng sập cửa trước các cuộc tấn công gây nhầm lẫn thuật toán vốn tìm cách thuyết phục một bộ xác thực chấp nhận sai lược đồ. Cuộc di trú là một pha hòa lẫn chuyển tiếp mà issuer tự thực hiện trên chính mình, chứ không phải một cái công tắc ai đó bật.

Bài học, đúc kết lại

Một khóa ký trông giống như một hằng số: một bí mật, đặt một lần, tham chiếu mãi mãi. Khóa của chúng tôi thì không. Trong đời mình, nó đã đổi quyền giám hộ, từ tiến trình sang một KMS, và nó đã đổi giống loài, từ RSA sang đường cong elliptic, và cả hai lần di chuyển đều phải thực hiện khi token còn đang lưu hành và các bộ xác thực đang cache những thứ mà chúng tôi không sở hữu. Tính chất giúp mỗi lần di chuyển sống sót đều là một tính chất giống nhau ở cả hai lần: hệ thống không bao giờ giả định rằng có đúng một khóa, một thuật toán, hay một mái nhà. Hãy xây kho khóa để giữ được nhiều khóa và để trung thực quảng bá những khóa nào nó đang giữ, thì việc xoay khóa — dù là nơi khóa cư ngụ hay loại khóa là gì — sẽ thôi là một sự cố mà bạn phải lên lịch và trở thành một tính chất mà issuer tự mình quản lý.

Nếu bạn vận hành hệ thống xác thực, thứ ký các token của bạn nên là món đồ nhàm chán nhất, dễ soi xét nhất, ít khôn lỏi nhất mà bạn sở hữu. Của chúng tôi phải mất ba kiếp mới tới được đó. Và mỗi kiếp đều đáng giá.