
간단하고 안전한 비밀 보관함
Keeper는 Go를 위한 암호화 비밀 저장소입니다. Argon2id 키 유도 및 XChaCha20-Poly1305(기본값) 인증 암호화를 사용하여 유휴 상태의 임의 바이트 페이로드를 암호화하고, 이를 내장 bbolt 데이터베이스에 저장합니다.
다음 세 가지로 독립적으로 사용할 수 있습니다:
x/keephandler) — 단 한 번의 호출로 모든 net/http 멀티플렉서에 keeper 엔드포인트를 마운트하며, 접근 제어 및 감사 로깅을 위한 플러그형 훅, 가드, 응답 인코더를 제공합니다.cmd/keeper) — 지속적인 REPL 세션, 에코 없는 비밀 입력, 쉘 히스토리 노출 제로를 제공하는 터미널 인터페이스입니다.Keeper는 Agbero 로드 밸런서의 기초 비밀 관리 계층으로 설계되었지만 Agbero에 대한 의존성이 없으며 모든 Go 프로젝트에서 작동합니다.
Keeper는 비밀을 버킷으로 분할합니다. 모든 버킷에는 데이터 암호화 키(DEK)가 보호되는 방식을 관리하는 변경 불가능한 BucketSecurityPolicy가 있습니다. 네 가지 수준을 사용할 수 있습니다.
스키마는 관련 버킷을 그룹화하는 URI 접두사입니다(vault://, certs://, space:// 또는 등록하는 모든 이름). 보안 수준은 생성 시 설정된 버킷 정책의 속성이며 그 이후로 변경 불가능합니다.
동일한 스키마 내에서 보안 수준을 자유롭게 혼합할 수 있습니다. 예를 들어, vault://system은 LevelPasswordOnly(시작 시 자동 잠금 해제)일 수 있고, vault://admin은 LevelAdminWrapped(명시적 자격 증명 필요)일 수 있습니다.
버킷 DEK는 마스터 키에서 HKDF-SHA256을 사용하여 버킷별 도메인 분리 정보 문자열(keeper-bucket-dek-v1:scheme:namespace)로 유도됩니다. UnlockDatabase가 올바른 마스터 암호로 호출되면 모든 LevelPasswordOnly 버킷이 자동으로 잠금 해제됩니다. 런타임에 버킷별 자격 증명이 필요하지 않습니다. 이 수준은 프로세스가 시작 시 사람의 개입 없이 필요한 비밀에 적합합니다.
버킷에는 해당 버킷 고유의 무작위로 생성된 32바이트 DEK가 있습니다. DEK는 평문으로 저장되지 않습니다. 각 승인된 관리자에 대해 HKDF(masterKey‖adminCred, dekSalt)에서 키 암호화 키(KEK)가 유도되어 XChaCha20-Poly1305를 통해 DEK를 래핑하는 데 사용됩니다. 관리자가 자격 증명으로 UnlockBucket을 호출하기 전까지 버킷에 접근할 수 없습니다. 마스터 암호만으로는 버킷을 해독할 수 없습니다. 한 관리자를 취소해도 다른 관리자의 래핑된 복사본에는 영향을 미치지 않습니다.
버킷 DEK는 CreateBucket 시 생성되고 호출자가 제공한 HSMProvider에 의해 즉시 래핑됩니다. 공급자는 래핑 및 언래핑 작업을 수행합니다. keeper는 공급자에게 전달한 후 원시 DEK를 처리하지 않습니다. UnlockDatabase는 자동으로 공급자를 호출하여 등록된 모든 HSM 버킷에 대해 봉투(Envelope)를 언래핑하고 시드합니다. 마스터 키 교체는 이러한 버킷을 다시 암호화하지 않습니다. DEK는 공급자 제어 하에 있습니다.
A built-in SoftHSM implementation backed by a memguard-protected wrapping key is available in pkg/hsm for testing and CI environments. Do not use it in production.
Identical to LevelHSM in key management behaviour, but the HSMProvider is implemented by pkg/remote.Provider — a configurable HTTPS adapter that delegates wrap and unwrap to any remote KMS service over TLS. Pre-built configurations for HashiCorp Vault Transit, AWS KMS, and GCP Cloud KMS are provided in pkg/remote. For production use, configure TLSClientCert and TLSClientKey to enable mutual TLS authentication.
salt ← random 32 bytes, generated once, stored as a versioned SaltStore (unencrypted) masterKey ← Argon2id(passphrase, salt, t=3, m=64 MiB, p=4) → 32 bytes
검증 해시는 첫 번째 유도 시 저장됩니다:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
이후의 DeriveMaster 호출은 이 해시를 다시 계산하고 crypto/subtle.ConstantTimeCompare와 비교합니다. 불일치 시 ErrInvalidPassphrase를 반환합니다.
KDF 솔트는 의도적으로 암호화되지 않은 상태로 저장됩니다. 마스터 키를 도출하기 위해 UnlockDatabase 이전에 읽을 수 있어야 합니다. 마스터에서 파생된 키로 암호화하는 것은 순환적일 것입니다. KDF 솔트는 비밀이 아닙니다. 그 목적은 고유성(uniqueness)이지 기밀성(confidentiality)이 아닙니다.
각 평문 값은 버킷 DEK를 사용하여 XChaCha20-Poly1305로 암호화됩니다.``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
저장된 레코드는 msgpack으로 인코딩된 `Secret` 구조체로, 암호문, 암호화된 메타데이터, 스키마 버전을 포함합니다. 인증은 암시적입니다. 잘못된 키로 복호화된 암호문은 일반 텍스트가 반환되기 전에 AEAD 인증 실패를 발생시킵니다.
### KEK 유도 — LevelAdminWrapped```
salt ← random 32 bytes, generated at bucket creation, stored in policy
ikm ← masterKey ‖ adminCredential
KEK ← HKDF-SHA256(ikm, salt, info="keeper-kek-v1") → 32 bytes
wrappedDEK ← XChaCha20-Poly1305.Seal(nonce, KEK, DEK)
KEK는 두 번째 Argon2 패스 대신 HKDF를 사용하여 파생됩니다. 마스터 키
는 이미 고비용 KDF에 의해 생성되었습니다. 두 번째 Argon2 호출은 보안 이점 없이 모든
UnlockBucket 호출에 수백 밀리초의 지연을 추가할 것입니다. HKDF-SHA256은 약 1마이크로초 내에 작동합니다.
심층 방어: 데이터베이스만 손상시킨 공격자는 래핑된 DEK와 HKDF 솔트를 얻지만, 마스터 키 없이는 KEK를 파생할 수 없습니다.
마스터 키만 손상시킨 공격자는 관리자 자격 증명을 알지 못하면 어떤
LevelAdminWrapped DEK도 언래핑할 수 없습니다.
비밀 메타데이터(생성 시간, 업데이트 시간, 액세스 횟수, 버전)는 암호문과 별도로 암호화됩니다:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
For `LevelAdminWrapped`, `LevelHSM`, `LevelRemote` 버킷의 경우, 이는 버킷 자격 증명 없이는 메타데이터에 접근할 수 없음을 의미하며, 데이터베이스 파일에 읽기 접근 권한이 있는 공격자가 접근 패턴이나 타임스탬프를 알아내는 것을 방지합니다.
**타이밍 사이드 채널 참고:** XChaCha20-Poly1305는 인증 오류를 반환하기 전에 전체 암호문을 처리합니다. 대체 복호화 경로(새로운 파생 DEK → 이전 마스터 키를 DEK로 사용)는 어떤 키가 성공하든 동일한 실제 시간이 소요됩니다. 타이밍 사이드 채널이 레코드의 마이그레이션 상태를 유출하지 않습니다.
### 메타데이터 암호화 — 정책, WAL 및 감사
모든 구조적 메타데이터도 저장 시 암호화됩니다. `UnlockDatabase` 시간에 마스터 키로부터 두 개의 키가 파생됩니다:```
policyEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-enc-v1") → 32 bytes
auditEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-enc-v1") → 32 bytes
policyEncKey는 BucketSecurityPolicy 값과 순환 WAL을 암호화합니다.
auditEncKey는 모든 감사 이벤트의 Scheme, Namespace, Details 필드를 암호화합니다.
두 키 모두 Lock()에서 메모리에서 지워집니다. 메타데이터 암호화에 사용되는 암호는 비밀에 사용되는 것과 동일한 구성 가능한 crypt.Cipher 인터페이스입니다. 사용자의 암호 선택(AES-256-GCM for FIPS, 기본값 XChaCha20-Poly1305)이 자동으로 적용됩니다.
모든 암호화된 메타데이터 블롭의 와이어 형식:``` nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
### 정책 버킷 키 해싱
디스크 상의 정책 키는 일반 텍스트 `scheme:namespace` 문자열이 아닌 불투명 해시값으로, 버킷 이름의 오프라인 열거를 방지합니다:```
base ← hex(SHA-256("scheme:namespace"))[:32] // 32 hex chars = 128-bit key space
_policies/<base> → encrypted BucketSecurityPolicy
_policies/<base>__hash__ → SHA-256(encrypted policy bytes)
_policies/<base>__hmac__ → HMAC-SHA256(policyKey, encrypted policy bytes)
메모리 내 schemeRegistry는 계속해서 "scheme:namespace"를 키로 사용합니다 — 단, 디스크 상의 표현만 변경됩니다.
각 정책 레코드는 하나의 bbolt 트랜잭션에서 원자적으로 기록되는 두 개의 무결성 태그를 포함합니다:``` hash ← SHA-256(encryptedPolicyBytes) — unauthenticated, pre-unlock integrity policyKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-hmac-v1") → 32 bytes hmac ← HMAC-SHA256(policyKey, encryptedPolicyBytes) — authenticated, post-unlock integrity
`UnlockDatabase` 이전에는 SHA-256 해시만 사용할 수 있습니다. 잠금 해제 후, `loadPolicy`가 HMAC 태그를 확인합니다. `UnlockDatabase`는 이 기능이 추가되기 전에 생성된 정책에 HMAC 태그를 역으로 채우기 위해 `upgradePolicyHMACs`를 호출합니다.
### HMAC 서명 감사```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
서명 키는 UnlockDatabase에서 활성화되고 Lock에서 해제됩니다. 마스터 키가 교체되면 Rotate는 키 교체 체크포인트 이벤트를 모든 활성 감사 체인에 추가하며, 이전 에포크의 마지막 이벤트로 이전 감사 키로 서명합니다. 기록은 절대 다시 쓰이지 않으며, 체크포인트는 에포크 간의 신뢰 브리지입니다.