Refactor encryption configuration (#1387)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-03-13 10:58:52 -04:00 committed by GitHub
parent 4c4d9bca67
commit 586c45fe5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 86 additions and 127 deletions

View File

@ -92,20 +92,17 @@ Finally, the user must reference the method for use in their state file, plan fi
terraform {
encryption {
//...
statefile {
state {
method = method.aes_gcm.abc
}
planfile {
plan {
method = method.aes_gcm.cde
}
backend {
method = method.some_derivative_key_provider.efg
}
remote_data_sources {
remote_state_data_sources {
default {
method = method.aes_gcm.ghi
}
remote_data_source "some_module.remote_data_source.foo" {
remote_state_data_source "some_module.remote_data_source.foo" {
method = method.aes_gcm.ijk
}
}
@ -119,7 +116,7 @@ To facilitate key and method rollover, the user can specify a fallback configura
terraform {
encryption {
//...
statefile {
state {
method = method.aes_gcm.bar
fallback {
method = method.aes_gcm.baz
@ -172,7 +169,7 @@ key_provider "static" "my_key" {
method "aes_gcm" "foo" {
key_provider = key_provider.static.my_key
}
statefile {
state {
method = method.aes_gcm.foo
}
```
@ -193,7 +190,7 @@ statefile {
}
}
},
"statefile": {
"state": {
"method": "${method.aes_gcm.foo}"
}
}
@ -202,7 +199,7 @@ statefile {
The user can set either of these structures in the `TF_ENCRYPTION` environment variable:
```bash
export TF_ENCRYPTION='{"key_provider":{...},"method":{...},"statefile":{...}}'
export TF_ENCRYPTION='{"key_provider":{...},"method":{...},"state":{...}}'
```
When the user specifies both an environment and a code configuration, `tofu` merges the two configurations. If two values conflict, the environment configuration takes precedence.
@ -213,13 +210,10 @@ To ensure that the encryption cannot be accidentally forgotten or disabled and t
terraform {
encryption {
//...
statefile {
state {
enforced = true
}
planfile {
enforced = true
}
backend {
plan {
enforced = true
}
}
@ -266,23 +260,17 @@ The main component of the library should be the `Encryption` interface. This int
```go
type Encryption interface {
StateFile() StateEncryption
PlanFile() PlanEncryption
Backend() StateEncryption
RemoteState(string) ReadOnlyStateEncryption
State() StateEncryption
Plan() PlanEncryption
RemoteState(string) StateEncryption
}
```
Each of the returned encryption tools should provide methods to encrypt the data of the specified purpose, such as:
```go
type ReadOnlyStateEncryption interface {
DecryptState([]byte) ([]byte, error)
}
type StateEncryption interface {
ReadOnlyStateEncryption
DecryptState([]byte) ([]byte, error)
EncryptState([]byte) ([]byte, error)
}
```

View File

@ -172,7 +172,7 @@ func (b *Local) opPlan(
StateFile: plannedStateFile,
Plan: plan,
DependencyLocks: op.DependencyLocks,
}, op.Encryption.PlanFile())
}, op.Encryption.Plan())
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,

View File

@ -108,7 +108,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
// Prepare the backend, passing the plan file if present, and the
// backend-specific arguments
be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType, enc.Backend())
be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType, enc.State())
diags = diags.Append(beDiags)
if diags.HasErrors() {
view.Diagnostics(diags)
@ -168,7 +168,7 @@ func (c *ApplyCommand) LoadPlanFile(path string, enc encryption.Encryption) (*pl
// Try to load plan if path is specified
if path != "" {
var err error
planFile, err = c.PlanFile(path, enc.PlanFile())
planFile, err = c.PlanFile(path, enc.Plan())
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,

View File

@ -70,7 +70,7 @@ func (c *ConsoleCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -69,7 +69,7 @@ func (c *GraphCommand) Run(args []string) int {
// Try to load plan if path is specified
var planFile *planfile.WrappedPlanFile
if planPath != "" {
planFile, err = c.PlanFile(planPath, enc.PlanFile())
planFile, err = c.PlanFile(planPath, enc.Plan())
if err != nil {
c.Ui.Error(err.Error())
return 1
@ -86,7 +86,7 @@ func (c *GraphCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -175,7 +175,7 @@ func (c *ImportCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: config.Module.Backend,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -213,7 +213,7 @@ func (c *InitCommand) Run(args []string) int {
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, enc)
default:
// load the previously-stored backend config
back, backDiags = c.Meta.backendFromState(ctx, enc.Backend())
back, backDiags = c.Meta.backendFromState(ctx, enc.State())
}
if backendOutput {
header = true
@ -445,7 +445,7 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
Init: true,
}
back, backDiags := c.Backend(opts, enc.Backend())
back, backDiags := c.Backend(opts, enc.State())
diags = diags.Append(backDiags)
return back, true, diags
}
@ -528,7 +528,7 @@ the backend configuration is present and valid.
Init: true,
}
back, backDiags := c.Backend(opts, enc.Backend())
back, backDiags := c.Backend(opts, enc.State())
diags = diags.Append(backDiags)
return back, true, diags
}

View File

@ -74,7 +74,7 @@ func (c *OutputCommand) Outputs(statePath string, enc encryption.Encryption) (ma
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if diags.HasErrors() {
return nil, diags

View File

@ -140,7 +140,7 @@ func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.V
be, beDiags := c.Backend(&BackendOpts{
Config: backendConfig,
ViewType: viewType,
}, enc.Backend())
}, enc.State())
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
return nil, diags

View File

@ -93,7 +93,7 @@ func (c *ProvidersCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: config.Module.Backend,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -137,7 +137,7 @@ func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType argument
be, beDiags := c.Backend(&BackendOpts{
Config: backendConfig,
ViewType: viewType,
}, enc.Backend())
}, enc.State())
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
return nil, diags

View File

@ -170,7 +170,7 @@ func (c *ShowCommand) showFromLatestStateSnapshot(enc encryption.Encryption) (*s
var diags tfdiags.Diagnostics
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
return nil, diags
@ -281,7 +281,7 @@ func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*
var stateFile *statefile.File
var config *configs.Config
pf, err := planfile.OpenWrapped(path, enc.PlanFile())
pf, err := planfile.OpenWrapped(path, enc.Plan())
if err != nil {
return nil, nil, nil, nil, err
}
@ -298,7 +298,7 @@ func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*
func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool, enc encryption.Encryption) (*cloudplan.RemotePlanJSON, error) {
// Set up the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
if backendDiags.HasErrors() {
return nil, errUnusable(backendDiags.Err(), "cloud plan")
}
@ -347,7 +347,7 @@ func getStateFromPath(path string, enc encryption.Encryption) (*statefile.File,
defer file.Close()
var stateFile *statefile.File
stateFile, err = statefile.Read(file, enc.StateFile())
stateFile, err = statefile.Read(file, enc.State())
if err != nil {
return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err)
}

View File

@ -47,7 +47,7 @@ func (c *StateListCommand) Run(args []string) int {
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
if backendDiags.HasErrors() {
c.showDiagnostics(backendDiags)
return 1

View File

@ -38,7 +38,7 @@ func (c *StateMeta) State(enc encryption.Encryption) (statemgr.Full, error) {
realState = statemgr.NewFilesystem(c.statePath, encryption.StateEncryptionDisabled()) // User specified state file should not be encrypted
} else {
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
if backendDiags.HasErrors() {
return nil, backendDiags.Err()
}
@ -62,7 +62,7 @@ func (c *StateMeta) State(enc encryption.Encryption) (statemgr.Full, error) {
}
// Get a local backend
localRaw, backendDiags := c.Backend(&BackendOpts{ForceLocal: true}, enc.Backend())
localRaw, backendDiags := c.Backend(&BackendOpts{ForceLocal: true}, enc.State())
if backendDiags.HasErrors() {
// This should never fail
panic(backendDiags.Err())

View File

@ -77,7 +77,7 @@ func (c *StateMvCommand) Run(args []string) int {
}
if len(setLegacyLocalBackendOptions) > 0 {
currentBackend, diags := c.backendFromConfig(&BackendOpts{}, enc.Backend())
currentBackend, diags := c.backendFromConfig(&BackendOpts{}, enc.State())
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
@ -399,7 +399,7 @@ func (c *StateMvCommand) Run(args []string) int {
return 0 // This is as far as we go in dry-run mode
}
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -42,7 +42,7 @@ func (c *StatePullCommand) Run(args []string) int {
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
if backendDiags.HasErrors() {
c.showDiagnostics(backendDiags)
return 1

View File

@ -87,7 +87,7 @@ func (c *StatePushCommand) Run(args []string) int {
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
if backendDiags.HasErrors() {
c.showDiagnostics(backendDiags)
return 1

View File

@ -175,7 +175,7 @@ func (c *StateReplaceProviderCommand) Run(args []string) int {
resource.ProviderConfig.Provider = to
}
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -124,7 +124,7 @@ func (c *StateRmCommand) Run(args []string) int {
return 0 // This is as far as we go in dry-run mode
}
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -58,7 +58,7 @@ func (c *StateShowCommand) Run(args []string) int {
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
if backendDiags.HasErrors() {
c.showDiagnostics(backendDiags)
return 1

View File

@ -75,7 +75,7 @@ func (c *TaintCommand) Run(args []string) int {
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -66,7 +66,7 @@ func (c *UntaintCommand) Run(args []string) int {
}
// Load the backend
b, backendDiags := c.Backend(nil, enc.Backend())
b, backendDiags := c.Backend(nil, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -74,7 +74,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -90,7 +90,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -71,7 +71,7 @@ func (c *WorkspaceSelectCommand) Run(args []string) int {
// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
}, enc.Backend())
}, enc.State())
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)

View File

@ -17,10 +17,9 @@ type EncryptionConfig struct {
KeyProviderConfigs []KeyProviderConfig `hcl:"key_provider,block"`
MethodConfigs []MethodConfig `hcl:"method,block"`
Backend *EnforcableTargetConfig `hcl:"backend,block"`
StateFile *EnforcableTargetConfig `hcl:"statefile,block"`
PlanFile *EnforcableTargetConfig `hcl:"planfile,block"`
Remote *RemoteConfig `hcl:"remote,block"`
State *EnforcableTargetConfig `hcl:"state,block"`
Plan *EnforcableTargetConfig `hcl:"plan,block"`
Remote *RemoteConfig `hcl:"remote_state_data_sources,block"`
// Not preserved through merge operations
DeclRange hcl.Range
@ -61,7 +60,7 @@ func (m MethodConfig) Addr() (method.Addr, hcl.Diagnostics) {
// sources.
type RemoteConfig struct {
Default *TargetConfig `hcl:"default,block"`
Targets []NamedTargetConfig `hcl:"remote_state,block"`
Targets []NamedTargetConfig `hcl:"remote_state_data_source,block"`
}
// TargetConfig describes the target.encryption.state, target.encryption.plan, etc blocks.

View File

@ -22,10 +22,9 @@ func MergeConfigs(cfg *EncryptionConfig, override *EncryptionConfig) *Encryption
KeyProviderConfigs: mergeKeyProviderConfigs(cfg.KeyProviderConfigs, override.KeyProviderConfigs),
MethodConfigs: mergeMethodConfigs(cfg.MethodConfigs, override.MethodConfigs),
StateFile: mergeEnforcableTargetConfigs(cfg.StateFile, override.StateFile),
PlanFile: mergeEnforcableTargetConfigs(cfg.PlanFile, override.PlanFile),
Backend: mergeEnforcableTargetConfigs(cfg.Backend, override.Backend),
Remote: mergeRemoteConfigs(cfg.Remote, override.Remote),
State: mergeEnforcableTargetConfigs(cfg.State, override.State),
Plan: mergeEnforcableTargetConfigs(cfg.Plan, override.Plan),
Remote: mergeRemoteConfigs(cfg.Remote, override.Remote),
}
}

View File

@ -14,14 +14,11 @@ import (
// Encryption contains the methods for obtaining a StateEncryption or PlanEncryption correctly configured for a specific
// purpose. If no encryption configuration is present, it should return a pass through method that doesn't do anything.
type Encryption interface {
// StateFile produces a StateEncryption overlay for encrypting and decrypting state files for local storage.
StateFile() StateEncryption
// State produces a StateEncryption overlay for encrypting and decrypting state files for local storage.
State() StateEncryption
// PlanFile produces a PlanEncryption overlay for encrypting and decrypting plan files.
PlanFile() PlanEncryption
// Backend produces a StateEncryption overlay for storing state files on remote backends, such as an S3 bucket.
Backend() StateEncryption
// Plan produces a PlanEncryption overlay for encrypting and decrypting plan files.
Plan() PlanEncryption
// RemoteState produces a StateEncryption for reading remote states using the terraform_remote_state data
// source.
@ -29,9 +26,8 @@ type Encryption interface {
}
type encryption struct {
statefile StateEncryption
planfile PlanEncryption
backend StateEncryption
state StateEncryption
plan PlanEncryption
remoteDefault StateEncryption
remotes map[string]StateEncryption
@ -55,25 +51,18 @@ func New(reg registry.Registry, cfg *config.EncryptionConfig) (Encryption, hcl.D
var diags hcl.Diagnostics
var encDiags hcl.Diagnostics
if cfg.StateFile != nil {
enc.statefile, encDiags = newStateEncryption(enc, cfg.StateFile.AsTargetConfig(), cfg.StateFile.Enforced, "statefile")
if cfg.State != nil {
enc.state, encDiags = newStateEncryption(enc, cfg.State.AsTargetConfig(), cfg.State.Enforced, "state")
diags = append(diags, encDiags...)
} else {
enc.statefile = StateEncryptionDisabled()
enc.state = StateEncryptionDisabled()
}
if cfg.PlanFile != nil {
enc.planfile, encDiags = newPlanEncryption(enc, cfg.PlanFile.AsTargetConfig(), cfg.PlanFile.Enforced, "planfile")
if cfg.Plan != nil {
enc.plan, encDiags = newPlanEncryption(enc, cfg.Plan.AsTargetConfig(), cfg.Plan.Enforced, "plan")
diags = append(diags, encDiags...)
} else {
enc.planfile = PlanEncryptionDisabled()
}
if cfg.Backend != nil {
enc.backend, encDiags = newStateEncryption(enc, cfg.Backend.AsTargetConfig(), cfg.Backend.Enforced, "backend")
diags = append(diags, encDiags...)
} else {
enc.backend = StateEncryptionDisabled()
enc.plan = PlanEncryptionDisabled()
}
if cfg.Remote != nil && cfg.Remote.Default != nil {
@ -97,16 +86,12 @@ func New(reg registry.Registry, cfg *config.EncryptionConfig) (Encryption, hcl.D
return enc, diags
}
func (e *encryption) StateFile() StateEncryption {
return e.statefile
func (e *encryption) State() StateEncryption {
return e.state
}
func (e *encryption) PlanFile() PlanEncryption {
return e.planfile
}
func (e *encryption) Backend() StateEncryption {
return e.backend
func (e *encryption) Plan() PlanEncryption {
return e.plan
}
func (e *encryption) RemoteState(name string) StateEncryption {
@ -122,9 +107,8 @@ type encryptionDisabled struct{}
func Disabled() Encryption {
return &encryptionDisabled{}
}
func (e *encryptionDisabled) StateFile() StateEncryption { return StateEncryptionDisabled() }
func (e *encryptionDisabled) PlanFile() PlanEncryption { return PlanEncryptionDisabled() }
func (e *encryptionDisabled) Backend() StateEncryption { return StateEncryptionDisabled() }
func (e *encryptionDisabled) State() StateEncryption { return StateEncryptionDisabled() }
func (e *encryptionDisabled) Plan() PlanEncryption { return PlanEncryptionDisabled() }
func (e *encryptionDisabled) RemoteState(name string) StateEncryption {
return StateEncryptionDisabled()
}

View File

@ -45,16 +45,13 @@ func EncryptionRequired() encryption.Encryption {
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
statefile {
state {
method = method.aes_gcm.example
}
planfile {
plan {
method = method.aes_gcm.example
}
backend {
method = method.aes_gcm.example
}
remote {
remote_state_data_sources {
default {
method = method.aes_gcm.example
}
@ -70,19 +67,15 @@ func EncryptionWithFallback() encryption.Encryption {
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
statefile {
state {
method = method.aes_gcm.example
fallback {}
}
planfile {
plan {
method = method.aes_gcm.example
fallback {}
}
backend {
method = method.aes_gcm.example
fallback {}
}
remote {
remote_state_data_sources {
default {
method = method.aes_gcm.example
fallback {}

View File

@ -18,7 +18,7 @@ import (
var (
ConfigA = `
backend {
state {
enforced = true
}
`
@ -29,11 +29,7 @@ key_provider "static" "basic" {
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
statefile {
method = method.aes_gcm.example
}
backend {
state {
method = method.aes_gcm.example
}
`
@ -65,7 +61,7 @@ func Example() {
enc, diags := encryption.New(reg, cfg)
handleDiags(diags)
sfe := enc.StateFile()
sfe := enc.State()
// Encrypt the data, for this example we will be using the string "test",
// but in a real world scenario this would be the plan file.

View File

@ -24,7 +24,7 @@ method "aes_gcm" "bar" {
keys = key_provider.static.foo
}
planfile {
plan {
method = method.aes_gcm.bar
}
`
@ -49,7 +49,7 @@ func Example() {
panic(diags)
}
encryptor := enc.PlanFile()
encryptor := enc.Plan()
encryptedPlan, err := encryptor.EncryptPlan([]byte("Hello world!"))
if err != nil {

View File

@ -39,7 +39,7 @@ func TestReadErrNoState_nilFile(t *testing.T) {
func TestReadEmptyWithEncryption(t *testing.T) {
payload := bytes.NewBufferString("")
_, err := Read(payload, enctest.EncryptionRequired().Backend())
_, err := Read(payload, enctest.EncryptionRequired().State())
if !errors.Is(err, ErrNoState) {
t.Fatalf("expected ErrNoState, got %T", err)
}
@ -47,7 +47,7 @@ func TestReadEmptyWithEncryption(t *testing.T) {
func TestReadEmptyJsonWithEncryption(t *testing.T) {
payload := bytes.NewBufferString("{}")
_, err := Read(payload, enctest.EncryptionRequired().Backend())
_, err := Read(payload, enctest.EncryptionRequired().State())
if err == nil || err.Error() != "unable to determine data structure during decryption: Given payload is not a state file" {
t.Fatalf("expected encryption error, got %v", err)

View File

@ -84,7 +84,7 @@ func TestRoundtrip(t *testing.T) {
func TestRoundtripEncryption(t *testing.T) {
const path = "testdata/roundtrip/v4-modules.out.tfstate"
enc := enctest.EncryptionWithFallback().Backend()
enc := enctest.EncryptionWithFallback().State()
unencryptedInput, err := os.Open(path)
if err != nil {