
Простой безопасный хранитель секретов
Keeper — это криптографическое хранилище секретов для Go. Он шифрует произвольные байтовые нагрузки в состоянии покоя, используя вывод ключа Argon2id и аутентифицированное шифрование XChaCha20-Poly1305 (по умолчанию), и сохраняет их во встроенной базе данных bbolt.
Он поставляется в виде трех компонентов, которые можно использовать независимо:
x/keephandler) — монтируйте конечные точки keeper на любой net/http mux одним вызовом, с подключаемыми хуками, защитами и кодировщиками ответов для контроля доступа и аудита.cmd/keeper) — терминальный интерфейс с постоянным REPL-сеансом, вводом секретов без эха и отсутствием истории в оболочке.Keeper был разработан как базовый уровень управления секретами для балансировщика нагрузки Agbero, но не имеет зависимостей от Agbero и работает в любом проекте Go.
Keeper разделяет секреты на сегменты (buckets). Каждый сегмент имеет неизменяемую политику BucketSecurityPolicy, которая определяет, как защищается его ключ шифрования данных (Data Encryption Key, DEK). Доступны четыре уровня.
Схема — это префикс URI, который группирует связанные сегменты (vault://, certs://, space:// или любое другое зарегистрированное имя). Уровень безопасности — это свойство политики сегмента, задаваемое при создании и неизменяемое впоследствии.
Вы можете свободно смешивать уровни безопасности в рамках одной схемы. Например, vault://system может быть LevelPasswordOnly (автоматически разблокируется при запуске), а vault://admin — LevelAdminWrapped (требует явного ввода учетных данных).
DEK сегмента выводится из мастер-ключа с помощью HKDF-SHA256 с разделенной по домену строкой информации для каждого сегмента (keeper-bucket-dek-v1:scheme:namespace). Все сегменты LevelPasswordOnly разблокируются автоматически при вызове UnlockDatabase с правильной мастер-парольной фразой. Во время выполнения не требуется учетных данных для каждого сегмента. Этот уровень подходит для секретов, которые процессу нужны при запуске без участия человека.
Сегмент имеет случайно сгенерированный 32-байтовый DEK, уникальный для этого сегмента. DEK никогда не хранится в открытом виде. Для каждого авторизованного администратора из HKDF(masterKey‖adminCred, dekSalt) выводится ключ шифрования ключей (KEK), который используется для оборачивания DEK с помощью XChaCha20-Poly1305. Сегмент недоступен до тех пор, пока администратор не вызовет UnlockBucket со своими учетными данными. Одна лишь мастер-парольная фраза не может расшифровать сегмент. Отзыв одного администратора не влияет на завернутую копию любого другого администратора.
DEK сегмента генерируется во время CreateBucket и сразу же оборачивается поставщиком HSMProvider, предоставленным вызывающей стороной. Поставщик выполняет операции оборачивания и разворачивания — keeper никогда не обрабатывает сырой DEK после передачи его поставщику. UnlockDatabase автоматически вызывает поставщика для разворачивания и начальной загрузки конверта для всех зарегистрированных HSM-сегментов. Ротация мастер-ключа не перешифровывает эти сегменты; DEK контролируется поставщиком.
Встроенная реализация SoftHSM, основанная на оберточном ключе, защищенном memguard, доступна в pkg/hsm для тестирования и CI-сред. Не используйте её в производстве.
Идентичен LevelHSM по поведению управления ключами, но HSMProvider реализуется через pkg/remote.Provider — настраиваемый HTTPS-адаптер, который делегирует оборачивание и разворачивание любому удаленному сервису KMS через TLS. В pkg/remote предоставлены готовые конфигурации для HashiCorp Vault Transit, AWS KMS и GCP Cloud KMS. Для производственного использования настройте TLSClientCert и TLSClientKey, чтобы включить взаимную TLS-аутентификацию.
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 не является секретом; её цель — уникальность, а не конфиденциальность.
Каждое значение открытого текста шифруется с помощью XChaCha20-Poly1305 с использованием bucket DEK:``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
### Вывод KEK — LevelAdminWrapped
Сохранённая запись представляет собой закодированную в msgpack структуру `Secret`, содержащую шифротекст, зашифрованные метаданные и версию схемы. Аутентификация неявная: расшифровка шифротекста неверным ключом приводит к ошибке аутентификации AEAD до того, как будет возвращён открытый текст.```
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 выводится с помощью HKDF, а не второго прохода Argon2. Мастер-ключ уже был получен с помощью дорогостоящего KDF; второй вызов Argon2 добавил бы сотни миллисекунд задержки к каждому вызову UnlockBucket без выгоды для безопасности. HKDF-SHA256 работает примерно за одну микросекунду.
Защита в глубину: Злоумышленник, скомпрометировавший только базу данных, получает упакованный DEK и соль HKDF, но не может вывести KEK без мастер-ключа. Злоумышленник, скомпрометировавший только мастер-ключ, не сможет распаковать ни один LevelAdminWrapped DEK без знания учётных данных администратора.
Секретные метаданные (время создания, время обновления, количество обращений, версия) шифруются отдельно от шифротекста:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
Для `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 для FIPS, XChaCha20-Poly1305 по умолчанию)
применяется автоматически.
Проводной формат для всех зашифрованных блобов метаданных:``` nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
### Хеширование ключей политики bucket
Ключи политики на диске представляют собой непрозрачные хеши, а не строки `scheme:namespace` в открытом виде, что предотвращает офлайн-перебор имен bucket:```
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` вызывает `upgradePolicyHMACs`, чтобы заполнить недостающие теги HMAC в политиках, созданных до появления этой функции.
### Аудит подписи HMAC```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
Ключ подписи активируется при UnlockDatabase и очищается при Lock. При ротации мастер-ключа Rotate добавляет событие контрольной точки ротации ключей в каждую активную цепочку аудита, подписанное старым ключом аудита в качестве последнего события старой эпохи. История никогда не перезаписывается; контрольная точка является мостом доверия между эпохами.
passphrase │ └─ Argon2id(salt) ──→ masterKey (32 bytes, memguard Enclave) │ ├─ HKDF("keeper-audit-hmac-v1") ──→ auditKey (HMAC signing) ├─ HKDF("keeper-audit-enc-v1") ──→ auditEncKey (audit field encryption) ├─ HKDF("keeper-policy-hmac-v1") ──→ policyKey (policy HMAC) ├─ HKDF("keeper-policy-enc-v1") ──→ policyEncKey (policy/WAL encryption) │ ├─ [LevelPasswordOnly] │ └─ HKDF("keeper-bucket-dek-v1:scheme:ns") ──→ DEK │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ ├─ [LevelAdminWrapped] │ ├─ random 32 bytes ──→ DEK │ │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ │ │ └─ HKDF("keeper-kek-v1", masterKey‖adminCred, dekSalt) │ └─ KEK │ └─ XChaCha20-Poly1305(KEK, DEK) ──→ wrappedDEK │ └─ [LevelHSM / LevelRemote] ├─ random 32 bytes ──→ DEK │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ └─ HSMProvider.WrapDEK(DEK) ──→ wrappedDEK (stored; provider controls the wrapping key)
Все промежуточные ключи обнуляются сразу после использования. Мастер-ключ никогда не записывается на диск в любом виде.
---
## Схема хранения
Базовая база данных — bbolt. Все bucket'ы и их содержимое:
| bbolt bucket | Ключ | Значение |
|---|---|---|
| `__meta__` | `salt` | msgpack — SaltStore (незашифровано; циклическая зависимость, если зашифровано) |
| `__meta__` | `verify` | raw bytes — Argon2id verification hash |
| `__meta__` | `rotation_wal` | `nonce‖AEAD(msgpack(RotationWAL))` |
| `__meta__` | `bucket_dek_done` | `"1"` — маркер завершения миграции DEK |
| `__policies__` | `hex(SHA-256(scheme:ns))[:32]` | `nonce‖AEAD(msgpack(BucketSecurityPolicy))` |
| `__policies__` | `<base>__hash__` | hex SHA-256 зашифрованных байтов политики |
| `__policies__` | `<base>__hmac__` | hex HMAC-SHA256(policyKey, encrypted policy bytes) |
| `__audit__/scheme/namespace` | event UUID | JSON — аудит Event |
| `__audit__/scheme/namespace` | `__chain_index__` | JSON — chainIndex |
| `scheme/namespace` | key string | msgpack — Secret struct |
### Secret struct (msgpack)```go
type Secret struct {
Ciphertext []byte `msgpack:"ct"`
EncryptedMeta []byte `msgpack:"em,omitempty"`
SchemaVersion int `msgpack:"sv"` // always 1
}
Структура Event использует отдельные поля маршрутизации в открытом виде (Scheme,
Namespace) наряду с зашифрованными полями полезной нагрузки (EncScheme, EncNamespace,
EncDetails). Контрольные суммы вычисляются по полям маршрутизации в открытом виде и зашифрованным байтам EncDetails, поэтому целостность цепочки можно проверить на трёх уровнях без какого-либо ключа:
| Уровень | Имеет | Может проверить |
|---|---|---|
| Публичный | Ничего | Цепочка контрольных сумм SHA-256 (обнаруживает подделку и вставку) |
| Владелец ключа аудита | auditEncKey |
Пример: Сотрудник отдела комплаенса получает только auditEncKey. Он может проверить полную цепочку HMAC при смене ключей и прочитать все детали события, но не может расшифровать никакие секретные значения. Сторонний наблюдатель, имеющий только файл базы данных, всё ещё может обнаружить, было ли какое-либо событие изменено или вставлено после факта.
Соль KDF хранится в виде закодированной в msgpack структуры SaltStore под мета-ключом salt. Каждая ротация соли добавляет новую запись SaltEntry и увеличивает CurrentVersion. Старые записи сохраняются как аудиторский след. SaltStore хранится незашифрованным — см. Решения по безопасности.
Rotate записывает WAL перед тем, как коснуться любой записи. WAL содержит WrappedOldKey: ключ до ротации, зашифрованный новым главным ключом. После сбоя старая парольная фраза исчезает; WrappedOldKey — единственный корректный способ перенести старый ключ через границу. При UnlockDatabase, если WAL присутствует, новый главный ключ расшифровывает WrappedOldKey, и ротация возобновляется с курсора WAL. Сам WAL шифруется ключом policyEncKey.
Каждая значимая операция добавляет в цепочку аудита корзины событие, устойчивое к подделке. Целостность цепочки обеспечивается двумя механизмами.
Контрольная сумма. SHA-256 от prevChecksum, ID, BucketID, Scheme, Namespace,
EncDetails, EventType и Timestamp. Использование Scheme/Namespace в открытом виде (всегда сохраняемых вместе с зашифрованными формами) гарантирует, что контрольная сумма стабильна при всех путях загрузки. EncDetails обеспечивает целостность зашифрованной полезной нагрузки.
HMAC. HMAC-SHA256 по всем полям, включая Seq. Злоумышленник, который может записывать в базу данных, но не знает ключа аудита, не может создать корректный HMAC. VerifyIntegrity проверяет оба уровня для каждого события.
Граница эпохи ротации ключей. При Rotate в каждую активную цепочку добавляется контрольное событие, несущее отпечатки как исходящего, так и входящего ключа аудита. Контрольное событие подписано исходящим ключом. Аудиторы, владеющие любым ключом эпохи, могут восстановить последующие ключи эпохи из поля wrapped_new_key и проверить непрерывность HMAC по всей цепочке.
Автоматическая очистка. Когда в Config установлен AuditPruneInterval, jack.Scheduler запускается периодически и вызывает PruneEvents для каждой зарегистрированной корзины. Корзины LevelHSM и LevelRemote никогда не очищаются, независимо от этой настройки.
Jack — опциональная библиотека для супервизии процессов. Если JackConfig предоставлен через WithJack, хранитель автоматически активирует фоновые компоненты:
LevelAdminWrapped после AutoLockInterval. Корзины LevelPasswordOnly остаются разблокированными, чтобы фоновые задания продолжались без перерыва. Паттерн с одной блокировкой на запись внутри задачи цикла устраняет состояние гонки RUnlock→Lock, присутствовавшее в более ранних реализациях.LevelAdminWrapped на основе TTL.jack.Doctor.PruneEvents на всех корзинах, кроме HSM.Если JackConfig не предоставлен, хранитель работает без этих фоновых задач. Хранитель никогда не вызывает pool.Shutdown — жизненный цикл пула принадлежит вызывающему коду.
x/keepcmd предоставляет переиспользуемые операции хранителя, независимые от какой-либо CLI-платформы. Встраивайте его в своё приложение, чтобы получить типизированное, тестируемое управление секретами без подключения двоичного файла CLI.```go
import "github.com/agberohq/keeper/x/keepcmd"
cmds := &keepcmd.Commands{ Store: func() (*keeper.Keeper, error) { return security.KeeperOpen(cfg) // your own config }, Out: keepcmd.PlainOutput{}, NoClose: false, // true in REPL / session contexts }
cmds.List() // all keys: scheme://namespace/key cmds.List("vault") // all keys in scheme vault cmds.List("vault", "system") // all keys in vault://system cmds.Get("vault://system/jwt_secret") cmds.Set("vault://system/jwt_secret", "newsecret", keepcmd.SetOptions{}) cmds.Rotate(newPassphraseBytes) // caller resolved the passphrase — no prompter dependency cmds.RotateSalt(currentPassBytes) // same
`keepcmd` никогда не вызывает `prompter` и не читает из stdin. Разрешение парольной фразы
полностью лежит на вызывающем коде — это обеспечивает безопасность пакета в
контекстах безголовых серверов.
`NoClose: true` предотвращает вызов `store.Close()` после каждой
операции. Используйте это в контекстах REPL/сессий, где одно хранилище используется
во множестве вызовов.
---
## x/keephandler
`x/keephandler` монтирует HTTP-эндпоинты keeper на любой `net/http` mux. Нет
внешней зависимости от маршрутизатора — он использует маршрутизацию по методу и шаблону из Go 1.22+ с
stdlib `http.ServeMux`.```go
import "github.com/agberohq/keeper/x/keephandler"
keephandler.Mount(mux, store,
keephandler.WithPrefix("/api/keeper"),
keephandler.WithGuard(func(w http.ResponseWriter, r *http.Request, route string) bool {
if !acl.Allow(r.Header.Get("X-Principal"), route) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return false
}
return true
}),
keephandler.WithHooks(
keephandler.Hook{
Route: keephandler.RouteGet,
CaptureBody: false,
After: func(r *http.Request, status int, _ []byte) {
audit.Log(r.Context(), route, status)
},
},
),
keephandler.WithEncoder(func(w http.ResponseWriter, route string, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]any{
"ok": status < 400,
"route": route,
"data": data,
})
}),
keephandler.WithRoutes(func(m *http.ServeMux) {
m.HandleFunc("POST /api/keeper/totp/{user}", myTOTPHandler)
}),
)
BeforeFunc возвращает (allow bool, err error).
(true, nil) — разрешить выполнение запроса.(false, nil) — прервать; хук уже записал полный ответ.(false, err) — прервать; фреймворк записывает 500 с помощью err.Error(). Хук не должен ничего записать в w.Hook.CaptureBody bool управляет тем, получает ли AfterFunc тело ответа. false (по умолчанию) стоит одного легковесного обёртки statusWriter; true буферизирует полное тело в bytes.Buffer для AfterFunc — одно выделение памяти на запрос.
Хуки выполняются в порядке регистрации. Несколько вызовов WithHooks суммируются. Используется только первый хук, зарегистрированный для данного имени маршрута — последующие регистрации для того же маршрута игнорируются.
store, err := keeper.New(keeper.Config{ DBPath: "/var/lib/agbero/keeper.db", AutoLockInterval: 30 * time.Minute, EnableAudit: true, AuditPruneInterval: 24 * time.Hour, AuditPruneKeepLastN: 10_000, AuditPruneOlderThan: 90 * 24 * time.Hour, DBLatencyThreshold: 200 * time.Millisecond, Logger: logger, }, keeper.WithJack(keeper.JackConfig{ Pool: jackPool, Shutdown: jackShutdown, })) defer store.Close()
// Shorthand (wraps DeriveMaster + UnlockDatabase): if err := store.Unlock([]byte(os.Getenv("KEEPER_PASSPHRASE"))); err != nil { log.Fatal(err) // ErrInvalidPassphrase on wrong passphrase }
`UnlockDatabase` выполняет следующее по порядку:
1. Выводит и активирует ключ подписи audit HMAC
2. Выводит и активирует ключ policy HMAC
3. Выводит и активирует `policyEncKey` и `auditEncKey`
4. Очищает и перезагружает `schemeRegistry` (расшифровывает все blob-объекты политик)
5. Возобновляет любую прерванную ротацию WAL
6. Обновляет теги policy HMAC
7. Загружает все DEK корзины `LevelPasswordOnly` в Envelope
8. Запускает фоновые задачи (цикл миграции, автоматическая блокировка, health patients)
### Корзина LevelPasswordOnly — полный жизненный цикл```go
err := store.CreateBucket("vault", "system", keeper.LevelPasswordOnly, "init")
store.Set("vault://system/jwt_secret", []byte("supersecret"))
val, err := store.Get("vault://system/jwt_secret")
// Namespaced convenience wrappers
store.SetNamespaced("admin", "jwt_secret", secretBytes)
val, err = store.GetNamespaced("admin", "jwt_secret")
err := store.CreateBucket("finance", "payroll", keeper.LevelAdminWrapped, "ops-team") err = store.AddAdminToPolicy("finance", "payroll", "alice", []byte("alicepass"))
store.SetNamespacedFull("finance", "payroll", "salary_key", []byte("AES256..."))
store.LockBucket("finance", "payroll") err = store.UnlockBucket("finance", "payroll", "bob", []byte("bobpass")) // ErrAuthFailed — does not distinguish wrong password from unknown admin (CWE-204)
err = store.RevokeAdmin("finance", "payroll", "alice") err = store.RotateAdminWrappedDEK("finance", "payroll", "bob", []byte("bobpass"))
needs, err := store.NeedsAdminRekey("finance", "payroll")
### LevelHSM / LevelRemote бакеты```go
import (
"github.com/agberohq/keeper/pkg/hsm"
"github.com/agberohq/keeper/pkg/remote"
)
// SoftHSM — testing only
provider, _ := hsm.NewSoftHSM()
store.RegisterHSMProvider("secure", "keys", provider)
store.CreateBucket("secure", "keys", keeper.LevelHSM, "ops")
// Vault Transit
cfg := remote.VaultTransit("https://vault.corp:8200", vaultToken, "my-key")
cfg.TLSClientCert = "/etc/keeper/client.crt"
cfg.TLSClientKey = "/etc/keeper/client.key"
provider, _ = remote.New(cfg)
store.RegisterHSMProvider("tenant", "secrets", provider)
store.CreateBucket("tenant", "secrets", keeper.LevelRemote, "ops")
// Export the audit encryption key to allow a third-party auditor to decrypt // event details without access to the master passphrase. auditKey, err := store.ExportAuditKey() defer zero.Bytes(auditKey)
events, err := auditStore.LoadChain("vault", "system", auditKey)
### Ротация ключей```go
// Rotate passphrase — crash-safe WAL, resumes on next Unlock if interrupted
store.Rotate([]byte("new-passphrase"))
// Rotate KDF salt — re-derives master key, re-encrypts LevelPasswordOnly
store.RotateSalt([]byte("current-passphrase"))
err := store.CompareAndSwapNamespacedFull("vault", "system", "counter", []byte("old"), []byte("new")) // ErrCASConflict if current value does not match old
### Резервное копирование```go
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath
Все sentinel-ошибки работают с errors.Is и errors.As. Стек вызовов
захватывается в момент создания с помощью github.com/olekukonko/errors.
ErrAuthFailed объединяет все сбои UnlockBucket (CWE-204 / CVSS 5.3). Как
неизвестный ID администратора, так и неверный пароль возвращают ErrAuthFailed. Это предотвращает
перечисление ID администратора по времени или сравнению строк ошибок. RevokeAdmin сохраняет
ErrAdminNotFound, поскольку это административная операция в
уже разблокированном хранилище. Сравнение за константное время для проверки наличия ID администратора
намеренно опущено. Злоумышленник, способный измерить субмикросекундные различия
в поиске по bucket-ам bbolt, должен иметь доступ к локальной файловой системе — после чего
он может напрямую прочитать bucket политик. Модель угроз предполагает, что файл базы данных
может быть скомпрометирован; защита от удалённого перечисления по времени является основной задачей.
Argon2id доминирует во временных характеристиках. Argon2id занимает 200–500 мс на типовом оборудовании. Различия после вычисления на несколько порядков меньше и не могут быть измерены удалённо. Никакого искусственного выравнивания не применяется.
DEK извлекается внутри границ транзакции CAS. CompareAndSwapNamespacedFull
извлекает DEK bucket внутри транзакции записи bbolt, устраняя окно,
в котором одновременный вызов Rotate мог бы изменить DEK между извлечением и
использованием.
Парольная фраза никогда не хранится как строка Go в HTTP-обработчике. Все три
поля парольной фразы (passphrase, new_passphrase) декодируются из JSON
непосредственно в []byte через извлечение сырой карты, сохраняя массив строки
вне долгоживущей кучи. Копия []byte затирается с помощью wipeBytes после использования.
В CLI нет флага --passphrase. Флаги видны в выводе ps и истории
команд оболочки. CLI принимает парольную фразу только из переменной KEEPER_PASSPHRASE или
интерактивного приглашения без эха.
Секретные значения в REPL никогда не видны. set <key> в REPL без
встроенного значения использует term.ReadPassword — оно не отображается в
прокрутке терминала, истории команд или ps. Встроенное значение (set key value) может быть
передано для нечувствительных данных, если это удобно.
SaltStore намеренно не зашифровано. Соль KDF должна быть читаема
до UnlockDatabase для вывода мастер-ключа. policyEncKey (используемый для
шифрования всех остальных метаданных) сам выводится из мастер-ключа — шифрование
соли с помощью policyEncKey было бы циклическим. Соль KDF обеспечивает уникальность,
а не конфиденциальность; нет никакой ценности безопасности в её шифровании.
Ключи bucket-ов политик хэшируются, а не хранятся в открытом виде. Ключи политик на диске —
это hex(SHA-256("схема:пространство_имён"))[:32] — 128 бит ключевого пространства — а не
читаемые строки. Офлайн-злоумышленник, читающий файл bbolt, не сможет перечислить
имена bucket-ов без расшифровки blob-ов политик.
Шифрование метаданных использует тот же интерфейс шифрования, что и секреты. Все
операции с policyEncKey и auditEncKey проходят через s.config.NewCipher(key)
— тот же интерфейс crypt.Cipher, настроенный для секретных значений. Выбор пользователем
шифра (AES-256-GCM для FIPS 140, XChaCha20-Poly1305 по умолчанию) автоматически применяется
к шифрованию политик, WAL и аудита. Ни один путь кода не содержит жёстко заданный алгоритм.
Bucket-ы LevelHSM и LevelRemote пропускаются во время ротации мастер-ключа.
reencryptAllWithKey и RotateSalt явно пропускают эти bucket-ы. DEK
управляется провайдером; ротация мастер-соли на него не влияет.
Безопасная при сбоях ротация с WrappedOldKey. Rotate записывает WAL перед
любыми изменениями записей. WAL содержит WrappedOldKey: мастер-ключ до ротации,
зашифрованный новым мастер-ключом. После сбоя UnlockDatabase расшифровывает
WrappedOldKey, используя проверенный новый ключ, и возобновляет ротацию с курсора.
| Полная цепочка + расшифровка Scheme/Namespace/Details |
| Оператор | Главная парольная фраза | Всё |
| Метод | Путь | Описание |
|---|
POST | {prefix}/unlock | Разблокировать хранилище с помощью парольной фразы |
POST | {prefix}/lock | Заблокировать хранилище |
GET | {prefix}/status | Состояние блокировки — безопасно опрашивать без аутентификации |
GET | {prefix}/keys | Список всех секретных ключей |
GET | {prefix}/keys/{key} | Получить секретное значение |
POST | {prefix}/keys | Сохранить секрет (JSON или multipart) |
DELETE | {prefix}/keys/{key} | Удалить секрет |
POST | {prefix}/rotate | Сменить мастер-пароль |
POST | {prefix}/rotate/salt | Сменить соль KDF |
GET | {prefix}/backup | Потоковая передача снимка базы данных |
| Ошибка | Значение |
|---|
ErrStoreLocked | Попытка выполнения операции, пока хранилище заблокировано |
ErrInvalidPassphrase | Неверная мастер-парольная фраза |
ErrAuthFailed | Любой сбой UnlockBucket — не различает неверный пароль и неизвестный ID администратора (CWE-204) |
ErrKeyNotFound | Секретный ключ не существует |
ErrBucketLocked | Bucket не был разблокирован |
ErrPolicyImmutable | Вторая политика для существующего bucket |
ErrPolicyNotFound | Нет политики для данной схемы/пространства имён |
ErrAdminNotFound | ID администратора отсутствует в политике — только для RevokeAdmin |
ErrHSMProviderNil | HSM/Remote bucket создан без зарегистрированного провайдера |
ErrCheckLatency | Задержка чтения БД превысила DBLatencyThreshold |
ErrCASConflict | Текущее значение не совпадает с ожидаемым в CompareAndSwap |
ErrSecurityDowngrade | Перемещение между bucket-ами с более высокого уровня безопасности на более низкий |
ErrAlreadyUnlocked | UnlockDatabase вызван для уже разблокированного хранилища |
ErrMasterRequired | UnlockDatabase вызван с nil или уничтоженным Master |
ErrChainBroken | Сбой проверки целостности цепочки аудита |
ErrMetadataDecrypt | Не удалось расшифровать зашифрованные метаданные |
ErrPolicySignature | Сбой проверки HMAC политики — запись была подделана |
| Пакет | Назначение |
|---|
go.etcd.io/bbolt | Встраиваемое хранилище ключ-значение |
golang.org/x/crypto | Argon2id, XChaCha20-Poly1305, HKDF, scrypt |
github.com/awnumar/memguard | Безопасный анклав ключей в памяти (мастер-ключ, DEK) |
github.com/vmihailenco/msgpack/v5 | Бинарная сериализация секретов и политик |
github.com/olekukonko/jack | Супервизор процессов (опциональная интеграция Jack) |
github.com/olekukonko/ll | Структурированное логирование |
github.com/olekukonko/errors | Sentinel-ошибки со стеком вызовов |
github.com/olekukonko/zero | Безопасное зануление байтовых срезов |
github.com/olekukonko/prompter | Приглашения терминала без эха (только CLI) |
github.com/integrii/flaggy | Разбор флагов CLI (только cmd/keeper) |
golang.org/x/term | Определение TTY и чтение пароля без эха (только CLI) |