Go DevOps Vault Initialization and Keys Security

預備知識

在閱讀下方內容之前,建議先對 Vault 架構有個基本了解,相關官方文章如下:

  1. Vault Architecture
  2. Vault Key Rotation
  3. Vault Seal/Unseal

另外, Barrier 這個元件在 Vault 相當重要,在 Vault Architecture 頁面中有提及,可以先了解其用意,再來看它是如何被實現。

問題

Vault 在進行 unseal 流程時,其中最重要的就是將加密過的 master key 解密,接著再利用 master key 解出 encryption Key,並且建立起 barrier 防護,讓 storage backend 的資料可以安全地來往於 Vault server 與 storage backend 之間。

其中,基於安全考量,我們想要確認幾件事情:

  1. Encryption Key 負責加解密 Vault 核心資訊,所以它是如何產生的?
  2. Encryption Key 的安全性?
  3. Barrier 與 storage backend 之間的安全性是如何被實作的?

以下從 source code 逐步解析這些過程,並且找出上述幾個問題的解答。

環境

Vault 1.5.3 本文主要在說明 Vault 初次 initialize 的流程

Vault Initialization

要知道 encryption Key 是如何產生的,就從 Vault 初始化步驟著手。

vault

當 Vault 啟動階段,會先根據使用者的 configuration file 進行參數設定,然後進入到 Vault system 初始化流程。而初始化後的 Vault 是屬於 seal 階段,此時如果使用者設定的 seal provider 支持自動 unseal,就會進行 unseal,最後啟動完成。

Seal Initialization

在 concept 文中有提到,Vault 會對 master key 進行加解密,以確保 master key 安全性。而 seal 就是提供此加解密流程的 object。

The seal configures the seal type to use for additional data protection, such as using HSM or Cloud KMS solutions to encrypt and decrypt the master key.

Seal 在 source code 為 interface 型態,而目前提供 defaultSealautoSeal 兩種實作 struct。透過此抽象層,我們就能應用不同 seal provider。關於 Vault 實作不同 Seal provider,可以參考 Go-KMS-Wrapping repository,而預設的 seal provider 是 Shamir seal,也就是利用 shared shamir keys 來加解密。

Seal Initialization 會呼叫底層 seal provider 所提供的 Init function,其行為根據各 provider 需求而有所不同,也有可能這階段什麼都不需要做(例如 GCP seal provider)。

Barrier Initialization

Barrier 負責加解密往來於 Vault server 和 storage backend 的資料,因此它的初始化步驟就相當的重要。Barrier 也被抽象成一個 interface,實際實作的 struct 為 AESGCMBarrier

 1type AESGCMBarrier struct {
 2    backend physical.Backend
 3
 4    l      sync.RWMutex
 5    sealed bool
 6
 7    // keyring is used to maintain all of the encryption keys, including
 8    // the active key used for encryption, but also prior keys to allow
 9    // decryption of keys encrypted under previous terms.
10    keyring *Keyring
11
12    // cache is used to reduce the number of AEAD constructions we do
13    cache     map[uint32]cipher.AEAD
14    cacheLock sync.RWMutex
15
16    // currentAESGCMVersionByte is prefixed to a message to allow for
17    // future versioning of barrier implementations. It's var instead
18    // of const to allow for testing
19    currentAESGCMVersionByte byte
20
21    initialized atomic.Bool
22}

首先,在初始化之前,會先產生 master key。

1// vault/init.go
2// BarrierKey is master key. barrierKeyShares is only used for testing. 
3barrierKey, barrierKeyShares, err := c.generateShares(barrierConfig)

接著,進入到 AESGCMBarrierInitialize function:

 1func (b *AESGCMBarrier) Initialize(ctx context.Context, key, sealKey []byte, reader io.Reader) error {
 2    // ...
 3    // Generate encryption key
 4    encrypt, err := b.GenerateKey(reader)
 5    if err != nil {
 6        return errwrap.Wrapf("failed to generate encryption key: {{err}}", err)
 7    }
 8
 9    // Create a new keyring, install the keys
10    keyring := NewKeyring()
11    keyring = keyring.SetMasterKey(key)
12    keyring, err = keyring.AddKey(&Key{
13        Term:    1,
14        Version: 1,
15        Value:   encrypt,
16    })
17    if err != nil {
18        return errwrap.Wrapf("failed to create keyring: {{err}}", err)
19    }
20
21    err = b.persistKeyring(ctx, keyring)
22    if err != nil {
23        return err
24    }
25
26    // ...
27}

就可以看到產生 encryption key 的 function b.GenerateKey(reader)。 其中,這樣的設計很有巧思,reader(io.Reader) parameter 在 Vault 中又稱作 secureRandomReader,實際上就是 random number generator (PNG)。在預設情況下,會使用 OS 本身所提供的 random number generator,但使用者也能根據需求來使用自己想要的 generator,例如 hardware / true number generator。

此外,這邊也引入 Keyring 來建立起 master key 與 encryption key 的關係。

1type Keyring struct {
2	masterKey  []byte
3	keys       map[uint32]*Key
4	activeTerm uint32
5}

每個 keyring 由一組 master key 與多組 encryption key 結合而成,keyring 結構設計對於 keys 來說十分重要,除了方便 Barrier 管理多組 encryption key 之外,同時也實現 master key 與 encryption key 的更新方法。

產生出 encryption key 之後,透過 persistKeyring 把 encryption key 使用 master key AES 加密後存到 storage 中。

 1func (b *AESGCMBarrier) persistKeyring(ctx context.Context, keyring *Keyring) error {
 2    // Create the keyring entry
 3    keyringBuf, err := keyring.Serialize()
 4    defer memzero(keyringBuf)
 5    if err != nil {
 6        return errwrap.Wrapf("failed to serialize keyring: {{err}}", err)
 7    }
 8
 9    // Create the AES-GCM
10    gcm, err := b.aeadFromKey(keyring.MasterKey())
11    if err != nil {
12        return err
13    }
14
15    // Encrypt the barrier init value
16    value, err := b.encrypt(keyringPath, initialKeyTerm, gcm, keyringBuf)
17    if err != nil {
18        return err
19    }
20
21    // Create the keyring physical entry
22    pe := &physical.Entry{
23        Key:   keyringPath,
24        Value: value,
25    }
26    if err := b.backend.Put(ctx, pe); err != nil {
27        return errwrap.Wrapf("failed to persist keyring: {{err}}", err)
28    }
29
30    // Serialize the master key value
31    key := &Key{
32        Term:    1,
33        Version: 1,
34        Value:   keyring.MasterKey(),
35    }
36    keyBuf, err := key.Serialize()
37    defer memzero(keyBuf)
38    if err != nil {
39        return errwrap.Wrapf("failed to serialize master key: {{err}}", err)
40    }
41
42    // Encrypt the master key
43    activeKey := keyring.ActiveKey()
44    aead, err := b.aeadFromKey(activeKey.Value)
45    if err != nil {
46        return err
47    }
48    value, err = b.encrypt(masterKeyPath, activeKey.Term, aead, keyBuf)
49    if err != nil {
50        return err
51    }
52
53    // Update the masterKeyPath for standby instances
54    pe = &physical.Entry{
55        Key:   masterKeyPath,
56        Value: value,
57    }
58    if err := b.backend.Put(ctx, pe); err != nil {
59        return errwrap.Wrapf("failed to persist master key: {{err}}", err)
60    }
61    return nil
62}

Encrypt the master key using the encryption key

上述 source code 可以看到一個很奇怪的地方,就是 Encrypt the master key ,文件說 master key 是由 seal provider 所加密,為什麼這裡的 master key 是由 encryption key 加密呢?

我們在 vault/barrier.go 可以找到答案:

This is encrypted by the latest key in the keyring. This is only used by standby instances to handle the case of a rekey. If the active instance does a rekey, the standby instances can no longer reload the keyring since they have the old master key.

這段把 master key 使用 encryption key 加密,並且儲存在 masterKeyPath,其用途僅是針對 rekey 流程。而把 master key 使用 seal provider 加密後儲存則是在下一段 code 中出現。

 1// vault/init.go
 2switch c.seal.StoredKeysSupported() {
 3    case seal.StoredKeysSupportedShamirMaster:
 4        // store the master key(barrierKey)
 5		keysToStore := [][]byte{barrierKey}
 6		if err := c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(sealKey); err != nil {
 7			c.logger.Error("failed to set seal key", "error", err)
 8			return nil, errwrap.Wrapf("failed to set seal key: {{err}}", err)
 9		}
10		if err := c.seal.SetStoredKeys(ctx, keysToStore); err != nil {
11			c.logger.Error("failed to store keys", "error", err)
12			return nil, errwrap.Wrapf("failed to store keys: {{err}}", err)
13		}
14		results.SecretShares = sealKeyShares
15	case seal.StoredKeysSupportedGeneric:
16		keysToStore := [][]byte{barrierKey}
17		if err := c.seal.SetStoredKeys(ctx, keysToStore); err != nil {
18			c.logger.Error("failed to store keys", "error", err)
19			return nil, errwrap.Wrapf("failed to store keys: {{err}}", err)
20		}
21	default:
22		// We don't support initializing an old-style Shamir seal anymore, so
23		// this case is only reachable by tests.
24		results.SecretShares = barrierKeyShares
25	}

Barrier Unseal

所謂的 Vault Unseal,正確來說應該是 Barrier Unseal,實際流程為:

  1. 從 storage backend 中取出加密過的 keyring(bytes)
  2. 使用 master key 解密 keyring(bytes)
  3. 組成 keyring struct
  4. Unseal 完成

Barrier 需要得到正確的 keyring 才能用其中的 encryption key 來加解密 storage 的資料,因此 unseal 最重要的目的就是取得 keyring。

Barrier Workflow

了解 barrier 初始化過程後,我們來看一下資料是如何進出 Barrier 與 storage backend 的。

vault

實際實作有三層 Layer:LogicalBarrierPhysical Layers。當一個 request 進來,會根據 URL path 來選擇(route) secret engine 和所要執行的 operation,接著如果有資料需要更新儲存到 storage backend,就會呼叫 logical.Storage put 方法。

而 logical.Storage 只是個抽象層,實際上是執行 barrier 的 put 方法,Barrier 會先將 data 使用 keyring 中最新的 encryption key 加密後,再呼叫 physical 的 put 方法來儲存,反之亦然。

Generate a Root Token

在 Vault 初始化時,會需要一組 root token 以供使用者進行初步驗證與設定。Root token 是由 base 62 組合而成的 24bytes 長度字串,當產生出 token 之後,會需要將 token 存入 storage 中,因此在這個階段 barrier 須維持 unseal 狀態。

Entropy of Token Generation

產生 token 的 source code 有個很有意思的地方,就是討論 token entropy,Information Entropy 介紹可參考 Entropy (Information Theory)

 1const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
 2const csLen = byte(len(charset))
 3
 4// Resulting entropy is ~5.95 bits/character.
 5func RandomWithReader(length int, reader io.Reader) (string, error) {
 6    if length == 0 {
 7        return "", nil
 8    }
 9    output := make([]byte, 0, length)
10
11    // Request a bit more than length to reduce the chance
12    // of needing more than one batch of random bytes
13    batchSize := length + length/4
14
15    for {
16        buf, err := uuid.GenerateRandomBytesWithReader(batchSize, reader)
17        if err != nil {
18            return "", err
19        }
20
21        for _, b := range buf {
22            // Avoid bias by using a value range that's a multiple of 62
23            if b < (csLen * 4) {
24                output = append(output, charset[b%csLen])
25
26                if len(output) == length {
27                    return string(output), nil
28                }
29            }
30        }
31    }
32}

另一個可以注意的地方是,除了使用 crypto/rand 產生出隨機 byte 之外,由於 token 有特定的 charset,避免某個 char 的分配數量不均,因此還加入 b < (csLen * 4) 此判斷,是一個很簡單但是又易被忽略的地方。

uuid.GenerateRandomBytesWithReader(batchSize, reader),此 reader 在預設情況下為 crypto/rand。

Barrier Seal

當所有需要 unseal 狀態的指令動作都執行完之後,最後一個階段就是再次將 barrier 切換到 seal 狀態,初始化完成。

問題討論

回到一開始我們所要討論的問題:

1. Encryption Key 負責加解密 Vault 核心資訊,所以它是如何產生的?

不論是 encryption Key 或是 master key,皆是 256bit ,以供 AES-256 加密算法使用,安全度也較高。至於 random source 是利用 OS 的 random number generator,其實作方式則根據各 OS 而有所不同。

2. Encryption Key 的安全性

上述有提到,master key 和 encryption Key 的產生仰賴 OS random number generator,而此 random number generator (PNG) 通常為 entropy input + algorithm true random number generator (TPNG or HRNG),對於需要高度資安考量的應用場景來說,會有安全疑慮。

以 Linux 來說,entropy input 來自硬體 event(例如 interrupt),其 entropy source 受到當前硬體環境、行為等影響,導致無法保證 PNG 所產生的亂數品質。

延伸閱讀:Documentation and Analysis of the Linux Random Number Generator

當然,Vault 本身也針對此問題導入 HSM module(Vault HSM Integration - Entropy Augmentation),讓使用者可以設定 master key 或是 encryption Key 的生成是來自外部的安全模組,保證 encrption Key 的隨機性。

3. Barrier 與 storage backend 之間的安全性是如何被實作的?

當資料需要儲存到 storage backend 時,barrier 應為 unseal 狀態,也就是 barrier 擁有 keyring ,並且使用 keyring 中最新一把 encryption key 對資料進行加解密。keyring 可以存在多把新舊版本的 encryption key,以確保 encryption key 可以被更新,而資料也可以被前一版本的 encryption key 所解密。