From 36c6daa11c4bce70f8baf9365da80260e573f595 Mon Sep 17 00:00:00 2001 From: Borja Clemente Date: Wed, 6 May 2026 11:11:45 +0200 Subject: [PATCH 1/3] feat(install): Add render-sensitive flag Introduce the --render-sensitive flag to the hypershift install render command to avoid leaking secrets. This is now needed because the new webhookcerts controller introduces secrets that should not be rendered by default. Signed-off-by: Borja Clemente --- cmd/install/install.go | 1 + cmd/install/install_render.go | 14 +++++++++ cmd/install/install_test.go | 56 +++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/cmd/install/install.go b/cmd/install/install.go index a38def732fd..e022f1ecb7b 100644 --- a/cmd/install/install.go +++ b/cmd/install/install.go @@ -155,6 +155,7 @@ type Options struct { ScaleFromZeroCreds string ScaleFromZeroCredentialsSecret string ScaleFromZeroCredentialsSecretKey string + RenderSensitive bool } func (o *Options) Validate() error { diff --git a/cmd/install/install_render.go b/cmd/install/install_render.go index c18eba8aa26..db251a691cc 100644 --- a/cmd/install/install_render.go +++ b/cmd/install/install_render.go @@ -9,6 +9,7 @@ import ( hyperapi "github.com/openshift/hypershift/support/api" "github.com/openshift/hypershift/support/config" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" @@ -69,6 +70,7 @@ func NewRenderCommand(opts *Options) *cobra.Command { cmd.Flags().StringVar(&opts.Format, "format", RenderFormatYaml, fmt.Sprintf("Output format for the manifests, supports %s and %s", RenderFormatYaml, RenderFormatJson)) cmd.Flags().StringVar(&opts.OutputTypes, "outputs", string(OutputAll), fmt.Sprintf("Which manifests to output, one of %s, %s, or %s. Output CRDs separately to allow applying them first and waiting for them to be established.", OutputAll, OutputCRDs, OutputResources)) cmd.Flags().StringVar(&opts.OutputFile, "output-file", "", "File to write the rendered manifests to. Writes to STDOUT if not specified.") + cmd.Flags().BoolVar(&opts.RenderSensitive, "render-sensitive", false, "Render secrets in the output. By default secrets are excluded to avoid leaking private key material into GitOps repositories") cmd.MarkFlagsMutuallyExclusive("template", "outputs") cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -129,6 +131,18 @@ func RenderHyperShiftOperator(ctx context.Context, cmdOut io.Writer, opts *Optio case OutputResources: objectsToRender = objects } + + if !opts.RenderSensitive { + filtered := make([]crclient.Object, 0, len(objectsToRender)) + for _, obj := range objectsToRender { + if _, isSecret := obj.(*corev1.Secret); isSecret { + continue + } + filtered = append(filtered, obj) + } + objectsToRender = filtered + } + var out io.Writer if opts.OutputFile != "" { file, err := os.Create(opts.OutputFile) diff --git a/cmd/install/install_test.go b/cmd/install/install_test.go index f6ea7ca255b..0badad0b55a 100644 --- a/cmd/install/install_test.go +++ b/cmd/install/install_test.go @@ -1,6 +1,7 @@ package install import ( + "bytes" "context" "io" "io/fs" @@ -490,6 +491,61 @@ func TestSetupCRDs(t *testing.T) { } } +func TestRenderHyperShiftOperator_RenderSensitive(t *testing.T) { + tests := []struct { + name string + renderSensitive bool + expectSecrets bool + }{ + { + name: "When render-sensitive is false it should exclude secrets from output", + renderSensitive: false, + expectSecrets: false, + }, + { + name: "When render-sensitive is true it should include secrets in output", + renderSensitive: true, + expectSecrets: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + var buf bytes.Buffer + opts := &Options{ + PrivatePlatform: string(hyperv1.NonePlatform), + EnableDefaultingWebhook: true, + EnableValidatingWebhook: true, + EnableConversionWebhook: true, + RenderSensitive: tc.renderSensitive, + Format: RenderFormatYaml, + OutputTypes: string(OutputAll), + } + err := RenderHyperShiftOperator(t.Context(), &buf, opts) + g.Expect(err).ToNot(HaveOccurred()) + + var secretNames []string + for doc := range strings.SplitSeq(buf.String(), "\n---\n") { + obj, _, err := hyperapi.YamlSerializer.Decode([]byte(doc), nil, nil) + if err != nil { + continue + } + if secret, ok := obj.(*corev1.Secret); ok { + secretNames = append(secretNames, secret.Name) + } + } + + if tc.expectSecrets { + g.Expect(secretNames).ToNot(BeEmpty(), "expected secrets in rendered output") + } else { + g.Expect(secretNames).To(BeEmpty(), "expected no secrets in rendered output") + } + }) + } +} + func TestHyperShiftOperatorManifests_SharedIngress(t *testing.T) { tests := []struct { name string From c41aeae3d554dd028a5079f246806cc3101089da Mon Sep 17 00:00:00 2001 From: Borja Clemente Date: Wed, 6 May 2026 11:46:56 +0200 Subject: [PATCH 2/3] fix(install): require --render-sensitive when using --template The hypershift install render --template command still leaks secrets after introducing the --render-sensitive flag. This makes it required to have the flag set to true to acknowledge that the template contains sensitive data. Signed-off-by: Borja Clemente --- cmd/install/install_render.go | 4 ++++ cmd/install/install_render_test.go | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/install/install_render.go b/cmd/install/install_render.go index db251a691cc..e420ecaed8d 100644 --- a/cmd/install/install_render.go +++ b/cmd/install/install_render.go @@ -105,6 +105,10 @@ func RenderHyperShiftOperator(ctx context.Context, cmdOut io.Writer, opts *Optio return err } + if opts.Template && !opts.RenderSensitive { + return fmt.Errorf("--template requires --render-sensitive=true because Template output can embed Secret objects") + } + var crds []crclient.Object var objects []crclient.Object diff --git a/cmd/install/install_render_test.go b/cmd/install/install_render_test.go index fe31c1835a1..be2c72fe470 100644 --- a/cmd/install/install_render_test.go +++ b/cmd/install/install_render_test.go @@ -65,7 +65,7 @@ func TestMultiDocYamlRendering(t *testing.T) { } func TestTemplateYamlRendering(t *testing.T) { - template, err := ExecuteTemplateYamlGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "yaml", "--template"}) + template, err := ExecuteTemplateYamlGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "yaml", "--template", "--render-sensitive"}) if err != nil { t.Fatal(err) } @@ -104,6 +104,17 @@ func ExecuteJsonGenerationCommand(args []string) (map[string]interface{}, error) return doc, nil } +func TestWhenTemplateWithoutRenderSensitiveItShouldFail(t *testing.T) { + _, err := ExecuteTestCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "yaml", "--template"}) + if err == nil { + t.Fatal("expected error when using --template without --render-sensitive") + } + expectedMsg := "--template requires --render-sensitive=true because Template output can embed Secret objects" + if err.Error() != expectedMsg { + t.Fatalf("expected error message %q, got %q", expectedMsg, err.Error()) + } +} + func TestJsonListRendering(t *testing.T) { doc, err := ExecuteJsonGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "json"}) if err != nil { @@ -120,7 +131,7 @@ func TestJsonListRendering(t *testing.T) { } func TestJsonTemplateRendering(t *testing.T) { - doc, err := ExecuteJsonGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "json", "--template"}) + doc, err := ExecuteJsonGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "json", "--template", "--render-sensitive"}) if err != nil { t.Fatal(err) } From 2376a26a469e39ebfb4bfe7e67eada3e3fa15bc3 Mon Sep 17 00:00:00 2001 From: Borja Clemente Date: Wed, 6 May 2026 13:00:28 +0200 Subject: [PATCH 3/3] fix(review): tighten unit test sucess conditions Fail on decode errors and assert at least one manifest was decoded. --- cmd/install/install_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/install/install_test.go b/cmd/install/install_test.go index 0badad0b55a..b01725f0b92 100644 --- a/cmd/install/install_test.go +++ b/cmd/install/install_test.go @@ -527,16 +527,19 @@ func TestRenderHyperShiftOperator_RenderSensitive(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) var secretNames []string + decodedCount := 0 for doc := range strings.SplitSeq(buf.String(), "\n---\n") { - obj, _, err := hyperapi.YamlSerializer.Decode([]byte(doc), nil, nil) - if err != nil { + if strings.TrimSpace(doc) == "" { continue } + obj, _, err := hyperapi.YamlSerializer.Decode([]byte(doc), nil, nil) + g.Expect(err).ToNot(HaveOccurred(), "failed to decode rendered manifest") + decodedCount++ if secret, ok := obj.(*corev1.Secret); ok { secretNames = append(secretNames, secret.Name) } } - + g.Expect(decodedCount).To(BeNumerically(">", 0), "expected rendered manifests to be decodable") if tc.expectSecrets { g.Expect(secretNames).ToNot(BeEmpty(), "expected secrets in rendered output") } else {