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)).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* Added changes to migrate to new TUF repository from https://tuf.fleetctl.com to https://updates.fleetdm.com.
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
+153
-30
File diff suppressed because one or more lines are too long
+9
-11
@@ -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
|
||||
```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
|
||||
```
|
||||
|
||||
The human with the `root` role will run the following commands to sign the provided `staged/root.json`:
|
||||
1. First, run the following script
|
||||
```sh
|
||||
tuf gen-key targets && echo
|
||||
tuf gen-key snapshot && echo
|
||||
tuf gen-key timestamp && echo
|
||||
```
|
||||
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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-35
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
+13
-3
@@ -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':"
|
||||
|
||||
@@ -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 \
|
||||
```
|
||||
Executable
+597
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user