diff --git a/lib/service/service.go b/lib/service/service.go index e2e76574b414d..7defc6e192fad 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -146,6 +146,7 @@ import ( "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/cert" vc "github.com/gravitational/teleport/lib/versioncontrol" + "github.com/gravitational/teleport/lib/versioncontrol/endpoint" uw "github.com/gravitational/teleport/lib/versioncontrol/upgradewindow" "github.com/gravitational/teleport/lib/web" ) @@ -1076,6 +1077,37 @@ func NewTeleport(cfg *servicecfg.Config) (*TeleportProcess, error) { process.log.Warnf("Use of external upgraders on control-plane instances is not recommended.") } + if upgraderKind == "unit" { + process.RegisterFunc("autoupdates.endpoint.export", func() error { + component := teleport.Component("autoupdates:endpoint:export", process.id) + logger := process.log.WithFields(logrus.Fields{ + trace.Component: component, + }) + conn, err := waitForInstanceConnector(process, logger) + if err != nil { + return trace.Wrap(err) + } + if conn == nil { + return trace.BadParameter("process exiting and Instance connector never became available") + } + + resp, err := conn.Client.Ping(process.ExitContext()) + if err != nil { + return trace.Wrap(err) + } + if !resp.GetServerFeatures().GetCloud() { + return nil + } + + if err := endpoint.Export(process.ExitContext(), resolverAddr.String()); err != nil { + logger.Warnf("Failed to export and validate autoupdates endpoint (addr=%s): %v", resolverAddr.String(), err) + return trace.Wrap(err) + } + logger.Infof("Exported autoupdates endpoint (addr=%s).", resolverAddr.String()) + return nil + }) + } + driver, err := uw.NewDriver(upgraderKind) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/versioncontrol/constants.go b/lib/versioncontrol/constants.go new file mode 100644 index 0000000000000..c5a5e7a59308b --- /dev/null +++ b/lib/versioncontrol/constants.go @@ -0,0 +1,24 @@ +/* + * Teleport + * Copyright (C) 2024 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 versioncontrol + +const ( + // UnitConfigDir is the configuration directory of the teleport-upgrade unit. + UnitConfigDir = "/etc/teleport-upgrade.d" +) diff --git a/lib/versioncontrol/endpoint/endpoint.go b/lib/versioncontrol/endpoint/endpoint.go new file mode 100644 index 0000000000000..41d451612a80c --- /dev/null +++ b/lib/versioncontrol/endpoint/endpoint.go @@ -0,0 +1,102 @@ +/* + * Teleport + * Copyright (C) 2024 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 endpoint + +import ( + "context" + "fmt" + "io" + "net/url" + "os" + "path" + "strings" + + "github.com/gravitational/trace" + + versionlib "github.com/gravitational/teleport/lib/automaticupgrades/version" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/versioncontrol" +) + +const stableCloudPath = "v1/webapi/automaticupgrades/channel/stable/cloud" + +// Export exports the proxy version server config. +func Export(ctx context.Context, proxyAddr string) error { + versionEndpoint := fmt.Sprint(path.Join(proxyAddr, stableCloudPath)) + if err := verifyVersionEndpoint(ctx, versionEndpoint); err != nil { + return trace.Wrap(err, "version endpoint may be invalid or unreachable") + } + + appliedEndpoint, err := exportEndpoint(versioncontrol.UnitConfigDir, versionEndpoint) + if err != nil { + return trace.Wrap(err, "failed to export version endpoint") + } + + if err := verifyVersionEndpoint(ctx, appliedEndpoint); err != nil { + return trace.Wrap(err, "applied version endpoint may be invalid or unreachable") + } + + return nil +} + +// verifyVersionEndpoint verifies that the provided endpoint serves a valid teleport +// version. +func verifyVersionEndpoint(ctx context.Context, endpoint string) error { + baseURL, err := url.Parse(fmt.Sprintf("https://%s", endpoint)) + if err != nil { + return trace.Wrap(err) + } + versionGetter := versionlib.NewBasicHTTPVersionGetter(baseURL) + _, err = versionGetter.GetVersion(ctx) + return trace.Wrap(err) +} + +// exportEndpoint exports the versionEndpoint to the specified config directory. +// If an existing value is already present, it will not be overwritten. The resulting +// version endpoint value will be returned. +func exportEndpoint(configDir, versionEndpoint string) (string, error) { + // ensure config dir exists. if created it is set to 755, which is reasonably safe and seems to + // be the standard choice for config dirs like this in /etc/. + if err := os.MkdirAll(configDir, defaults.DirectoryPermissions); err != nil { + return "", trace.Wrap(err) + } + + // open/create endpoint file. if created it is set to 644, which is reasonable for a sensitive but non-secret config value. + endpointFile, err := os.OpenFile(path.Join(configDir, "endpoint"), os.O_RDWR|os.O_CREATE, defaults.FilePermissions) + if err != nil { + return "", trace.Wrap(err, "failed to open endpoint config file") + } + defer endpointFile.Close() + + b, err := io.ReadAll(endpointFile) + if err != nil { + return "", trace.Wrap(err, "failed to read endpoint config file") + } + + // Do not overwrite if an endpoint value is already configured. + if len(b) != 0 { + return strings.TrimSuffix(string(b), "\n"), nil + } + + _, err = endpointFile.Write([]byte(versionEndpoint)) + if err != nil { + return "", trace.Wrap(err, "failed to write endpoint config file") + } + return versionEndpoint, nil +} diff --git a/lib/versioncontrol/endpoint/endpoint_test.go b/lib/versioncontrol/endpoint/endpoint_test.go new file mode 100644 index 0000000000000..be6fca21a3b68 --- /dev/null +++ b/lib/versioncontrol/endpoint/endpoint_test.go @@ -0,0 +1,91 @@ +/* + * Teleport + * Copyright (C) 2024 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 endpoint + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/require" +) + +const testDir = "export-endpoint-test" + +func Test_exportEndpoint(t *testing.T) { + tests := []struct { + name string + endpoint string + expected string + initConfigDir func() string + }{ + { + name: "create endpoint file and write value", + endpoint: "v1/stable/cloud", + expected: "v1/stable/cloud", + initConfigDir: func() string { + tmpDir, err := os.MkdirTemp("", testDir) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + return tmpDir + }, + }, + { + name: "write value", + endpoint: "v1/stable/cloud", + expected: "v1/stable/cloud", + initConfigDir: func() string { + tmpDir, err := os.MkdirTemp("", testDir) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + + endpointFile, err := os.Create(path.Join(tmpDir, "endpoint")) + require.NoError(t, err) + require.NoError(t, endpointFile.Close()) + return tmpDir + }, + }, + { + name: "endpoint value already configured", + endpoint: "v1/stable/cloud", + expected: "existing/endpoint", + initConfigDir: func() string { + tmpDir, err := os.MkdirTemp("", testDir) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + + endpointFile, err := os.Create(path.Join(tmpDir, "endpoint")) + require.NoError(t, err) + + _, err = endpointFile.Write([]byte("existing/endpoint")) + require.NoError(t, err) + require.NoError(t, endpointFile.Close()) + return tmpDir + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configDir := tt.initConfigDir() + appliedEndpoint, err := exportEndpoint(configDir, tt.endpoint) + require.NoError(t, err) + require.Equal(t, tt.expected, appliedEndpoint) + }) + } +} diff --git a/lib/versioncontrol/upgradewindow/upgradewindow.go b/lib/versioncontrol/upgradewindow/upgradewindow.go index 51a05a7cd5b93..53c2905a6dccd 100644 --- a/lib/versioncontrol/upgradewindow/upgradewindow.go +++ b/lib/versioncontrol/upgradewindow/upgradewindow.go @@ -34,6 +34,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/interval" + "github.com/gravitational/teleport/lib/versioncontrol" ) const ( @@ -42,9 +43,6 @@ const ( // unitScheduleFile is the name of the file to which the unit schedule is exported. unitScheduleFile = "schedule" - - // unitConfigDir is the configuration directory of the teleport-upgrade unit. - unitConfigDir = "/etc/teleport-upgrade.d" ) // ExportFunc represents the ExportUpgradeWindows rpc exposed by auth servers. @@ -397,7 +395,7 @@ type systemdDriver struct { func NewSystemdUnitDriver(cfg SystemdUnitDriverConfig) (Driver, error) { if cfg.ConfigDir == "" { - cfg.ConfigDir = unitConfigDir + cfg.ConfigDir = versioncontrol.UnitConfigDir } return &systemdDriver{cfg: cfg}, nil