Fixes #1605: Customizable metadata key on encryption key providers (#2080)

Signed-off-by: AbstractionFactory <179820029+abstractionfactory@users.noreply.github.com>
This commit is contained in:
AbstractionFactory 2024-10-30 19:52:23 +01:00 committed by GitHub
parent 0550798ea8
commit 9d842aa920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 571 additions and 81 deletions

View File

@ -7,6 +7,7 @@ UPGRADE NOTES:
NEW FEATURES:
ENHANCEMENTS:
* State encryption key providers now support customizing the metadata key via `encrypted_metadata_alias` ([#1605](https://github.com/opentofu/opentofu/issues/1605))
* Added user input prompt for static variables. ([#1792](https://github.com/opentofu/opentofu/issues/1792))
* Added `-show-sensitive` flag to tofu plan, apply, state-show and output commands to display sensitive data in output. ([#1554](https://github.com/opentofu/opentofu/pull/1554))
* Improved performance for large graphs when debug logs are not enabled. ([#1810](https://github.com/opentofu/opentofu/pull/1810))

View File

@ -23,23 +23,25 @@ const (
)
type baseEncryption struct {
enc *encryption
target *config.TargetConfig
enforced bool
name string
encMethods []method.Method
encMeta map[keyprovider.Addr][]byte
staticEval *configs.StaticEvaluator
enc *encryption
target *config.TargetConfig
enforced bool
name string
encMethods []method.Method
inputEncMeta map[keyprovider.MetaStorageKey][]byte
outputEncMeta map[keyprovider.MetaStorageKey][]byte
staticEval *configs.StaticEvaluator
}
func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string, staticEval *configs.StaticEvaluator) (*baseEncryption, hcl.Diagnostics) {
base := &baseEncryption{
enc: enc,
target: target,
enforced: enforced,
name: name,
encMeta: make(map[keyprovider.Addr][]byte),
staticEval: staticEval,
enc: enc,
target: target,
enforced: enforced,
name: name,
inputEncMeta: make(map[keyprovider.MetaStorageKey][]byte),
outputEncMeta: make(map[keyprovider.MetaStorageKey][]byte),
staticEval: staticEval,
}
// Setup the encryptor
//
@ -69,16 +71,16 @@ func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bo
// This performs a e2e validation run of the config -> methods flow. It serves as a validation step and allows us to return detailed
// diagnostics here and simple errors in the decrypt function below.
//
methods, diags := base.buildTargetMethods(base.encMeta)
methods, diags := base.buildTargetMethods(base.inputEncMeta, base.outputEncMeta)
base.encMethods = methods
return base, diags
}
type basedata struct {
Meta map[keyprovider.Addr][]byte `json:"meta"`
Data []byte `json:"encrypted_data"`
Version string `json:"encryption_version"` // This is both a sigil for a valid encrypted payload and a future compatibility field
Meta map[keyprovider.MetaStorageKey][]byte `json:"meta"`
Data []byte `json:"encrypted_data"`
Version string `json:"encryption_version"` // This is both a sigil for a valid encrypted payload and a future compatibility field
}
func IsEncryptionPayload(data []byte) (bool, error) {
@ -92,10 +94,10 @@ func IsEncryptionPayload(data []byte) (bool, error) {
return es.Version != "", nil
}
func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}) ([]byte, error) {
func (base *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}) ([]byte, error) {
// buildTargetMethods above guarantees that there will be at least one encryption method. They are not optional in the common target
// block, which is required to get to this code.
encryptor := s.encMethods[0]
encryptor := base.encMethods[0]
if unencrypted.Is(encryptor) {
return data, nil
@ -103,12 +105,12 @@ func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}
encd, err := encryptor.Encrypt(data)
if err != nil {
return nil, fmt.Errorf("encryption failed for %s: %w", s.name, err)
return nil, fmt.Errorf("encryption failed for %s: %w", base.name, err)
}
es := basedata{
Version: encryptionVersion,
Meta: s.encMeta,
Meta: base.outputEncMeta,
Data: encd,
}
jsond, err := json.Marshal(enhance(es))
@ -120,11 +122,11 @@ func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}
}
// TODO Find a way to make these errors actionable / clear
func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, error) {
es := basedata{}
err := json.Unmarshal(data, &es)
func (base *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, error) {
inputData := basedata{}
err := json.Unmarshal(data, &inputData)
if len(es.Version) == 0 || err != nil {
if len(inputData.Version) == 0 || err != nil {
// Not a valid payload, might be already decrypted
verr := validator(data)
if verr != nil {
@ -140,20 +142,25 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
}
// Yep, it's already decrypted
for _, method := range s.encMethods {
for _, method := range base.encMethods {
if unencrypted.Is(method) {
return data, nil
}
}
return nil, fmt.Errorf("encountered unencrypted payload without unencrypted method configured")
}
if es.Version != encryptionVersion {
return nil, fmt.Errorf("invalid encrypted payload version: %s != %s", es.Version, encryptionVersion)
// This is not actually used, only the map inside the Meta parameter is. This is because we are passing the map
// around.
outputData := basedata{
Meta: make(map[keyprovider.MetaStorageKey][]byte),
}
// TODO Discuss if we should potentially cache this based on a json-encoded version of es.Meta and reduce overhead dramatically
methods, diags := s.buildTargetMethods(es.Meta)
if inputData.Version != encryptionVersion {
return nil, fmt.Errorf("invalid encrypted payload version: %s != %s", inputData.Version, encryptionVersion)
}
// TODO Discuss if we should potentially cache this based on a json-encoded version of inputData.Meta and reduce overhead dramatically
methods, diags := base.buildTargetMethods(inputData.Meta, outputData.Meta)
if diags.HasErrors() {
// This cast to error here is safe as we know that at least one error exists
// This is also quite unlikely to happen as the constructor already has checked this code path
@ -166,13 +173,13 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
// Not applicable
continue
}
uncd, err := method.Decrypt(es.Data)
uncd, err := method.Decrypt(inputData.Data)
if err == nil {
// Success
return uncd, nil
}
// Record the failure
errs = append(errs, fmt.Errorf("attempted decryption failed for %s: %w", s.name, err))
errs = append(errs, fmt.Errorf("attempted decryption failed for %s: %w", base.name, err))
}
// This is good enough for now until we have better/distinct errors

View File

@ -44,9 +44,11 @@ func (c *EncryptionConfig) GetKeyProvider(kpType, kpName string) (KeyProviderCon
// KeyProviderConfig describes the terraform.encryption.key_provider.* block you can use to declare a key provider for
// encryption. The Body field will contain the remaining undeclared fields the key provider can consume.
type KeyProviderConfig struct {
Type string `hcl:"type,label"`
Name string `hcl:"name,label"`
Body hcl.Body `hcl:",remain"`
// EncryptedMetadataAlias contains the key to identify the metadata by.
EncryptedMetadataAlias string `hcl:"encrypted_metadata_alias,optional"`
Type string `hcl:"type,label"`
Name string `hcl:"name,label"`
Body hcl.Body `hcl:",remain"`
}
// Addr returns a keyprovider.Addr from the current configuration.

View File

@ -0,0 +1,81 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package encryption
import (
"testing"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/encryption/config"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/pbkdf2"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/xor"
"github.com/opentofu/opentofu/internal/encryption/method/aesgcm"
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
"github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry"
)
func TestDualCustody(t *testing.T) {
// Note: the XOR provider is not available in final OpenTofu builds because its security constraints have not
// been properly evaluated. The code below doesn't work in OpenTofu and is for tests only.
sourceConfig := `key_provider "pbkdf2" "base1" {
passphrase = "Hello world! 123"
}
key_provider "pbkdf2" "base2" {
passphrase = "OpenTofu has Encryption"
}
key_provider "xor" "dualcustody" {
a = key_provider.pbkdf2.base1
b = key_provider.pbkdf2.base2
}
method "aes_gcm" "example" {
keys = key_provider.xor.dualcustody
}
state {
method = method.aes_gcm.example
}`
reg := lockingencryptionregistry.New()
if err := reg.RegisterKeyProvider(xor.New()); err != nil {
panic(err)
}
if err := reg.RegisterKeyProvider(pbkdf2.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(aesgcm.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(unencrypted.New()); err != nil {
panic(err)
}
parsedSourceConfig, diags := config.LoadConfigFromString("source", sourceConfig)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
staticEval := configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting())
enc, diags := New(reg, parsedSourceConfig, staticEval)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
sfe := enc.State()
testData := []byte(`{"serial": 42, "lineage": "magic"}`)
encryptedState, err := sfe.EncryptState(testData)
if err != nil {
t.Fatalf("%v", err)
}
if string(encryptedState) == string(testData) {
t.Fatalf("The state has not been encrypted.")
}
decryptedState, err := sfe.DecryptState(encryptedState)
if err != nil {
t.Fatalf("%v", err)
}
if string(decryptedState) != string(testData) {
t.Fatalf("Incorrect decrypted state: %s", decryptedState)
}
}

View File

@ -30,15 +30,20 @@ func (e *targetBuilder) setupKeyProviders() hcl.Diagnostics {
e.keyValues = make(map[string]map[string]cty.Value)
kpMap := make(map[string]cty.Value)
for _, keyProviderConfig := range e.cfg.KeyProviderConfigs {
diags = append(diags, e.setupKeyProvider(keyProviderConfig, nil)...)
if diags.HasErrors() {
return diags
}
for name, kps := range e.keyValues {
kpMap[name] = cty.ObjectVal(kps)
}
e.ctx.Variables["key_provider"] = cty.ObjectVal(kpMap)
}
// Regenerate the context now that the key provider is loaded
kpMap := make(map[string]cty.Value)
for name, kps := range e.keyValues {
kpMap[name] = cty.ObjectVal(kps)
}
// Make sure that the key_provider variable is set even if no key providers are configured. This will ultimately
// result in an error, but we want to avoid unpredictable behavior.
e.ctx.Variables["key_provider"] = cty.ObjectVal(kpMap)
return diags
@ -82,10 +87,14 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c
stack = append(stack, cfg)
// Pull the meta key out for error messages and meta storage
metakey, diags := cfg.Addr()
tmpMetaKey, diags := cfg.Addr()
if diags.HasErrors() {
return diags
}
metaKey := keyprovider.MetaStorageKey(tmpMetaKey)
if cfg.EncryptedMetadataAlias != "" {
metaKey = keyprovider.MetaStorageKey(cfg.EncryptedMetadataAlias)
}
// Lookup the KeyProviderDescriptor from the registry
id := keyprovider.ID(cfg.Type)
@ -209,18 +218,18 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c
return append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to build encryption key data",
Detail: fmt.Sprintf("%s failed with error: %s", metakey, err.Error()),
Detail: fmt.Sprintf("%s failed with error: %s", metaKey, err.Error()),
})
}
// Add the metadata
if meta, ok := e.keyProviderMetadata[metakey]; ok {
if meta, ok := e.inputKeyProviderMetadata[metaKey]; ok {
err := json.Unmarshal(meta, keyMetaIn)
if err != nil {
return append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to decode encrypted metadata (did you change your encryption config?)",
Detail: fmt.Sprintf("metadata decoder for %s failed with error: %s", metakey, err.Error()),
Detail: fmt.Sprintf("metadata decoder for %s failed with error: %s", metaKey, err.Error()),
})
}
}
@ -230,18 +239,25 @@ func (e *targetBuilder) setupKeyProvider(cfg config.KeyProviderConfig, stack []c
return append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to fetch encryption key data",
Detail: fmt.Sprintf("%s failed with error: %s", metakey, err.Error()),
Detail: fmt.Sprintf("%s failed with error: %s", metaKey, err.Error()),
})
}
if keyMetaOut != nil {
e.keyProviderMetadata[metakey], err = json.Marshal(keyMetaOut)
if _, ok := e.outputKeyProviderMetadata[metaKey]; ok {
return append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate metadata key",
Detail: fmt.Sprintf("The metadata key %s is duplicated across multiple key providers for the same method; use the encrypted_metadata_alias option to specify unique metadata keys for each key provider in an encryption method", metaKey),
})
}
e.outputKeyProviderMetadata[metaKey], err = json.Marshal(keyMetaOut)
if err != nil {
return append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to encode encrypted metadata",
Detail: fmt.Sprintf("metadata encoder for %s failed with error: %s", metakey, err.Error()),
Detail: fmt.Sprintf("The metadata encoder for %s failed with error: %s", metaKey, err.Error()),
})
}
}

View File

@ -11,3 +11,6 @@ package keyprovider
// Key providers can use this to store, for example, a randomly generated salt value which is required to later provide
// the same decryption key.
type KeyMeta any
// MetaStorageKey signals the key under which the metadata for a specific key provider is stored.
type MetaStorageKey string

View File

@ -0,0 +1,27 @@
# XOR-based dual-custody key provider
This key provider combines two keys to create a dual-custody encryption key using XOR. This provider is meant for testing purposes only.
> [!WARNING]
> This file is not an end-user documentation, it is intended for developers. Please follow the user documentation on the OpenTofu website unless you want to work on the encryption code.
## Configuration
You can configure the key provider as follows. Note, the input keys must have the same length.
```hcl2
terraform {
encryption {
key_provider "pbkdf2" "a" {
passphrase = "This is passphrase 1"
}
key_provider "pbkdf2" "b" {
passphrase = "This is passphrase 2"
}
key_provider "xor" "myprovider" {
a = key_provider.pbkdf2.a
b = key_provider.pbkdf2.b
}
}
}
```

View File

@ -0,0 +1,56 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package xor
import (
"fmt"
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)
// Config contains the configuration for this key provider supplied by the user. This struct must have hcl tags in order
// to function.
type Config struct {
A keyprovider.Output `hcl:"a"`
B keyprovider.Output `hcl:"b"`
}
// Build will create the usable key provider.
func (c Config) Build() (keyprovider.KeyProvider, keyprovider.KeyMeta, error) {
if len(c.A.EncryptionKey) == 0 {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Message: "Missing A encryption key",
}
}
if len(c.B.EncryptionKey) == 0 {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Message: "Missing B encryption key",
}
}
if len(c.A.EncryptionKey) != len(c.B.EncryptionKey) {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Message: fmt.Sprintf("The two provided encryption keys are not equal in length (%d vs %d bytes)", len(c.A.EncryptionKey), len(c.B.EncryptionKey)),
}
}
if len(c.A.DecryptionKey) != len(c.B.DecryptionKey) {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Message: fmt.Sprintf("The two provided decryption keys are not equal in length (%d vs %d bytes)", len(c.A.DecryptionKey), len(c.B.DecryptionKey)),
}
}
encryptionKey := make([]byte, len(c.A.EncryptionKey))
for i := range c.A.EncryptionKey {
encryptionKey[i] = c.A.EncryptionKey[i] ^ c.B.EncryptionKey[i]
}
decryptionKey := make([]byte, len(c.A.DecryptionKey))
for i := range c.A.DecryptionKey {
decryptionKey[i] = c.A.DecryptionKey[i] ^ c.B.DecryptionKey[i]
}
return &xorKeyProvider{keyprovider.Output{
EncryptionKey: encryptionKey,
DecryptionKey: decryptionKey,
}}, nil, nil
}

View File

@ -0,0 +1,30 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package xor
import (
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)
func New() Descriptor {
return &descriptor{}
}
// Descriptor is an additional interface to allow for providing custom methods.
type Descriptor interface {
keyprovider.Descriptor
}
type descriptor struct {
}
func (f descriptor) ID() keyprovider.ID {
return "xor"
}
func (f descriptor) ConfigStruct() keyprovider.Config {
return &Config{}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package xor contains a key provider that combines two other keys.
package xor
import (
"fmt"
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)
type xorKeyProvider struct {
key keyprovider.Output
}
func (p xorKeyProvider) Provide(meta keyprovider.KeyMeta) (keyprovider.Output, keyprovider.KeyMeta, error) {
if meta != nil {
return keyprovider.Output{}, nil, &keyprovider.ErrInvalidMetadata{
Message: fmt.Sprintf("bug: metadata provider despite none being required: %T", meta),
}
}
return p.key, nil, nil
}

View File

@ -0,0 +1,144 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package encryption
import (
"strings"
"testing"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/encryption/config"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/pbkdf2"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/xor"
"github.com/opentofu/opentofu/internal/encryption/method/aesgcm"
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
"github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry"
)
func TestChangingKeyProviderAddr(t *testing.T) {
sourceConfig := `key_provider "pbkdf2" "basic" {
encrypted_metadata_alias = "foo"
passphrase = "Hello world! 123"
}
method "aes_gcm" "example" {
keys = key_provider.pbkdf2.basic
}
state {
method = method.aes_gcm.example
}`
dstConfig := `key_provider "pbkdf2" "simple" {
encrypted_metadata_alias = "foo"
passphrase = "Hello world! 123"
}
method "aes_gcm" "example" {
keys = key_provider.pbkdf2.simple
}
state {
method = method.aes_gcm.example
}`
reg := lockingencryptionregistry.New()
if err := reg.RegisterKeyProvider(pbkdf2.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(aesgcm.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(unencrypted.New()); err != nil {
panic(err)
}
parsedSourceConfig, diags := config.LoadConfigFromString("source", sourceConfig)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
parsedDestinationConfig, diags := config.LoadConfigFromString("destination", dstConfig)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
staticEval := configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting())
enc1, diags := New(reg, parsedSourceConfig, staticEval)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
enc2, diags := New(reg, parsedDestinationConfig, staticEval)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
sfe1 := enc1.State()
sfe2 := enc2.State()
testData := []byte(`{"serial": 42, "lineage": "magic"}`)
encryptedState, err := sfe1.EncryptState(testData)
if err != nil {
t.Fatalf("%v", err)
}
if string(encryptedState) == string(testData) {
t.Fatalf("The state has not been encrypted.")
}
decryptedState, err := sfe2.DecryptState(encryptedState)
if err != nil {
t.Fatalf("%v", err)
}
if string(decryptedState) != string(testData) {
t.Fatalf("Incorrect decrypted state: %s", decryptedState)
}
}
func TestDuplicateKeyProvider(t *testing.T) {
// Note: the XOR provider is not available in final OpenTofu builds because its security constraints have not
// been properly evaluated. The code below doesn't work in OpenTofu and is for tests only.
sourceConfig := `key_provider "pbkdf2" "base1" {
encrypted_metadata_alias = "foo"
passphrase = "Hello world! 123"
}
key_provider "pbkdf2" "base2" {
encrypted_metadata_alias = "foo"
passphrase = "OpenTofu has Encryption"
}
key_provider "xor" "dualcustody" {
a = key_provider.pbkdf2.base1
b = key_provider.pbkdf2.base2
}
method "aes_gcm" "example" {
keys = key_provider.xor.dualcustody
}
state {
method = method.aes_gcm.example
}`
reg := lockingencryptionregistry.New()
if err := reg.RegisterKeyProvider(xor.New()); err != nil {
panic(err)
}
if err := reg.RegisterKeyProvider(pbkdf2.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(aesgcm.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(unencrypted.New()); err != nil {
panic(err)
}
parsedSourceConfig, diags := config.LoadConfigFromString("source", sourceConfig)
if diags.HasErrors() {
t.Fatalf("%v", diags.Error())
}
staticEval := configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting())
_, diags = New(reg, parsedSourceConfig, staticEval)
if diags.HasErrors() {
if !strings.Contains(diags.Error(), "Duplicate metadata key") {
t.Fatalf("No error due to duplicate metadata key: %v", diags)
}
} else {
t.Fatalf("Encrypted state despite duplicate metadata key.")
}
}

View File

@ -26,7 +26,8 @@ type targetBuilder struct {
// Used to evaluate hcl expressions
ctx *hcl.EvalContext
keyProviderMetadata map[keyprovider.Addr][]byte
inputKeyProviderMetadata map[keyprovider.MetaStorageKey][]byte
outputKeyProviderMetadata map[keyprovider.MetaStorageKey][]byte
// Used to build EvalContext (and related mappings)
keyValues map[string]map[string]cty.Value
@ -35,7 +36,7 @@ type targetBuilder struct {
staticEval *configs.StaticEvaluator
}
func (base *baseEncryption) buildTargetMethods(meta map[keyprovider.Addr][]byte) ([]method.Method, hcl.Diagnostics) {
func (base *baseEncryption) buildTargetMethods(inputMeta map[keyprovider.MetaStorageKey][]byte, outputMeta map[keyprovider.MetaStorageKey][]byte) ([]method.Method, hcl.Diagnostics) {
var diags hcl.Diagnostics
builder := &targetBuilder{
@ -47,7 +48,8 @@ func (base *baseEncryption) buildTargetMethods(meta map[keyprovider.Addr][]byte)
Variables: map[string]cty.Value{},
},
keyProviderMetadata: meta,
inputKeyProviderMetadata: inputMeta,
outputKeyProviderMetadata: outputMeta,
}
keyDiags := append(diags, builder.setupKeyProviders()...)

View File

@ -235,14 +235,15 @@ func (testCase btmTestCase) newTestRun(reg registry.Registry, staticEval *config
cfg: cfg,
reg: reg,
},
target: cfg.State.AsTargetConfig(),
enforced: cfg.State.Enforced,
name: "test",
encMeta: make(map[keyprovider.Addr][]byte),
staticEval: staticEval,
target: cfg.State.AsTargetConfig(),
enforced: cfg.State.Enforced,
name: "test",
inputEncMeta: make(map[keyprovider.MetaStorageKey][]byte),
outputEncMeta: make(map[keyprovider.MetaStorageKey][]byte),
staticEval: staticEval,
}
methods, diags := base.buildTargetMethods(base.encMeta)
methods, diags := base.buildTargetMethods(base.inputEncMeta, base.outputEncMeta)
if diags.HasErrors() {
if !hasDiagWithMsg(diags, testCase.wantErr) {

View File

@ -20,6 +20,8 @@ import Fallback from '!!raw-loader!./examples/encryption/fallback.tf'
import FallbackFromUnencrypted from '!!raw-loader!./examples/encryption/fallback_from_unencrypted.tf'
import FallbackToUnencrypted from '!!raw-loader!./examples/encryption/fallback_to_unencrypted.tf'
import RemoteState from '!!raw-loader!./examples/encryption/terraform_remote_state.tf'
import RemoteStateFullA from '!!raw-loader!./examples/encryption/terraform_remote_state_full_a.tf'
import RemoteStateFullB from '!!raw-loader!./examples/encryption/terraform_remote_state_full_b.tf'
# State and Plan Encryption
@ -72,8 +74,7 @@ The basic configuration structure looks as follows:
:::warning
Once your data is encrypted, do not rename key providers and methods in your configuration! The encrypted data stored in the backend contains metadata related to their specific names. Instead, use a [fallback block](#key-and-method-rollover) to handle changes to key providers.
Once your data is encrypted, do not rename key providers and methods in your configuration! The encrypted data stored in the backend contains metadata related to their specific names. Instead, use a [fallback block](#key-and-method-rollover) to handle changes to key providers. Alternatively, you can specify a unique metadata storage key in the `encrypted_metadata_alias` field on the key provider, which makes it possible to change the name of a key provider without problems.
:::
:::tip
@ -140,6 +141,16 @@ For specific remote states, you can use the following syntax:
- `mymodule.myname` to target a data source in the specified module with the given name.
- `mymodule.myname[0]` to target the first data source in the specified module with the given name.
In some cases key names between projects can conflict and you will need to use a different name for the key provider in one project than the other. In this case, you should use the `encrypted_metadata_alias` option to set a fixed metadata key in order to ensure the encryption works.
For example, you may create certificates in project "A" and want to reference them in project "B". In project "A", you could create the following setup:
<CodeBlock language="hcl">{RemoteStateFullA}</CodeBlock>
Then you can reference it in project "B" as follows:
<CodeBlock language="hcl">{RemoteStateFullB}</CodeBlock>
## Key providers
### PBKDF2
@ -148,22 +159,24 @@ The PBKDF2 key provider allows you to use a long passphrase as to generate a key
<CodeBlock language="hcl">{PBKDF2}</CodeBlock>
| Option | Description | Min. | Default |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|---------|
| passphrase *(required)* | Enter a long and complex passphrase. | 16 chars. | - |
| key_length | Number of bytes to generate as a key. | 1 | 32 |
| iterations | Number of iterations. See [this document](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) for recommendations. | 200.000 | 600.000 |
| salt_length | Length of the salt for the key derivation. | 1 | 32 |
| hash_function | Specify either `sha256` or `sha512` to use as a hash function. `sha1` is not supported. | N/A | sha512 |
| Option | Description | Min. | Default |
|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|------------------------------------|
| passphrase *(required)* | Enter a long and complex passphrase. | 16 chars. | - |
| key_length | Number of bytes to generate as a key. | 1 | 32 |
| iterations | Number of iterations. See [this document](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) for recommendations. | 200.000 | 600.000 |
| salt_length | Length of the salt for the key derivation. | 1 | 32 |
| hash_function | Specify either `sha256` or `sha512` to use as a hash function. `sha1` is not supported. | N/A | sha512 |
| encrypted_metadata_alias | Optional identifier to store metadata in the encrypted state/plan files under. Specify this to allow changing the name of a key provider. | - | derived from the key provider name |
### AWS KMS
This key provider uses the [Amazon Web Servers Key Management Service](https://aws.amazon.com/kms/) to generate keys. The authentication options are identical to the [S3 backend](../../language/settings/backends/s3.mdx) excluding any deprecated options. In addition, please provide the following options:
| Option | Description | Min. | Default |
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------|---------|
| kms_key_id | [Key ID for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id). | 1 | - |
| key_spec | [Key spec for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-spec). Adapt this to your encryption method (e.g. `AES_256`). | 1 | - |
| Option | Description | Min. | Default |
|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------------------|
| kms_key_id | [Key ID for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id). | 1 | - |
| key_spec | [Key spec for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-spec). Adapt this to your encryption method (e.g. `AES_256`). | 1 | - |
| encrypted_metadata_alias | Optional identifier to store metadata in the encrypted state/plan files under. Specify this to allow changing the name of a key provider. | - | derived from the key provider name |
The following example illustrates a minimal configuration:
@ -173,10 +186,11 @@ The following example illustrates a minimal configuration:
This key provider uses the [Google Cloud Key Management Service](https://cloud.google.com/kms/docs) to generate keys. The authentication options are identical to the [GCS backend](../../language/settings/backends/gcs.mdx) excluding any deprecated options. In addition, please provide the following options:
| Option | Description | Min. | Default |
|---------------------------------------|------------------------------------------------------------------------------------------------------------------|------|---------|
| kms_encryption_key *(required)* | [Key ID for GCP KMS](https://cloud.google.com/kms/docs/create-key#kms-create-symmetric-encrypt-decrypt-console). | N/A | - |
| key_length *(required)* | Number of bytes to generate as a key. Must be in range from `1` to `1024` bytes. | 1 | - |
| Option | Description | Min. | Default |
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------------------|
| kms_encryption_key *(required)* | [Key ID for GCP KMS](https://cloud.google.com/kms/docs/create-key#kms-create-symmetric-encrypt-decrypt-console). | N/A | - |
| key_length *(required)* | Number of bytes to generate as a key. Must be in range from `1` to `1024` bytes. | 1 | - |
| encrypted_metadata_alias | Optional identifier to store metadata in the encrypted state/plan files under. Specify this to allow changing the name of a key provider. | - | derived from the key provider name |
The following example illustrates a minimal configuration:
@ -186,13 +200,14 @@ The following example illustrates a minimal configuration:
This key provider uses the [OpenBao Transit Secret Engine](https://openbao.org/docs/secrets/transit) to generate data keys. You can configure it as follows:
| Option | Description | Min. | Default |
|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------|
| key_name *(required)* | Name of the transit encryption key to use to encrypt/decrypt the datakey. [Pre-configure](https://openbao.org/docs/secrets/transit/#setup) it in your in OpenBao server. | N/A | - |
| token | [Authorization Token](https://openbao.org/docs/concepts/tokens/) to use when accessing OpenBao API. OpenTofu can read it from the `BAO_TOKEN` environment variable as well. | N/A | - |
| address | OpenBao server address to access the API. OpenTofu can read it from the `BAO_ADDR` environment variable as well. Your system must trust the TLS certificate of the server. | N/A | https://127.0.0.1:8200 |
| transit_engine_path | Path at which the Transit Secret Engine is enabled in OpenBao. Customize this if you changed the transit engine path. | N/A | /transit |
| key_length | Number of bytes to generate as a key. Available options are `16`, `32` or `64` bytes. | 16 | 32 |
| Option | Description | Min. | Default |
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------------------|
| key_name *(required)* | Name of the transit encryption key to use to encrypt/decrypt the datakey. [Pre-configure](https://openbao.org/docs/secrets/transit/#setup) it in your in OpenBao server. | N/A | - |
| token | [Authorization Token](https://openbao.org/docs/concepts/tokens/) to use when accessing OpenBao API. OpenTofu can read it from the `BAO_TOKEN` environment variable as well. | N/A | - |
| address | OpenBao server address to access the API. OpenTofu can read it from the `BAO_ADDR` environment variable as well. Your system must trust the TLS certificate of the server. | N/A | https://127.0.0.1:8200 |
| transit_engine_path | Path at which the Transit Secret Engine is enabled in OpenBao. Customize this if you changed the transit engine path. | N/A | /transit |
| key_length | Number of bytes to generate as a key. Available options are `16`, `32` or `64` bytes. | 16 | 32 |
| encrypted_metadata_alias | Optional identifier to store metadata in the encrypted state/plan files under. Specify this to allow changing the name of a key provider. | - | derived from the key provider name |
The following example illustrates a possible configuration:

View File

@ -0,0 +1,40 @@
terraform {
encryption {
key_provider "pbkdf2" "mykey" {
passphrase = "OpenTofu has encryption"
# Note the fixed encrypted_metadata_alias here:
encrypted_metadata_alias = "certificates"
}
method "aes_gcm" "mymethod" {
keys = key_provider.pbkdf2.mykey
}
state {
method = method.aes_gcm.mymethod
}
}
}
resource "tls_private_key" "webserver" {
algorithm = "ED25519"
}
resource "tls_self_signed_cert" "webserver" {
private_key_pem = tls_private_key.webserver.private_key_pem
subject {
common_name = "someserver.opentofu.org"
organization = "OpenTofu"
}
validity_period_hours = 24*365*10
allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
]
}
output "cert_pem" {
value = tls_self_signed_cert.webserver.cert_pem
}

View File

@ -0,0 +1,32 @@
terraform {
encryption {
# Note that the name of the key here is different:
key_provider "pbkdf2" "mykeyrenamed" {
passphrase = "OpenTofu has encryption"
# Note the fixed encrypted_metadata_alias here:
encrypted_metadata_alias = "certificates"
}
method "aes_gcm" "mymethod" {
keys = key_provider.pbkdf2.mykeyrenamed
}
remote_state_data_sources {
default {
method = method.aes_gcm.mymethod
}
}
}
}
data "terraform_remote_state" "cert" {
backend = "local"
config = {
# Refer to the other project here:
path = "../a/terraform.tfstate"
}
}
output "cert" {
# Use data from the other project by referencing it as follows:
value = data.terraform_remote_state.cert.outputs.cert_pem
}

View File

@ -11,6 +11,12 @@ from some other OpenTofu configuration.
You can use the `terraform_remote_state` data source without requiring or configuring a provider. It is always available through a built-in provider with the [source address](../../language/providers/requirements.mdx#source-addresses) `terraform.io/builtin/terraform`. That provider does not include any other resources or data sources.
:::warning
Be careful when using [state encryption](./encryption.mdx)! Sharing encrypted state between projects requires careful coordination of metadata keys. Please read the [Remote state data sources](./encryption.mdx#remote-state-data-sources) section of the state encryption guide for details.
:::
## Alternative Ways to Share Data Between Configurations
Sharing data with root module outputs is convenient, but it has drawbacks.