mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
[backport v1.8] Force state change if encryption used fallback (#2236)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
parent
8f2c997396
commit
4a86cddc33
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
* Error messages related to validation of sensitive input variables will no longer disclose the sensitive value in the UI. ([#2219](https://github.com/opentofu/opentofu/pull/2219))
|
* Error messages related to validation of sensitive input variables will no longer disclose the sensitive value in the UI. ([#2219](https://github.com/opentofu/opentofu/pull/2219))
|
||||||
|
* Changes to encryption configuration now auto-apply the migration ([#2232](https://github.com/opentofu/opentofu/pull/2232))
|
||||||
|
|
||||||
|
|
||||||
## 1.8.6
|
## 1.8.6
|
||||||
|
@ -7,6 +7,7 @@ package encryption
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/opentofu/opentofu/internal/configs"
|
"github.com/opentofu/opentofu/internal/configs"
|
||||||
@ -119,11 +120,23 @@ func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}
|
|||||||
return jsond, nil
|
return jsond, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:revive // this name is fine
|
||||||
|
type EncryptionStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusUnknown EncryptionStatus = 0
|
||||||
|
StatusSatisfied EncryptionStatus = 1
|
||||||
|
StatusMigration EncryptionStatus = 2
|
||||||
|
)
|
||||||
|
|
||||||
// TODO Find a way to make these errors actionable / clear
|
// TODO Find a way to make these errors actionable / clear
|
||||||
func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, error) {
|
//
|
||||||
|
//nolint:cyclop // backport
|
||||||
|
func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, EncryptionStatus, error) {
|
||||||
es := basedata{}
|
es := basedata{}
|
||||||
err := json.Unmarshal(data, &es)
|
err := json.Unmarshal(data, &es)
|
||||||
|
|
||||||
|
//nolint:nestif // bugger off
|
||||||
if len(es.Version) == 0 || err != nil {
|
if len(es.Version) == 0 || err != nil {
|
||||||
// Not a valid payload, might be already decrypted
|
// Not a valid payload, might be already decrypted
|
||||||
verr := validator(data)
|
verr := validator(data)
|
||||||
@ -132,24 +145,34 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
|||||||
|
|
||||||
// Return the outer json error if we have one
|
// Return the outer json error if we have one
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid data format for decryption: %w, %w", err, verr)
|
return nil, StatusUnknown, fmt.Errorf("invalid data format for decryption: %w, %w", err, verr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must have been invalid json payload
|
// Must have been invalid json payload
|
||||||
return nil, fmt.Errorf("unable to determine data structure during decryption: %w", verr)
|
return nil, StatusUnknown, fmt.Errorf("unable to determine data structure during decryption: %w", verr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yep, it's already decrypted
|
// Yep, it's already decrypted
|
||||||
|
unencryptedSupported := false
|
||||||
for _, method := range s.encMethods {
|
for _, method := range s.encMethods {
|
||||||
if unencrypted.Is(method) {
|
if unencrypted.Is(method) {
|
||||||
return data, nil
|
unencryptedSupported = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("encountered unencrypted payload without unencrypted method configured")
|
if !unencryptedSupported {
|
||||||
|
return nil, StatusUnknown, fmt.Errorf("encountered unencrypted payload without unencrypted method configured")
|
||||||
|
}
|
||||||
|
if unencrypted.Is(s.encMethods[0]) {
|
||||||
|
// Decrypted and no pending migration
|
||||||
|
return data, StatusSatisfied, nil
|
||||||
|
}
|
||||||
|
// Decrypted and pending migration
|
||||||
|
return data, StatusMigration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if es.Version != encryptionVersion {
|
if es.Version != encryptionVersion {
|
||||||
return nil, fmt.Errorf("invalid encrypted payload version: %s != %s", es.Version, encryptionVersion)
|
return nil, StatusUnknown, fmt.Errorf("invalid encrypted payload version: %s != %s", es.Version, encryptionVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Discuss if we should potentially cache this based on a json-encoded version of es.Meta and reduce overhead dramatically
|
// TODO Discuss if we should potentially cache this based on a json-encoded version of es.Meta and reduce overhead dramatically
|
||||||
@ -157,11 +180,11 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
|||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
// This cast to error here is safe as we know that at least one error exists
|
// 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
|
// This is also quite unlikely to happen as the constructor already has checked this code path
|
||||||
return nil, diags
|
return nil, StatusUnknown, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := make([]error, 0)
|
errs := make([]error, 0)
|
||||||
for _, method := range methods {
|
for i, method := range methods {
|
||||||
if unencrypted.Is(method) {
|
if unencrypted.Is(method) {
|
||||||
// Not applicable
|
// Not applicable
|
||||||
continue
|
continue
|
||||||
@ -169,7 +192,12 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
|||||||
uncd, err := method.Decrypt(es.Data)
|
uncd, err := method.Decrypt(es.Data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Success
|
// Success
|
||||||
return uncd, nil
|
if i == 0 {
|
||||||
|
// Decrypted with first method (encryption method)
|
||||||
|
return uncd, StatusSatisfied, nil
|
||||||
|
}
|
||||||
|
// Used a fallback
|
||||||
|
return uncd, StatusMigration, nil
|
||||||
}
|
}
|
||||||
// Record the failure
|
// 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", s.name, err))
|
||||||
@ -182,5 +210,5 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
|||||||
errMessage += err.Error() + sep
|
errMessage += err.Error() + sep
|
||||||
sep = "\n"
|
sep = "\n"
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf(errMessage)
|
return nil, StatusUnknown, errors.New(errMessage)
|
||||||
}
|
}
|
||||||
|
@ -80,11 +80,15 @@ func Example() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
decryptedState, err := sfe.DecryptState(encrypted)
|
decryptedState, status, err := sfe.DecryptState(encrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if status != encryption.StatusSatisfied {
|
||||||
|
panic(status)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n", decryptedState)
|
fmt.Printf("%s\n", decryptedState)
|
||||||
// Output: {"serial": 42, "lineage": "magic"}
|
// Output: {"serial": 42, "lineage": "magic"}
|
||||||
}
|
}
|
||||||
|
@ -60,13 +60,14 @@ func (p planEncryption) EncryptPlan(data []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) {
|
func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) {
|
||||||
return p.base.decrypt(data, func(data []byte) error {
|
data, _, err := p.base.decrypt(data, func(data []byte) error {
|
||||||
// Check magic bytes
|
// Check magic bytes
|
||||||
if len(data) < 2 || string(data[:2]) != "PK" {
|
if len(data) < 2 || string(data[:2]) != "PK" {
|
||||||
return fmt.Errorf("Invalid plan file %v", string(data[:2]))
|
return fmt.Errorf("Invalid plan file %v", string(data[:2]))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func PlanEncryptionDisabled() PlanEncryption {
|
func PlanEncryptionDisabled() PlanEncryption {
|
||||||
|
@ -32,7 +32,7 @@ type StateEncryption interface {
|
|||||||
// function. Do not attempt to determine if the state file is encrypted as this function will take care of any
|
// function. Do not attempt to determine if the state file is encrypted as this function will take care of any
|
||||||
// and all encryption-related matters. After the function returns, use the returned byte array as a normal state
|
// and all encryption-related matters. After the function returns, use the returned byte array as a normal state
|
||||||
// file.
|
// file.
|
||||||
DecryptState([]byte) ([]byte, error)
|
DecryptState([]byte) ([]byte, EncryptionStatus, error)
|
||||||
|
|
||||||
// EncryptState encrypts a state file and returns the encrypted form.
|
// EncryptState encrypts a state file and returns the encrypted form.
|
||||||
//
|
//
|
||||||
@ -87,8 +87,8 @@ func (s *stateEncryption) EncryptState(plainState []byte) ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stateEncryption) DecryptState(encryptedState []byte) ([]byte, error) {
|
func (s *stateEncryption) DecryptState(encryptedState []byte) ([]byte, EncryptionStatus, error) {
|
||||||
decryptedState, err := s.base.decrypt(encryptedState, func(data []byte) error {
|
decryptedState, status, err := s.base.decrypt(encryptedState, func(data []byte) error {
|
||||||
tmp := struct {
|
tmp := struct {
|
||||||
FormatVersion string `json:"terraform_version"`
|
FormatVersion string `json:"terraform_version"`
|
||||||
}{}
|
}{}
|
||||||
@ -105,32 +105,32 @@ func (s *stateEncryption) DecryptState(encryptedState []byte) ([]byte, error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure that the state passthrough fields match
|
// Make sure that the state passthrough fields match
|
||||||
var encrypted statedata
|
var encrypted statedata
|
||||||
err = json.Unmarshal(encryptedState, &encrypted)
|
err = json.Unmarshal(encryptedState, &encrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, status, err
|
||||||
}
|
}
|
||||||
var state statedata
|
var state statedata
|
||||||
err = json.Unmarshal(decryptedState, &state)
|
err = json.Unmarshal(decryptedState, &state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO make encrypted.Serial non-optional. This is only for supporting alpha1 states!
|
// TODO make encrypted.Serial non-optional. This is only for supporting alpha1 states!
|
||||||
if encrypted.Serial != nil && state.Serial != nil && *state.Serial != *encrypted.Serial {
|
if encrypted.Serial != nil && state.Serial != nil && *state.Serial != *encrypted.Serial {
|
||||||
return nil, fmt.Errorf("invalid state metadata, serial field mismatch %v vs %v", *encrypted.Serial, *state.Serial)
|
return nil, status, fmt.Errorf("invalid state metadata, serial field mismatch %v vs %v", *encrypted.Serial, *state.Serial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO make encrypted.Lineage non-optional. This is only for supporting alpha1 states!
|
// TODO make encrypted.Lineage non-optional. This is only for supporting alpha1 states!
|
||||||
if encrypted.Lineage != "" && state.Lineage != encrypted.Lineage {
|
if encrypted.Lineage != "" && state.Lineage != encrypted.Lineage {
|
||||||
return nil, fmt.Errorf("invalid state metadata, linage field mismatch %v vs %v", encrypted.Lineage, state.Lineage)
|
return nil, status, fmt.Errorf("invalid state metadata, linage field mismatch %v vs %v", encrypted.Lineage, state.Lineage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return decryptedState, nil
|
return decryptedState, status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StateEncryptionDisabled() StateEncryption {
|
func StateEncryptionDisabled() StateEncryption {
|
||||||
@ -142,6 +142,6 @@ type stateDisabled struct{}
|
|||||||
func (s *stateDisabled) EncryptState(plainState []byte) ([]byte, error) {
|
func (s *stateDisabled) EncryptState(plainState []byte) ([]byte, error) {
|
||||||
return plainState, nil
|
return plainState, nil
|
||||||
}
|
}
|
||||||
func (s *stateDisabled) DecryptState(encryptedState []byte) ([]byte, error) {
|
func (s *stateDisabled) DecryptState(encryptedState []byte) ([]byte, EncryptionStatus, error) {
|
||||||
return encryptedState, nil
|
return encryptedState, StatusSatisfied, nil
|
||||||
}
|
}
|
||||||
|
@ -46,12 +46,14 @@ func TestRoundtrip(t *testing.T) {
|
|||||||
Serial: 2,
|
Serial: 2,
|
||||||
Lineage: "abc123",
|
Lineage: "abc123",
|
||||||
State: states.NewState(),
|
State: states.NewState(),
|
||||||
|
EncryptionStatus: encryption.StatusSatisfied,
|
||||||
}
|
}
|
||||||
prevStateFileIn := &statefile.File{
|
prevStateFileIn := &statefile.File{
|
||||||
TerraformVersion: tfversion.SemVer,
|
TerraformVersion: tfversion.SemVer,
|
||||||
Serial: 1,
|
Serial: 1,
|
||||||
Lineage: "abc123",
|
Lineage: "abc123",
|
||||||
State: states.NewState(),
|
State: states.NewState(),
|
||||||
|
EncryptionStatus: encryption.StatusSatisfied,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal plan too, since the serialization of the tfplan portion of the
|
// Minimal plan too, since the serialization of the tfplan portion of the
|
||||||
|
@ -41,6 +41,7 @@ type State struct {
|
|||||||
// state has changed from an existing state we read in.
|
// state has changed from an existing state we read in.
|
||||||
lineage, readLineage string
|
lineage, readLineage string
|
||||||
serial, readSerial uint64
|
serial, readSerial uint64
|
||||||
|
readEncryption encryption.EncryptionStatus
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
state, readState *states.State
|
state, readState *states.State
|
||||||
disableLocks bool
|
disableLocks bool
|
||||||
@ -176,6 +177,7 @@ func (s *State) refreshState() error {
|
|||||||
// track changes as lineage, serial and/or state are mutated
|
// track changes as lineage, serial and/or state are mutated
|
||||||
s.readLineage = stateFile.Lineage
|
s.readLineage = stateFile.Lineage
|
||||||
s.readSerial = stateFile.Serial
|
s.readSerial = stateFile.Serial
|
||||||
|
s.readEncryption = stateFile.EncryptionStatus
|
||||||
s.readState = s.state.DeepCopy()
|
s.readState = s.state.DeepCopy()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -192,7 +194,7 @@ func (s *State) PersistState(schemas *tofu.Schemas) error {
|
|||||||
lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage
|
lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage
|
||||||
serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial
|
serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial
|
||||||
stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState)
|
stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState)
|
||||||
if stateUnchanged && lineageUnchanged && serialUnchanged {
|
if stateUnchanged && lineageUnchanged && serialUnchanged && s.readEncryption != encryption.StatusMigration {
|
||||||
// If the state, lineage or serial haven't changed at all then we have nothing to do.
|
// If the state, lineage or serial haven't changed at all then we have nothing to do.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -238,6 +240,7 @@ func (s *State) PersistState(schemas *tofu.Schemas) error {
|
|||||||
// operation would correctly detect no changes to the lineage, serial or state.
|
// operation would correctly detect no changes to the lineage, serial or state.
|
||||||
s.readState = s.state.DeepCopy()
|
s.readState = s.state.DeepCopy()
|
||||||
s.readLineage = s.lineage
|
s.readLineage = s.lineage
|
||||||
|
s.readEncryption = encryption.StatusSatisfied
|
||||||
s.readSerial = s.serial
|
s.readSerial = s.serial
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ package statefile
|
|||||||
import (
|
import (
|
||||||
version "github.com/hashicorp/go-version"
|
version "github.com/hashicorp/go-version"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/encryption"
|
||||||
"github.com/opentofu/opentofu/internal/states"
|
"github.com/opentofu/opentofu/internal/states"
|
||||||
tfversion "github.com/opentofu/opentofu/version"
|
tfversion "github.com/opentofu/opentofu/version"
|
||||||
)
|
)
|
||||||
@ -34,6 +35,8 @@ type File struct {
|
|||||||
|
|
||||||
// State is the actual state represented by this file.
|
// State is the actual state represented by this file.
|
||||||
State *states.State
|
State *states.State
|
||||||
|
|
||||||
|
EncryptionStatus encryption.EncryptionStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(state *states.State, lineage string, serial uint64) *File {
|
func New(state *states.State, lineage string, serial uint64) *File {
|
||||||
@ -49,6 +52,7 @@ func New(state *states.State, lineage string, serial uint64) *File {
|
|||||||
State: state,
|
State: state,
|
||||||
Lineage: lineage,
|
Lineage: lineage,
|
||||||
Serial: serial,
|
Serial: serial,
|
||||||
|
EncryptionStatus: encryption.StatusUnknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,5 +67,6 @@ func (f *File) DeepCopy() *File {
|
|||||||
Serial: f.Serial,
|
Serial: f.Serial,
|
||||||
Lineage: f.Lineage,
|
Lineage: f.Lineage,
|
||||||
State: f.State.DeepCopy(),
|
State: f.State.DeepCopy(),
|
||||||
|
EncryptionStatus: f.EncryptionStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func Read(r io.Reader, enc encryption.StateEncryption) (*File, error) {
|
|||||||
return nil, ErrNoState
|
return nil, ErrNoState
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypted, err := enc.DecryptState(src)
|
decrypted, status, err := enc.DecryptState(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -91,6 +91,8 @@ func Read(r io.Reader, enc encryption.StateEncryption) (*File, error) {
|
|||||||
panic("readState returned nil state with no errors")
|
panic("readState returned nil state with no errors")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.EncryptionStatus = status
|
||||||
|
|
||||||
return state, diags.Err()
|
return state, diags.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +97,10 @@ func TestRoundtripEncryption(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %s", err)
|
t.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
// Check status
|
||||||
|
if originalState.EncryptionStatus != encryption.StatusMigration {
|
||||||
|
t.Fatal("wrong status")
|
||||||
|
}
|
||||||
|
|
||||||
// Write encrypted
|
// Write encrypted
|
||||||
var encrypted bytes.Buffer
|
var encrypted bytes.Buffer
|
||||||
@ -117,6 +121,13 @@ func TestRoundtripEncryption(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %s", err)
|
t.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
// Check status
|
||||||
|
if newState.EncryptionStatus != encryption.StatusSatisfied {
|
||||||
|
t.Fatal("wrong status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite status for deep comparison
|
||||||
|
originalState.EncryptionStatus = newState.EncryptionStatus
|
||||||
|
|
||||||
// Compare before/after encryption workflow
|
// Compare before/after encryption workflow
|
||||||
problems := deep.Equal(newState, originalState)
|
problems := deep.Equal(newState, originalState)
|
||||||
|
Loading…
Reference in New Issue
Block a user