From fc3d8bd714eb0842cad476bd1b714ab3fb94b8e6 Mon Sep 17 00:00:00 2001 From: randy Date: Thu, 20 Aug 2026 13:10:58 +0000 Subject: [PATCH] teleport reconfigure (#250) Export-Source-Commit: e06a63c176c6e97c5f87ed832a6d4aae6d1b22e6 --- api/types/constants.go | 5 + lib/config/reconfigure.go | 297 ++++++++++++++++++ ...join_method_away_from_bound_keypair.golden | 9 + .../v2_auth_server_retarget.golden | 11 + .../v3_proxy_retarget.golden | 21 ++ tool/teleport/common/reconfigure.go | 162 ++++++++++ tool/teleport/common/teleport.go | 22 ++ 7 files changed, 527 insertions(+) create mode 100644 lib/config/reconfigure.go create mode 100644 lib/config/testdata/TestReconfigureGoldenOutput/join_method_away_from_bound_keypair.golden create mode 100644 lib/config/testdata/TestReconfigureGoldenOutput/v2_auth_server_retarget.golden create mode 100644 lib/config/testdata/TestReconfigureGoldenOutput/v3_proxy_retarget.golden create mode 100644 tool/teleport/common/reconfigure.go diff --git a/api/types/constants.go b/api/types/constants.go index 01c4732c26e..d857ec2489c 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -851,6 +851,11 @@ const ( // CreatedByIaCLabel is a resource metadata label name used to identify if resource was created by IaC tooling. CreatedByIaCLabel = TeleportNamespace + "/iac-tool" + + // ReconfigureSourceUUIDLabel is used to identify original source uuid + // of node where `teleport reconfigure` was used. + ReconfigureSourceUUIDLabel = TeleportNamespace + "/reconfigure-source-uuid" + // OriginDefaults is an origin value indicating that the resource was // constructed as a default value. OriginDefaults = common.OriginDefaults diff --git a/lib/config/reconfigure.go b/lib/config/reconfigure.go new file mode 100644 index 00000000000..c64e83db027 --- /dev/null +++ b/lib/config/reconfigure.go @@ -0,0 +1,297 @@ +/* + * Teleport + * Copyright (C) 2026 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "fmt" + "maps" + "slices" + "strings" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + apiutils "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils" +) + +// ReconfigureRequest describes the changes Reconfigure applies to a parsed +// FileConfig. A zero-valued field leaves the corresponding setting untouched; +// none of the fields has a meaningful empty value. +type ReconfigureRequest struct { + // ProxyServer sets proxy_server. Valid only for v3 configs. + ProxyServer string + // AuthServer sets auth_server (v3) or replaces auth_servers with the + // single given address (v1/v2). Mutually exclusive with ProxyServer. + AuthServer string + // CAPins replaces ca_pin with the target cluster's pins. Required when + // ProxyServer or AuthServer retargets a config that already pins a CA. + CAPins []string + // TokenName sets join_params.token_name and clears the legacy auth_token. + TokenName string + // JoinMethod sets join_params.method. It is never inferred from the + // other join fields. + JoinMethod string + // BoundKeypairRegistrationSecret sets the bound_keypair registration secret + // value and clears the incompatible registration_secret_path. + BoundKeypairRegistrationSecret string + // BoundKeypairRegistrationSecretPath sets the bound_keypair registration + // secret path and clears the incompatible registration_secret_value. + // Mutually exclusive with BoundKeypairRegistrationSecret. + BoundKeypairRegistrationSecretPath string + // NodeLabels is a comma-separated k=v list merged additively into + // ssh_service labels; on a key conflict the last value wins. + NodeLabels string + // SourceHostUUID sets the node label types.ReconfigureSourceUUIDLabel, + // recording the host the input config was read on. + SourceHostUUID string + // DataDir sets teleport.data_dir. + DataDir string + // PIDFile sets teleport.pid_file. + PIDFile string + // DiagAddr sets teleport.diag_addr. + DiagAddr string + // SSHListenAddr sets ssh_service.listen_addr. + SSHListenAddr string + // KubeListenAddr sets kubernetes_service.listen_addr. + KubeListenAddr string + // MetricsListenAddr sets metrics_service.listen_addr. + MetricsListenAddr string +} + +// Reconfigure applies req to fc in place, leaving everything not named by a +// request field untouched. It rejects cases where an unspecified request +// field would cause a collision with a second agent on the same host. +func Reconfigure(fc *FileConfig, req ReconfigureRequest) error { + if req.ProxyServer != "" && req.AuthServer != "" { + return trace.BadParameter("--proxy-server and --auth-server are mutually exclusive") + } + if req.BoundKeypairRegistrationSecret != "" && req.BoundKeypairRegistrationSecretPath != "" { + return trace.BadParameter("--registration-secret and --registration-secret-path are mutually exclusive") + } + if req.ProxyServer != "" && fc.Version != defaults.TeleportConfigVersionV3 { + return trace.BadParameter("--proxy-server requires a v3 config; use --auth-server for v1/v2 configs") + } + if req.ProxyServer != "" && fc.Proxy.Enabled() { + return trace.BadParameter("--proxy-server cannot be specified if the proxy_service is enabled") + } + // Mirror the parsers the agent runs at start (applyConfig and the + // per-service apply functions), or the tool writes a config the agent rejects. + if req.ProxyServer != "" { + if _, err := utils.ParseHostPortAddr(req.ProxyServer, defaults.HTTPListenPort); err != nil { + return trace.Wrap(err, "parsing --proxy-server") + } + } + if req.AuthServer != "" { + if _, err := utils.ParseHostPortAddr(req.AuthServer, defaults.AuthListenPort); err != nil { + return trace.Wrap(err, "parsing --auth-server") + } + } + if len(req.CAPins) > 0 { + // Nil certs: format check only; the pins are verified against the + // target cluster's CA when the agent joins. + if err := utils.CheckSPKI(req.CAPins, nil); err != nil { + return trace.Wrap(err, "parsing --ca-pin") + } + } + if req.JoinMethod != "" { + if err := types.ValidateJoinMethod(types.JoinMethod(req.JoinMethod)); err != nil { + return trace.Wrap(err, "parsing --join-method") + } + } + if req.DiagAddr != "" { + if _, err := utils.ParseAddr(req.DiagAddr); err != nil { + return trace.Wrap(err, "parsing --diag-addr") + } + } + if req.SSHListenAddr != "" { + if _, err := utils.ParseHostPortAddr(req.SSHListenAddr, defaults.SSHServerListenPort); err != nil { + return trace.Wrap(err, "parsing --ssh-listen-addr") + } + } + if req.KubeListenAddr != "" { + // Not a typo: applyKubeConfig uses the SSH proxy port as kube's default. + if _, err := utils.ParseHostPortAddr(req.KubeListenAddr, defaults.SSHProxyListenPort); err != nil { + return trace.Wrap(err, "parsing --kube-listen-addr") + } + } + if req.MetricsListenAddr != "" { + if _, err := utils.ParseHostPortAddr(req.MetricsListenAddr, defaults.MetricsListenPort); err != nil { + return trace.Wrap(err, "parsing --metrics-listen-addr") + } + } + + // The join method comes from the request or the config, never inference. + // Reject what the agent would reject at startup (join_params without a + // method) or silently ignore (a registration secret without bound_keypair). + joinMethod := fc.JoinParams.Method + if req.JoinMethod != "" { + joinMethod = types.JoinMethod(req.JoinMethod) + } + if (req.BoundKeypairRegistrationSecret != "" || req.BoundKeypairRegistrationSecretPath != "") && joinMethod != types.JoinMethodBoundKeypair { + flag := "--registration-secret" + if req.BoundKeypairRegistrationSecretPath != "" { + flag = "--registration-secret-path" + } + return trace.BadParameter("%s requires the %s join method; pass --join-method %s", flag, types.JoinMethodBoundKeypair, types.JoinMethodBoundKeypair) + } + joinParamsInUse := !fc.JoinParams.IsEqual(&JoinParams{}) || + req.TokenName != "" || req.BoundKeypairRegistrationSecret != "" || req.BoundKeypairRegistrationSecretPath != "" + if joinParamsInUse && joinMethod == "" { + return trace.BadParameter("the output config would set join_params without a join method, which the agent rejects at startup; pass --join-method") + } + if req.JoinMethod != "" && !joinParamsInUse && fc.AuthToken == "" { + return trace.BadParameter("--join-method would create join_params without a token name; pass --token as well") + } + // ca_pin validates the CA of the cluster the agent joins, so pins carried + // over from the old cluster cannot be reused after a retarget. Requiring + // new ones keeps a pinned config from silently becoming an unpinned one. + if (req.ProxyServer != "" || req.AuthServer != "") && len(fc.CAPin) > 0 && len(req.CAPins) == 0 { + return trace.BadParameter("the input config sets ca_pin, and retargeting would leave the next join unpinned; pass --ca-pin with the target cluster's pins, which `tctl status` prints") + } + + if req.ProxyServer != "" { + fc.ProxyServer = req.ProxyServer + fc.AuthServer = "" + fc.AuthServers = nil + } + if req.AuthServer != "" { + if fc.Version == defaults.TeleportConfigVersionV3 { + fc.AuthServer = req.AuthServer + fc.ProxyServer = "" + fc.AuthServers = nil + } else { + fc.AuthServers = []string{req.AuthServer} + } + } + if len(req.CAPins) > 0 { + fc.CAPin = apiutils.Strings(slices.Clone(req.CAPins)) + } + + if req.TokenName != "" { + fc.JoinParams.TokenName = req.TokenName + fc.AuthToken = "" + } + if req.BoundKeypairRegistrationSecret != "" { + fc.JoinParams.BoundKeypair.RegistrationSecretValue = req.BoundKeypairRegistrationSecret + fc.JoinParams.BoundKeypair.RegistrationSecretPath = "" + } + if req.BoundKeypairRegistrationSecretPath != "" { + fc.JoinParams.BoundKeypair.RegistrationSecretPath = req.BoundKeypairRegistrationSecretPath + fc.JoinParams.BoundKeypair.RegistrationSecretValue = "" + } + if req.JoinMethod != "" { + fc.JoinParams.Method = types.JoinMethod(req.JoinMethod) + // Leaving the bound_keypair block under another method would carry a + // possibly live registration secret into the output. + if fc.JoinParams.Method != types.JoinMethodBoundKeypair { + fc.JoinParams.BoundKeypair = BoundKeypairParams{} + } + } + // The agent rejects a config that sets both join_params and the legacy + // auth_token. + if !fc.JoinParams.IsEqual(&JoinParams{}) && fc.AuthToken != "" { + if fc.JoinParams.TokenName == "" { + fc.JoinParams.TokenName = fc.AuthToken + } + fc.AuthToken = "" + } + + if req.NodeLabels != "" { + static, dynamic, err := parseLabels(req.NodeLabels) + if err != nil { + return trace.Wrap(err, "parsing --node-labels") + } + if len(dynamic) > 0 { + return trace.BadParameter("--node-labels only accepts static labels") + } + if fc.SSH.Labels == nil { + fc.SSH.Labels = make(map[string]string, len(static)) + } + maps.Copy(fc.SSH.Labels, static) + } + // After the node-labels merge, so the recorded source wins over both a + // stale label carried in the input and a --node-labels value. + if req.SourceHostUUID != "" { + if fc.SSH.Labels == nil { + fc.SSH.Labels = make(map[string]string, 1) + } + fc.SSH.Labels[types.ReconfigureSourceUUIDLabel] = req.SourceHostUUID + } + + if req.DataDir != "" { + fc.DataDir = req.DataDir + } + if req.PIDFile != "" { + fc.PIDFile = req.PIDFile + } + if req.DiagAddr != "" { + fc.DiagAddr = req.DiagAddr + } + if req.SSHListenAddr != "" { + fc.SSH.ListenAddress = req.SSHListenAddr + } + if req.KubeListenAddr != "" { + fc.Kube.ListenAddress = req.KubeListenAddr + } + if req.MetricsListenAddr != "" { + fc.Metrics.ListenAddress = req.MetricsListenAddr + } + + return trace.Wrap(collisionError(fc, req)) +} + +// collisionError rejects values kept from the input that a second agent on +// the same host would contend for; any explicitly requested value passes. +// Only values the input sets are checked; implicit defaults (like SSH's +// 3022) cannot be detected here. +func collisionError(fc *FileConfig, req ReconfigureRequest) error { + var collisions []string + add := func(field, value, flag string) { + collisions = append(collisions, fmt.Sprintf("%s %q (%s)", field, value, flag)) + } + if req.PIDFile == "" && fc.PIDFile != "" { + add("pid_file", fc.PIDFile, "--pid-file") + } + if req.DiagAddr == "" && fc.DiagAddr != "" { + add("diag_addr", fc.DiagAddr, "--diag-addr") + } + if req.SSHListenAddr == "" && listenAddrCollides(fc.SSH.Service) { + add("ssh_service.listen_addr", fc.SSH.ListenAddress, "--ssh-listen-addr") + } + if req.KubeListenAddr == "" && listenAddrCollides(fc.Kube.Service) { + add("kubernetes_service.listen_addr", fc.Kube.ListenAddress, "--kube-listen-addr") + } + if req.MetricsListenAddr == "" && listenAddrCollides(fc.Metrics.Service) { + add("metrics_service.listen_addr", fc.Metrics.ListenAddress, "--metrics-listen-addr") + } + if len(collisions) == 0 { + return nil + } + return trace.BadParameter( + "the output would keep these values from the input, and two agents on one host would collide on them:\n\t%s\npass each listed flag with a new value, or repeat the current value to keep it", + strings.Join(collisions, "\n\t")) +} + +// listenAddrCollides reports whether a service pins a listen address a second +// agent would contend for: explicitly set, on an enabled service. +func listenAddrCollides(s Service) bool { + return s.ListenAddress != "" && s.Enabled() +} diff --git a/lib/config/testdata/TestReconfigureGoldenOutput/join_method_away_from_bound_keypair.golden b/lib/config/testdata/TestReconfigureGoldenOutput/join_method_away_from_bound_keypair.golden new file mode 100644 index 00000000000..577a8a8b9d5 --- /dev/null +++ b/lib/config/testdata/TestReconfigureGoldenOutput/join_method_away_from_bound_keypair.golden @@ -0,0 +1,9 @@ +version: v3 +teleport: + data_dir: /var/lib/teleport + join_params: + token_name: bk-token + method: token + proxy_server: old.example.com:443 + ca_pin: [] + diag_addr: "" diff --git a/lib/config/testdata/TestReconfigureGoldenOutput/v2_auth_server_retarget.golden b/lib/config/testdata/TestReconfigureGoldenOutput/v2_auth_server_retarget.golden new file mode 100644 index 00000000000..8991d548271 --- /dev/null +++ b/lib/config/testdata/TestReconfigureGoldenOutput/v2_auth_server_retarget.golden @@ -0,0 +1,11 @@ +version: v2 +teleport: + nodename: node-04 + data_dir: /var/lib/teleport + join_params: + token_name: new-token + method: token + auth_servers: + - new.example.com:3025 + ca_pin: sha256:bbb + diag_addr: "" diff --git a/lib/config/testdata/TestReconfigureGoldenOutput/v3_proxy_retarget.golden b/lib/config/testdata/TestReconfigureGoldenOutput/v3_proxy_retarget.golden new file mode 100644 index 00000000000..47efcc9cba8 --- /dev/null +++ b/lib/config/testdata/TestReconfigureGoldenOutput/v3_proxy_retarget.golden @@ -0,0 +1,21 @@ +version: v3 +teleport: + nodename: node-04 + data_dir: /var/lib/teleport_new + join_params: + token_name: new-token + method: token + proxy_server: new.example.com:443 + ca_pin: sha256:bbb + diag_addr: "" +ssh_service: + enabled: "yes" + labels: + env: dev + team: a + teleport.dev/reconfigure-source-uuid: 8a2b5c70-97d8-4d97-b3c4-6e54b4a63cfe +proxy_service: + enabled: "no" + https_keypairs: [] + https_keypairs_reload_interval: 0s + acme: {} diff --git a/tool/teleport/common/reconfigure.go b/tool/teleport/common/reconfigure.go new file mode 100644 index 00000000000..78562a6a851 --- /dev/null +++ b/tool/teleport/common/reconfigure.go @@ -0,0 +1,162 @@ +/* + * Teleport + * Copyright (C) 2026 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package common + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/google/renameio/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils/hostid" +) + +// reconfigureFlags holds the command line flags for `teleport reconfigure`. +type reconfigureFlags struct { + input string + output string + overwrite bool + proxyServer string + authServer string + caPins []string + tokenName string + joinMethod string + boundKeypairRegistrationSecret string + boundKeypairRegistrationSecretPath string + nodeLabels string + dataDir string + pidFile string + diagAddr string + sshListenAddr string + kubeListenAddr string + metricsListenAddr string +} + +// onReconfigure is the handler for the "reconfigure" CLI command. +func onReconfigure(flags reconfigureFlags) error { + return trace.Wrap(runReconfigure(flags, os.Stdout)) +} + +func runReconfigure(flags reconfigureFlags, stdout io.Writer) error { + if flags.overwrite && flags.output == "" { + return trace.BadParameter("--overwrite requires --output") + } + fc, err := config.ReadFromFile(flags.input) + if err != nil { + return trace.Wrap(err) + } + // The uuid names the host the input config was read on, if present. Best effort: when the uuid is + // unreadable (a machine that never ran an agent, an unprivileged user), + // ReadFile returns "" and the label is not added. + sourceDataDir := fc.DataDir + if sourceDataDir == "" { + sourceDataDir = defaults.DataDir + } + sourceHostUUID, _ := hostid.ReadFile(sourceDataDir) + if err := config.Reconfigure(fc, config.ReconfigureRequest{ + ProxyServer: flags.proxyServer, + AuthServer: flags.authServer, + CAPins: flags.caPins, + TokenName: flags.tokenName, + JoinMethod: flags.joinMethod, + BoundKeypairRegistrationSecret: flags.boundKeypairRegistrationSecret, + BoundKeypairRegistrationSecretPath: flags.boundKeypairRegistrationSecretPath, + NodeLabels: flags.nodeLabels, + SourceHostUUID: sourceHostUUID, + DataDir: flags.dataDir, + PIDFile: flags.pidFile, + DiagAddr: flags.diagAddr, + SSHListenAddr: flags.sshListenAddr, + KubeListenAddr: flags.kubeListenAddr, + MetricsListenAddr: flags.metricsListenAddr, + }); err != nil { + return trace.Wrap(err) + } + out, err := fc.YAMLString() + if err != nil { + return trace.Wrap(err) + } + // Round-trip the output through the same reader the agent uses at + // start, so an invalid file is never written. + if _, err := config.ReadConfig(strings.NewReader(out)); err != nil { + return trace.Wrap(err, "internal error: generated config failed validation, nothing written") + } + if flags.output == "" { + if _, err := fmt.Fprint(stdout, out); err != nil { + return trace.ConvertSystemError(err) + } + return nil + } + return trace.Wrap(writeReconfigureOutput(flags.input, flags.output, flags.overwrite, []byte(out))) +} + +// writeReconfigureOutput writes data to output with owner-only permissions. +// It refuses to write over the input config and refuses an existing output +// unless overwrite is set. +func writeReconfigureOutput(input, output string, overwrite bool, data []byte) error { + inInfo, err := os.Stat(input) + if err != nil { + return trace.ConvertSystemError(err) + } + if outInfo, err := os.Stat(output); err == nil && os.SameFile(inInfo, outInfo) { + return trace.BadParameter("refusing to write over the input config %q", input) + } + + if overwrite { + // Temp file + atomic rename, forcing owner-only permissions: + // renameio.WriteFile would keep an existing file's broader mode. + pending, err := renameio.NewPendingFile(output, renameio.WithPermissions(teleport.FileMaskOwnerOnly)) + if err != nil { + return trace.ConvertSystemError(err) + } + defer pending.Cleanup() + if _, err := pending.Write(data); err != nil { + return trace.ConvertSystemError(err) + } + return trace.ConvertSystemError(pending.CloseAtomicallyReplace()) + } + + // O_EXCL atomically rejects anything already at the path, symlinks included. + f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, teleport.FileMaskOwnerOnly) + if err != nil { + if os.IsExist(err) { + return trace.AlreadyExists("output file %q already exists; use --overwrite to replace it", output) + } + return trace.ConvertSystemError(err) + } + // On failure, remove the partial file so a retry is not blocked by the + // already-exists guard. + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(output) + return trace.ConvertSystemError(err) + } + // Close can carry the real write failure. + if err := f.Close(); err != nil { + os.Remove(output) + return trace.ConvertSystemError(err) + } + return nil +} diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index eec29d663aa..33fb9252763 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -97,6 +97,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con ccf config.CommandLineFlags scpFlags scp.Flags dumpFlags dumpFlags + reconfigureFlags reconfigureFlags configureDatabaseAWSPrintFlags configureDatabaseAWSPrintFlags configureDatabaseAWSCreateFlags configureDatabaseAWSCreateFlags configureDiscoveryBootstrapFlags configureDiscoveryBootstrapFlags @@ -457,6 +458,25 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con dump.Flag("node-name", "Name for the Teleport node.").StringVar(&dumpFlags.NodeName) dump.Flag("node-labels", "Comma-separated list of labels to add to newly created nodes, for example env=staging,cloud=aws.").StringVar(&dumpFlags.NodeLabels) + reconf := app.Command("reconfigure", "Generate a new agent config file from an existing one, applying changes given by flags.").Hidden() + reconf.Flag("input", "Path to the source config file.").Default(defaults.ConfigFilePath).StringVar(&reconfigureFlags.input) + reconf.Flag("output", "Path to write the new config file to. Writes to stdout when omitted.").StringVar(&reconfigureFlags.output) + reconf.Flag("overwrite", "Replace the output file if it already exists.").BoolVar(&reconfigureFlags.overwrite) + reconf.Flag("proxy-server", "Proxy address of the target cluster; sets proxy_server (v3 configs only).").StringVar(&reconfigureFlags.proxyServer) + reconf.Flag("auth-server", "Auth server address of the target cluster; sets auth_server (v3) or auth_servers (v1/v2).").StringVar(&reconfigureFlags.authServer) + reconf.Flag("ca-pin", "CA pin of the target cluster (can be repeated for multiple pins).").StringsVar(&reconfigureFlags.caPins) + reconf.Flag("token", "Name of the join token to register with the target cluster.").StringVar(&reconfigureFlags.tokenName) + reconf.Flag("join-method", "Join method to set in join_params. Defaults to the method already in the config.").EnumVar(&reconfigureFlags.joinMethod, joinMethods...) + reconf.Flag("registration-secret", "Bound keypair registration secret. Prefer --registration-secret-path, which keeps the secret off the command line.").StringVar(&reconfigureFlags.boundKeypairRegistrationSecret) + reconf.Flag("registration-secret-path", "Path to a file containing the bound keypair registration secret; the agent reads it at join time.").StringVar(&reconfigureFlags.boundKeypairRegistrationSecretPath) + reconf.Flag("node-labels", "Comma-separated labels to merge into ssh_service labels, for example env=dev,team=a; new values win on conflict.").StringVar(&reconfigureFlags.nodeLabels) + reconf.Flag("data-dir", "Path to the new agent's data directory.").StringVar(&reconfigureFlags.dataDir) + reconf.Flag("pid-file", "Full path to the new agent's PID file.").StringVar(&reconfigureFlags.pidFile) + reconf.Flag("diag-addr", "Address for the new agent's diagnostic endpoint.").StringVar(&reconfigureFlags.diagAddr) + reconf.Flag("ssh-listen-addr", "Listen address for the new agent's SSH service.").StringVar(&reconfigureFlags.sshListenAddr) + reconf.Flag("kube-listen-addr", "Listen address for the new agent's Kubernetes service.").StringVar(&reconfigureFlags.kubeListenAddr) + reconf.Flag("metrics-listen-addr", "Listen address for the new agent's metrics service.").StringVar(&reconfigureFlags.metricsListenAddr) + ver.Flag("raw", "Print the raw teleport version string.").BoolVar(&rawVersion) dumpNode := app.Command("node", "SSH Node configuration commands") @@ -752,6 +772,8 @@ Examples: err = onStatus() case dump.FullCommand(): err = onConfigDump(dumpFlags) + case reconf.FullCommand(): + err = onReconfigure(reconfigureFlags) case dumpNodeConfigure.FullCommand(): dumpFlags.Roles = defaults.RoleNode err = onConfigDump(dumpFlags)