From 009f54bdda5e99caec34120c42cba2066ed1217a Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 10 Jan 2025 14:27:30 -0300 Subject: [PATCH] Changes to migrate to new TUF repository (#23588) # Changes - orbit >= 1.38.0, when configured to connect to https://tuf.fleetctl.com (existing fleetd deployments) will now connect to https://updates.fleetdm.com and start using the metadata in path `/opt/orbit/updates-metadata.json`. - orbit >= 1.38.0, when configured to connect to some custom TUF (not Fleet's TUFs) will copy `/opt/orbit/tuf-metadata.json` to `/opt/orbit/updates-metadata.json` (if it doesn't exist) and start using the latter. - fleetctl `4.63.0` will now generate artifacts using https://updates.fleetdm.com by default (or a custom TUF if `--update-url` is set) and generate two (same file) metadata files `/opt/orbit/updates-metadata.json` and the legacy one to support downgrades `/opt/orbit/tuf-metadata.json`. - fleetctl `4.62.0` when configured to use custom TUF (not Fleet's TUF) will generate just the legacy metadata file `/opt/orbit/tuf-metadata.json`. ## User stories See "User stories" in https://github.com/fleetdm/confidential/issues/8488. - [x] Update `update.defaultRootMetadata` and `update.DefaultURL` when the new repository is ready. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [X] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --- .github/workflows/check-automated-doc.yml | 6 +- cmd/fleetctl/package.go | 3 +- cmd/fleetctl/preview.go | 2 +- orbit/changes/8488-new-tuf-repository | 1 + orbit/cmd/orbit/orbit.go | 65 ++- orbit/cmd/orbit/shell.go | 2 +- orbit/pkg/packaging/packaging.go | 13 +- orbit/pkg/update/options_darwin.go | 4 +- orbit/pkg/update/options_linux_amd64.go | 4 +- orbit/pkg/update/options_linux_arm64.go | 4 +- orbit/pkg/update/options_windows.go | 4 +- orbit/pkg/update/runner.go | 20 + orbit/pkg/update/update.go | 183 +++++-- tools/tuf/README.md | 18 +- tools/tuf/migrate/README.md | 22 + tools/tuf/migrate/migrate.go | 207 +++++++ tools/tuf/releaser.sh | 42 +- tools/tuf/test/README.md | 3 +- tools/tuf/test/main.sh | 16 +- tools/tuf/test/migration/README.md | 21 + tools/tuf/test/migration/migration_test.sh | 597 +++++++++++++++++++++ tools/tuf/test/push_target.sh | 6 +- tools/tuf/test/run_server.sh | 12 +- 23 files changed, 1149 insertions(+), 106 deletions(-) create mode 100644 orbit/changes/8488-new-tuf-repository create mode 100644 tools/tuf/migrate/README.md create mode 100644 tools/tuf/migrate/migrate.go create mode 100644 tools/tuf/test/migration/README.md create mode 100755 tools/tuf/test/migration/migration_test.sh diff --git a/.github/workflows/check-automated-doc.yml b/.github/workflows/check-automated-doc.yml index d289c55318de..a3a638bb4f2a 100644 --- a/.github/workflows/check-automated-doc.yml +++ b/.github/workflows/check-automated-doc.yml @@ -51,7 +51,8 @@ jobs: make generate-doc if [[ $(git diff) ]]; then echo "❌ fail: uncommited changes" - echo "please run `make generate-doc` and commit the changes" + echo "please run 'make generate-doc' and commit the changes" + git --no-pager diff exit 1 fi @@ -62,6 +63,7 @@ jobs: ./node_modules/sails/bin/sails.js run generate-merged-schema if [[ $(git diff) ]]; then echo "❌ fail: uncommited changes" - echo "please run `cd website && npm install && ./node_modules/sails/bin/sails.js run generate-merged-schema` and commit the changes" + echo "please run 'cd website && npm install && ./node_modules/sails/bin/sails.js run generate-merged-schema' and commit the changes" + git --no-pager diff exit 1 fi diff --git a/cmd/fleetctl/package.go b/cmd/fleetctl/package.go index 281cec25e9a5..271203390693 100644 --- a/cmd/fleetctl/package.go +++ b/cmd/fleetctl/package.go @@ -13,6 +13,7 @@ import ( eefleetctl "github.com/fleetdm/fleet/v4/ee/fleetctl" "github.com/fleetdm/fleet/v4/orbit/pkg/packaging" + "github.com/fleetdm/fleet/v4/orbit/pkg/update" "github.com/fleetdm/fleet/v4/pkg/filepath_windows" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/rs/zerolog" @@ -127,7 +128,7 @@ func packageCommand() *cli.Command { &cli.StringFlag{ Name: "update-url", Usage: "URL for update server", - Value: "https://tuf.fleetctl.com", + Value: update.DefaultURL, Destination: &opt.UpdateURL, }, &cli.StringFlag{ diff --git a/cmd/fleetctl/preview.go b/cmd/fleetctl/preview.go index 4cbc5a5d6c4c..e4286d940dc0 100644 --- a/cmd/fleetctl/preview.go +++ b/cmd/fleetctl/preview.go @@ -762,7 +762,7 @@ func previewResetCommand() *cli.Command { return fmt.Errorf("Failed to stop orbit: %w", err) } - if err := os.RemoveAll(filepath.Join(orbitDir, "tuf-metadata.json")); err != nil { + if err := os.RemoveAll(filepath.Join(orbitDir, update.MetadataFileName)); err != nil { return fmt.Errorf("failed to remove preview update metadata file: %w", err) } if err := os.RemoveAll(filepath.Join(orbitDir, "bin")); err != nil { diff --git a/orbit/changes/8488-new-tuf-repository b/orbit/changes/8488-new-tuf-repository new file mode 100644 index 000000000000..6b054b047f27 --- /dev/null +++ b/orbit/changes/8488-new-tuf-repository @@ -0,0 +1 @@ +* Added changes to migrate to new TUF repository from https://tuf.fleetctl.com to https://updates.fleetdm.com. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 2cf8138045fd..c8a561ad2c1e 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -471,7 +471,26 @@ func main() { } } - localStore, err := filestore.New(filepath.Join(c.String("root-dir"), "tuf-metadata.json")) + if updateURL := c.String("update-url"); updateURL != update.OldFleetTUFURL && updateURL != update.DefaultURL { + // Migrate agents running with a custom TUF to use the new metadata file. + // We'll keep the old metadata file to support downgrades. + newMetadataFilePath := filepath.Join(c.String("root-dir"), update.MetadataFileName) + ok, err := file.Exists(newMetadataFilePath) + if err != nil { + // If we cannot stat this file then we cannot do other operations on it thus we fail with fatal error. + log.Fatal().Err(err).Msg("failed to check for new metadata file path") + } + if !ok { + oldMetadataFilePath := filepath.Join(c.String("root-dir"), update.OldMetadataFileName) + err := file.Copy(oldMetadataFilePath, newMetadataFilePath, constant.DefaultFileMode) + if err != nil { + // If we cannot write to this file then we cannot do other operations on it thus we fail with fatal error. + log.Fatal().Err(err).Msg("failed to copy new metadata file path") + } + } + } + + localStore, err := filestore.New(filepath.Join(c.String("root-dir"), update.MetadataFileName)) if err != nil { log.Fatal().Err(err).Msg("create local metadata store") } @@ -503,6 +522,29 @@ func main() { opt.RootDirectory = c.String("root-dir") opt.ServerURL = c.String("update-url") + checkAccessToNewTUF := false + if opt.ServerURL == update.OldFleetTUFURL { + // + // This only gets executed on orbit 1.38.0+ + // when it is configured to connect to the old TUF server + // (fleetd instances packaged before the migration, + // built by fleetctl previous to v4.63.0). + // + + if ok := update.HasAccessToNewTUFServer(opt); ok { + // orbit 1.38.0+ will use the new TUF server if it has access to the new TUF repository. + opt.ServerURL = update.DefaultURL + } else { + // orbit 1.38.0+ will use the old TUF server and old metadata path if it does not have access + // to the new TUF repository. During its execution (update.Runner) it will exit once it finds + // out it can access the new TUF server. + localStore, err = filestore.New(filepath.Join(c.String("root-dir"), update.OldMetadataFileName)) + if err != nil { + log.Fatal().Err(err).Msg("create local old metadata store") + } + checkAccessToNewTUF = true + } + } opt.LocalStore = localStore opt.InsecureTransport = c.Bool("insecure") opt.ServerCertificatePath = c.String("update-tls-certificate") @@ -545,13 +587,12 @@ func main() { var updater *update.Updater var updateRunner *update.Runner if !c.Bool("disable-updates") || c.Bool("dev-mode") { - updater, err = update.NewUpdater(opt) + updater, err := update.NewUpdater(opt) if err != nil { return fmt.Errorf("create updater: %w", err) } - if err := updater.UpdateMetadata(); err != nil { - log.Info().Err(err).Msg("update metadata, using saved metadata") + log.Info().Err(err).Msg("update metadata") } signaturesExpiredAtStartup := updater.SignaturesExpired() @@ -571,6 +612,7 @@ func main() { CheckInterval: c.Duration("update-interval"), Targets: targets, SignaturesExpiredAtStartup: signaturesExpiredAtStartup, + CheckAccessToNewTUF: checkAccessToNewTUF, }) if err != nil { return err @@ -1394,10 +1436,17 @@ func getFleetdComponentPaths( log.Error().Err(err).Msg("update metadata before getting components") } - // "root", "targets", or "snapshot" signatures have expired, thus - // we attempt to get local paths for the targets (updater.Get will fail - // because of the expired signatures). - if updater.SignaturesExpired() { + // + // updater.SignaturesExpired(): + // "root", "targets", or "snapshot" signatures have expired, thus + // we attempt to get local paths for the targets (updater.Get will fail + // because of the expired signatures). + // + // updater.LookupsFail(): + // Any of the targets fails to load thus we resort to the local executables we have. + // This could happen if the new TUF server is down during the first run of the TUF migration. + // + if updater.SignaturesExpired() || updater.LookupsFail() { log.Error().Err(err).Msg("expired metadata, using local targets") // Attempt to get local path of osqueryd. diff --git a/orbit/cmd/orbit/shell.go b/orbit/cmd/orbit/shell.go index 5acf521122ad..7a7d0900f71d 100644 --- a/orbit/cmd/orbit/shell.go +++ b/orbit/cmd/orbit/shell.go @@ -47,7 +47,7 @@ var shellCommand = &cli.Command{ return fmt.Errorf("initialize root dir: %w", err) } - localStore, err := filestore.New(filepath.Join(c.String("root-dir"), "tuf-metadata.json")) + localStore, err := filestore.New(filepath.Join(c.String("root-dir"), update.MetadataFileName)) if err != nil { log.Fatal().Err(err).Msg("failed to create local metadata store") } diff --git a/orbit/pkg/packaging/packaging.go b/orbit/pkg/packaging/packaging.go index 39ba97e4d880..279da1dc5e71 100644 --- a/orbit/pkg/packaging/packaging.go +++ b/orbit/pkg/packaging/packaging.go @@ -172,7 +172,7 @@ func (u UpdatesData) String() string { } func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) { - localStore, err := filestore.New(filepath.Join(updateOpt.RootDirectory, "tuf-metadata.json")) + localStore, err := filestore.New(filepath.Join(updateOpt.RootDirectory, update.MetadataFileName)) if err != nil { return nil, fmt.Errorf("failed to create local metadata store: %w", err) } @@ -236,6 +236,17 @@ func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) { } } + // Copy the new metadata file to the old location (pre-migration) to + // support orbit downgrades to 1.37.0 or lower. + // + // Once https://tuf.fleetctl.com is brought down (which means downgrades to 1.37.0 or + // lower won't be possible), we can remove this copy. + oldMetadataPath := filepath.Join(updateOpt.RootDirectory, update.OldMetadataFileName) + newMetadataPath := filepath.Join(updateOpt.RootDirectory, update.MetadataFileName) + if err := file.Copy(newMetadataPath, oldMetadataPath, constant.DefaultFileMode); err != nil { + return nil, fmt.Errorf("failed to create %s copy: %w", oldMetadataPath, err) + } + return &UpdatesData{ OrbitPath: orbitPath, OrbitVersion: orbitCustom.Version, diff --git a/orbit/pkg/update/options_darwin.go b/orbit/pkg/update/options_darwin.go index 54f243b64685..1d0c1365cab9 100644 --- a/orbit/pkg/update/options_darwin.go +++ b/orbit/pkg/update/options_darwin.go @@ -6,8 +6,8 @@ import ( var defaultOptions = Options{ RootDirectory: "/opt/orbit", - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: DarwinTargets, diff --git a/orbit/pkg/update/options_linux_amd64.go b/orbit/pkg/update/options_linux_amd64.go index 69e002e7c103..c3ab3b3089a6 100644 --- a/orbit/pkg/update/options_linux_amd64.go +++ b/orbit/pkg/update/options_linux_amd64.go @@ -6,8 +6,8 @@ import ( var defaultOptions = Options{ RootDirectory: "/opt/orbit", - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: LinuxTargets, diff --git a/orbit/pkg/update/options_linux_arm64.go b/orbit/pkg/update/options_linux_arm64.go index 5ed37667ddcc..aa431f2662b1 100644 --- a/orbit/pkg/update/options_linux_arm64.go +++ b/orbit/pkg/update/options_linux_arm64.go @@ -6,8 +6,8 @@ import ( var defaultOptions = Options{ RootDirectory: "/opt/orbit", - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: LinuxArm64Targets, diff --git a/orbit/pkg/update/options_windows.go b/orbit/pkg/update/options_windows.go index efee81416ebd..98a63b71ab67 100644 --- a/orbit/pkg/update/options_windows.go +++ b/orbit/pkg/update/options_windows.go @@ -9,8 +9,8 @@ import ( var defaultOptions = Options{ RootDirectory: `C:\Program Files\Orbit`, - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: WindowsTargets, diff --git a/orbit/pkg/update/runner.go b/orbit/pkg/update/runner.go index ae30f9c81cf2..498451e1d16a 100644 --- a/orbit/pkg/update/runner.go +++ b/orbit/pkg/update/runner.go @@ -39,6 +39,11 @@ type RunnerOptions struct { // An expired signature for the "timestamp" role does not cause issues // at start up (the go-tuf libary allows loading the targets). SignaturesExpiredAtStartup bool + + // CheckAccessToNewTUF, if set to true, will perform a check of access to the new Fleet TUF + // server on every update interval (once the access is confirmed it will store the confirmation + // of access to disk and will exit to restart). + CheckAccessToNewTUF bool } // Runner is a specialized runner for an Updater. It is designed with Execute and @@ -121,6 +126,14 @@ func NewRunner(updater *Updater, opt RunnerOptions) (*Runner, error) { return runner, nil } + if _, err := updater.Lookup(constant.OrbitTUFTargetName); errors.Is(err, client.ErrNoLocalSnapshot) { + // Return early and skip optimization, this will cause an unnecessary auto-update of orbit + // but allows orbit to start up if there's no local metadata AND if the TUF server is down + // (which may be the case during the migration from https://tuf.fleetctl.com to + // https://updates.fleetdm.com). + return runner, nil + } + // Initialize the hashes of the local files for all tracked targets. // // This is an optimization to not compute the hash of the local files every opt.CheckInterval @@ -204,6 +217,13 @@ func (r *Runner) Execute() error { case <-ticker.C: ticker.Reset(r.opt.CheckInterval) + if r.opt.CheckAccessToNewTUF { + if HasAccessToNewTUFServer(r.updater.opt) { + log.Info().Msg("detected access to new TUF repository, exiting") + return nil + } + } + if r.opt.SignaturesExpiredAtStartup { if r.updater.SignaturesExpired() { log.Debug().Msg("signatures still expired") diff --git a/orbit/pkg/update/update.go b/orbit/pkg/update/update.go index 51277c9d429d..79ce594f0792 100644 --- a/orbit/pkg/update/update.go +++ b/orbit/pkg/update/update.go @@ -21,7 +21,9 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/build" "github.com/fleetdm/fleet/v4/orbit/pkg/constant" "github.com/fleetdm/fleet/v4/orbit/pkg/platform" + "github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore" "github.com/fleetdm/fleet/v4/pkg/certificate" + "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/pkg/retry" "github.com/fleetdm/fleet/v4/pkg/secure" @@ -34,9 +36,32 @@ import ( const ( binDir = "bin" stagingDir = "staging" +) + +const ( + // + // For users using Fleet's TUF: + // - orbit 1.38.0+ we migrate TUF from https://tuf.fleetctl.com to https://updates.fleetdm.com. + // - orbit 1.38.0+ will start using `updates-metadata.json` instead of `tuf-metadata.json`. If it is missing + // (which will be the case for the first run after the auto-update) then it will generate it from the new pinned roots. + // + // For users using a custom TUF: + // - orbit 1.38.0+ will start using `updates-metadata.json` instead of `tuf-metadata.json` (if it is missing then + // it will perform a copy). + // + // For both Fleet's TUF and custom TUF, fleetd packages built with fleetctl 4.63.0+ will contain both files + // `updates-metadata.json` and `tuf-metadata.json` (same content) to support downgrades to orbit 1.37.0 or lower. - defaultURL = "https://tuf.fleetctl.com" - defaultRootKeys = `{"signed":{"_type":"root","spec_version":"1.0","version":4,"expires":"2024-10-06T17:47:49Z","keys":{"0cd79ade57d278957069e03a0fca6b975b95c2895fb20bdc3075f71fc19a4474":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"4627d9071a4b4a78c5ee867ea70439583b08dbe4ff23514e3bcb0a292de9406f"}},"1a4d9beb826d1ff4e036d757cfcd6e36d0f041e58d25f99ef3a20ae3f8dd71e3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"1083b5fedbcaf8f98163f2f7083bbb2761a334b2ba8de44df7be3feb846725f6"}},"3c1fbd1f3b3429d8ccadfb1abfbae5826d0cf74b0a6bcd384c3045d2fe27613c":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b07555d05d4260410bdf12de7f76be905e288e801071877c7ca3d7f0459bee0f"}},"5003eae9f614f7e2a6c94167d20803eabffc6f65b8731e828e56d068f1b1d834":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"5d91bdfddc381e03d109e3e2f08413ed4ba181d98766eb97802967fb6cf2b87d"}},"5b99d0520321d0177d66b84136f3fc800dde1d36a501c12e28aa12d59a239315":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8113955a28517752982ed6521e2162cf207605cfd316b8cba637c0a9b7a72856"}},"5f42172605e1a317c6bdae3891b4312a952759185941490624e052889821c929":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"86e26b13b9a64f7de4ad24b47e2bb9779a8628cae0e1afa61e56f2003c2ab586"}},"6c0e404295d4bf8915b46754b5f4546ab0d11ff7d83804d4aa2d178cfc38eafc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"3782135dcec329bcd0e1eefc1acead987dc6a7d629db62c9fdde8bc8ff3fa781"}},"7cbbc9772d4d6acea33b7edf5a4bc52c85ff283475d428ffee73f9dbd0f62c89":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"f79d0d357aaa534a251abc7b0604725ba7b035eb53d1bdf5cc3173d73e3d9678"}},"7ea5cd46d58ac97ec1424007b7a6b0b3403308bb8aa8de885a75841f6f1d50dd":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"978cdddce95311d56b7fed39419a31019a38a1dab179cddb541ffaf99f442f1b"}},"94ca5921eb097bb871272c1cc3ea2cad833cb8d4c2dea4a826646be656059640":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6512498c7596f55a23405889539fadbcefecd0909e4af0b54e29f45d49f9b9f7"}},"ae943cb8be8a849b37c66ed46bdd7e905ba3118c0c051a6ee3cd30625855a076":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"e7ffa6355dedd0cd34defc903dfac05a7a8c1855d63be24cecb5577cfde1f990"}},"d940df08b59b12c30f95622a05cc40164b78a11dd7d408395ee4f79773331b30":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"64d15cc3cbaac7eccfd9e0de5a56a0789aadfec3d02e77bf9180b8090a2c48d6"}},"efb4e9bd7a7d9e045edf6f5140c9835dbcbb7770850da44bf15a800b248c810e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"0b8b28b30b44ddb733c7457a7c0f75fcbac563208ea1fe7179b5888a4f1d2237"}}},"roles":{"root":{"keyids":["5f42172605e1a317c6bdae3891b4312a952759185941490624e052889821c929"],"threshold":1},"snapshot":{"keyids":["94ca5921eb097bb871272c1cc3ea2cad833cb8d4c2dea4a826646be656059640","1a4d9beb826d1ff4e036d757cfcd6e36d0f041e58d25f99ef3a20ae3f8dd71e3","7ea5cd46d58ac97ec1424007b7a6b0b3403308bb8aa8de885a75841f6f1d50dd","5003eae9f614f7e2a6c94167d20803eabffc6f65b8731e828e56d068f1b1d834"],"threshold":1},"targets":{"keyids":["0cd79ade57d278957069e03a0fca6b975b95c2895fb20bdc3075f71fc19a4474","ae943cb8be8a849b37c66ed46bdd7e905ba3118c0c051a6ee3cd30625855a076","6c0e404295d4bf8915b46754b5f4546ab0d11ff7d83804d4aa2d178cfc38eafc","3c1fbd1f3b3429d8ccadfb1abfbae5826d0cf74b0a6bcd384c3045d2fe27613c"],"threshold":1},"timestamp":{"keyids":["efb4e9bd7a7d9e045edf6f5140c9835dbcbb7770850da44bf15a800b248c810e","d940df08b59b12c30f95622a05cc40164b78a11dd7d408395ee4f79773331b30","7cbbc9772d4d6acea33b7edf5a4bc52c85ff283475d428ffee73f9dbd0f62c89","5b99d0520321d0177d66b84136f3fc800dde1d36a501c12e28aa12d59a239315"],"threshold":1}},"consistent_snapshot":false},"signatures":[{"keyid":"39a1db745ca254d8f8eb27493df8867264d9fb394572ecee76876f4d7e9cb753","sig":"841a44dcd98bbd78727f0b4b2a6e7dbb6d54e8469ca14965c9c5f9f7bb792dfe792f05e90a2724c75e966c007928ff7e7809de4608aab0bd27771f7b049c230f"},{"keyid":"5f42172605e1a317c6bdae3891b4312a952759185941490624e052889821c929","sig":"f6a16446edbbb632521649d21c2188b11eafeacb826caf2b8f3e2b8e9a343b573bca0a786c16ed2aeade25471c6d5103aac810ee05c50b044acd98d4b31d190c"},{"keyid":"63b4cb9241c93bca9218c67334da3651394de4cf36c44bb1320bad7111df7bba","sig":"62b5effddc00f7c9c06f4227cc1bfd4c09c47326a6c388451df28af798386d0e8d93412850bcc55f89147f439b5511bb63581ad09cd9ca215f72086348f9260b"},{"keyid":"656c44011cf8b80a4da765bec1516ee9598ffca5fa7ccb51f0a9feb04e6e6cbd","sig":"7b786c3825b206ed0c43fdfc852ebc5d363f7547a2f4940965c4c3eb89a8be069a5eddc942f8e796e645eea76b321dbbafc7f4c8d153181070da84d7a39bbe03"},{"keyid":"950097b911794bb554d7e83aa20c8aad11efcdc98f54b775fda76ac39eafa8fb","sig":"14e281d44c3384928e80a458214e4773f6c6c068a8d53e7458e8615fa5d1fe8f3daff11f736bec614cdba9e62d6f43850c6746cf2af7615445703af3ddeddb03"},{"keyid":"d6e90309d70431729bf722b089a8049efaf449230d94dc90bafa1cfc12d2b36f","sig":"bb7278ba1affc0c2bcbd952b7678ffa95268722121011df9ac18c19c1901e9c17ee3a1048a8471ca7c833ce86ecb054dc446c1ae473f118c1dc81a6e9b1dfb04"},{"keyid":"e5d1873c4d5268f650a26ea3c6ffb4bec1e523875888ebb6303fac2bfd578cd0","sig":"82019b8aba472b25f90899944db0ce94fd4ae1314f6336e2828bb30d592a9e3e34e6a66a75b1d310e0e85119a826bff345b99fe8647515057315da32e9847b04"}]}` + // + // The following variables are used by `fleetctl package` and by orbit in the migration to the new TUF repository. + // + + DefaultURL = `https://updates.fleetdm.com` + MetadataFileName = "updates-metadata.json" + defaultRootMetadata = `{"signed":{"_type":"root","spec_version":"1.0","version":6,"expires":"2026-01-08T22:23:47Z","keys":{"13f738181c9c50798d66316afaccf117658481b4db7b38715c358f522dc3bc40":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"34684874493cce2ac307c0dca88c241a287180c3eec9225c5f3e29bc4aeae080"}},"1ab0b9598e8b6ea653a24112f69b3eb3a84152c6a73d8dfdf43de4f63a93d3af":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"47a38623303bbe7b4ce47948011042b9387d7ec184642c156e06459d8fb6411b"}},"216e2dae905e73df943e003c428c7d340f21280750edb9d9ce2b15beeada667a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"5c73ff497dc14380861892706c23bba0e3272c77c7f6f9524238b3a6b808b844"}},"2a83f45d24101ba91f669dca42981f97fc8bcde7cdf29c1887bc55c485103c49":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"e0bae1fae56d87e7f30f6359fd0a8cbfed231b19f58882e155c091c3bdb13c40"}},"3bc9408c1bcd999e69ba56b39e747656c6ebdafbd1e2c3e378c53e96e4477a64":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8899adaa7ccd5bceb6a8c5f552fe4a9e59eb67e2a364db6638e06bbcf6f6eaeb"}},"4c0a5f49dc9665f13329d8157a2184985bd183503990469d06e32ad1bd6e70ee":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"eceeea79c6a353f5c7ed3be552a6144458ecf5fe78972eba88a77a45a230c58b"}},"57227c64d19605636d0afbab41d0887455de4287c6f328c5f69650005f793de0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d962cdf1d3e974f6c2b3d602260c87e0647fd54372afe7c31238f26a56a75443"}},"61e70c06858064c5e33e5009734222000610013e26fb6846ee17f55ddfb22da3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"7e42b715cd9eedd8252a6b715fcfb8ef789531782ed19027a3c2ae11a2b0243b"}},"79a257e77793cb26d5d0cc0af6b2d2c94e7e8ca8b875dc504eb10fb343753f94":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"93409c7be4e3942ecff111d36cd097cda5778cb4f53305a07f20855b08f26071"}},"868858a9723ce357e8e251617ae11f7d3ae8a348588872cb2ce4149ee70ba155":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"f91c5b1fcb4ed3a1a65b956fa9025a89f458cea9036259b8cdfa276bc04faf45"}},"91629787db6e18b226027587733b2f667a7982eed9509c2e39dfeaf4cfb1a17a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"3e2ac750e2e0eb22f87f35ad5309932b7b081c40891d249493fa9e2cef28066b"}},"97b3353aa23d09f88323e63cdc587a368df0a8818da67b91720b2cab00e68297":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6b92f54f51eb617069a963a41aed75b4a23fca45e4c9ca8fc6d748d9b58b0451"}},"bc711f19576de2d71d1ca41eeccf7412f12c3ee4971185cd69066f8dc51d1ce6":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"7e39afe9a0310e7645ed389f243dc7156069d6972505cfbb460f8147949343cd"}},"c1ce9675f7302d2f09514f78ec7b3bdc00758d69b659e00c1c6731a4d0836bb9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a97c44dc10ee979ead46beb3be22f3407238e72b68240a41f92e65051eb27cb1"}}},"roles":{"root":{"keyids":["bc711f19576de2d71d1ca41eeccf7412f12c3ee4971185cd69066f8dc51d1ce6","4c0a5f49dc9665f13329d8157a2184985bd183503990469d06e32ad1bd6e70ee","57227c64d19605636d0afbab41d0887455de4287c6f328c5f69650005f793de0"],"threshold":1},"snapshot":{"keyids":["1ab0b9598e8b6ea653a24112f69b3eb3a84152c6a73d8dfdf43de4f63a93d3af","868858a9723ce357e8e251617ae11f7d3ae8a348588872cb2ce4149ee70ba155","97b3353aa23d09f88323e63cdc587a368df0a8818da67b91720b2cab00e68297"],"threshold":1},"targets":{"keyids":["3bc9408c1bcd999e69ba56b39e747656c6ebdafbd1e2c3e378c53e96e4477a64","c1ce9675f7302d2f09514f78ec7b3bdc00758d69b659e00c1c6731a4d0836bb9","2a83f45d24101ba91f669dca42981f97fc8bcde7cdf29c1887bc55c485103c49"],"threshold":1},"timestamp":{"keyids":["13f738181c9c50798d66316afaccf117658481b4db7b38715c358f522dc3bc40","79a257e77793cb26d5d0cc0af6b2d2c94e7e8ca8b875dc504eb10fb343753f94","91629787db6e18b226027587733b2f667a7982eed9509c2e39dfeaf4cfb1a17a","61e70c06858064c5e33e5009734222000610013e26fb6846ee17f55ddfb22da3","216e2dae905e73df943e003c428c7d340f21280750edb9d9ce2b15beeada667a"],"threshold":1}},"consistent_snapshot":false},"signatures":[{"keyid":"bc711f19576de2d71d1ca41eeccf7412f12c3ee4971185cd69066f8dc51d1ce6","sig":"7a0d8eda3e6058bf10f21bdda4b876c499b4182335ab943737c2121603d0e2ec707222e92eace3d10051264988705d9c51e2159d13e234d57441e60ba1a3c10a"}]}` + + OldFleetTUFURL = "https://tuf.fleetctl.com" + OldMetadataFileName = "tuf-metadata.json" ) // Updater is responsible for managing update state. @@ -122,32 +147,10 @@ func NewUpdater(opt Options) (*Updater, error) { return nil, errors.New("opt.LocalStore must be non-nil") } - tlsConfig := &tls.Config{ - InsecureSkipVerify: opt.InsecureTransport, - } - - if opt.ServerCertificatePath != "" { - rootCAs, err := certificate.LoadPEM(opt.ServerCertificatePath) - if err != nil { - return nil, fmt.Errorf("loading server root CA: %w", err) - } - tlsConfig.RootCAs = rootCAs - } - - if opt.ClientCertificate != nil { - tlsConfig.Certificates = []tls.Certificate{*opt.ClientCertificate} - } - - httpClient := fleethttp.NewClient(fleethttp.WithTLSClientConfig(tlsConfig)) - - remoteOpt := &client.HTTPRemoteOptions{ - UserAgent: fmt.Sprintf("orbit/%s (%s %s)", build.Version, runtime.GOOS, runtime.GOARCH), - } - remoteStore, err := client.HTTPRemoteStore(opt.ServerURL, remoteOpt, httpClient) + remoteStore, err := createTUFRemoteStore(opt, opt.ServerURL) if err != nil { - return nil, fmt.Errorf("init remote store: %w", err) + return nil, fmt.Errorf("get tls config: %w", err) } - tufClient := client.NewClient(opt.LocalStore, remoteStore) // TODO(lucas): Related to the NOTE below. @@ -164,10 +167,13 @@ func NewUpdater(opt Options) (*Updater, error) { return nil, fmt.Errorf("read metadata: %w", err) } if meta["root.json"] == nil { - // NOTE: This path is currently only used when (1) packaging Orbit (`fleetctl package`) and - // (2) in the edge-case when Orbit's metadata JSON local file is removed for some reason. - // When edge-case (2) happens, Orbit will attempt to use Fleet DM's root JSON + // NOTE: This path is currently only used when (1) packaging Orbit (`fleetctl package`), or + // (2) in the edge-case when orbit's metadata JSON local file is removed for some reason, or + // (3) first run on TUF migration from https://tuf.fleetctl.com to https://updates.fleetdm.com. + // + // When edge-case (2) happens, orbit will attempt to use Fleet DM's root JSON // (which may be unexpected on custom TUF Orbit deployments). + log.Info().Msg("initialize TUF from embedded root keys") if err := tufClient.Init([]byte(opt.RootKeys)); err != nil { return nil, fmt.Errorf("client init with configuration metadata: %w", err) } @@ -203,7 +209,7 @@ func NewDisabled(opt Options) *Updater { // UpdateMetadata downloads and verifies remote repository metadata. func (u *Updater) UpdateMetadata() error { if _, err := u.client.Update(); err != nil { - return fmt.Errorf("update metadata: %w", err) + return fmt.Errorf("client update: %w", err) } return nil } @@ -225,6 +231,16 @@ func (u *Updater) SignaturesExpired() bool { return IsExpiredErr(err) } +// LookupsFail returns true if lookups are failing for any of the targets. +func (u *Updater) LookupsFail() bool { + for target := range u.opt.Targets { + if _, err := u.Lookup(target); err != nil { + return true + } + } + return false +} + // repoPath returns the path of the target in the remote repository. func (u *Updater) repoPath(target string) (string, error) { u.mu.Lock() @@ -736,3 +752,110 @@ func CanRun(rootDirPath, targetName string, targetInfo TargetInfo) bool { return true } + +// HasAccessToNewTUFServer will verify if the agent has access to Fleet's new TUF +// by downloading the metadata and the orbit stable target. +// +// The metadata and the test target files will be downloaded to a temporary directory +// that will be removed before this method returns. +func HasAccessToNewTUFServer(opt Options) bool { + fp := filepath.Join(opt.RootDirectory, "new-tuf-checked") + ok, err := file.Exists(fp) + if err != nil { + log.Error().Err(err).Msg("failed to check new-tuf-checked file exists") + return false + } + if ok { + return true + } + tmpDir, err := os.MkdirTemp(opt.RootDirectory, "tuf-tmp*") + if err != nil { + log.Error().Err(err).Msg("failed to create tuf-tmp directory") + return false + } + defer os.RemoveAll(tmpDir) + localStore, err := filestore.New(filepath.Join(tmpDir, "tuf-tmp.json")) + if err != nil { + log.Error().Err(err).Msg("failed to create tuf-tmp local store") + return false + } + remoteStore, err := createTUFRemoteStore(opt, DefaultURL) + if err != nil { + log.Error().Err(err).Msg("failed to create TUF remote store") + return false + } + tufClient := client.NewClient(localStore, remoteStore) + if err := tufClient.Init([]byte(opt.RootKeys)); err != nil { + log.Error().Err(err).Msg("failed to pin root keys") + return false + } + if _, err := tufClient.Update(); err != nil { + // Logging as debug to not fill logs until users allow connections to new TUF server. + log.Debug().Err(err).Msg("failed to update metadata from new TUF") + return false + } + tmpFile, err := secure.OpenFile( + filepath.Join(tmpDir, "orbit"), + os.O_CREATE|os.O_WRONLY, + constant.DefaultFileMode, + ) + if err != nil { + log.Error().Err(err).Msg("failed open temp file for download") + return false + } + defer tmpFile.Close() + // We are using the orbit stable target as the test target. + var ( + platform string + executable string + ) + switch runtime.GOOS { + case "darwin": + platform = "macos" + executable = "orbit" + case "windows": + platform = "windows" + executable = "orbit.exe" + case "linux": + platform = "linux" + executable = "orbit" + } + if err := tufClient.Download(fmt.Sprintf("orbit/%s/stable/%s", platform, executable), &fileDestination{tmpFile}); err != nil { + // Logging as debug to not fill logs until users allow connections to new TUF server. + log.Debug().Err(err).Msg("failed to download orbit from TUF") + return false + } + + if err := os.WriteFile(fp, []byte("new-tuf-checked"), constant.DefaultFileMode); err != nil { + // We log the error and return success below anyway because the access check was successful. + log.Error().Err(err).Msg("failed to write new-tuf-checked file") + } + // We assume access to the whole repository + // if the orbit macOS stable target is downloaded successfully. + return true +} + +func createTUFRemoteStore(opt Options, serverURL string) (client.RemoteStore, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: opt.InsecureTransport, + } + if opt.ServerCertificatePath != "" { + rootCAs, err := certificate.LoadPEM(opt.ServerCertificatePath) + if err != nil { + return nil, fmt.Errorf("loading server root CA: %w", err) + } + tlsConfig.RootCAs = rootCAs + } + if opt.ClientCertificate != nil { + tlsConfig.Certificates = []tls.Certificate{*opt.ClientCertificate} + } + remoteOpt := &client.HTTPRemoteOptions{ + UserAgent: fmt.Sprintf("orbit/%s (%s %s)", build.Version, runtime.GOOS, runtime.GOARCH), + } + httpClient := fleethttp.NewClient(fleethttp.WithTLSClientConfig(tlsConfig)) + remoteStore, err := client.HTTPRemoteStore(serverURL, remoteOpt, httpClient) + if err != nil { + return nil, fmt.Errorf("init remote store: %w", err) + } + return remoteStore, nil +} diff --git a/tools/tuf/README.md b/tools/tuf/README.md index 3915fdb89278..f89140131362 100644 --- a/tools/tuf/README.md +++ b/tools/tuf/README.md @@ -69,18 +69,16 @@ AWS_PROFILE=tuf aws sso login > You can skip this step if you already have authorized keys to sign and publish updates. To release updates to our TUF repository you need the `root` role (ask in Slack who has such `root` role) to sign your signing keys. -First, run the following script + +1. First, run the following script ```sh -AWS_PROFILE=tuf \ -ACTION=generate-signing-keys \ -TUF_DIRECTORY=/Users/foobar/tuf3.fleetctl.com \ -TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ -SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ -TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ -./tools/tuf/releaser.sh +tuf gen-key targets && echo +tuf gen-key snapshot && echo +tuf gen-key timestamp && echo ``` - -The human with the `root` role will run the following commands to sign the provided `staged/root.json`: +2. Store the '$TUF_DIRECTORY/keys' folder (that contains the encrypted keys) on a USB flash drive that you will ONLY use for releasing fleetd updates. +3. Share '$TUF_DIRECTORY/staged/root.json' with Fleet member with the 'root' role, who will sign with its root key and push it to the remote repository. +4. The human with the `root` role will run the following commands to sign the provided `staged/root.json`: ```sh tuf sign tuf snapshot diff --git a/tools/tuf/migrate/README.md b/tools/tuf/migrate/README.md new file mode 100644 index 000000000000..c3e2e9aba1a4 --- /dev/null +++ b/tools/tuf/migrate/README.md @@ -0,0 +1,22 @@ +# migrate + +This tool will be used to migrate all current targets (except unused ones) from https://tuf.fleetctl.com to https://updates.fleetdm.com. + +Usage: +```sh +# The tool requires the 'targets', 'snapshot' and 'timestamp' roles of the new repository. +export FLEET_TARGETS_PASSPHRASE=p4ssphr4s3 +export FLEET_SNAPSHOT_PASSPHRASE=p4ssphr4s3 +export FLEET_TIMESTAMP_PASSPHRASE=p4ssphr4s3 + +# +# It assumes the following: +# - https://tuf.fleetctl.com was fully fetched into -source-repository-directory. +# - https://updates.fleetdm.com was fully fetched into -dest-repository-directory. +# +# Migration may take several minutes due to sha512 verification after targets are +# added to the new repository. +go run ./tools/tuf/migrate/migrate.go \ + -source-repository-directory ./source-tuf-directory \ + -dest-repository-directory ./dest-tuf-directory +``` diff --git a/tools/tuf/migrate/migrate.go b/tools/tuf/migrate/migrate.go new file mode 100644 index 000000000000..d891d505b9ce --- /dev/null +++ b/tools/tuf/migrate/migrate.go @@ -0,0 +1,207 @@ +// Package main contains an executable that migrates all targets from one source TUF repository +// to a destination TUF repository. It migrates all targets except a few known unused targets. +package main + +import ( + "crypto/sha512" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + if runtime.GOOS == "windows" { + log.Fatalf("%s is not supported on windows", os.Args[0]) + } + + sourceRepositoryDirectory := flag.String("source-repository-directory", "", "Absolute path directory for the source TUF") + destRepositoryDirectory := flag.String("dest-repository-directory", "", "Absolute path directory for the destination TUF") + + flag.Parse() + + if *sourceRepositoryDirectory == "" { + log.Fatal("missing --source-repository-directory") + } + if *destRepositoryDirectory == "" { + log.Fatal("missing --dest-repository-directory") + } + + type targetEntry struct { + sha512 string + length int + } + + // Perform addition of targets by iterating source repository. + sourceEntries := make(map[string]targetEntry) + iterateRepository(*sourceRepositoryDirectory, func(target, targetPath, platform, targetName, version, channel, hashSHA512 string, length int) error { + cmd := exec.Command("fleetctl", "updates", "add", //nolint:gosec + "--path", *destRepositoryDirectory, + "--target", targetPath, + "--platform", platform, + "--name", targetName, + "--version", version, + "-t", channel, + ) + log.Print(cmd.String()) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + if err := cmd.Run(); err != nil { + log.Fatalf("target: %q: failed to add target: %s", target, err) + } + + sourceEntries[target] = targetEntry{ + sha512: hashSHA512, + length: length, + } + + return nil + }) + + // Perform validation of destination repository. + iterateRepository(*destRepositoryDirectory, func(target, targetPath, platform, targetName, version, channel, hashSHA512 string, length int) error { + sourceEntry, ok := sourceEntries[target] + if !ok { + return errors.New("entry not found in source directory") + } + + // It seems this very old version has invalid length and sha256. + // Validation fails with: + // 2025/01/07 18:11:40 target: "desktop/macos/1.11.0/desktop.app.tar.gz": failed to process target: mismatch length: 10518528 vs 30373384 + if target == "desktop/macos/1.11.0/desktop.app.tar.gz" { + log.Printf("Skipping %s (old version) due to invalid length and sha256", target) + return nil + } + + if sourceEntry.length != length { + return fmt.Errorf("mismatch length: %d vs %d", length, sourceEntry.length) + } + if sourceEntry.sha512 != hashSHA512 { + return fmt.Errorf("mismatch sha512: %s vs %s", hashSHA512, sourceEntry.sha512) + } + + targetBytes, err := os.ReadFile(targetPath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + h := sha512.New() + if _, err := h.Write(targetBytes); err != nil { + return fmt.Errorf("failed to hash file: %w", err) + } + fileHash := hex.EncodeToString(h.Sum(nil)) + if fileHash != sourceEntry.sha512 { + return fmt.Errorf("mismatch sha512 and file contents: %s vs %s", fileHash, sourceEntry.sha512) + } + + return nil + }) +} + +func iterateRepository(repositoryDirectory string, fn func(target, targetPath, platform, targetName, version, channel, sha512 string, length int) error) { + repositoryPath := filepath.Join(repositoryDirectory, "repository") + targetsFile := filepath.Join(repositoryPath, "targets.json") + targetsBytes, err := os.ReadFile(targetsFile) + if err != nil { + log.Fatal("failed to read the source targets.json file") + } + + var targetsJSON map[string]interface{} + if err := json.Unmarshal(targetsBytes, &targetsJSON); err != nil { + log.Fatal("failed to parse the source targets.json file") + } + + signed_ := targetsJSON["signed"] + if signed_ == nil { + log.Fatal("missing signed key in targets.json file") + } + signed, ok := signed_.(map[string]interface{}) + if !ok { + log.Fatalf("invalid signed key in targets.json file: %T, expected map", signed_) + } + targets_ := signed["targets"] + if targets_ == nil { + log.Fatal("missing signed.targets key in targets.json file") + } + targets, ok := targets_.(map[string]interface{}) + if !ok { + log.Fatalf("invalid signed.targets key in targets.json file: %T, expected map", targets_) + } + + for target, metadata_ := range targets { + targetPath := filepath.Join(repositoryPath, "targets", target) + + parts := strings.Split(target, "/") + if len(parts) != 4 { + log.Fatalf("target %q: invalid number of parts, expected 4", target) + } + + targetName := parts[0] + platform := parts[1] + channel := parts[2] + executable := parts[3] + + // Unused targets (probably accidentally pushed). + if targetName == "desktop.tar.gz" || // correct target name is just "desktop". + (targetName == "desktop" && executable == "desktop") { // correct executable for Linux is "desktop.tar.gz". + continue + } + + metadata, ok := metadata_.(map[string]interface{}) + if !ok { + log.Fatalf("target: %q: invalid metadata field: %T, expected map", target, metadata_) + } + custom_ := metadata["custom"] + if custom_ == nil { + log.Fatalf("target: %q: missing custom field", target) + } + custom, ok := custom_.(map[string]interface{}) + if !ok { + log.Fatalf("target: %q: invalid custom field: %T, expected map", target, custom_) + } + version_ := custom["version"] + if version_ == nil { + log.Fatalf("target: %q: missing custom.version field", target) + } + version, ok := version_.(string) + if !ok { + log.Fatalf("target: %q: invalid custom.version field: %T", target, version_) + } + length_ := metadata["length"] + if length_ == nil { + log.Fatalf("target: %q: missing length field", target) + } + lengthf, ok := length_.(float64) + if !ok { + log.Fatalf("target: %q: invalid length field: %T", target, length_) + } + length := int(lengthf) + hashes_ := metadata["hashes"] + if hashes_ == nil { + log.Fatalf("target: %q: missing hashes field", target) + } + hashes, ok := hashes_.(map[string]interface{}) + if !ok { + log.Fatalf("target: %q: invalid hashes field: %T", target, hashes_) + } + sha512_ := hashes["sha512"] + if sha512_ == nil { + log.Fatalf("target: %q: missing hashes.sha512 field", target) + } + hashSHA512, ok := sha512_.(string) + if !ok { + log.Fatalf("target: %q: invalid hashes.sha512 field: %T", target, sha512_) + } + + if err := fn(target, targetPath, platform, targetName, version, channel, hashSHA512, length); err != nil { + log.Fatalf("target: %q: failed to process target: %s", target, err) + } + } +} diff --git a/tools/tuf/releaser.sh b/tools/tuf/releaser.sh index 746c7f7d9d6c..f56e4fed75e6 100755 --- a/tools/tuf/releaser.sh +++ b/tools/tuf/releaser.sh @@ -277,41 +277,6 @@ prompt () { done } -setup_to_become_publisher () { - echo "Running setup to become publisher..." - - REPOSITORY_DIRECTORY=$TUF_DIRECTORY/repository - STAGED_DIRECTORY=$TUF_DIRECTORY/staged - KEYS_DIRECTORY=$TUF_DIRECTORY/keys - mkdir -p "$REPOSITORY_DIRECTORY" - mkdir -p "$STAGED_DIRECTORY" - mkdir -p "$KEYS_DIRECTORY" - if ! aws sts get-caller-identity &> /dev/null; then - aws sso login - prompt "AWS SSO login was successful." - fi - # These need to be exported for use by `tuf` commands. - FLEET_TARGETS_PASSPHRASE=$(op read "op://$TARGETS_PASSPHRASE_1PASSWORD_PATH") - export TUF_TARGETS_PASSPHRASE=$FLEET_TARGETS_PASSPHRASE - FLEET_SNAPSHOT_PASSPHRASE=$(op read "op://$SNAPSHOT_PASSPHRASE_1PASSWORD_PATH") - export TUF_SNAPSHOT_PASSPHRASE=$FLEET_SNAPSHOT_PASSPHRASE - FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH") - export TUF_TIMESTAMP_PASSPHRASE=$FLEET_TIMESTAMP_PASSPHRASE -} - -if [[ $ACTION == "generate-signing-keys" ]]; then - setup_to_become_publisher - pull_from_remote - cd "$TUF_DIRECTORY" - tuf gen-key targets && echo - tuf gen-key snapshot && echo - tuf gen-key timestamp && echo - echo "Keys have been generated, now do the following actions:" - echo "- Share '$TUF_DIRECTORY/staged/root.json' with Fleet member with the 'root' role, who will sign with its root key and push it to the remote repository." - echo "- Store the '$TUF_DIRECTORY/keys' folder (that contains the encrypted keys) on a USB flash drive that you will ONLY use for releasing fleetd updates." - exit 0 -fi - print_reminder () { if [[ $ACTION == "release-to-edge" ]]; then if [[ $COMPONENT == "fleetd" ]]; then @@ -333,8 +298,15 @@ print_reminder () { fi } +fleetctl_version_check () { + which fleetctl + fleetctl --version + prompt "Make sure the fleetctl executable and version are correct." +} + trap clean_up EXIT print_reminder +fleetctl_version_check setup pull_from_remote diff --git a/tools/tuf/test/README.md b/tools/tuf/test/README.md index e22b5642e2a3..d7b207035930 100644 --- a/tools/tuf/test/README.md +++ b/tools/tuf/test/README.md @@ -61,7 +61,8 @@ LINUX_TEST_EXTENSIONS="./tools/test_extensions/hello_world/linux/hello_world_lin To build for a specific architecture, you can pass the `GOARCH` environment variable: ``` shell [...] -GOARCH=arm64 # defaults to amd64 +# defaults to amd64 +GOARCH=arm64 \ [...] ./tools/tuf/test/main.sh ``` diff --git a/tools/tuf/test/main.sh b/tools/tuf/test/main.sh index 65c179e5fe16..88ff63a025ad 100755 --- a/tools/tuf/test/main.sh +++ b/tools/tuf/test/main.sh @@ -6,10 +6,19 @@ export FLEET_ROOT_PASSPHRASE=p4ssphr4s3 export FLEET_TARGETS_PASSPHRASE=p4ssphr4s3 export FLEET_SNAPSHOT_PASSPHRASE=p4ssphr4s3 export FLEET_TIMESTAMP_PASSPHRASE=p4ssphr4s3 -export TUF_PATH=test_tuf export NUDGE=1 -if ( [ -n "$GENERATE_PKG" ] || [ -n "$GENERATE_DEB" ] || [ -n "$GENERATE_RPM" ] || [ -n "$GENERATE_MSI" ] ) && [ -z "$ENROLL_SECRET" ]; then +if [ -z "$TUF_PATH" ]; then + TUF_PATH=test_tuf +fi +export TUF_PATH + +if [ -z "$TUF_PORT" ]; then + TUF_PORT=8081 +fi +export TUF_PORT + +if { [ -n "$GENERATE_PKG" ] || [ -n "$GENERATE_DEB" ] || [ -n "$GENERATE_RPM" ] || [ -n "$GENERATE_MSI" ] ; } && [ -z "$ENROLL_SECRET" ]; then echo "Error: To generate packages you must set ENROLL_SECRET variable." exit 1 fi @@ -30,7 +39,8 @@ fi make fleetctl ./tools/tuf/test/create_repository.sh -export ROOT_KEYS=$(./build/fleetctl updates roots --path $TUF_PATH) +ROOT_KEYS=$(./build/fleetctl updates roots --path "$TUF_PATH") +export ROOT_KEYS echo "#########" echo "To generate packages set the following options in 'fleetctl package':" diff --git a/tools/tuf/test/migration/README.md b/tools/tuf/test/migration/README.md new file mode 100644 index 000000000000..3e92c8ace83c --- /dev/null +++ b/tools/tuf/test/migration/README.md @@ -0,0 +1,21 @@ +# `migration_test.sh` + +This script is used to test the migration from one local TUF repository to a new local TUF repository (with new roots). + +> Currently supports running on macOS only. + +The script is interactive and assumes the user will use a Windows and Ubuntu VM to install fleetd and test the changes on those platforms too. + +Usage: +```sh +FLEET_URL=https://host.docker.internal:8080 \ +NO_TEAM_ENROLL_SECRET=... \ +WINDOWS_HOST_HOSTNAME=DESKTOP-USFLJ3H \ +LINUX_HOST_HOSTNAME=foobar-ubuntu \ +./tools/tuf/test/migration/migration_test.sh +``` + +To simulate an outage of the TUF during the migration run the above with: +```sh +SIMULATE_NEW_TUF_OUTAGE=1 \ +``` diff --git a/tools/tuf/test/migration/migration_test.sh b/tools/tuf/test/migration/migration_test.sh new file mode 100755 index 000000000000..8b21454cac81 --- /dev/null +++ b/tools/tuf/test/migration/migration_test.sh @@ -0,0 +1,597 @@ +#!/bin/bash + +# Script used to test the migration from a TUF repository to a new one. +# It assumes the following: +# - User runs the script on macOS +# - User has a Ubuntu 22.04 and a Windows 10/11 VM (running on the same macOS host script runs on). +# - Fleet is running on the macOS host. +# - `fleetctl login` was ran on the localhost Fleet instance (to be able to run `fleectl query` commands). +# - host.docker.internal points to localhost on the macOS host. +# - host.docker.internal points to the macOS host on the two VMs (/etc/hosts on Ubuntu and C:\Windows\System32\Drivers\etc\hosts on Windows). +# - 1.37.0 is the last version of orbit that uses the old TUF repository +# - 1.38.0 is the new version of orbit that will use the new TUF repository. +# - Old TUF repository directory is ./test_tuf_old and server listens on 8081 (runs on the macOS host). +# - New TUF repository directory is ./test_tuf_new and server listens on 8082 (runs on the macOS host). + +set -e + +if [ -z "$FLEET_URL" ]; then + echo "Missing FLEET_URL" + exit 1 +fi +if [ -z "$NO_TEAM_ENROLL_SECRET" ]; then + echo "Missing NO_TEAM_ENROLL_SECRET" + exit 1 +fi + +if [ -z "$WINDOWS_HOST_HOSTNAME" ]; then + echo "Missing WINDOWS_HOST_HOSTNAME" + exit 1 +fi + +if [ -z "$LINUX_HOST_HOSTNAME" ]; then + echo "Missing LINUX_HOST_HOSTNAME" + exit 1 +fi + +prompt () { + printf "%s\n" "$1" + printf "Type 'yes' to continue... " + while read -r word; + do + if [[ "$word" == "yes" ]]; then + printf "\n" + return + fi + done +} + +echo "Uinstalling fleetd from macOS..." +sudo orbit/tools/cleanup/cleanup_macos.sh +prompt "Please manually uninstall fleetd from $WINDOWS_HOST_HOSTNAME and $LINUX_HOST_HOSTNAME." + +OLD_TUF_PORT=8081 +OLD_TUF_URL=http://host.docker.internal:$OLD_TUF_PORT +OLD_TUF_PATH=test_tuf_old +OLD_FULL_VERSION=1.37.0 +OLD_MINOR_VERSION=1.37 + +NEW_TUF_PORT=8082 +NEW_TUF_URL=http://host.docker.internal:$NEW_TUF_PORT +NEW_TUF_PATH=test_tuf_new +NEW_FULL_VERSION=1.38.0 +NEW_MINOR_VERSION=1.38 +NEW_PATCH_VERSION=1.38.1 + +echo "Cleaning up existing directories and file servers..." +rm -rf "$OLD_TUF_PATH" +rm -rf "$NEW_TUF_PATH" +pkill file-server || true + +echo "Restoring update_channels for \"No team\" to 'stable' defaults..." +cat << EOF > upgrade.yml +--- +apiVersion: v1 +kind: config +spec: + agent_options: + config: + options: + pack_delimiter: / + distributed_plugin: tls + disable_distributed: false + logger_tls_endpoint: /api/v1/osquery/log + distributed_interval: 10 + distributed_tls_max_attempts: 3 + distributed_denylist_duration: 10 + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + update_channels: + orbit: stable + desktop: stable + osqueryd: stable +EOF +fleetctl apply -f upgrade.yml + +echo "Generating a TUF repository on $OLD_TUF_PATH (aka \"old\")..." +SYSTEMS="macos linux windows" \ +TUF_PATH=$OLD_TUF_PATH \ +TUF_PORT=$OLD_TUF_PORT \ +FLEET_DESKTOP=1 \ +./tools/tuf/test/main.sh + +export FLEET_ROOT_PASSPHRASE=p4ssphr4s3 +export FLEET_TARGETS_PASSPHRASE=p4ssphr4s3 +export FLEET_SNAPSHOT_PASSPHRASE=p4ssphr4s3 +export FLEET_TIMESTAMP_PASSPHRASE=p4ssphr4s3 + +echo "Downloading and pushing latest released orbit from https://tuf.fleetctl.com to the old repository..." +curl https://tuf.fleetctl.com/targets/orbit/macos/$OLD_FULL_VERSION/orbit --output orbit-darwin +./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit-darwin --platform macos --name orbit --version $OLD_FULL_VERSION -t $OLD_MINOR_VERSION -t 1 -t stable +curl https://tuf.fleetctl.com/targets/orbit/linux/$OLD_FULL_VERSION/orbit --output orbit-linux +./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit-linux --platform linux --name orbit --version $OLD_FULL_VERSION -t $OLD_MINOR_VERSION -t 1 -t stable +curl https://tuf.fleetctl.com/targets/orbit/windows/$OLD_FULL_VERSION/orbit.exe --output orbit.exe +./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit.exe --platform windows --name orbit --version $OLD_FULL_VERSION -t $OLD_MINOR_VERSION -t 1 -t stable + +echo "Building fleetd packages using old repository and old fleetctl version..." +curl -L https://github.com/fleetdm/fleet/releases/download/fleet-v4.60.0/fleetctl_v4.60.0_macos.tar.gz --output ./build/fleetctl_v4.60.0_macos.tar.gz +cd ./build +tar zxf fleetctl_v4.60.0_macos.tar.gz +cp fleetctl_v4.60.0_macos/fleetctl fleetctl-v4.60.0 +cd .. +chmod +x ./build/fleetctl-v4.60.0 +ROOT_KEYS1=$(./build/fleetctl-v4.60.0 updates roots --path $OLD_TUF_PATH) +declare -a pkgTypes=("pkg" "deb" "msi") +for pkgType in "${pkgTypes[@]}"; do + ./build/fleetctl-v4.60.0 package --type="$pkgType" \ + --enable-scripts \ + --fleet-desktop \ + --fleet-url="$FLEET_URL" \ + --enroll-secret="$NO_TEAM_ENROLL_SECRET" \ + --fleet-certificate=./tools/osquery/fleet.crt \ + --debug \ + --update-roots="$ROOT_KEYS1" \ + --update-url=$OLD_TUF_URL \ + --disable-open-folder \ + --disable-keystore \ + --update-interval=30s +done + +# Install fleetd generated with old fleetctl and using old TUF on devices. +echo "Installing fleetd package on macOS..." +sudo installer -pkg fleet-osquery.pkg -verbose -target / +CURRENT_DIR=$(pwd) +prompt "Please install $CURRENT_DIR/fleet-osquery.msi and $CURRENT_DIR/fleet-osquery_${OLD_FULL_VERSION}_amd64.deb." + +echo "Generating a new TUF repository from scratch on $NEW_TUF_PATH..." +./build/fleetctl updates init --path $NEW_TUF_PATH + +echo "Migrating all targets from old to new repository..." +go run ./tools/tuf/migrate/migrate.go \ + -source-repository-directory "$OLD_TUF_PATH" \ + -dest-repository-directory "$NEW_TUF_PATH" + +echo "Serving new TUF repository..." +TUF_PORT=$NEW_TUF_PORT TUF_PATH=$NEW_TUF_PATH ./tools/tuf/test/run_server.sh + +echo "Building the new orbit that will perform the migration..." +ROOT_KEYS2=$(./build/fleetctl updates roots --path $NEW_TUF_PATH) +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \ + -o orbit-darwin \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_FULL_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -o orbit-linux \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_FULL_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \ + -o orbit.exe \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_FULL_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit + +echo "Pushing new orbit to new repository on stable channel..." +./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit-darwin --platform macos --name orbit --version $NEW_FULL_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable +./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit-linux --platform linux --name orbit --version $NEW_FULL_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable +./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit.exe --platform windows --name orbit --version $NEW_FULL_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + +if [ "$SIMULATE_NEW_TUF_OUTAGE" = "1" ]; then + echo "Simulating outage of the new TUF repository by killing the new TUF server..." + # We kill the two servers and bring back the old one. + pkill file-server || true + TUF_PORT=$OLD_TUF_PORT TUF_PATH=$OLD_TUF_PATH ./tools/tuf/test/run_server.sh +fi + +echo "Pushing new orbit to old repository!..." +./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit-darwin --platform macos --name orbit --version $NEW_FULL_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable +./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit-linux --platform linux --name orbit --version $NEW_FULL_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable +./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit.exe --platform windows --name orbit --version $NEW_FULL_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + +if [ "$SIMULATE_NEW_TUF_OUTAGE" = "1" ]; then + echo "Checking version of updated orbit (to check device is responding even if TUF server is down)..." + THIS_HOSTNAME=$(hostname) + declare -a hostnames=("$THIS_HOSTNAME" "$WINDOWS_HOST_HOSTNAME" "$LINUX_HOST_HOSTNAME") + for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done + done + + prompt "Please check for errors in orbit logs that new TUF server is unavailable (network errors). Errors should be shown every 10s." + + echo "Bring new TUF server back but still unavailable (404s errors)." + mkdir -p $NEW_TUF_PATH/tmp + mv $NEW_TUF_PATH/repository/targets/* $NEW_TUF_PATH/tmp/ + + TUF_PORT=$NEW_TUF_PORT TUF_PATH=$NEW_TUF_PATH ./tools/tuf/test/run_server.sh + + prompt "Please check for errors in orbit logs that new TUF server is still unavailable (404s errors). Errors should be shown every 10s." + + echo "Checking version of orbit (to check device is responding even if TUF server is down)..." + for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done + done + + # We kill the two servers and bring back the old one. + pkill file-server || true + TUF_PORT=$OLD_TUF_PORT TUF_PATH=$OLD_TUF_PATH ./tools/tuf/test/run_server.sh + # Restore files on the new repository. + mv $NEW_TUF_PATH/tmp/* $NEW_TUF_PATH/repository/targets/ + + if [ "$ORBIT_PATCH_IN_OLD_TUF" = "1" ]; then + echo "Build and push a new update to orbit to old and new repository (to test patching an invalid 1.38.0 would work for customers without access to new TUF)" + ROOT_KEYS2=$(./build/fleetctl updates roots --path $NEW_TUF_PATH) + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \ + -o orbit-darwin \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_PATCH_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -o orbit-linux \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_PATCH_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \ + -o orbit.exe \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_PATCH_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit + ./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit-darwin --platform macos --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + ./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit-linux --platform linux --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + ./build/fleetctl updates add --path $OLD_TUF_PATH --target ./orbit.exe --platform windows --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + ./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit-darwin --platform macos --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + ./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit-linux --platform linux --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + ./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit.exe --platform windows --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + + echo "Checking orbit has auto-updated to $NEW_PATCH_VERSION using old TUF..." + for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_PATCH_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done + done + + # Now the next patch version will be 1.38.2. + NEW_FULL_VERSION=1.38.1 + NEW_PATCH_VERSION=1.38.2 + fi + + echo "Restoring new TUF repository..." + TUF_PORT=$NEW_TUF_PORT TUF_PATH=$NEW_TUF_PATH ./tools/tuf/test/run_server.sh + + prompt "Please check that devices have restarted and started communicating with the new TUF (now that it's available)" +fi + +echo "Checking version of updated orbit..." +THIS_HOSTNAME=$(hostname) +declare -a hostnames=("$THIS_HOSTNAME" "$WINDOWS_HOST_HOSTNAME" "$LINUX_HOST_HOSTNAME") +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Restarting fleetd on the macOS host..." +sudo launchctl unload /Library/LaunchDaemons/com.fleetdm.orbit.plist && sudo launchctl load /Library/LaunchDaemons/com.fleetdm.orbit.plist + +prompt "Please restart fleetd on the Linux and Windows host." + +echo "Checking version of updated orbit..." +THIS_HOSTNAME=$(hostname) +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Building and pushing a new update to orbit on the new repository (to test upgrades are working)..." +ROOT_KEYS2=$(./build/fleetctl updates roots --path $NEW_TUF_PATH) +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \ + -o orbit-darwin \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_PATCH_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -o orbit-linux \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_PATCH_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \ + -o orbit.exe \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$NEW_PATCH_VERSION \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.OldFleetTUFURL=$OLD_TUF_URL" \ + ./orbit/cmd/orbit +./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit-darwin --platform macos --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable +./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit-linux --platform linux --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable +./build/fleetctl updates add --path $NEW_TUF_PATH --target ./orbit.exe --platform windows --name orbit --version $NEW_PATCH_VERSION -t $NEW_MINOR_VERSION -t 1 -t stable + +echo "Waiting until update happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_PATCH_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Downgrading to $OLD_FULL_VERSION..." +cat << EOF > downgrade.yml +--- +apiVersion: v1 +kind: config +spec: + agent_options: + config: + options: + pack_delimiter: / + distributed_plugin: tls + disable_distributed: false + logger_tls_endpoint: /api/v1/osquery/log + distributed_interval: 10 + distributed_tls_max_attempts: 3 + distributed_denylist_duration: 10 + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + update_channels: + orbit: '$OLD_FULL_VERSION' + desktop: stable + osqueryd: stable +EOF +fleetctl apply -f downgrade.yml + +echo "Waiting until downgrade happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$OLD_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Restoring to latest orbit version..." +cat << EOF > upgrade.yml +--- +apiVersion: v1 +kind: config +spec: + agent_options: + config: + options: + pack_delimiter: / + distributed_plugin: tls + disable_distributed: false + logger_tls_endpoint: /api/v1/osquery/log + distributed_interval: 10 + distributed_tls_max_attempts: 3 + distributed_denylist_duration: 10 + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + update_channels: + orbit: stable + desktop: stable + osqueryd: stable +EOF +fleetctl apply -f upgrade.yml + +echo "Waiting until upgrade happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_PATCH_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Building fleetd packages using old repository and old fleetctl version that should auto-update to new orbit that talks to new repository..." +for pkgType in "${pkgTypes[@]}"; do + ./build/fleetctl-v4.60.0 package --type="$pkgType" \ + --enable-scripts \ + --fleet-desktop \ + --fleet-url="$FLEET_URL" \ + --enroll-secret="$NO_TEAM_ENROLL_SECRET" \ + --fleet-certificate=./tools/osquery/fleet.crt \ + --debug \ + --update-roots="$ROOT_KEYS1" \ + --update-url=$OLD_TUF_URL \ + --disable-open-folder \ + --disable-keystore \ + --update-interval=30s +done + +echo "Installing fleetd package on macOS..." +sudo installer -pkg fleet-osquery.pkg -verbose -target / + +CURRENT_DIR=$(pwd) +prompt "Please install $CURRENT_DIR/fleet-osquery.msi and $CURRENT_DIR/fleet-osquery_${NEW_FULL_VERSION}_amd64.deb." + +echo "Waiting until installation and auto-update to new repository happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_PATCH_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Downgrading to $OLD_FULL_VERSION..." +cat << EOF > downgrade.yml +--- +apiVersion: v1 +kind: config +spec: + agent_options: + config: + options: + pack_delimiter: / + distributed_plugin: tls + disable_distributed: false + logger_tls_endpoint: /api/v1/osquery/log + distributed_interval: 10 + distributed_tls_max_attempts: 3 + distributed_denylist_duration: 10 + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + update_channels: + orbit: '$OLD_FULL_VERSION' + desktop: stable + osqueryd: stable +EOF +fleetctl apply -f downgrade.yml + +echo "Waiting until downgrade happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$OLD_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Restoring to latest orbit version..." +cat << EOF > upgrade.yml +--- +apiVersion: v1 +kind: config +spec: + agent_options: + config: + options: + pack_delimiter: / + distributed_plugin: tls + disable_distributed: false + logger_tls_endpoint: /api/v1/osquery/log + distributed_interval: 10 + distributed_tls_max_attempts: 3 + distributed_denylist_duration: 10 + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + update_channels: + orbit: stable + desktop: stable + osqueryd: stable +EOF +fleetctl apply -f upgrade.yml + +echo "Waiting until upgrade happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_PATCH_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + + +echo "Building fleetd packages using new repository and new fleetctl version..." + +CGO_ENABLED=0 go build \ + -o ./build/fleetctl \ + -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/update.defaultRootMetadata=$ROOT_KEYS2 \ + -X github.com/fleetdm/fleet/v4/orbit/pkg/update.DefaultURL=$NEW_TUF_URL" \ + ./cmd/fleetctl + +for pkgType in "${pkgTypes[@]}"; do + ./build/fleetctl package --type="$pkgType" \ + --enable-scripts \ + --fleet-desktop \ + --fleet-url="$FLEET_URL" \ + --enroll-secret="$NO_TEAM_ENROLL_SECRET" \ + --fleet-certificate=./tools/osquery/fleet.crt \ + --debug \ + --disable-open-folder \ + --disable-keystore \ + --update-interval=30s +done + +echo "Installing fleetd package on macOS..." +sudo installer -pkg fleet-osquery.pkg -verbose -target / + +CURRENT_DIR=$(pwd) +prompt "Please install $CURRENT_DIR/fleet-osquery.msi and $CURRENT_DIR/fleet-osquery_${NEW_PATCH_VERSION}_amd64.deb." + +echo "Waiting until installation and auto-update to new repository happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$NEW_PATCH_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +cat << EOF > downgrade.yml +--- +apiVersion: v1 +kind: config +spec: + agent_options: + config: + options: + pack_delimiter: / + distributed_plugin: tls + disable_distributed: false + logger_tls_endpoint: /api/v1/osquery/log + distributed_interval: 10 + distributed_tls_max_attempts: 3 + distributed_denylist_duration: 10 + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + update_channels: + orbit: '$OLD_FULL_VERSION' + desktop: stable + osqueryd: stable +EOF +fleetctl apply -f downgrade.yml + +echo "Waiting until downgrade happens..." +for host_hostname in "${hostnames[@]}"; do + ORBIT_VERSION="" + until [ "$ORBIT_VERSION" = "\"$OLD_FULL_VERSION\"" ]; do + sleep 1 + ORBIT_VERSION=$(fleetctl query --hosts "$host_hostname" --exit --query 'SELECT * FROM orbit_info;' 2>/dev/null | jq '.rows[0].version') + done +done + +echo "Migration testing completed." diff --git a/tools/tuf/test/push_target.sh b/tools/tuf/test/push_target.sh index a29b3da17de2..08771ece6e4f 100755 --- a/tools/tuf/test/push_target.sh +++ b/tools/tuf/test/push_target.sh @@ -4,7 +4,11 @@ system=$1 target_name=$2 target_path=$3 major_version=$4 -TUF_PATH=test_tuf + +if [ -z "$TUF_PATH" ]; then + TUF_PATH=test_tuf +fi +export TUF_PATH export FLEET_ROOT_PASSPHRASE=p4ssphr4s3 export FLEET_TARGETS_PASSPHRASE=p4ssphr4s3 diff --git a/tools/tuf/test/run_server.sh b/tools/tuf/test/run_server.sh index cb87a7d2ca83..b734d0a2975c 100755 --- a/tools/tuf/test/run_server.sh +++ b/tools/tuf/test/run_server.sh @@ -2,10 +2,14 @@ set -e -pkill file-server || true -echo "Running TUF server" -go run ./tools/file-server 8081 "${TUF_PATH}/repository" & -until curl --silent -o /dev/null http://localhost:8081/root.json; do +if curl --silent -o /dev/null "http://localhost:$TUF_PORT/root.json" ; then + echo "TUF server already running" + exit 0 +fi + +echo "Start TUF server" +go run ./tools/file-server "$TUF_PORT" "${TUF_PATH}/repository" & +until curl --silent -o /dev/null "http://localhost:$TUF_PORT/root.json"; do sleep 1 done echo "TUF server started" \ No newline at end of file