서버가 해킹당했습니다. 공격자가 프로세스 메모리를 덤프하고, 디스크의 모든 파일을 읽어갔습니다. 일반적인 상황이라면 서버에 저장된 모든 평문 데이터가 노출됩니다. 하지만 Quipu-Log의 쓰기 전용(write-only) 배포 모드에서는 다릅니다 — 서버가 완전히 탈취당해도 RSA로 보호된 필드의 평문은 얻을 수 없습니다. 개인키가 서버에 없으니까요.
RSA 공개키만 있으면 쓰기는 가능합니다. 개인키가 없으면 복호화가 불가능합니다. 서버가 공개키만 보유하면 데이터를 쓰지만 읽지 못하는 "쓰기 전용" 서버가 됩니다.
공개키 암호화의 비대칭 성질 다시 보기
25장에서 배운 RSA의 핵심 성질을 다시 떠올려 봅시다. 공개키로 암호화하고 개인키로만 복호화합니다. 이 방향성이 배포 모델의 기반이 됩니다.
코드로 보는 키 경계
quipu-server의 주석이 이 설계 결정을 명확히 기술합니다.
crates/quipu-server/src/lib.rs/// Key boundary: the server only ever needs the HMAC key and the RSA *public*
/// key (append + hash-based search). The private key is optional; without it
/// RSA-protected values come back as ciphertext for the querying client to
/// decrypt, keeping plaintext recovery out of the server's blast radius.
서버 설정 파일에서도 이 경계가 드러납니다.
quipu-server config.json — 쓰기 전용 설정{
"keys": {
"hmac_key_file": "/etc/quipu/hmac.key", // HMAC 키: 쓰기+검색에 필요
"public_key_pem_file": "/etc/quipu/rsa-pub.pem" // 공개키만: 암호화 가능
/* private_key_pem_file 생략 = 쓰기 전용 모드 */
}
}
private_key_pem_file을 설정하지 않으면 서버는 KeyRing에 개인키 없이 시작됩니다. KeyRing::decrypt()를 호출하면 "RSA private key version N is not in the key ring" 에러가 발생합니다. 이것은 실수가 아니라 의도된 설계입니다.
쿼리 시 무슨 일이 일어나나
감사 클라이언트가 POST /v1/logs/query로 로그를 조회하면 어떻게 될까요? RSA 보호 필드는 복호화되지 않은 채 암호문 그대로 응답에 포함됩니다.
quipu-server README — 쓰기 전용 조회 응답 예시// 서버 응답: RSA 필드는 암호문 구조체로 반환
{ "Rsa": {
"wrapped_key": "AQID...",
"nonce": "BAUG...",
"ciphertext": "ZW5j..."
}}
// 클라이언트가 개인키로 KeyRing::decrypt()를 호출해 복호화
클라이언트는 개인키를 보유하고 있으므로 KeyRing::decrypt()로 암호문을 평문으로 되돌릴 수 있습니다. 평문은 네트워크를 통해 전송되는 최소 시간 동안만 클라이언트 메모리에 존재하고, 서버의 어느 단계에도 존재하지 않습니다.
blast radius(피해 반경) 축소
"blast radius"는 보안 사고 발생 시 공격자가 얼마나 많은 것을 손에 넣을 수 있느냐입니다. 쓰기 전용 배포는 이 피해 반경을 의도적으로 줄입니다.
서버가 완전히 탈취당한 최악의 시나리오에서, 공격자가 가져갈 수 있는 것은:
• 로그의 method, url, content (항상 평문) ← 이것은 막을 수 없음
• HMAC/Sha256 보호 필드의 다이제스트 ← HMAC 키 없이는 역산 불가
• RSA 보호 필드의 암호문 ← 개인키 없이는 복호화 불가
RSA 보호 필드의 평문은 서버에 없으므로 절대 가져갈 수 없습니다.
개인키 있는 서버: 편리함과 보안의 교환
물론 쓰기 전용 모드에도 비용이 있습니다. RSA 필드에 대한 Contains 검색(부분 문자열)을 서버에서 할 수 없습니다. 개인키 없이는 복호화해서 비교할 수 없으니까요. 블라인드 Ngram 인덱스로 후보를 좁힌 뒤 클라이언트가 복호화해 확인하는 방식을 써야 합니다.
개인키를 서버에 두면(private_key_pem_file 설정 + plaintext_cache: true) 서버가 RSA 필드를 직접 복호화해 Contains 검색을 처리할 수 있습니다. 단 이 순간 서버는 RSA 보호 필드의 평문을 알 수 있게 되고, blast radius가 다시 넓어집니다.
| 쓰기 전용 (공개키만) | 일반 (개인키 포함) | |
|---|---|---|
| RSA 필드 암호화(쓰기) | 가능 ✓ | 가능 ✓ |
| RSA 필드 복호화(읽기) | 불가 — 클라이언트가 수행 | 가능 ✓ |
| RSA 필드 Contains 검색 | Ngram 인덱스로 후보 + 클라이언트 복호화 | 서버에서 직접 가능 |
| 서버 탈취 시 RSA 평문 노출 | 없음 ✓ | 있음 (캐시에 존재) |
공중 전화박스를 상상해 보세요. 누구나 편지를 넣을 수 있는 슬롯(공개키)이 있고, 열쇠(개인키)는 우편함 주인만 갖고 있습니다. 박스를 통째로 훔쳐가도 편지를 열어볼 수 없습니다 — 키가 없으니까요. 이것이 쓰기 전용 배포입니다.
체크포인트 서명은 어떻게 되나
흥미로운 결과가 하나 있습니다. 개인키 없이는 체크포인트 서명도 불가능합니다. KeyRing::can_sign()이 활성 RSA 버전의 개인키 보유 여부를 확인하고, 없으면 서명 단계를 건너뜁니다.
crates/quipu-core/src/crypto.rs — can_signpub fn can_sign(&self) -> bool {
self.active_rsa_version()
.is_some_and(|v| self.has_rsa_private(v))
}
쓰기 전용 서버는 체크포인트에 서명할 수 없습니다. 외부 앵커링을 활용하는 환경이라면, 개인키는 서버 외부의 HSM이나 서명 전용 서비스에 두고 서명 요청만 받아 처리하는 구조를 고려해야 합니다.
정리
- RSA의 비대칭성: 공개키로 암호화하면 개인키로만 복호화 가능.
- 서버에 공개키만 두면 쓰기는 가능하지만 복호화 불가 = 쓰기 전용 배포.
- 서버 탈취 시 RSA 필드 평문은 얻을 수 없음 — blast radius 축소.
- 대신 서버측
Contains검색 불가, 체크포인트 서명 불가의 트레이드오프. - 편리한 검색이 필요하면 개인키를 서버에 두되, 그 트레이드오프를 인식하고 선택해야 한다.
① 쓰기 전용 서버에서 RSA 보호 필드를 조회하면 클라이언트는 어떤 형태의 응답을 받나요? 클라이언트는 이후 어떤 단계를 거치나요?
② 서버가 탈취당했을 때, 공격자가 얻을 수 있는 것과 없는 것을 각각 나열해 보세요.
③ HMAC 키도 서버에 없다면 어떤 기능이 추가로 불가능해질까요?