Skip to content

Commit eadbe2d

Browse files
authored
Refactor node-join script to take safer options and reuse install option logic (#52196)
* Add install script using teleport-update and oneoff.sh * Refactor node-join script to take safer options and reuse install option logic * GoDoc + make functions private * Address edoardo's feedback
1 parent 317860d commit eadbe2d

File tree

6 files changed

+475
-392
lines changed

6 files changed

+475
-392
lines changed

lib/web/apiserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2250,7 +2250,7 @@ func (h *Handler) installer(w http.ResponseWriter, r *http.Request, p httprouter
22502250
// https://updates.releases.teleport.dev/v1/stable/cloud/version
22512251
installUpdater := automaticUpgrades(*ping.ServerFeatures)
22522252
if installUpdater {
2253-
repoChannel = stableCloudChannelRepo
2253+
repoChannel = automaticupgrades.DefaultCloudChannelName
22542254
}
22552255
azureClientID := r.URL.Query().Get("azure-client-id")
22562256

lib/web/apiserver_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3643,6 +3643,7 @@ func TestKnownWebPathsWithAndWithoutV1Prefix(t *testing.T) {
36433643

36443644
func TestInstallDatabaseScriptGeneration(t *testing.T) {
36453645
const username = "[email protected]"
3646+
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildCommunity})
36463647

36473648
// Users should be able to create Tokens even if they can't update them
36483649
roleTokenCRD, err := types.NewRole(services.RoleNameForUser(username), types.RoleSpecV6{

lib/web/join_tokens.go

Lines changed: 52 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,26 @@
1919
package web
2020

2121
import (
22-
"bytes"
2322
"context"
2423
"encoding/hex"
2524
"fmt"
2625
"hash/fnv"
2726
"net/http"
2827
"net/url"
2928
"reflect"
30-
"regexp"
3129
"sort"
32-
"strconv"
3330
"strings"
3431
"time"
3532

36-
"github.com/google/safetext/shsprintf"
3733
"github.com/google/uuid"
3834
"github.com/gravitational/trace"
3935
"github.com/julienschmidt/httprouter"
40-
"k8s.io/apimachinery/pkg/util/validation"
4136

42-
"github.com/gravitational/teleport"
4337
"github.com/gravitational/teleport/api/client/proto"
4438
"github.com/gravitational/teleport/api/types"
4539
apiutils "github.com/gravitational/teleport/api/utils"
4640
"github.com/gravitational/teleport/lib/defaults"
4741
"github.com/gravitational/teleport/lib/httplib"
48-
"github.com/gravitational/teleport/lib/modules"
4942
"github.com/gravitational/teleport/lib/services"
5043
"github.com/gravitational/teleport/lib/tlsca"
5144
"github.com/gravitational/teleport/lib/ui"
@@ -55,8 +48,7 @@ import (
5548
)
5649

5750
const (
58-
stableCloudChannelRepo = "stable/cloud"
59-
HeaderTokenName = "X-Teleport-TokenName"
51+
HeaderTokenName = "X-Teleport-TokenName"
6052
)
6153

6254
// nodeJoinToken contains node token fields for the UI.
@@ -80,15 +72,9 @@ type scriptSettings struct {
8072
appURI string
8173
joinMethod string
8274
databaseInstallMode bool
83-
installUpdater bool
8475

8576
discoveryInstallMode bool
8677
discoveryGroup string
87-
88-
// automaticUpgradesVersion is the target automatic upgrades version.
89-
// The version must be valid semver, with the leading 'v'. e.g. v15.0.0-dev
90-
// Required when installUpdater is true.
91-
automaticUpgradesVersion string
9278
}
9379

9480
// automaticUpgrades returns whether automaticUpgrades should be enabled.
@@ -377,41 +363,16 @@ func (h *Handler) createTokenForDiscoveryHandle(w http.ResponseWriter, r *http.R
377363
}, nil
378364
}
379365

380-
// getAutoUpgrades checks if automaticUpgrades are enabled and returns the
381-
// version that should be used according to auto upgrades default channel.
382-
// If something bad happens, the error is logged and the function falls back to
383-
// the process Teleport version.
384-
func (h *Handler) getAutoUpgrades(ctx context.Context) (bool, string) {
385-
var autoUpgradesVersion string
386-
var err error
387-
autoUpgrades := automaticUpgrades(h.GetClusterFeatures())
388-
if autoUpgrades {
389-
const group, updaterUUID = "", ""
390-
autoUpgradesVersion, err = h.autoUpdateAgentVersion(ctx, group, updaterUUID)
391-
if err != nil {
392-
h.logger.WarnContext(ctx, "Failed to get auto upgrades version, falling back to self version.", "error", err)
393-
return autoUpgrades, teleport.Version
394-
}
395-
autoUpgradesVersion = fmt.Sprintf("v%s", autoUpgradesVersion)
396-
}
397-
return autoUpgrades, autoUpgradesVersion
398-
399-
}
400-
401366
func (h *Handler) getNodeJoinScriptHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params) (interface{}, error) {
402367
httplib.SetScriptHeaders(w.Header())
403368

404-
autoUpgrades, autoUpgradesVersion := h.getAutoUpgrades(r.Context())
405-
406369
settings := scriptSettings{
407-
token: params.ByName("token"),
408-
appInstallMode: false,
409-
joinMethod: r.URL.Query().Get("method"),
410-
installUpdater: autoUpgrades,
411-
automaticUpgradesVersion: autoUpgradesVersion,
370+
token: params.ByName("token"),
371+
appInstallMode: false,
372+
joinMethod: r.URL.Query().Get("method"),
412373
}
413374

414-
script, err := getJoinScript(r.Context(), settings, h.GetProxyClient())
375+
script, err := h.getJoinScript(r.Context(), settings)
415376
if err != nil {
416377
h.logger.InfoContext(r.Context(), "Failed to return the node install script", "error", err)
417378
w.Write(scripts.ErrorBashScript)
@@ -451,18 +412,14 @@ func (h *Handler) getAppJoinScriptHandle(w http.ResponseWriter, r *http.Request,
451412
return nil, nil
452413
}
453414

454-
autoUpgrades, autoUpgradesVersion := h.getAutoUpgrades(r.Context())
455-
456415
settings := scriptSettings{
457-
token: params.ByName("token"),
458-
appInstallMode: true,
459-
appName: name,
460-
appURI: uri,
461-
installUpdater: autoUpgrades,
462-
automaticUpgradesVersion: autoUpgradesVersion,
416+
token: params.ByName("token"),
417+
appInstallMode: true,
418+
appName: name,
419+
appURI: uri,
463420
}
464421

465-
script, err := getJoinScript(r.Context(), settings, h.GetProxyClient())
422+
script, err := h.getJoinScript(r.Context(), settings)
466423
if err != nil {
467424
h.logger.InfoContext(r.Context(), "Failed to return the app install script", "error", err)
468425
w.Write(scripts.ErrorBashScript)
@@ -481,16 +438,12 @@ func (h *Handler) getAppJoinScriptHandle(w http.ResponseWriter, r *http.Request,
481438
func (h *Handler) getDatabaseJoinScriptHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params) (interface{}, error) {
482439
httplib.SetScriptHeaders(w.Header())
483440

484-
autoUpgrades, autoUpgradesVersion := h.getAutoUpgrades(r.Context())
485-
486441
settings := scriptSettings{
487-
token: params.ByName("token"),
488-
databaseInstallMode: true,
489-
installUpdater: autoUpgrades,
490-
automaticUpgradesVersion: autoUpgradesVersion,
442+
token: params.ByName("token"),
443+
databaseInstallMode: true,
491444
}
492445

493-
script, err := getJoinScript(r.Context(), settings, h.GetProxyClient())
446+
script, err := h.getJoinScript(r.Context(), settings)
494447
if err != nil {
495448
h.logger.InfoContext(r.Context(), "Failed to return the database install script", "error", err)
496449
w.Write(scripts.ErrorBashScript)
@@ -511,8 +464,6 @@ func (h *Handler) getDiscoveryJoinScriptHandle(w http.ResponseWriter, r *http.Re
511464
queryValues := r.URL.Query()
512465
const discoveryGroupQueryParam = "discoveryGroup"
513466

514-
autoUpgrades, autoUpgradesVersion := h.getAutoUpgrades(r.Context())
515-
516467
discoveryGroup, err := url.QueryUnescape(queryValues.Get(discoveryGroupQueryParam))
517468
if err != nil {
518469
h.logger.DebugContext(r.Context(), "Failed to return the discovery install script",
@@ -531,14 +482,12 @@ func (h *Handler) getDiscoveryJoinScriptHandle(w http.ResponseWriter, r *http.Re
531482
}
532483

533484
settings := scriptSettings{
534-
token: params.ByName("token"),
535-
discoveryInstallMode: true,
536-
discoveryGroup: discoveryGroup,
537-
installUpdater: autoUpgrades,
538-
automaticUpgradesVersion: autoUpgradesVersion,
485+
token: params.ByName("token"),
486+
discoveryInstallMode: true,
487+
discoveryGroup: discoveryGroup,
539488
}
540489

541-
script, err := getJoinScript(r.Context(), settings, h.GetProxyClient())
490+
script, err := h.getJoinScript(r.Context(), settings)
542491
if err != nil {
543492
h.logger.InfoContext(r.Context(), "Failed to return the discovery install script", "error", err)
544493
w.Write(scripts.ErrorBashScript)
@@ -554,8 +503,9 @@ func (h *Handler) getDiscoveryJoinScriptHandle(w http.ResponseWriter, r *http.Re
554503
return nil, nil
555504
}
556505

557-
func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter) (string, error) {
558-
switch types.JoinMethod(settings.joinMethod) {
506+
func (h *Handler) getJoinScript(ctx context.Context, settings scriptSettings) (string, error) {
507+
joinMethod := types.JoinMethod(settings.joinMethod)
508+
switch joinMethod {
559509
case types.JoinMethodUnspecified, types.JoinMethodToken:
560510
if err := validateJoinToken(settings.token); err != nil {
561511
return "", trace.Wrap(err)
@@ -565,141 +515,55 @@ func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter
565515
return "", trace.BadParameter("join method %q is not supported via script", settings.joinMethod)
566516
}
567517

518+
clt := h.GetProxyClient()
519+
568520
// The provided token can be attacker controlled, so we must validate
569521
// it with the backend before using it to generate the script.
570-
token, err := m.GetToken(ctx, settings.token)
522+
token, err := clt.GetToken(ctx, settings.token)
571523
if err != nil {
572524
return "", trace.BadParameter("invalid token")
573525
}
574526

575-
// Get hostname and port from proxy server address.
576-
proxyServers, err := m.GetProxies()
577-
if err != nil {
578-
return "", trace.Wrap(err)
579-
}
580-
581-
if len(proxyServers) == 0 {
582-
return "", trace.NotFound("no proxy servers found")
583-
}
584-
585-
version := proxyServers[0].GetTeleportVersion()
586-
587-
publicAddr := proxyServers[0].GetPublicAddr()
588-
if publicAddr == "" {
589-
return "", trace.Errorf("proxy public_addr is not set, you must set proxy_service.public_addr to the publicly reachable address of the proxy before you can generate a node join script")
590-
}
591-
592-
hostname, portStr, err := utils.SplitHostPort(publicAddr)
593-
if err != nil {
594-
return "", trace.Wrap(err)
595-
}
527+
// TODO(hugoShaka): hit the local accesspoint which has a cache instead of asking the auth every time.
596528

597529
// Get the CA pin hashes of the cluster to join.
598-
localCAResponse, err := m.GetClusterCACert(ctx)
530+
localCAResponse, err := clt.GetClusterCACert(ctx)
599531
if err != nil {
600532
return "", trace.Wrap(err)
601533
}
534+
602535
caPins, err := tlsca.CalculatePins(localCAResponse.TLSCA)
603536
if err != nil {
604537
return "", trace.Wrap(err)
605538
}
606539

607-
labelsList := []string{}
608-
for labelKey, labelValues := range token.GetSuggestedLabels() {
609-
labels := strings.Join(labelValues, " ")
610-
labelsList = append(labelsList, fmt.Sprintf("%s=%s", labelKey, labels))
611-
}
612-
613-
var dbServiceResourceLabels []string
614-
if settings.databaseInstallMode {
615-
suggestedAgentMatcherLabels := token.GetSuggestedAgentMatcherLabels()
616-
dbServiceResourceLabels, err = scripts.MarshalLabelsYAML(suggestedAgentMatcherLabels, 6)
617-
if err != nil {
618-
return "", trace.Wrap(err)
619-
}
620-
}
621-
622-
var buf bytes.Buffer
623-
var appServerResourceLabels []string
624-
// If app install mode is requested but parameters are blank for some reason,
625-
// we need to return an error.
626-
if settings.appInstallMode {
627-
if errs := validation.IsDNS1035Label(settings.appName); len(errs) > 0 {
628-
return "", trace.BadParameter("appName %q must be a valid DNS subdomain: https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/#application-name", settings.appName)
629-
}
630-
if !appURIPattern.MatchString(settings.appURI) {
631-
return "", trace.BadParameter("appURI %q contains invalid characters", settings.appURI)
632-
}
633-
634-
suggestedLabels := token.GetSuggestedLabels()
635-
appServerResourceLabels, err = scripts.MarshalLabelsYAML(suggestedLabels, 4)
636-
if err != nil {
637-
return "", trace.Wrap(err)
638-
}
639-
}
640-
641-
if settings.discoveryInstallMode {
642-
if settings.discoveryGroup == "" {
643-
return "", trace.BadParameter("discovery group is required")
644-
}
645-
}
646-
647-
packageName := types.PackageNameOSS
648-
if modules.GetModules().BuildType() == modules.BuildEnterprise {
649-
packageName = types.PackageNameEnt
650-
}
651-
652-
// By default, it will use `stable/v<majorVersion>`, eg stable/v12
653-
repoChannel := ""
654-
655-
// The install script will install the updater (teleport-ent-updater) for Cloud customers enrolled in Automatic Upgrades.
656-
// The repo channel used must be `stable/cloud` which has the available packages for the Cloud Customer's agents.
657-
// It pins the teleport version to the one specified by the default version channel
658-
// This ensures the initial installed version is the same as the `teleport-ent-updater` would install.
659-
if settings.installUpdater {
660-
if settings.automaticUpgradesVersion == "" {
661-
return "", trace.Wrap(err, "automatic upgrades version must be set when installUpdater is true")
662-
}
663-
664-
repoChannel = stableCloudChannelRepo
665-
// automaticUpgradesVersion has vX.Y.Z format, however the script
666-
// expects the version to not include the `v` so we strip it
667-
version = strings.TrimPrefix(settings.automaticUpgradesVersion, "v")
668-
}
669-
670-
// This section relies on Go's default zero values to make sure that the settings
671-
// are correct when not installing an app.
672-
err = scripts.InstallNodeBashScript.Execute(&buf, map[string]interface{}{
673-
"token": settings.token,
674-
"hostname": hostname,
675-
"port": portStr,
676-
// The install.sh script has some manually generated configs and some
677-
// generated by the `teleport <service> config` commands. The old bash
678-
// version used space delimited values whereas the teleport command uses
679-
// a comma delimeter. The Old version can be removed when the install.sh
680-
// file has been completely converted over.
681-
"caPinsOld": strings.Join(caPins, " "),
682-
"caPins": strings.Join(caPins, ","),
683-
"packageName": packageName,
684-
"repoChannel": repoChannel,
685-
"installUpdater": strconv.FormatBool(settings.installUpdater),
686-
"version": shsprintf.EscapeDefaultContext(version),
687-
"appInstallMode": strconv.FormatBool(settings.appInstallMode),
688-
"appServerResourceLabels": appServerResourceLabels,
689-
"appName": shsprintf.EscapeDefaultContext(settings.appName),
690-
"appURI": shsprintf.EscapeDefaultContext(settings.appURI),
691-
"joinMethod": shsprintf.EscapeDefaultContext(settings.joinMethod),
692-
"labels": strings.Join(labelsList, ","),
693-
"databaseInstallMode": strconv.FormatBool(settings.databaseInstallMode),
694-
"db_service_resource_labels": dbServiceResourceLabels,
695-
"discoveryInstallMode": settings.discoveryInstallMode,
696-
"discoveryGroup": shsprintf.EscapeDefaultContext(settings.discoveryGroup),
697-
})
540+
installOpts, err := h.installScriptOptions(ctx)
698541
if err != nil {
699-
return "", trace.Wrap(err)
700-
}
701-
702-
return buf.String(), nil
542+
return "", trace.Wrap(err, "Building install script options")
543+
}
544+
545+
nodeInstallOpts := scripts.InstallNodeScriptOptions{
546+
InstallOptions: installOpts,
547+
Token: token.GetName(),
548+
CAPins: caPins,
549+
// We are using the joinMethod from the script settings instead of the one from the token
550+
// to reproduce the previous script behavior. I'm also afraid that using the
551+
// join method from the token would provide an oracle for an attacker wanting to discover
552+
// the join method.
553+
// We might want to change this in the future to lookup the join method from the token
554+
// to avoid potential mismatch and allow the caller to not care about the join method.
555+
JoinMethod: joinMethod,
556+
Labels: token.GetSuggestedLabels(),
557+
LabelMatchers: token.GetSuggestedAgentMatcherLabels(),
558+
AppServiceEnabled: settings.appInstallMode,
559+
AppName: settings.appName,
560+
AppURI: settings.appURI,
561+
DatabaseServiceEnabled: settings.databaseInstallMode,
562+
DiscoveryServiceEnabled: settings.discoveryInstallMode,
563+
DiscoveryGroup: settings.discoveryGroup,
564+
}
565+
566+
return scripts.GetNodeInstallScript(ctx, nodeInstallOpts)
703567
}
704568

705569
// validateJoinToken validate a join token.
@@ -789,17 +653,3 @@ func isSameAzureRuleSet(r1, r2 []*types.ProvisionTokenSpecV2Azure_Rule) bool {
789653
sortAzureRules(r2)
790654
return reflect.DeepEqual(r1, r2)
791655
}
792-
793-
type nodeAPIGetter interface {
794-
// GetToken looks up a provisioning token.
795-
GetToken(ctx context.Context, token string) (types.ProvisionToken, error)
796-
797-
// GetClusterCACert returns the CAs for the local cluster without signing keys.
798-
GetClusterCACert(ctx context.Context) (*proto.GetClusterCACertResponse, error)
799-
800-
// GetProxies returns a list of registered proxies.
801-
GetProxies() ([]types.Server, error)
802-
}
803-
804-
// appURIPattern is a regexp excluding invalid characters from application URIs.
805-
var appURIPattern = regexp.MustCompile(`^[-\w/:. ]+$`)

0 commit comments

Comments
 (0)