From 5954a8e99bb5d944ebe902a5ef23d5aba7d0bf36 Mon Sep 17 00:00:00 2001 From: eding Date: Sat, 12 Sep 2026 19:52:36 +0200 Subject: [PATCH] fix: bound OpenBao mutation commands --- internal/openbao/bootstrap.go | 59 ++++++++++++++++++++++++------ internal/openbao/bootstrap_test.go | 54 ++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 20 deletions(-) diff --git a/internal/openbao/bootstrap.go b/internal/openbao/bootstrap.go index d177608..6554b79 100644 --- a/internal/openbao/bootstrap.go +++ b/internal/openbao/bootstrap.go @@ -53,6 +53,8 @@ var openBaoStatus = getStatus var commandTimeout = time.Minute +var openBaoMutationTimeout = 5 * time.Minute + func EnsureRecoveryIdentity(identityPath string) (string, error) { if _, err := os.Stat(identityPath); os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(identityPath), 0700); err != nil { @@ -262,8 +264,11 @@ func writeSecret(kubeconfig, rootToken, secretPath string, values map[string]str decodes = append(decodes, fmt.Sprintf("value%d=$(printf '%%s' \"$value%d_b64\" | base64 -d; printf x)\nvalue%d=${value%d%%x}", index, index, index, index)) } script := "read -r root_token\n" + strings.Join(reads, "\n") + "\n" + strings.Join(decodes, "\n") + "\nexport BAO_TOKEN=\"$root_token\"\nbao kv put secret/" + secretPath + " " + strings.Join(arguments, " ") + " >/dev/null" - _, err := execInPod(kubeconfig, []byte(input.String()), "sh", "-ec", script) - return err + _, err := execInPodMutation(kubeconfig, []byte(input.String()), "sh", "-ec", script) + if err != nil { + return fmt.Errorf("write OpenBao secret %q", secretPath) + } + return nil } func waitForPod(kubeconfig string) error { @@ -359,13 +364,13 @@ path "secret/metadata/cicd/*" { capabilities = ["list", "read"] } EOF -bao policy write external-secrets /tmp/external-secrets.hcl >/dev/null 2>&1 || fail platform-external-secrets-policy -rm -f /tmp/external-secrets.hcl -bao write auth/kubernetes/role/external-secrets bound_service_account_names=external-secrets bound_service_account_namespaces=external-secrets policies=external-secrets ttl=1h >/dev/null 2>&1 || fail platform-external-secrets-role` + bao policy write external-secrets /tmp/external-secrets.hcl >/dev/null || fail platform-external-secrets-policy + rm -f /tmp/external-secrets.hcl + bao write auth/kubernetes/role/external-secrets bound_service_account_names=external-secrets bound_service_account_namespaces=external-secrets policies=external-secrets ttl=1h >/dev/null 2>&1 || fail platform-external-secrets-role` input := []byte(rootToken + "\n" + reviewerToken + "\n") - output, err := execInPod(kubeconfig, input, "sh", "-ec", script) + output, err := execInPodMutation(kubeconfig, input, "sh", "-ec", script) if err != nil { - return fmt.Errorf("configure Kubernetes auth: %w: %s", err, strings.TrimSpace(string(output))) + return openBaoMutationError("configure Kubernetes auth", err, output, rootToken, reviewerToken) } return nil } @@ -404,8 +409,31 @@ func ConfigureSecretGrants(kubeconfig, identityPath, bundlePath string, grants [ script.WriteString("rm -f /tmp/" + name + ".hcl\n") script.WriteString("bao write auth/kubernetes/role/" + name + " bound_service_account_names=" + name + " bound_service_account_namespaces=" + namespace + " policies=" + name + " ttl=1h >/dev/null\n") } - _, err = execInPod(kubeconfig, []byte(material.RootToken+"\n"), "sh", "-ec", script.String()) - return err + output, err := execInPodMutation(kubeconfig, []byte(material.RootToken+"\n"), "sh", "-ec", script.String()) + if err != nil { + return openBaoMutationError("configure OpenBao secret grants", err, output, material.RootToken) + } + return nil +} + +func openBaoMutationError(action string, err error, output []byte, sensitive ...string) error { + diagnostic := redactOpenBaoDiagnostic(strings.TrimSpace(string(output)), sensitive...) + if diagnostic == "" && err != nil { + diagnostic = redactOpenBaoDiagnostic(err.Error(), sensitive...) + } + if diagnostic == "" { + return errors.New(action) + } + return fmt.Errorf("%s: %s", action, diagnostic) +} + +func redactOpenBaoDiagnostic(diagnostic string, sensitive ...string) string { + for _, value := range sensitive { + if value != "" { + diagnostic = strings.ReplaceAll(diagnostic, value, "[REDACTED]") + } + } + return diagnostic } func refreshExternalSecrets(kubeconfig string) error { @@ -453,6 +481,11 @@ var execInPod = func(kubeconfig string, input []byte, args ...string) ([]byte, e return commandOutput(input, "kubectl", command...) } +var execInPodMutation = func(kubeconfig string, input []byte, args ...string) ([]byte, error) { + command := append([]string{"--kubeconfig", kubeconfig, "-n", "openbao", "exec", "-i", "openbao-0", "--"}, args...) + return commandOutputWithTimeout(input, openBaoMutationTimeout, "kubectl", command...) +} + var execInUnsealController = func(kubeconfig, script string) ([]byte, error) { command := []string{"--kubeconfig", kubeconfig, "-n", "openbao", "exec", "deployment/openbao-unseal", "--", "sh", "-ec", script} return commandOutput(nil, "kubectl", command...) @@ -464,13 +497,17 @@ var kubectlOutput = func(kubeconfig string, args ...string) ([]byte, error) { } func commandOutput(input []byte, name string, args ...string) ([]byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), commandTimeout) + return commandOutputWithTimeout(input, commandTimeout, name, args...) +} + +func commandOutputWithTimeout(input []byte, timeout time.Duration, name string, args ...string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() cmd := exec.CommandContext(ctx, name, args...) cmd.Stdin = bytes.NewReader(input) output, err := cmd.CombinedOutput() if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return output, fmt.Errorf("%s timed out after %s", name, commandTimeout) + return output, fmt.Errorf("%s timed out after %s", name, timeout) } return output, err } diff --git a/internal/openbao/bootstrap_test.go b/internal/openbao/bootstrap_test.go index 12c5fcc..a82e8db 100644 --- a/internal/openbao/bootstrap_test.go +++ b/internal/openbao/bootstrap_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/Pingu-Studio/MaidnCLI/internal/config" ) @@ -64,10 +65,10 @@ func TestUnsealFallsBackToControllerSecret(t *testing.T) { } func TestWriteSecretFramesMultilineValues(t *testing.T) { - original := execInPod - t.Cleanup(func() { execInPod = original }) + original := execInPodMutation + t.Cleanup(func() { execInPodMutation = original }) var input, script string - execInPod = func(_ string, contents []byte, args ...string) ([]byte, error) { + execInPodMutation = func(_ string, contents []byte, args ...string) ([]byte, error) { input, script = string(contents), args[len(args)-1] return nil, nil } @@ -77,6 +78,19 @@ func TestWriteSecretFramesMultilineValues(t *testing.T) { } } +func TestWriteSecretRedactsMutationFailure(t *testing.T) { + original := execInPodMutation + t.Cleanup(func() { execInPodMutation = original }) + const value = "must-not-leak" + execInPodMutation = func(_ string, _ []byte, _ ...string) ([]byte, error) { + return []byte(value), errors.New(value) + } + err := writeSecret("kubeconfig", "root-token", "cicd/demo", map[string]string{"password": value}) + if err == nil || err.Error() != `write OpenBao secret "cicd/demo"` || strings.Contains(err.Error(), value) { + t.Fatalf("secret write error leaked a value: %v", err) + } +} + func TestRefreshExternalSecretsIsReadyGatedAndScoped(t *testing.T) { original := kubectlOutput t.Cleanup(func() { kubectlOutput = original }) @@ -113,13 +127,13 @@ func TestRefreshExternalSecretsSkipsWebhookBeforeTekton(t *testing.T) { } func TestConfigureSecretGrantsScopesApplicationAndSharedPaths(t *testing.T) { - originalDecrypt, originalExec := decryptRecovery, execInPod - t.Cleanup(func() { decryptRecovery, execInPod = originalDecrypt, originalExec }) + originalDecrypt, originalExec := decryptRecovery, execInPodMutation + t.Cleanup(func() { decryptRecovery, execInPodMutation = originalDecrypt, originalExec }) decryptRecovery = func(_, _ string) ([]byte, error) { return []byte(`{"unseal_keys_b64":["share"],"unseal_threshold":1,"root_token":"root"}`), nil } var script string - execInPod = func(_ string, input []byte, args ...string) ([]byte, error) { + execInPodMutation = func(_ string, input []byte, args ...string) ([]byte, error) { if string(input) != "root\n" || len(args) != 3 || args[0] != "sh" || args[1] != "-ec" { t.Fatal("secret grant did not use root token through stdin") } @@ -150,11 +164,27 @@ func TestConfigureSecretGrantsScopesApplicationAndSharedPaths(t *testing.T) { } } +func TestConfigureSecretGrantsPreservesRedactedPolicyDiagnostics(t *testing.T) { + originalDecrypt, originalExec := decryptRecovery, execInPodMutation + t.Cleanup(func() { decryptRecovery, execInPodMutation = originalDecrypt, originalExec }) + const rootToken = "must-not-leak" + decryptRecovery = func(_, _ string) ([]byte, error) { + return []byte(`{"unseal_keys_b64":["share"],"unseal_threshold":1,"root_token":"must-not-leak"}`), nil + } + execInPodMutation = func(_ string, _ []byte, _ ...string) ([]byte, error) { + return []byte("policy write denied for " + rootToken), errors.New("exit status 1") + } + err := ConfigureSecretGrants("kubeconfig", "identity", "bundle", []config.SecretGrant{{Application: "orders-api", Consumer: "publish"}}) + if err == nil || !strings.Contains(err.Error(), "policy write denied") || strings.Contains(err.Error(), rootToken) { + t.Fatalf("policy diagnostics were not useful and redacted: %v", err) + } +} + func TestConfigureKubernetesAuthLimitsPlatformStore(t *testing.T) { - original := execInPod - t.Cleanup(func() { execInPod = original }) + original := execInPodMutation + t.Cleanup(func() { execInPodMutation = original }) var script string - execInPod = func(_ string, _ []byte, args ...string) ([]byte, error) { + execInPodMutation = func(_ string, _ []byte, args ...string) ([]byte, error) { script = args[len(args)-1] return nil, nil } @@ -171,6 +201,12 @@ func TestConfigureKubernetesAuthLimitsPlatformStore(t *testing.T) { } } +func TestOpenBaoMutationTimeoutIsSeparateFromProbeTimeout(t *testing.T) { + if commandTimeout != time.Minute || openBaoMutationTimeout != 5*time.Minute { + t.Fatalf("probe timeout %s, mutation timeout %s", commandTimeout, openBaoMutationTimeout) + } +} + func TestReadRecoveryMaterialDecryptsAndValidatesBundle(t *testing.T) { original := decryptRecovery t.Cleanup(func() { decryptRecovery = original }) -- 2.43.7