Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ Images are published to [Docker Hub](https://hub.docker.com/r/temporalio/tempora
docker pull temporalio/temporal-proxy:latest
```

To guard against a deployment that would forward payloads unencrypted, start the proxy with
`--require-encryption` (or set `PROXY_REQUIRE_ENCRYPTION=true`). The proxy then refuses to start unless the config
has an encryption key (`encryption.default`). It checks for a key rather than `encryption.enabled`, so a decrypt-only
proxy (a key with `enabled: false`) still starts. The flag sits outside the config file so that editing the config
alone cannot turn it off.

### Helm

A chart is published to the Temporal Helm repo at `https://go.temporal.io/helm-charts`:
Expand Down
6 changes: 6 additions & 0 deletions cmd/proxy/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ func serve() *cli.Command {
Value: "info",
Sources: cli.EnvVars("LOG_LEVEL"),
},
&cli.BoolFlag{
Name: "require-encryption",
Usage: "Refuse to start unless the config has an encryption key (encryption.default)",
Sources: cli.EnvVars("PROXY_REQUIRE_ENCRYPTION"),
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
log := logger.NewZeroLogger(os.Stderr, logger.ParseLevel(cmd.String("level")))
Expand All @@ -81,6 +86,7 @@ func serve() *cli.Command {
auth.Module,
codecserver.Module,
config.Module,
config.RequireKey(cmd.Bool("require-encryption")),
connect.Module,
dataplane.Module,
httpserver.Module,
Expand Down
15 changes: 15 additions & 0 deletions internal/config/encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@ func (e *Encryption) Validate() error {
return validation.Validate("", rules...)
}

// RequireKey fails unless a Default key policy is configured, whether or not
// encryption is Enabled.
func (e *Encryption) RequireKey() error {
Comment thread
pseudomuto marked this conversation as resolved.
return validation.Validate(
"encryption",
validation.Field("default", e.Default, func(p *KeyPolicy) error {
if p == nil {
return errors.New("a key is required when encryption is required")
}

return nil
}),
)
}

// Mode maps b to the vault's binding mode. An absent value is an error rather
// than a default, since the right mode depends on whether earlier payloads exist.
// It is the only place the spellings are known, so validation and vault
Expand Down
13 changes: 13 additions & 0 deletions internal/config/fx.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,16 @@ func Supply(cfg *Config) fx.Option {
NewAllowlist,
)
}

// RequireKey fails the graph unless the provided *Config has an encryption key
// (see [Encryption.RequireKey]). When required is false it adds nothing. It
// works with either [Module] or [Supply].
func RequireKey(required bool) fx.Option {
if !required {
return fx.Options()
}

return fx.Invoke(func(cfg *Config) error {
return cfg.Encryption.RequireKey()
})
}
46 changes: 46 additions & 0 deletions internal/config/fx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,49 @@ func TestModuleRejectsInvalidFile(t *testing.T) {
)
require.ErrorContains(t, app.Err(), "at least one upstream is required")
}

// TestRequireKey covers the startup guard against a proxy that would forward
// plaintext. It keys on a configured key, not on enabled, so a decrypt-only
// proxy (a key with enabled false) still starts.
func TestRequireKey(t *testing.T) {
t.Parallel()

const key = " namespaceBinding: strict\n default:\n uri: testing://a2V5\n duration: 1h\n renewBefore: 10m\n"

tests := []struct {
name string
require bool
yaml string
wantErr bool
}{
{name: "no encryption block", require: true, yaml: "", wantErr: true},
{name: "disabled without a key", require: true, yaml: "encryption:\n enabled: false\n", wantErr: true},
{name: "decrypt-only with a key", require: true, yaml: "encryption:\n enabled: false\n" + key},
{name: "enabled with a key", require: true, yaml: "encryption:\n enabled: true\n" + key},
{name: "not required and no key", require: false, yaml: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

path := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(path, []byte("hostPort: :7233\n"+upstreamYAML+tt.yaml), 0o600))

app := fx.New(
fx.NopLogger,
fx.Supply(fx.Annotate(path, config.ConfigFileTag)),
config.Module,
config.RequireKey(tt.require),
fx.Invoke(func(*config.Config) {}),
)

if tt.wantErr {
require.ErrorContains(t, app.Err(), "encryption: default: a key is required when encryption is required")
return
}

require.NoError(t, app.Err())
})
}
}
Loading