From a75fe7600d554c7d8404a32e9a790c27dfdebb44 Mon Sep 17 00:00:00 2001 From: Utku Ozdemir Date: Tue, 12 Jul 2022 14:24:32 +0200 Subject: [PATCH] feat: gen secrets from kubernetes pki dir This PR allows the ability to generate `secrets.yaml` (`talosctl gen secrets`) using a Kubernetes PKI directory path (e.g. `/etc/kubernetes/pki`) as input. Also introduces the flag `--kubernetes-bootstrap-token` to be able to set a static Kubernetes bootstrap token to the generated `secrets.yaml` file instead of a randomly-generated one. Closes siderolabs/talos#5894. Signed-off-by: Utku Ozdemir --- cmd/talosctl/cmd/mgmt/gen/secrets.go | 32 +- hack/release.toml | 17 + internal/integration/cli/gen.go | 48 ++- internal/integration/cli/pki.go | 67 ++++ internal/integration/cli/testdata/pki/ca.crt | 19 ++ internal/integration/cli/testdata/pki/ca.key | 27 ++ .../integration/cli/testdata/pki/etcd/ca.crt | 18 ++ .../integration/cli/testdata/pki/etcd/ca.key | 27 ++ .../cli/testdata/pki/front-proxy-ca.crt | 19 ++ .../cli/testdata/pki/front-proxy-ca.key | 27 ++ internal/integration/cli/testdata/pki/sa.key | 27 ++ .../types/v1alpha1/generate/generate.go | 304 +++++++++++++----- .../config/types/v1alpha1/generate/options.go | 2 +- website/content/v1.2/reference/cli.md | 8 +- 14 files changed, 553 insertions(+), 89 deletions(-) create mode 100644 internal/integration/cli/pki.go create mode 100644 internal/integration/cli/testdata/pki/ca.crt create mode 100644 internal/integration/cli/testdata/pki/ca.key create mode 100644 internal/integration/cli/testdata/pki/etcd/ca.crt create mode 100644 internal/integration/cli/testdata/pki/etcd/ca.key create mode 100644 internal/integration/cli/testdata/pki/front-proxy-ca.crt create mode 100644 internal/integration/cli/testdata/pki/front-proxy-ca.key create mode 100644 internal/integration/cli/testdata/pki/sa.key diff --git a/cmd/talosctl/cmd/mgmt/gen/secrets.go b/cmd/talosctl/cmd/mgmt/gen/secrets.go index 4cb80ba0e..6697e84d0 100644 --- a/cmd/talosctl/cmd/mgmt/gen/secrets.go +++ b/cmd/talosctl/cmd/mgmt/gen/secrets.go @@ -16,8 +16,10 @@ import ( ) var genSecretsCmdFlags struct { - outputFile string - talosVersion string + outputFile string + talosVersion string + fromKubernetesPki string + kubernetesBootstrapToken string } // genSecretsCmd represents the `gen secrets` command. @@ -27,18 +29,32 @@ var genSecretsCmd = &cobra.Command{ Long: ``, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - genOptions := make([]generate.GenOption, 0, 1) + var ( + secretsBundle *generate.SecretsBundle + versionContract *config.VersionContract + err error + ) if genSecretsCmdFlags.talosVersion != "" { - versionContract, err := config.ParseContractFromVersion(genSecretsCmdFlags.talosVersion) + versionContract, err = config.ParseContractFromVersion(genSecretsCmdFlags.talosVersion) if err != nil { return fmt.Errorf("invalid talos-version: %w", err) } - - genOptions = append(genOptions, generate.WithVersionContract(versionContract)) } - secretsBundle, err := generate.NewSecretsBundle(generate.NewClock(), genOptions...) + if genSecretsCmdFlags.fromKubernetesPki != "" { + secretsBundle, err = generate.NewSecretsBundleFromKubernetesPKI(genSecretsCmdFlags.fromKubernetesPki, + genSecretsCmdFlags.kubernetesBootstrapToken, versionContract) + if err != nil { + return fmt.Errorf("failed to create secrets bundle: %w", err) + } + + return writeSecretsBundleToFile(secretsBundle) + } + + secretsBundle, err = generate.NewSecretsBundle(generate.NewClock(), + generate.WithVersionContract(versionContract), + ) if err != nil { return fmt.Errorf("failed to create secrets bundle: %w", err) } @@ -59,6 +75,8 @@ func writeSecretsBundleToFile(bundle *generate.SecretsBundle) error { func init() { genSecretsCmd.Flags().StringVarP(&genSecretsCmdFlags.outputFile, "output-file", "o", "secrets.yaml", "path of the output file") genSecretsCmd.Flags().StringVar(&genSecretsCmdFlags.talosVersion, "talos-version", "", "the desired Talos version to generate secrets bundle for (backwards compatibility, e.g. v0.8)") + genSecretsCmd.Flags().StringVarP(&genSecretsCmdFlags.fromKubernetesPki, "from-kubernetes-pki", "p", "", "use a Kubernetes PKI directory (e.g. /etc/kubernetes/pki) as input") + genSecretsCmd.Flags().StringVarP(&genSecretsCmdFlags.kubernetesBootstrapToken, "kubernetes-bootstrap-token", "t", "", "use the provided bootstrap token as input") Cmd.AddCommand(genSecretsCmd) } diff --git a/hack/release.toml b/hack/release.toml index 3360e4a31..df3e3f4ce 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -79,6 +79,23 @@ machine: ``` Patch format is detected automatically. +""" + + [notes.gen-secrets-from-pki] + title = "Generating Talos secrets from PKI directory" + description="""\ +It is now possible to generate a secrets bundle from a Kubernetes PKI directory (e.g. `/etc/kubernetes/pki`). + +You can also specify a bootstrap token to be used in the secrets bundle. + +This secrets bundle can then be used to generate a machine config. + +This facilitates migrating clusters (e.g. created using `kubeadm`) to Talos. + +``` +talosctl gen secrets --kubernetes-bootstrap-token znzio1.1ifu15frz7jd59pv --from-kubernetes-pki /etc/kubernetes/pki +talosctl gen config --with-secrets secrets.yaml my-cluster https://172.20.0.1:6443 +``` """ [make_deps] diff --git a/internal/integration/cli/gen.go b/internal/integration/cli/gen.go index 2bbd5ea05..ee5b306d7 100644 --- a/internal/integration/cli/gen.go +++ b/internal/integration/cli/gen.go @@ -9,7 +9,6 @@ package cli import ( "encoding/json" - "io/ioutil" "os" "regexp" @@ -209,11 +208,56 @@ func (suite *GenSuite) TestSecrets() { suite.RunCLI([]string{"gen", "secrets"}, base.StdoutEmpty()) suite.Assert().FileExists("secrets.yaml") + defer os.Remove("secrets.yaml") // nolint:errcheck + suite.RunCLI([]string{"gen", "secrets", "--output-file", "/tmp/secrets2.yaml"}, base.StdoutEmpty()) suite.Assert().FileExists("/tmp/secrets2.yaml") + defer os.Remove("/tmp/secrets2.yaml") // nolint:errcheck + suite.RunCLI([]string{"gen", "secrets", "-o", "secrets3.yaml", "--talos-version", "v0.8"}, base.StdoutEmpty()) suite.Assert().FileExists("secrets3.yaml") + + defer os.Remove("secrets3.yaml") // nolint:errcheck +} + +// TestSecretsWithPKIDirAndToken ... +func (suite *GenSuite) TestSecretsWithPKIDirAndToken() { + path := "/tmp/secrets-with-pki-dir-and-token.yaml" + + tempDir := suite.T().TempDir() + + dir, err := writeKubernetesPKIFiles(tempDir) + suite.Assert().NoError(err) + + defer os.RemoveAll(dir) //nolint:errcheck + + suite.RunCLI([]string{ + "gen", "secrets", "--from-kubernetes-pki", dir, + "--kubernetes-bootstrap-token", "test-token", + "--output-file", path, + }, base.StdoutEmpty()) + + suite.Assert().FileExists(path) + + defer os.Remove(path) //nolint:errcheck + + secretsYaml, err := os.ReadFile(path) + suite.Assert().NoError(err) + + var secrets generate.SecretsBundle + + err = yaml.Unmarshal(secretsYaml, &secrets) + suite.Assert().NoError(err) + + suite.Assert().Equal("test-token", secrets.Secrets.BootstrapToken, "bootstrap token does not match") + suite.Assert().Equal(pkiCACrt, secrets.Certs.K8s.Crt, "k8s ca cert does not match") + suite.Assert().Equal(pkiCAKey, secrets.Certs.K8s.Key, "k8s ca key does not match") + suite.Assert().Equal(pkiFrontProxyCACrt, secrets.Certs.K8sAggregator.Crt, "k8s aggregator ca cert does not match") + suite.Assert().Equal(pkiFrontProxyCAKey, secrets.Certs.K8sAggregator.Key, "k8s aggregator ca key does not match") + suite.Assert().Equal(pkiSAKey, secrets.Certs.K8sServiceAccount.Key, "k8s service account key does not match") + suite.Assert().Equal(pkiEtcdCACrt, secrets.Certs.Etcd.Crt, "etcd ca cert does not match") + suite.Assert().Equal(pkiEtcdCAKey, secrets.Certs.Etcd.Key, "etcd ca key does not match") } // TestConfigWithSecrets tests the gen config command with secrets provided. @@ -221,7 +265,7 @@ func (suite *GenSuite) TestConfigWithSecrets() { suite.RunCLI([]string{"gen", "secrets"}, base.StdoutEmpty()) suite.Assert().FileExists("secrets.yaml") - secretsYaml, err := ioutil.ReadFile("secrets.yaml") + secretsYaml, err := os.ReadFile("secrets.yaml") suite.Assert().NoError(err) suite.RunCLI([]string{"gen", "config", "foo", "https://192.168.0.1:6443", "--with-secrets", "secrets.yaml"}) diff --git a/internal/integration/cli/pki.go b/internal/integration/cli/pki.go new file mode 100644 index 000000000..f6fe90f92 --- /dev/null +++ b/internal/integration/cli/pki.go @@ -0,0 +1,67 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package cli + +import ( + _ "embed" + "os" + "path/filepath" +) + +var ( + //go:embed "testdata/pki/ca.crt" + pkiCACrt []byte + //go:embed "testdata/pki/ca.key" + pkiCAKey []byte + //go:embed "testdata/pki/front-proxy-ca.crt" + pkiFrontProxyCACrt []byte + //go:embed "testdata/pki/front-proxy-ca.key" + pkiFrontProxyCAKey []byte + //go:embed "testdata/pki/sa.key" + pkiSAKey []byte + //go:embed "testdata/pki/etcd/ca.crt" + pkiEtcdCACrt []byte + //go:embed "testdata/pki/etcd/ca.key" + pkiEtcdCAKey []byte +) + +func writeKubernetesPKIFiles(dir string) (string, error) { + var err error + + if err = os.WriteFile(filepath.Join(dir, "ca.crt"), pkiCACrt, 0o777); err != nil { + return "", err + } + + if err = os.WriteFile(filepath.Join(dir, "ca.key"), pkiCAKey, 0o777); err != nil { + return "", err + } + + if err = os.WriteFile(filepath.Join(dir, "front-proxy-ca.crt"), pkiFrontProxyCACrt, 0o777); err != nil { + return "", err + } + + if err = os.WriteFile(filepath.Join(dir, "front-proxy-ca.key"), pkiFrontProxyCAKey, 0o777); err != nil { + return "", err + } + + if err = os.WriteFile(filepath.Join(dir, "sa.key"), pkiSAKey, 0o777); err != nil { + return "", err + } + + etcdDir := filepath.Join(dir, "etcd") + if err = os.Mkdir(etcdDir, 0o777); err != nil { + return "", err + } + + if err = os.WriteFile(filepath.Join(etcdDir, "ca.crt"), pkiEtcdCACrt, 0o777); err != nil { + return "", err + } + + if err = os.WriteFile(filepath.Join(etcdDir, "ca.key"), pkiEtcdCAKey, 0o777); err != nil { + return "", err + } + + return dir, nil +} diff --git a/internal/integration/cli/testdata/pki/ca.crt b/internal/integration/cli/testdata/pki/ca.crt new file mode 100644 index 000000000..1067a1f79 --- /dev/null +++ b/internal/integration/cli/testdata/pki/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl +cm5ldGVzMB4XDTIyMDcwODExMTM0OFoXDTMyMDcwNTExMTM0OFowFTETMBEGA1UE +AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGI +T8ncEbVJ7LtRTZY6Vc2bXlMtagzCqpP+29H7HtsgV4T64QJsPRnfCh0PzOit7JJq +SRj526wHZRfSvu0M9wZ2KaC4MVDMP2KhBUKW63nUmdXQMf+z1gHKzCMloAMa0Avb +DVsoc9NaiiHX8m59gX328xEHQjNxnNGIretBjsjZw/Xeo2BflVXahnnxMVJqnzEa +0jpnGh5BaO4aPKDrJbyELz8Y8F+NGJ2zkSVtBh0gYZrejnioEiSFkSvcxDw3Xhg2 +QL+NUsRrhYHq10apJhuSkPkraCogbHlN33dslJ+I85xszKt437gGkpDoTKXaxe9L +f9xB40PgToK9QfAOIFMCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFF06xQ3JTko0LcX5pvAdp+mLFWgMMBUGA1UdEQQO +MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAIkhsj6yVvEoN4q7nj97 +vY0RpAOyysmhigHK0miioKsd94GDb+aMBYFLKliU48B5/n/KXblu7xsTane8uB3C +VeBywkDXLN2a9ax4BaxIkleDOX1xZN4BtxIfdU1QGhFQU0JPDCMxbDjbfN2Kg2Wi +iESrsXYKDq2pLNeQdszxPGNlAOjssVHY6IivWOcMRHP0yCDTl5ooq180+U7smFdz +NM/6udMOhsgh6bUCeMu9mhaPXMBmK0Lcd68PFunAA8q7a5OfTgIhGC9n7Q0L6CMw +7yXb97bd9bPZqeiuw7G7+UiNkJrBdIMc0AYE+wG44Uxu9usrGZZt6zLYzfnJ2vRZ +qac= +-----END CERTIFICATE----- diff --git a/internal/integration/cli/testdata/pki/ca.key b/internal/integration/cli/testdata/pki/ca.key new file mode 100644 index 000000000..74ba61be2 --- /dev/null +++ b/internal/integration/cli/testdata/pki/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsYhPydwRtUnsu1FNljpVzZteUy1qDMKqk/7b0fse2yBXhPrh +Amw9Gd8KHQ/M6K3skmpJGPnbrAdlF9K+7Qz3BnYpoLgxUMw/YqEFQpbredSZ1dAx +/7PWAcrMIyWgAxrQC9sNWyhz01qKIdfybn2BffbzEQdCM3Gc0Yit60GOyNnD9d6j +YF+VVdqGefExUmqfMRrSOmcaHkFo7ho8oOslvIQvPxjwX40YnbORJW0GHSBhmt6O +eKgSJIWRK9zEPDdeGDZAv41SxGuFgerXRqkmG5KQ+StoKiBseU3fd2yUn4jznGzM +q3jfuAaSkOhMpdrF70t/3EHjQ+BOgr1B8A4gUwIDAQABAoIBAQCIvqY2pfw916Mw +5X8NqAFPTc1p5CE7kvYw6K4JH5S01ESVeWi3pQerVdFEcVc0IkOGw7dqNYqvB0Mn +Bn1pugLMR1fpI/dYdPqdzclvcTAPt2KG/saEXtEIsFxs9h46RfzaJPA0twQAWEzt +pJhn4uRLUlwHUb/8QBa6jrzn6KdCrLC0jc0g8c77x3omkfy0chug7q2s/yX2MwEX +p5pbyPiGZkNIjuDeic3D0ssrH7v7gEYBdybgl78LrLkKA8gEFLrGvrT5nuLYhxkF +dhRu3+p9mhhKmJQndto8Y4WeYhXZ7qN2YZbfcv1Xni6cq4UhLmr2czRaK0KVZ15H +kzYh0I8BAoGBAOTjpoVVc+IgVJnGeglrJP8zOKEiwj3R+eF9DiQuTdnop0SBiqYB +JT6/b/apdYEGz2nBs3M1k6CmNrkzGADMNins7m1MRHkljRqu1IHSF3Us2yifPUef +lEUHtXf3ANvf3YJWuGO7RT4sird/1vXE/0TCzjWEvkHc6Zvky/DCydTJAoGBAMaP +bWsiDSFpLV6iCiq18bmJJoUDnkXqnwsUVw9C978h2Hx5ysYfOUt6S6Q7UsR7hKW5 +iay1aiFPBjLQyD4Ac8feZFQEQ5gFYXPEfIV3OYZDz2U+gAZp33ow7/H/73kngelV +6EXhTYgDRUwfTBOMSlzhCZwQb8Rzpv+gC3TFsmY7AoGAWewB2LIYo8bV1c/+08Jv +N39VCSERtJ3QkMDDlH1Igop/ZE+MO+mJS1yETSCIFFerlr3NlT6AMAX8y8eB75ZK +1S/K/8+NuxaAl/IFdLcoFhW4R/4/YesUogYESgwVH0yUxobxS+Ufr+xp1ut3dPie +3NG3l5j98fwrHt7FLGIqTtkCgYEAjezfDQCd2g/PuiCgm77JNRDvU4wuiVMWs1iq +keIQK7IJh4+WfN68mVKk1pMAqiiPu9VOrwBNB9nwWEoblxXDrE0t8U/K8NKHwbPk +PZHmsC2wBHIUGIF8l157Y8LIbRTsKtiY2bodLOcJlUuZmS9hx9migMbO3OC9sWG4 +TpMw3RkCgYA8ssuhj5wrSQabCXbOU0pn/qbePHWfvh+DcRUXnTtRoothpqvDQxuS +73j2NB42UG7j0eWRK993ONGTJ/QNMAJxXwLEuLFtoKHdh7pkq3uFolyNzR+T0vna +awkSRmJsbuV7C4mDVHgEaqcEzv/VyeAokw/hqB1Ga9fvauj6MDPshw== +-----END RSA PRIVATE KEY----- diff --git a/internal/integration/cli/testdata/pki/etcd/ca.crt b/internal/integration/cli/testdata/pki/etcd/ca.crt new file mode 100644 index 000000000..57132ba41 --- /dev/null +++ b/internal/integration/cli/testdata/pki/etcd/ca.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9TCCAd2gAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdldGNk +LWNhMB4XDTIyMDcwODExMTM0OFoXDTMyMDcwNTExMTM0OFowEjEQMA4GA1UEAxMH +ZXRjZC1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOgw+tA7odeO +H5gKPlhWBjijuNoLlMCJSkGG0Ca/fI6Bk2Y+fzuVv8NWsRrhML3dEWJgXccMhoG0 +v9MatmvF5nYfm802ZaDCsA6RdJG3PygRTw69pmC4ETfv0+YXpFFfFBeWWb9MsXIP +ULooepYy4bWvjG7ZDCeFQ8ACHGE17U0O5rFBqiY0okBjSkl1/oHSzlAVCxa4dOaK +/Vj8A8CefT18JZ+jzAbgN7jRm1GWzYTWqACVa5CnRDYRglrZr9DLvaj0ASqck5DD +zdJYVQJeVqS7L8qghXfbXzxlkKesRtQsj7V2trpDBuDASJPk6ZmwEWlaI0Wdc3eK +nYHgaWybLykCAwEAAaNWMFQwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFKN5e0IuTpOoTqWB9+MQp/k/OgXgMBIGA1UdEQQLMAmCB2V0 +Y2QtY2EwDQYJKoZIhvcNAQELBQADggEBAGmGUUqeGe4RXd3kZGTRcdJMo7OkuEK5 +hH/FHfrLLc3VNoWQafdq4oVrF9bwqDUPRuPgrXl+QY+s4/ztDyHmuKGdLWIT2dCE +0Ztnpe17VoMrkJxmFvKEWrT74EnT0QIeFs+lf8+SJWTLYKBRpGrsQKq84uds++gV +BPLuu8Z0e5vvkAFnX8m65SqyZwKfs3HuzaAnA57VGSJHCBrnKojsP8JdlWeQiStG +CtmzePeLqjVJnpNF/n1ST5Ewr4Kq/Cvf707gzs+spn0bt97QyNke6b24ZrkpUNu6 +T/0bImLeYiGmSanRF3hAdN7IV0ah7tYOpmwk560kyF8tmd8jcKFgEd8= +-----END CERTIFICATE----- diff --git a/internal/integration/cli/testdata/pki/etcd/ca.key b/internal/integration/cli/testdata/pki/etcd/ca.key new file mode 100644 index 000000000..974c954ac --- /dev/null +++ b/internal/integration/cli/testdata/pki/etcd/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA6DD60Duh144fmAo+WFYGOKO42guUwIlKQYbQJr98joGTZj5/ +O5W/w1axGuEwvd0RYmBdxwyGgbS/0xq2a8Xmdh+bzTZloMKwDpF0kbc/KBFPDr2m +YLgRN+/T5hekUV8UF5ZZv0yxcg9Quih6ljLhta+MbtkMJ4VDwAIcYTXtTQ7msUGq +JjSiQGNKSXX+gdLOUBULFrh05or9WPwDwJ59PXwln6PMBuA3uNGbUZbNhNaoAJVr +kKdENhGCWtmv0Mu9qPQBKpyTkMPN0lhVAl5WpLsvyqCFd9tfPGWQp6xG1CyPtXa2 +ukMG4MBIk+TpmbARaVojRZ1zd4qdgeBpbJsvKQIDAQABAoIBAHI8xun0rOfU8Q5o +28uyZ1UumCAPWpxv76zVm0u1Ip8qeU7wqMC0KKj+2hwTd1uyjH8OUpVAQF1IhKhk +mCPmNkEfxBPvE4lIwD4qqmOW+OfJvE/QVy924GHZCTRHpXyzfrssKfPI0/T+PAWb +LNUBK7OsLzfKagR3uKGbaEMbuSkTn5zeCJ1vrFAuCaLmeauBsnoV7Z9AlbG6t8sp +hyD0O+pDPGuc+J99suGiysndwjczqntIKXcdgba2XxRE0QHM+ZYDPpyvCggKfoUH +sQ9Vz/UHpqTv8pbrTSZK59E8+16YfFyFC+iwTnTzoBAkO6NIivDTQvLcipweZEzt +5146EEECgYEA6O+Pd1tdkTVHLgSkKrkBo4p3g2iBFl626g7vlZGvDjVNNPBWwk5f +j3PbMKPfUfr96YB86lbetDzYdiB5LrLW5xG2GaSRziGI1/Tgdp6aR142TNljiEu7 +dv4v3eAR5VfiA5CGera+k1VzKLIygfCDkUWWdG17LvVdHFtjN6XVU2UCgYEA/y6M +hZbZqCX9cx58rhJ38YvH9fdWSzw1r9crgM40uPHE4SHvin6/lR/rbNU4Geg/b4bn +FWnffCem8ov2tXcFe/ZBM8ZWrsRGDinkTh8dReh8ujE90YOqf3YSRYgUvSx8ITUn +zqhe43sGVAelKeVKPW/3Ok7RJXnRRsDIbSmfqnUCgYB7sDmON4XHxXK2jOBfjz2/ +iZdMwAFLz59xSd0Onv1FnigRJE3tf5BerDaH7Xx4G78YbpHmHZrEOkr27udqVKyo +pk777tc9jbEMe4t1cWKa4vwScpzXkt9IoFDqkEDwd2ocWnIOV1t7ALTVt0n6laxH +R5xM1pXCqad3l09oDTbpwQKBgDlHwqVOCkeTV4QayNPuM1xWCymsPoOe3VI+U3aT +UwRcyNvcWT/WWbzosFj6t6AhIPQw7PhCjrb406HIRzXOpL2Btnsfv191kWAmiSf8 +Ff8WQ8ErwnugOYpo/4r6E+Wu8aImo2vhIYOgnvgHy0xPOs31ryI4hPwLjy15osPW +Pw/tAoGBAKaT53bqIl+wzltM2ROtH9ppA98prDPM0GyZMhFed+XMLAkUX3qmFtj5 +k51bhaq7ER4yByHX17Q7y3SoJ5NpccmFUhWhttRf/IraWEKsHXW6OY22wvtZN9Lx +Eh+rpZsjcozUCaoQWkOf20UpBSltIV5U4GmSpthnj0+zaRrorxWx +-----END RSA PRIVATE KEY----- diff --git a/internal/integration/cli/testdata/pki/front-proxy-ca.crt b/internal/integration/cli/testdata/pki/front-proxy-ca.crt new file mode 100644 index 000000000..406181594 --- /dev/null +++ b/internal/integration/cli/testdata/pki/front-proxy-ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCjCCAfKgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDEw5mcm9u +dC1wcm94eS1jYTAeFw0yMjA3MDgxMTEzNDhaFw0zMjA3MDUxMTEzNDhaMBkxFzAV +BgNVBAMTDmZyb250LXByb3h5LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4thL1utf/iXxylt24O3o3IV7oif5tiYR4SN9YnVb/GLyVtVdHuzPpigw +j+S7aw3mldNHVWbIPq9GncvVsgvlJYHu53WRzVfV7tAylt/ssSqHFv/p1e6rC2m9 +hETHUZjSs8v0APWi8pfh15lJPqLS1lGF3QPVCPdc1t+8dBVnx8wnTgGVE7FVLjmc +3qIcXTR1TfcB4+X1ZnY5FNK7kR/kgIS64uZqntyNSW5W0SCFgWQClwwkzVpo4v/Y +sCHiWI7cIuXHTTd7ntHJab1Og1jAPNEmENvEtza1SqGTSEUQs5wjS+p+ii5E09b3 +Nufj1e8hJaodTxr+24hoFcYxWIgxhwIDAQABo10wWzAOBgNVHQ8BAf8EBAMCAqQw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvw91sUxAgIX7zdqTKwJ0Sdxa9TUw +GQYDVR0RBBIwEIIOZnJvbnQtcHJveHktY2EwDQYJKoZIhvcNAQELBQADggEBAGHW +snsEJkN3d2vNJuFu6IVKurEZM8UbwHebJ5fQUHpCOKpFqSMdngF8rdcYNmHeUFMv +Qf9Fkm9nwKOhO9hApsQad7UgJ98YCzE+heTye7nKjrga+2lsJN/T3SgJGiAEkVjP +rq2SQ4MLCp56PyFI5CL9zqtjuXCI8Uhqqfru6tJEA75GA6VJZmfiOgzFQum7DeC2 +9dNcvQb8p91LIl5tjvuTgPbJDjF1YX4n0iwoiA05e+rPrsPhyySQnJwFgTi+hqPZ +cBUPwVDYeiHseRbj3ODOoCv6PO4koO5m+tiFeUXJTynMbCUkyJqZ11TAYC6snHxi +mg+fblveomK3F3hLFdk= +-----END CERTIFICATE----- diff --git a/internal/integration/cli/testdata/pki/front-proxy-ca.key b/internal/integration/cli/testdata/pki/front-proxy-ca.key new file mode 100644 index 000000000..af1940b01 --- /dev/null +++ b/internal/integration/cli/testdata/pki/front-proxy-ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4thL1utf/iXxylt24O3o3IV7oif5tiYR4SN9YnVb/GLyVtVd +HuzPpigwj+S7aw3mldNHVWbIPq9GncvVsgvlJYHu53WRzVfV7tAylt/ssSqHFv/p +1e6rC2m9hETHUZjSs8v0APWi8pfh15lJPqLS1lGF3QPVCPdc1t+8dBVnx8wnTgGV +E7FVLjmc3qIcXTR1TfcB4+X1ZnY5FNK7kR/kgIS64uZqntyNSW5W0SCFgWQClwwk +zVpo4v/YsCHiWI7cIuXHTTd7ntHJab1Og1jAPNEmENvEtza1SqGTSEUQs5wjS+p+ +ii5E09b3Nufj1e8hJaodTxr+24hoFcYxWIgxhwIDAQABAoIBAQDa55WQCcWxkNZa +y5bVimBbZeif2+nKj8RTOZdGyzAAR0/K4c0iCa58jm4GfdkqftiUnrVIwY3dh/Ei +V1CZp4byggeUjs0rlmaZNYqMM/zKHtsMI9t4mf+vXNQI7wJVSJ+T5+5IesJLTqwf +DQo0ipXhQfxnAsqzA1ow9Ol8MCfdEeFL5SFaVNiNhany/7PR0letngwMIAje6arY +6dPzjPYDsCjMTjoNarUADIZeVxMdbOCmiMy+yGDZlgV80fK0+Fw81axot2KOufhy +CvqClUlJV5Csz2egHuThuVd39kX2vAAGXdPEUbXga1JTDB3cQiHXJo0hLUsUGniS +6KdsJOyRAoGBAOmNX/V7awLiVyWsRLTNr1IeoMwHeS3Wdt5/nedqh6p2fLpfbN1D +M8R29yqvNn1YmpZsDBhQlnddq7i7OEKjSODwNnLPys6fqalsIY/AIRJPOmHvVJl6 +er4scaiGXA/FztvxGGQh4W92+Nb69dCXXXcG1QUljr+uGwrIGXc8aqzpAoGBAPil +4qGf/0s3Fv0tyLj+ZRWVjg5o4s/IsJyGRFc6RplU65y9VL9WsYvi5+d06w7lovac +omycJ/z+HiqTLcixdcHse54N9HHO5Ekui6cRbGAOdlzgS84TxYrmQK/FQkFvlcmO +rsfkDMmAgBc2yGP3N5IL60u90N2Kf3OiT5aHoCTvAoGAamdwioS6EkxQa+d6Pe1f +rMgrdgkJmmqVKXV22VHdkTn+RWLoVD4jvaR9o0LEToMpmtKLCCDfDG7up3EUhreh +ommOROyKd2yifX+4Iqfj6VWTQb8qCeqVNUNGXQMpuj3iqq3C8QvGi2PmpvsbNvdf +K7U/I+MikA2gYF8dywcJitECgYArDe5UNjQqffuJE2hyP/qY5jCW5ip/+Cw8rjMf +N4QKAN5bYZ1PFF/h7QRi26foCHNTaIPncpKqCAaJMLr4yWGulphBIgF1w3FcCqc7 +4pR1fYuZQW1e3aWTC5Of2/RBCGVTZVV2X1KngYyseFvyk1gX/eBcWR3VfqnbB/vo +AMwGGQKBgB/MXrB2Z61WlxR0Y289k+5gjUSS9foZIkCjr1TFDvLbEStAW8Z3WFcS +Gxzjs5s45VO3+KRRd3591WHaPWcVdNze+y5ICUr4u3WSBKg8D+9H3cMKWq8QLCRP +bgMDGq8TzTLdGmb9vw3n5OG2aAQUQfwoAp+uUwh2EDMDYixYd2UJ +-----END RSA PRIVATE KEY----- diff --git a/internal/integration/cli/testdata/pki/sa.key b/internal/integration/cli/testdata/pki/sa.key new file mode 100644 index 000000000..c5445ad8a --- /dev/null +++ b/internal/integration/cli/testdata/pki/sa.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0H28tj4Zgoj5y+/NF7Dhrf96+TQUolLR5j4YqoxbFRvgWtPE +0JFueoC0n6v4l58z4vog+1JjPHMO8rr0W3q9o/wWaY1CL3XqZJIkHI/fE/Eu1VLv +gLfDHsPevPNLczcpwXo7AadqxiUC/HRYZBJY+LA2OzWwO5ylx4YKnOzKxBXQ7a2q +qdpwZJSTTmGKyRx5pD6V5mCr47ITVAXqcjorTtJmG3zCFRhUjMscXIWC04S0IY/C ++cfMKaK4Jbqj1h2/9uNhbb55r9HdCEV/TLw+QL1Q+S6MWtng1S4Q3hyDXzB1LqfP +1brz7xl1pSLh+l0vF2pXom5zYiHlX4cAddR7MQIDAQABAoIBAAQtULeR/O7Zka+d +SU2dNJhI0wzlFzi9Ugk720CneTeuDEuljH7lOwJnS7cbOerHvMFiY4DFgMl4QKdq +SXT/u4bqiQRqWRYcVarYJrMPytdacKbDd5rrk5QtNmwwr6VKSKLgsQfyc7gui6XF +KvQuTewFk8CR7crz83pQ3CuSrulIwVny17HSE9NlXlv8SlHy7E2/a6H0UeOm7BRn +13An62CfRuBVFRd96oyTMpuNf2l+fcL5nnfM/F+tdr5MRR0pnEW2UKGdCbkTkAKd +AJnH2mmhWwbA8Gcx0Zn1X9/zu8Z3S6hTGnQmWIP25EcgipqKe/gocA9ervyR+0nb +FgwZZXECgYEA6D/mx3dI6AUY7RVIY9GexiJWLtS9O1d7PddmajoqVpwtjXT9jUf/ +9BAAVWNKcIEs1t4foncJeYph6JnTrQjDz1LORC2kAjPUYhDQod/m6dqHxvjRAIns +v0NezDvWQ2v8CgklDjUc5hYtvqj51OkkbKpphRClAdJZvrlxBhXTT1UCgYEA5c/c +A6D3xIcGUrSVrjtLWUyJ9hszjjBrI5txtmwMzWDSkV3Hp1VhSfhCdIxqQxELxlxS +A8SOU0XKxRB7QiwpgHfnbGxtQ8tJ5JufIecwJkqRcD5fqQk69xva/76fNfes41L8 +doNpFiu0fXbVdcB8UNN61VYsz31D/JD0qA9k5G0CgYEA5gq1egkrC7ZQ1DRqeYSd +8b79AnHx5Z9nEQAUD1ABs7wKWrzwkEoqugJHckxg5ULtuP5W80NY/SwWgqArTI8L +9IUejeVvOEdCLMhe/peaTzQHnQvDaPc0qtX+RelW9300LnSUYZg2QajiMqGIpF0x +mPjKf+TWrBFAl2tzCgYAQekCgYEAukqjaXWlI/To1UZ6R8DdNchr1cr7Ifpx/21U +4rH4NsyUJS7GWAlIUnQjOuNQiIla6DOScGd3kF11IAZaRKwUAIYyXZwPfvNeNSlJ ++Gu2hnPQLhMB7L8Ew6gbAVH/MfpSdfyhl1izaTuIlmQsacXdgI/OdP3kWVaMNEM1 +cL755IkCgYAv10G49Mc57WBi6knRHM2cXbYVxjzedJf8zBnAYkQoLdRiYT6uj34U +hI1EajSfXYJ2t8PG5nbHMNHAEbYNBikhjDvSrZW+HaTmjsVGV25cqCHPy7giSqVS +VftcMlIX/MlTdfcYtNT4wHORZdO2P4wW/y5JSFNol4z3xB4TNXp55A== +-----END RSA PRIVATE KEY----- diff --git a/pkg/machinery/config/types/v1alpha1/generate/generate.go b/pkg/machinery/config/types/v1alpha1/generate/generate.go index 68ee71dae..09a43cbba 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/generate.go +++ b/pkg/machinery/config/types/v1alpha1/generate/generate.go @@ -16,6 +16,8 @@ import ( "io" "net" "net/url" + "os" + "path/filepath" "time" "github.com/talos-systems/crypto/x509" @@ -192,8 +194,6 @@ func (c *SystemClock) SetFixedTimestamp(t time.Time) { } // NewSecretsBundle creates secrets bundle generating all secrets or reading from the input options if provided. -// -//nolint:gocyclo func NewSecretsBundle(clock Clock, opts ...GenOption) (*SecretsBundle, error) { options := DefaultGenOptions() @@ -208,115 +208,256 @@ func NewSecretsBundle(clock Clock, opts ...GenOption) (*SecretsBundle, error) { return options.Secrets, nil } + bundle := SecretsBundle{ + Clock: clock, + } + + err := populateSecretsBundle(options.VersionContract, &bundle) + if err != nil { + return nil, err + } + + return &bundle, nil +} + +// NewSecretsBundleFromKubernetesPKI creates secrets bundle by reading the contents +// of a Kubernetes PKI directory (typically `/etc/kubernetes/pki`) and using the provided bootstrapToken as input. +// +//nolint:gocyclo +func NewSecretsBundleFromKubernetesPKI(pkiDir, bootstrapToken string, versionContract *config.VersionContract) (*SecretsBundle, error) { + dirStat, err := os.Stat(pkiDir) + if err != nil { + return nil, err + } + + if !dirStat.IsDir() { + return nil, fmt.Errorf("%q is not a directory", pkiDir) + } + var ( - etcd *x509.CertificateAuthority - kubernetesCA *x509.CertificateAuthority - aggregatorCA *x509.CertificateAuthority - serviceAccount *x509.ECDSAKey - talosCA *x509.CertificateAuthority - trustdInfo *TrustdInfo - kubeadmTokens *Secrets - err error + ca *x509.PEMEncodedCertificateAndKey + etcdCA *x509.PEMEncodedCertificateAndKey + aggregatorCA *x509.PEMEncodedCertificateAndKey + sa *x509.PEMEncodedKey ) - etcd, err = NewEtcdCA(clock.Now(), options.VersionContract) + ca, err = x509.NewCertificateAndKeyFromFiles(filepath.Join(pkiDir, "ca.crt"), filepath.Join(pkiDir, "ca.key")) if err != nil { return nil, err } - kubernetesCA, err = NewKubernetesCA(clock.Now(), options.VersionContract) + err = validatePEMEncodedCertificateAndKey(ca) if err != nil { return nil, err } - if options.VersionContract.SupportsAggregatorCA() { - aggregatorCA, err = NewAggregatorCA(clock.Now(), options.VersionContract) + etcdDir := filepath.Join(pkiDir, "etcd") + + etcdCA, err = x509.NewCertificateAndKeyFromFiles(filepath.Join(etcdDir, "ca.crt"), filepath.Join(etcdDir, "ca.key")) + if err != nil { + return nil, err + } + + err = validatePEMEncodedCertificateAndKey(etcdCA) + if err != nil { + return nil, err + } + + aggregatorCACrtPath := filepath.Join(pkiDir, "front-proxy-ca.crt") + _, err = os.Stat(aggregatorCACrtPath) + + aggregatorCAFound := err == nil + if aggregatorCAFound && !versionContract.SupportsAggregatorCA() { + return nil, fmt.Errorf("aggregator CA found in pki dir but is not supported by the requested version") + } + + if versionContract.SupportsAggregatorCA() { + aggregatorCA, err = x509.NewCertificateAndKeyFromFiles(aggregatorCACrtPath, filepath.Join(pkiDir, "front-proxy-ca.key")) + if err != nil { + return nil, err + } + + err = validatePEMEncodedCertificateAndKey(aggregatorCA) if err != nil { return nil, err } } - if options.VersionContract.SupportsServiceAccount() { - serviceAccount, err = x509.NewECDSAKey() + saKeyPath := filepath.Join(pkiDir, "sa.key") + _, err = os.Stat(saKeyPath) + + saKeyFound := err == nil + if saKeyFound && !versionContract.SupportsServiceAccount() { + return nil, fmt.Errorf("service account key found in pki dir but is not supported by the requested version") + } + + if versionContract.SupportsServiceAccount() { + var saBytes []byte + + saBytes, err = os.ReadFile(filepath.Join(pkiDir, "sa.key")) + if err != nil { + return nil, err + } + + sa = &x509.PEMEncodedKey{ + Key: saBytes, + } + + _, err = sa.GetKey() if err != nil { return nil, err } } - talosCA, err = NewTalosCA(clock.Now()) - if err != nil { - return nil, err - } - - kubeadmTokens = &Secrets{} - - // Gen trustd token strings - kubeadmTokens.BootstrapToken, err = genToken(6, 16) - if err != nil { - return nil, err - } - - kubeadmTokens.AESCBCEncryptionSecret, err = cis.CreateEncryptionToken() - if err != nil { - return nil, err - } - - trustdInfo = &TrustdInfo{} - - // Gen trustd token strings - trustdInfo.Token, err = genToken(6, 16) - if err != nil { - return nil, err - } - - clusterID, err := randBytes(constants.DefaultClusterIDSize) - if err != nil { - return nil, fmt.Errorf("failed to generate cluster ID: %w", err) - } - - clusterSecret, err := randBytes(constants.DefaultClusterSecretSize) - if err != nil { - return nil, fmt.Errorf("failed to generate cluster secret: %w", err) - } - - result := &SecretsBundle{ - Cluster: &Cluster{ - ID: base64.URLEncoding.EncodeToString(clusterID), - Secret: base64.StdEncoding.EncodeToString(clusterSecret), + bundle := SecretsBundle{ + Secrets: &Secrets{ + BootstrapToken: bootstrapToken, }, - Clock: clock, - Secrets: kubeadmTokens, - TrustdInfo: trustdInfo, Certs: &Certs{ - Etcd: &x509.PEMEncodedCertificateAndKey{ - Crt: etcd.CrtPEM, - Key: etcd.KeyPEM, - }, - K8s: &x509.PEMEncodedCertificateAndKey{ - Crt: kubernetesCA.CrtPEM, - Key: kubernetesCA.KeyPEM, - }, - OS: &x509.PEMEncodedCertificateAndKey{ - Crt: talosCA.CrtPEM, - Key: talosCA.KeyPEM, - }, + Etcd: etcdCA, + K8s: ca, + K8sAggregator: aggregatorCA, + K8sServiceAccount: sa, }, } - if aggregatorCA != nil { - result.Certs.K8sAggregator = &x509.PEMEncodedCertificateAndKey{ + err = populateSecretsBundle(versionContract, &bundle) + if err != nil { + return nil, err + } + + return &bundle, nil +} + +// populateSecretsBundle fills all the missing fields in the secrets bundle. +// +//nolint:gocyclo,cyclop +func populateSecretsBundle(versionContract *config.VersionContract, bundle *SecretsBundle) error { + if bundle.Clock == nil { + bundle.Clock = NewClock() + } + + if bundle.Certs == nil { + bundle.Certs = &Certs{} + } + + if bundle.Certs.Etcd == nil { + etcd, err := NewEtcdCA(bundle.Clock.Now(), versionContract) + if err != nil { + return err + } + + bundle.Certs.Etcd = &x509.PEMEncodedCertificateAndKey{ + Crt: etcd.CrtPEM, + Key: etcd.KeyPEM, + } + } + + if bundle.Certs.K8s == nil { + kubernetesCA, err := NewKubernetesCA(bundle.Clock.Now(), versionContract) + if err != nil { + return err + } + + bundle.Certs.K8s = &x509.PEMEncodedCertificateAndKey{ + Crt: kubernetesCA.CrtPEM, + Key: kubernetesCA.KeyPEM, + } + } + + if versionContract.SupportsAggregatorCA() && bundle.Certs.K8sAggregator == nil { + aggregatorCA, err := NewAggregatorCA(bundle.Clock.Now(), versionContract) + if err != nil { + return err + } + + bundle.Certs.K8sAggregator = &x509.PEMEncodedCertificateAndKey{ Crt: aggregatorCA.CrtPEM, Key: aggregatorCA.KeyPEM, } } - if serviceAccount != nil { - result.Certs.K8sServiceAccount = &x509.PEMEncodedKey{ + if versionContract.SupportsServiceAccount() && bundle.Certs.K8sServiceAccount == nil { + serviceAccount, err := x509.NewECDSAKey() + if err != nil { + return err + } + + bundle.Certs.K8sServiceAccount = &x509.PEMEncodedKey{ Key: serviceAccount.KeyPEM, } } - return result, nil + if bundle.Certs.OS == nil { + talosCA, err := NewTalosCA(bundle.Clock.Now()) + if err != nil { + return err + } + + bundle.Certs.OS = &x509.PEMEncodedCertificateAndKey{ + Crt: talosCA.CrtPEM, + Key: talosCA.KeyPEM, + } + } + + if bundle.Secrets == nil { + bundle.Secrets = &Secrets{} + } + + if bundle.Secrets.BootstrapToken == "" { + token, err := genToken(6, 16) + if err != nil { + return err + } + + bundle.Secrets.BootstrapToken = token + } + + if bundle.Secrets.AESCBCEncryptionSecret == "" { + aesCBCEncryptionSecret, err := cis.CreateEncryptionToken() + if err != nil { + return err + } + + bundle.Secrets.AESCBCEncryptionSecret = aesCBCEncryptionSecret + } + + if bundle.TrustdInfo == nil { + bundle.TrustdInfo = &TrustdInfo{} + } + + if bundle.TrustdInfo.Token == "" { + token, err := genToken(6, 16) + if err != nil { + return err + } + + bundle.TrustdInfo.Token = token + } + + if bundle.Cluster == nil { + bundle.Cluster = &Cluster{} + } + + if bundle.Cluster.ID == "" { + clusterID, err := randBytes(constants.DefaultClusterIDSize) + if err != nil { + return fmt.Errorf("failed to generate cluster ID: %w", err) + } + + bundle.Cluster.ID = base64.URLEncoding.EncodeToString(clusterID) + } + + if bundle.Cluster.Secret == "" { + clusterSecret, err := randBytes(constants.DefaultClusterSecretSize) + if err != nil { + return fmt.Errorf("failed to generate cluster secret: %w", err) + } + + bundle.Cluster.Secret = base64.StdEncoding.EncodeToString(clusterSecret) + } + + return nil } // NewSecretsBundleFromConfig creates secrets bundle using existing config. @@ -608,3 +749,14 @@ func randBytes(size int) ([]byte, error) { return buf, nil } + +func validatePEMEncodedCertificateAndKey(certs *x509.PEMEncodedCertificateAndKey) error { + _, err := certs.GetKey() + if err != nil { + return err + } + + _, err = certs.GetCert() + + return err +} diff --git a/pkg/machinery/config/types/v1alpha1/generate/options.go b/pkg/machinery/config/types/v1alpha1/generate/options.go index fefbd2ec7..17d9868cf 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/options.go +++ b/pkg/machinery/config/types/v1alpha1/generate/options.go @@ -18,7 +18,7 @@ import ( // GenOption controls generate options specific to input generation. type GenOption func(o *GenOptions) error -// WithEndpointList specifies endpoints to use when acessing Talos cluster. +// WithEndpointList specifies endpoints to use when accessing Talos cluster. func WithEndpointList(endpoints []string) GenOption { return func(o *GenOptions) error { o.EndpointList = endpoints diff --git a/website/content/v1.2/reference/cli.md b/website/content/v1.2/reference/cli.md index 2faffb00b..d4feb3ac8 100644 --- a/website/content/v1.2/reference/cli.md +++ b/website/content/v1.2/reference/cli.md @@ -1253,9 +1253,11 @@ talosctl gen secrets [flags] ### Options ``` - -h, --help help for secrets - -o, --output-file string path of the output file (default "secrets.yaml") - --talos-version string the desired Talos version to generate secrets bundle for (backwards compatibility, e.g. v0.8) + -p, --from-kubernetes-pki string use a Kubernetes PKI directory (e.g. /etc/kubernetes/pki) as input + -h, --help help for secrets + -t, --kubernetes-bootstrap-token string use the provided bootstrap token as input + -o, --output-file string path of the output file (default "secrets.yaml") + --talos-version string the desired Talos version to generate secrets bundle for (backwards compatibility, e.g. v0.8) ``` ### Options inherited from parent commands