mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add unencrypted Method for migrations (#1458)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
parent
b868012192
commit
d7e96665f6
@ -143,7 +143,7 @@ func TestEncryptionFlow(t *testing.T) {
|
||||
|
||||
with("required.tf", func() {
|
||||
// Can't switch directly to encryption, need to migrate
|
||||
apply().Failure().StderrContains("decrypted payload provided without fallback specified")
|
||||
apply().Failure().StderrContains("encountered unencrypted payload without unencrypted method")
|
||||
requireUnencryptedState()
|
||||
})
|
||||
|
||||
@ -177,7 +177,7 @@ func TestEncryptionFlow(t *testing.T) {
|
||||
requireEncryptedState()
|
||||
|
||||
// Can't apply unencrypted plan
|
||||
applyPlan(unencryptedPlan).Failure().StderrContains("decrypted payload provided without fallback specified")
|
||||
applyPlan(unencryptedPlan).Failure().StderrContains("encountered unencrypted payload without unencrypted method")
|
||||
requireEncryptedState()
|
||||
|
||||
// Apply encrypted plan
|
||||
|
@ -10,12 +10,15 @@ terraform {
|
||||
method "aes_gcm" "example" {
|
||||
keys = key_provider.pbkdf2.basic
|
||||
}
|
||||
method "unencrypted" "fallback" {}
|
||||
state {
|
||||
method = method.unencrypted.fallback
|
||||
fallback {
|
||||
method = method.aes_gcm.example
|
||||
}
|
||||
}
|
||||
plan {
|
||||
method = method.unencrypted.fallback
|
||||
fallback {
|
||||
method = method.aes_gcm.example
|
||||
}
|
||||
|
@ -10,13 +10,18 @@ terraform {
|
||||
method "aes_gcm" "example" {
|
||||
keys = key_provider.pbkdf2.basic
|
||||
}
|
||||
method "unencrypted" "fallback" {}
|
||||
state {
|
||||
method = method.aes_gcm.example
|
||||
fallback {}
|
||||
fallback {
|
||||
method = method.unencrypted.fallback
|
||||
}
|
||||
}
|
||||
plan {
|
||||
method = method.aes_gcm.example
|
||||
fallback {}
|
||||
fallback {
|
||||
method = method.unencrypted.fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/encryption/config"
|
||||
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
@ -67,6 +68,7 @@ func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bo
|
||||
//
|
||||
methods, diags := base.buildTargetMethods(base.encMeta)
|
||||
base.encMethods = methods
|
||||
|
||||
return base, diags
|
||||
}
|
||||
|
||||
@ -88,21 +90,14 @@ func IsEncryptionPayload(data []byte) (bool, error) {
|
||||
}
|
||||
|
||||
func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}) ([]byte, error) {
|
||||
// No configuration provided, don't do anything
|
||||
if s.target == nil {
|
||||
return data, nil
|
||||
}
|
||||
// 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]
|
||||
|
||||
var encryptor method.Method = nil
|
||||
if len(s.encMethods) != 0 {
|
||||
// Use the pre-configured encryption method
|
||||
encryptor = s.encMethods[0]
|
||||
}
|
||||
|
||||
if encryptor == nil {
|
||||
if unencrypted.Is(encryptor) {
|
||||
// ensure that the method is defined when Enforced is true
|
||||
if s.enforced {
|
||||
return nil, fmt.Errorf("encryption of %q is enforced, and therefore requires a method to be provided", s.name)
|
||||
return nil, fmt.Errorf("unable to use unencrypted method for %q when enforced = true", s.name)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@ -127,10 +122,6 @@ 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) {
|
||||
if s.target == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
es := basedata{}
|
||||
err := json.Unmarshal(data, &es)
|
||||
|
||||
@ -149,20 +140,16 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
||||
return nil, fmt.Errorf("unable to determine data structure during decryption: %w", verr)
|
||||
}
|
||||
|
||||
methods, diags := s.buildTargetMethods(make(map[keyprovider.Addr][]byte))
|
||||
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
|
||||
return nil, diags
|
||||
}
|
||||
// Yep, it's already decrypted
|
||||
for _, method := range methods {
|
||||
if method == nil {
|
||||
// fallback allowed
|
||||
for _, method := range s.encMethods {
|
||||
if unencrypted.Is(method) {
|
||||
if s.enforced {
|
||||
return nil, fmt.Errorf("unable to use unencrypted method when enforced = true")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
return data, fmt.Errorf("decrypted payload provided without fallback specified")
|
||||
return nil, fmt.Errorf("encountered unencrypted payload without unencrypted method configured")
|
||||
}
|
||||
|
||||
if es.Version != encryptionVersion {
|
||||
@ -177,26 +164,10 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if len(methods) == 0 {
|
||||
err = validator(data)
|
||||
if err != nil {
|
||||
// TODO improve this error message
|
||||
return nil, err
|
||||
}
|
||||
// No methods/fallbacks specified and data is valid payload
|
||||
return data, nil
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
for _, method := range methods {
|
||||
if method == nil {
|
||||
// No method specified for this target
|
||||
err = validator(data)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
// TODO improve this error message
|
||||
errs = append(errs, fmt.Errorf("payload is not already decrypted: %w", err))
|
||||
if unencrypted.Is(method) {
|
||||
// Not applicable
|
||||
continue
|
||||
}
|
||||
uncd, err := method.Decrypt(es.Data)
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/encryption/keyprovider/openbao"
|
||||
"github.com/opentofu/opentofu/internal/encryption/keyprovider/pbkdf2"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method/aesgcm"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
|
||||
"github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry"
|
||||
)
|
||||
|
||||
@ -20,9 +21,6 @@ func init() {
|
||||
if err := DefaultRegistry.RegisterKeyProvider(pbkdf2.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := DefaultRegistry.RegisterMethod(aesgcm.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := DefaultRegistry.RegisterKeyProvider(aws_kms.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -32,4 +30,10 @@ func init() {
|
||||
if err := DefaultRegistry.RegisterKeyProvider(openbao.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := DefaultRegistry.RegisterMethod(aesgcm.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := DefaultRegistry.RegisterMethod(unencrypted.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/encryption/config"
|
||||
"github.com/opentofu/opentofu/internal/encryption/keyprovider/static"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method/aesgcm"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
|
||||
"github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry"
|
||||
)
|
||||
|
||||
@ -26,6 +27,9 @@ func EncryptionDirect(configData string) encryption.Encryption {
|
||||
if err := reg.RegisterMethod(aesgcm.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := reg.RegisterMethod(unencrypted.New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg, diags := config.LoadConfigFromString("Test Config Source", configData)
|
||||
|
||||
@ -67,18 +71,25 @@ func EncryptionWithFallback() encryption.Encryption {
|
||||
method "aes_gcm" "example" {
|
||||
keys = key_provider.static.basic
|
||||
}
|
||||
method "unencrypted" "migration" {}
|
||||
state {
|
||||
method = method.aes_gcm.example
|
||||
fallback {}
|
||||
fallback {
|
||||
method = method.unencrypted.migration
|
||||
}
|
||||
}
|
||||
plan {
|
||||
method = method.aes_gcm.example
|
||||
fallback {}
|
||||
fallback {
|
||||
method = method.unencrypted.migration
|
||||
}
|
||||
}
|
||||
remote_state_data_sources {
|
||||
default {
|
||||
method = method.aes_gcm.example
|
||||
fallback {}
|
||||
fallback {
|
||||
method = method.unencrypted.migration
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
38
internal/encryption/method/unencrypted/method.go
Normal file
38
internal/encryption/method/unencrypted/method.go
Normal file
@ -0,0 +1,38 @@
|
||||
package unencrypted
|
||||
|
||||
import (
|
||||
"github.com/opentofu/opentofu/internal/encryption/method"
|
||||
)
|
||||
|
||||
func New() method.Descriptor {
|
||||
return &descriptor{}
|
||||
}
|
||||
|
||||
type descriptor struct{}
|
||||
|
||||
func (f *descriptor) ID() method.ID {
|
||||
return "unencrypted"
|
||||
}
|
||||
func (f *descriptor) ConfigStruct() method.Config {
|
||||
return new(config)
|
||||
}
|
||||
|
||||
type config struct{}
|
||||
|
||||
func (c *config) Build() (method.Method, error) {
|
||||
return new(unenc), nil
|
||||
}
|
||||
|
||||
type unenc struct{}
|
||||
|
||||
func (a *unenc) Encrypt(data []byte) ([]byte, error) {
|
||||
panic("Placeholder for type check! Should never be called!")
|
||||
}
|
||||
func (a *unenc) Decrypt(data []byte) ([]byte, error) {
|
||||
panic("Placeholder for type check! Should never be called!")
|
||||
}
|
||||
|
||||
func Is(m method.Method) bool {
|
||||
_, ok := m.(*unenc)
|
||||
return ok
|
||||
}
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/opentofu/opentofu/internal/encryption/config"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method"
|
||||
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
|
||||
"github.com/opentofu/opentofu/internal/encryption/registry"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
@ -56,24 +57,25 @@ func (e *targetBuilder) setupMethod(cfg config.MethodConfig) hcl.Diagnostics {
|
||||
// Handle if the method was not found
|
||||
var notFoundError *registry.MethodNotFoundError
|
||||
if errors.Is(err, notFoundError) {
|
||||
return hcl.Diagnostics{&hcl.Diagnostic{
|
||||
return append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unknown encryption method type",
|
||||
Detail: fmt.Sprintf("Can not find %q", cfg.Type),
|
||||
}}
|
||||
})
|
||||
}
|
||||
|
||||
// Or, we don't know the error type, so we'll just return it as a generic error
|
||||
return hcl.Diagnostics{&hcl.Diagnostic{
|
||||
return append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Error fetching encryption method %q", cfg.Type),
|
||||
Detail: err.Error(),
|
||||
}}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: we could use varhcl here to provider better error messages
|
||||
methodConfig := encryptionMethod.ConfigStruct()
|
||||
diags = gohcl.DecodeBody(cfg.Body, e.ctx, methodConfig)
|
||||
methodDiags := gohcl.DecodeBody(cfg.Body, e.ctx, methodConfig)
|
||||
diags = append(diags, methodDiags...)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
@ -82,12 +84,21 @@ func (e *targetBuilder) setupMethod(cfg config.MethodConfig) hcl.Diagnostics {
|
||||
m, err := methodConfig.Build()
|
||||
if err != nil {
|
||||
// TODO this error handling could use some work
|
||||
return hcl.Diagnostics{&hcl.Diagnostic{
|
||||
return append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Encryption method configuration failed",
|
||||
Detail: err.Error(),
|
||||
}}
|
||||
})
|
||||
}
|
||||
e.methods[addr] = m
|
||||
return nil
|
||||
|
||||
if unencrypted.Is(m) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Unencrypted method configured",
|
||||
Detail: "Method unencrypted is present in configuration. This is a security risk and should only be enabled during migrations.",
|
||||
})
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
@ -46,16 +46,19 @@ func (base *baseEncryption) buildTargetMethods(meta map[keyprovider.Addr][]byte)
|
||||
keyProviderMetadata: meta,
|
||||
}
|
||||
|
||||
diags = append(diags, builder.setupKeyProviders()...)
|
||||
keyDiags := append(diags, builder.setupKeyProviders()...)
|
||||
diags = append(diags, keyDiags...)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
diags = append(diags, builder.setupMethods()...)
|
||||
methodDiags := append(diags, builder.setupMethods()...)
|
||||
diags = append(diags, methodDiags...)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return builder.build(base.target, base.name)
|
||||
methods, targetDiags := builder.build(base.target, base.name)
|
||||
return methods, append(diags, targetDiags...)
|
||||
}
|
||||
|
||||
// build sets up a single target for encryption. It returns the primary and fallback methods for the target, as well
|
||||
@ -74,7 +77,6 @@ func (e *targetBuilder) build(target *config.TargetConfig, targetName string) (m
|
||||
|
||||
// Only attempt to fetch the method if the decoding was successful
|
||||
if !decodeDiags.HasErrors() {
|
||||
|
||||
if methodIdent != nil {
|
||||
if method, ok := e.methods[method.Addr(*methodIdent)]; ok {
|
||||
methods = append(methods, method)
|
||||
@ -88,8 +90,12 @@ func (e *targetBuilder) build(target *config.TargetConfig, targetName string) (m
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// nil is a nop method
|
||||
methods = append(methods, nil)
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing encryption method",
|
||||
Detail: fmt.Sprintf("undefined or null method used for %q", targetName),
|
||||
Subject: target.Method.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,10 @@ AES-GCM is a secure, industry-standard encryption algorithm, but suffers from "k
|
||||
|
||||
:::
|
||||
|
||||
### Unencrypted
|
||||
|
||||
The `unencrypted` method is used to provide an explicit migration path to and from encryption. It takes no configuration and can be see in use below in the [Initial Setup](#initial-setup) block.
|
||||
|
||||
## Key and method rollover
|
||||
|
||||
In some cases, you may want to change your encryption configuration. This can include renaming a key provider or method, changing a passphrase for a key provider, or switching key-management systems. OpenTofu supports an automatic rollover of your encryption configuration if you provide your old configuration in a `fallback` block:
|
||||
@ -173,13 +177,13 @@ If OpenTofu fails to **read** your state or plan file with the new method, it wi
|
||||
|
||||
## Initial setup
|
||||
|
||||
When you first configure encryption, your state and plan files are unencrypted. OpenTofu, by default, refuses to read them because they could have been manipulated. To enable reading unencrypted data, you will have to specify an empty fallback block:
|
||||
When you first configure encryption, your state and plan files are unencrypted. OpenTofu, by default, refuses to read them because they could have been manipulated. To enable reading unencrypted data, you will have to specify an `unencrypted` method:
|
||||
|
||||
<CodeBlock language="hcl">{FallbackFromUnencrypted}</CodeBlock>
|
||||
|
||||
## Rolling back encryption
|
||||
|
||||
Similar to the initial setup above, migrating to unencrypted state and plan files is also possible in a similar manner. You simply have to specify no method in the target block as follows:
|
||||
Similar to the initial setup above, migrating to unencrypted state and plan files is also possible in a similar manner. You simply have to specify an `unencrypted` method in the target block as follows:
|
||||
|
||||
<CodeBlock language="hcl">{FallbackToUnencrypted}</CodeBlock>
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
terraform {
|
||||
encryption {
|
||||
# Methods and key providers here.
|
||||
method "unencrypted" "migrate" {}
|
||||
|
||||
state {
|
||||
method = method.some_method.new_method
|
||||
fallback {
|
||||
# The empty fallback block allows reading unencrypted state files.
|
||||
# The unencrypted method in a fallback block allows reading unencrypted state files.
|
||||
method = method.unencrypted.migrate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
terraform {
|
||||
encryption {
|
||||
# Methods and key providers here.
|
||||
method "unencrypted" "migrate" {}
|
||||
|
||||
state {
|
||||
# The empty block allows writing unencrypted state files
|
||||
# The unencrypted method allows writing unencrypted state files.
|
||||
# unless the enforced setting is set to true.
|
||||
method = method.unencrypted.migrate
|
||||
fallback {
|
||||
method = method.some_method.old_method
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user