Skip to content

Conversation

@banjoh
Copy link
Member

@banjoh banjoh commented Dec 10, 2025

What this PR does / why we need it:

Add a new CLI subcommand to securely update Admin Console TLS certificates without using the acceptAnonymousUploads workflow which exposes a security vulnerability.

Usage:
<installer> admin-console update-tls --tls-cert <path> --tls-key <path> [--hostname <hostname>]

The command reads and validates the certificate/key pair, then updates the kotsadm-tls secret. No pod restart is needed as pods using this secret are expected to watch for changes and reload automatically.

Optionally accepts --hostname flag for display URL (not TLS validation).

Which issue(s) this PR fixes:

https://app.shortcut.com/replicated/story/131995/add-cli-subcommand-to-update-admin-console-tls-cert-and-key

Does this PR require a test?

Yes. Automated tests and manual tests

./build/embedded-cluster-darwin-arm64 admin-console update-tls -h
Update the TLS certificate and key used by the embedded-cluster-darwin-arm64 Admin Console.

This command updates the kotsadm-tls secret. Pods using this secret are
expected to watch for changes and automatically reload the TLS configuration.
This provides a secure alternative to the acceptAnonymousUploads workflow.

The --hostname flag is optional and only affects the display URL shown
to users. It does not affect TLS certificate validation.

Usage:
  embedded-cluster-darwin-arm64 admin-console update-tls [flags]

Flags:
  -h, --help              help for update-tls
      --hostname string   Hostname for display in URLs (optional, does not affect TLS validation)
      --tls-cert string   Path to the TLS certificate file (required)
      --tls-key string    Path to the TLS key file (required)

Does this PR require a release note?

Add a new `<installer> admin-console update-tls` CLI subcommand to update Admin Console TLS certificate and key

Does this PR require documentation?

replicatedhq/replicated-docs#3708

Add a new CLI subcommand to securely update Admin Console TLS certificates
without using the acceptAnonymousUploads workflow which exposes a security
vulnerability.

Usage:
  <installer> admin-console update-tls --tls-cert <path> --tls-key <path> [--hostname <hostname>]

The command reads and validates the certificate/key pair, then updates
the kotsadm-tls secret. No pod restart is needed as pods using this
secret are expected to watch for changes and reload automatically.

Optionally accepts --hostname flag for display URL (not TLS validation).

Closes: sc-131995

Signed-off-by: Evans Mungai <[email protected]>
@github-actions
Copy link

github-actions bot commented Dec 10, 2025

This PR has been released (on staging) and is available for download with a embedded-cluster-smoke-test-staging-app license ID.

Online Installer:

curl "https://staging.replicated.app/embedded/embedded-cluster-smoke-test-staging-app/ci/appver-dev-50961ed" -H "Authorization: $EC_SMOKE_TEST_LICENSE_ID" -o embedded-cluster-smoke-test-staging-app-ci.tgz

Airgap Installer (may take a few minutes before the airgap bundle is built):

curl "https://staging.replicated.app/embedded/embedded-cluster-smoke-test-staging-app/ci-airgap/appver-dev-50961ed?airgap=true" -H "Authorization: $EC_SMOKE_TEST_LICENSE_ID" -o embedded-cluster-smoke-test-staging-app-ci.tgz

Happy debugging!

Signed-off-by: Evans Mungai <[email protected]>
Comment on lines 31 to 52
cmd := &cobra.Command{
Use: "update-tls",
Short: fmt.Sprintf("Update the TLS certificate and key for the %s Admin Console", name),
Long: fmt.Sprintf(`Update the TLS certificate and key used by the %s Admin Console.

This command updates the kotsadm-tls secret, or creates it if it does not exist.
Pods using this secret are expected to watch for changes and automatically reload
the TLS configuration. This provides a secure alternative to the acceptAnonymousUploads
workflow.

The --hostname flag is optional and only affects the display URL shown
to users. It does not affect TLS certificate validation.`, name),
PreRunE: func(cmd *cobra.Command, args []string) error {
if !dryrun.Enabled() && os.Getuid() != 0 {
return fmt.Errorf("update-tls command must be run as root")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdateTLS(cmd.Context(), tlsCertPath, tlsKeyPath, hostname)
},
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinging @ajp-io for a 👀 of the copy here.

Comment on lines 126 to 145
secret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: kotsadmTLSSecretName,
Namespace: namespace,
Labels: adminconsole.GetKotsadmLabels(),
Annotations: adminconsole.GetTLSSecretAnnotations(),
},
Type: corev1.SecretTypeTLS,
Data: map[string][]byte{
"tls.crt": certBytes,
"tls.key": keyBytes,
},
}
if hostname != "" {
secret.StringData = map[string]string{"hostname": hostname}
}
if err := kcli.Create(ctx, secret); err != nil {
return fmt.Errorf("creating kotsadm-tls secret: %w", err)
}
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having methods within the adminconsole package that handle creating the secret object (the actual corev1.Secret not the actual kcli.Create albeit that's just an extra step I guess) and also return the name of the kotsadm-tls secret? This means that if/whenever we change any of the aspects around the secret creation (e.g. add labels, change its name, etc.) we don't need to run around in multiple places in the code, everything is scoped to a single place:

  • secret := &corev1.Secret{
    TypeMeta: metav1.TypeMeta{
    Kind: "Secret",
    APIVersion: "v1",
    },
    ObjectMeta: metav1.ObjectMeta{
    Name: "kotsadm-tls",
    Namespace: a.Namespace(),
    Labels: map[string]string{
    "kots.io/kotsadm": "true",
    "replicated.com/disaster-recovery": "infra",
    "replicated.com/disaster-recovery-chart": "admin-console",
    },
    Annotations: map[string]string{
    "acceptAnonymousUploads": "0",
    },
    },
    Type: "kubernetes.io/tls",
    Data: map[string][]byte{
    "tls.crt": a.TLSCertBytes,
    "tls.key": a.TLSKeyBytes,
    },
    StringData: map[string]string{
    "hostname": a.Hostname,
    },
    }

A recent example of something similar I've tried to do:

The general idea being that external components shouldn't need to worry about the particular structure of the secret object, its name, namespace, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I'll make that change

Signed-off-by: Evans Mungai <[email protected]>
assert.Contains(t, cmd.Short, "Update the TLS certificate")
assert.Contains(t, cmd.Long, "kotsadm-tls secret")
assert.Contains(t, cmd.Long, "creates it if it does not exist")
assert.Contains(t, cmd.Long, "watch for changes")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Test assertions check for text not in command Long

The test TestAdminConsoleUpdateTLSCmd asserts that cmd.Long contains "kotsadm-tls secret", "creates it if it does not exist", and "watch for changes", but the actual Long string defined in AdminConsoleUpdateTLSCmd doesn't contain any of these phrases. The Long description only mentions the hostname flag behavior. These test assertions will fail.

Additional Locations (1)

Fix in Cursor Fix in Web

Signed-off-by: Evans Mungai <[email protected]>
Add a new CLI subcommand to regenerate the Admin Console TLS certificate
with a fresh self-signed certificate.

Usage:
<installer> admin-console reset-tls

The command generates a new self-signed certificate using tlsutils.GenerateCertificate
with all valid host IP addresses as SANs, then updates the kotsadm-tls secret.
Useful for certificate expiry or when host IP addresses have changed.

Signed-off-by: Evans Mungai <[email protected]>
Signed-off-by: Evans Mungai <[email protected]>
@banjoh banjoh changed the title feat: add admin-console update-tls command feat: add admin-console reset-tls and update-tls commands Dec 12, 2025
Copy link
Contributor

@JGAntunes JGAntunes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comment on the tests. IMO I don't know how worthwhile is to validate the cmd config, other than that LGTM 👍 thanks! Love the secret creation cleanup.

Comment on lines 111 to 118
func TestAdminConsoleResetTLSCmd(t *testing.T) {
ctx := context.Background()
cmd := AdminConsoleResetTLSCmd(ctx, "Test App")

assert.Equal(t, "reset-tls", cmd.Use)
assert.Contains(t, cmd.Short, "Reset the TLS certificate")
assert.Contains(t, cmd.Short, "self-signed")
assert.Contains(t, cmd.Long, "self-signed TLS certificate")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how worthwhile this test is since we're mostly asserting on the cmd configuration

Comment on lines 265 to 273
func TestAdminConsoleUpdateTLSCmd(t *testing.T) {
ctx := context.Background()
cmd := AdminConsoleUpdateTLSCmd(ctx, "Test App")

assert.Equal(t, "update-tls", cmd.Use)
assert.Contains(t, cmd.Short, "Update the TLS certificate")
assert.Contains(t, cmd.Long, "Update the TLS certificate and key")
assert.Contains(t, cmd.Long, "--hostname flag is optional")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Signed-off-by: Evans Mungai <[email protected]>
@banjoh
Copy link
Member Author

banjoh commented Dec 16, 2025

@ajp-io all changes asked for have been addressed

@banjoh banjoh merged commit 5bf586f into main Dec 16, 2025
54 checks passed
@banjoh banjoh deleted the evansmungai/sc-131995/add-cli-subcommand-to-update-admin-console branch December 16, 2025 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants