From 235bac947cc9c7f65c3ad0556471c9a87225c09f Mon Sep 17 00:00:00 2001 From: Slavek Kabrda Date: Thu, 8 Aug 2024 10:35:58 +0200 Subject: [PATCH 1/4] Add different run modes for the TUF server, allow saving TUF keys as a secret This commit implements features necessary to run/operate the TUF server in production much better: * The TUF server can now be run in 4 different modes: * `init` - only init the TUF repository and exit * `init-no-overwrite` - same as `init`, but won't overwrite the TUF repository if it already exists * `serve` - only serve an existing TUF repository (no `init`) * `init-and-serve` - `init` and then `serve` * The TUF repository can now be initialized/served from a given path, as opposed to always living in a `/tmp` directory. * The TUF keys can optionally be exported as an individual k8s secret. Signed-off-by: Slavek Kabrda --- cmd/tuf/server/main.go | 140 ++++++++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/cmd/tuf/server/main.go b/cmd/tuf/server/main.go index d562bfac4..e27795659 100644 --- a/cmd/tuf/server/main.go +++ b/cmd/tuf/server/main.go @@ -16,6 +16,7 @@ package main import ( "bytes" + "context" "flag" "fmt" "net/http" @@ -33,13 +34,24 @@ import ( "sigs.k8s.io/release-utils/version" ) +const ( + modeInit = "init" + modeInitAndServe = "init-and-serve" + modeServe = "serve" + modeInitNoOverwrite = "init-no-overwrite" +) + var ( - dir = flag.String("file-dir", "/var/run/tuf-secrets", "Directory where all the files that need to be added to TUF root live. File names are used to as targets.") + dir = flag.String("file-dir", "/var/run/tuf-secrets", "Directory where all the files that need to be added to TUF root live. File names are used to as targets.") + targetDir = flag.String("target-dir", "", "Directory where TUF repository should be created/served from. Defaults to temporary directory.") + mode = flag.String("mode", modeInitAndServe, "Run mode of the TUF server. One of: init, init-and-serve, serve, init-no-overwrite") // Name of the "secret" where we create two entries, one for: // root = Which holds 1.root.json // repository - Compressed repo, which has been tar/gzipped. secretName = flag.String("rootsecret", "tuf-root", "Name of the secret to create for the initial root file") - noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment") + // Name of the "secret" where we create one entry "keys" which is a compressed directory of private keys generated by TUF + keysSecretName = flag.String("keyssecret", "", "Name of the secret to create for generated keys (keys won't be stored unless this is provided)") + noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment") ) func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) { @@ -64,24 +76,20 @@ func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) return ns, clientset, nil } -func main() { - flag.Parse() - - ctx := signals.NewContext() - +func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysSecretName string) error { versionInfo := version.GetVersionInfo() logging.FromContext(ctx).Infof("running create_repo Version: %s GitCommit: %s BuildDate: %s", versionInfo.GitVersion, versionInfo.GitCommit, versionInfo.BuildDate) ns, clientset, err := getNamespaceAndClientset(*noK8s) if err != nil { - logging.FromContext(ctx).Panicf("Failed to get namespace and clientset: %v", err) + return fmt.Errorf("failed to get namespace and clientset: %v", err) } - tufFiles, err := os.ReadDir(*dir) + tufFiles, err := os.ReadDir(certsDir) if err != nil { - logging.FromContext(ctx).Fatalf("failed to read dir %s: %v", *dir, err) + return fmt.Errorf("failed to read dir %s: %v", certsDir, err) } - trimDir := strings.TrimSuffix(*dir, "/") + trimDir := strings.TrimSuffix(certsDir, "/") files := map[string][]byte{} for _, file := range tufFiles { if !file.IsDir() { @@ -95,7 +103,7 @@ func main() { fileName := fmt.Sprintf("%s/%s", trimDir, file.Name()) fileBytes, err := os.ReadFile(fileName) if err != nil { - logging.FromContext(ctx).Fatalf("failed to read file %s/%s: %v", fileName, err) + return fmt.Errorf("failed to read file %s/%s: %v", trimDir, fileName, err) } // If it's a TSA file, we need to split it into multiple TUF // targets. @@ -104,7 +112,7 @@ func main() { certFiles, err := certs.SplitCertChain(fileBytes, "tsa") if err != nil { - logging.FromContext(ctx).Fatalf("failed to parse %s/%s: %v", fileName, err) + return fmt.Errorf("failed to parse %s/%s: %v", trimDir, fileName, err) } for k, v := range certFiles { logging.FromContext(ctx).Infof("Got tsa cert file %s", k) @@ -120,15 +128,16 @@ func main() { // Create a new TUF root with the listed artifacts. local, dir, err := repo.CreateRepo(ctx, files) if err != nil { - logging.FromContext(ctx).Panicf("Failed to create repo: %v", err) + return fmt.Errorf("failed to create repo: %v", err) } + meta, err := local.GetMeta() if err != nil { - logging.FromContext(ctx).Panicf("Getting meta: %v", err) + return fmt.Errorf("getting meta: %v", err) } rootJSON, ok := meta["root.json"] if !ok { - logging.FromContext(ctx).Panicf("Getting root: %v", err) + return fmt.Errorf("getting root: %v", err) } // Add the initial 1.root.json to secrets. @@ -140,26 +149,101 @@ func main() { // worries here. var compressed bytes.Buffer if err := repo.CompressFS(os.DirFS(dir), &compressed, map[string]bool{"keys": true, "staged": true}); err != nil { - logging.FromContext(ctx).Fatalf("Failed to compress the repo: %v", err) + return fmt.Errorf("failed to compress the repo: %v", err) } data["repository"] = compressed.Bytes() if !*noK8s { nsSecret := clientset.CoreV1().Secrets(ns) - if err := secret.ReconcileSecret(ctx, *secretName, ns, data, nsSecret); err != nil { - logging.FromContext(ctx).Panicf("Failed to reconcile secret %s/%s: %v", ns, *secretName, err) + if err := secret.ReconcileSecret(ctx, repoSecretName, ns, data, nsSecret); err != nil { + return fmt.Errorf("failed to reconcile secret %s/%s: %v", ns, repoSecretName, err) + } + + // If we should also store created keys in a secret, compress the directory and put it into a secret + if keysSecretName != "" { + var compressedKeys bytes.Buffer + if err := repo.CompressFS(os.DirFS(dir), &compressedKeys, map[string]bool{"staged": true, "repository": true}); err != nil { + return fmt.Errorf("failed to compress the keys: %v", err) + } + dataKeys := map[string][]byte{"keys": compressedKeys.Bytes()} + if err := secret.ReconcileSecret(ctx, keysSecretName, ns, dataKeys, nsSecret); err != nil { + return fmt.Errorf("failed to reconcile keys secret %s/%s: %v", ns, keysSecretName, err) + } + } + } + + logging.FromContext(ctx).Infof("tuf repository was created in: %s, moving to %s", dir, targetDir) + + // Copy repository to the targetDir - until Go 1.23 which has os.CopyFS, we use + // a quick hack where we uncompress the compressed repository to the targetDir + repo.Uncompress(bytes.NewReader(data["repository"]), targetDir) + return nil +} + +func main() { + flag.Parse() + + ctx := signals.NewContext() + + serve := false + init := false + overwrite := true + + switch *mode { + case modeInit: + init = true + case modeInitAndServe: + init = true + serve = true + case modeInitNoOverwrite: + init = true + overwrite = false + case modeServe: + if *targetDir == "" { + logging.FromContext(ctx).Fatalf("'targetDir' must be specified to use the 'serve' mode") } + serve = true + default: + logging.FromContext(ctx).Fatalf("unknown mode %s", *mode) } - // Serve the TUF repository. - logging.FromContext(ctx).Infof("tuf repository was created in: %s", dir) - serveDir := filepath.Join(dir, "repository") - logging.FromContext(ctx).Infof("tuf repository was created in: %s serving tuf root at %s", dir, serveDir) - fs := http.FileServer(http.Dir(serveDir)) - http.Handle("/", fs) + if *targetDir == "" { + newTmpDir, err := os.MkdirTemp(os.TempDir(), "tuf-serve") + if err != nil { + logging.FromContext(ctx).Fatalf("failed creating temporary directory in %s: %v", os.TempDir(), err) + } + *targetDir = newTmpDir + } - /* #nosec G114 */ - if err := http.ListenAndServe(":8080", nil); err != nil { //nolint: gosec - panic(err) + if init { + repoExists := false + // See if the TUF repository already exists; right now we only see if root.json exists, + // but this could certainly be made much more sophisticated + if _, err := os.Stat(filepath.Join(*targetDir, "repository", "root.json")); err == nil { + repoExists = true + } + + if repoExists && !overwrite { + logging.FromContext(ctx).Infof("TUF repository already exists, skipping initialization...") + } else { + err := initTUFRepo(ctx, *dir, *targetDir, *secretName, *keysSecretName) + if err != nil { + logging.FromContext(ctx).Fatalf("%v", err) + } + logging.FromContext(ctx).Infof("tuf repository was created in: %s", *targetDir) + } + } + + if serve { + // Serve the TUF repository. + serveDir := filepath.Join(*targetDir, "repository") + logging.FromContext(ctx).Infof("serving tuf root at %s", serveDir) + fs := http.FileServer(http.Dir(serveDir)) + http.Handle("/", fs) + + /* #nosec G114 */ + if err := http.ListenAndServe(":8080", nil); err != nil { //nolint: gosec + panic(err) + } } } From 7553ef39901b224ebb41eff2a4a63baba3e79387 Mon Sep 17 00:00:00 2001 From: Slavek Kabrda Date: Fri, 9 Aug 2024 10:54:17 +0200 Subject: [PATCH 2/4] Provide TUF keys as individual values in the secret Signed-off-by: Slavek Kabrda --- cmd/tuf/server/main.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/tuf/server/main.go b/cmd/tuf/server/main.go index e27795659..3286eba49 100644 --- a/cmd/tuf/server/main.go +++ b/cmd/tuf/server/main.go @@ -161,11 +161,22 @@ func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysS // If we should also store created keys in a secret, compress the directory and put it into a secret if keysSecretName != "" { - var compressedKeys bytes.Buffer - if err := repo.CompressFS(os.DirFS(dir), &compressedKeys, map[string]bool{"staged": true, "repository": true}); err != nil { - return fmt.Errorf("failed to compress the keys: %v", err) + keyFiles, err := os.ReadDir(filepath.Join(dir, "keys")) + if err != nil { + return fmt.Errorf("failed to list keys directory %v", err) + } + dataKeys := map[string][]byte{} + for _, keyFile := range keyFiles { + if !strings.HasSuffix(keyFile.Name(), ".json") { + continue + } + keyFilePath := filepath.Join(filepath.Join(dir, "keys", keyFile.Name())) + content, err := os.ReadFile(keyFilePath) + if err != nil { + return fmt.Errorf("failed reading file %s: %v", keyFilePath, err) + } + dataKeys[keyFile.Name()] = content } - dataKeys := map[string][]byte{"keys": compressedKeys.Bytes()} if err := secret.ReconcileSecret(ctx, keysSecretName, ns, dataKeys, nsSecret); err != nil { return fmt.Errorf("failed to reconcile keys secret %s/%s: %v", ns, keysSecretName, err) } From f5e033c725d9a0804d37850eb7dbfe8925535343 Mon Sep 17 00:00:00 2001 From: Slavek Kabrda Date: Fri, 9 Aug 2024 10:55:11 +0200 Subject: [PATCH 3/4] Fix comment Signed-off-by: Slavek Kabrda --- cmd/tuf/server/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tuf/server/main.go b/cmd/tuf/server/main.go index 3286eba49..04da1e506 100644 --- a/cmd/tuf/server/main.go +++ b/cmd/tuf/server/main.go @@ -159,7 +159,7 @@ func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysS return fmt.Errorf("failed to reconcile secret %s/%s: %v", ns, repoSecretName, err) } - // If we should also store created keys in a secret, compress the directory and put it into a secret + // If we should also store created keys in a secret, read all their files and save them in the secret if keysSecretName != "" { keyFiles, err := os.ReadDir(filepath.Join(dir, "keys")) if err != nil { From 88ac0e1ea0cba611ec1035e10d5d50559b7fefd9 Mon Sep 17 00:00:00 2001 From: Slavek Kabrda Date: Tue, 13 Aug 2024 09:39:46 +0200 Subject: [PATCH 4/4] Address review Signed-off-by: Slavek Kabrda --- cmd/tuf/server/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/tuf/server/main.go b/cmd/tuf/server/main.go index 04da1e506..edc1dbf6f 100644 --- a/cmd/tuf/server/main.go +++ b/cmd/tuf/server/main.go @@ -49,7 +49,7 @@ var ( // root = Which holds 1.root.json // repository - Compressed repo, which has been tar/gzipped. secretName = flag.String("rootsecret", "tuf-root", "Name of the secret to create for the initial root file") - // Name of the "secret" where we create one entry "keys" which is a compressed directory of private keys generated by TUF + // Name of the "secret" where we create one entry per key JSON definition as generated by TUF, e.g. "root.json", "timestamp.json", ... keysSecretName = flag.String("keyssecret", "", "Name of the secret to create for generated keys (keys won't be stored unless this is provided)") noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment") ) @@ -85,11 +85,11 @@ func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysS return fmt.Errorf("failed to get namespace and clientset: %v", err) } - tufFiles, err := os.ReadDir(certsDir) + trimDir := strings.TrimSuffix(certsDir, "/") + tufFiles, err := os.ReadDir(trimDir) if err != nil { - return fmt.Errorf("failed to read dir %s: %v", certsDir, err) + return fmt.Errorf("failed to read dir %s: %v", trimDir, err) } - trimDir := strings.TrimSuffix(certsDir, "/") files := map[string][]byte{} for _, file := range tufFiles { if !file.IsDir() { @@ -103,7 +103,7 @@ func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysS fileName := fmt.Sprintf("%s/%s", trimDir, file.Name()) fileBytes, err := os.ReadFile(fileName) if err != nil { - return fmt.Errorf("failed to read file %s/%s: %v", trimDir, fileName, err) + return fmt.Errorf("failed to read file %s: %v", fileName, err) } // If it's a TSA file, we need to split it into multiple TUF // targets. @@ -112,7 +112,7 @@ func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysS certFiles, err := certs.SplitCertChain(fileBytes, "tsa") if err != nil { - return fmt.Errorf("failed to parse %s/%s: %v", trimDir, fileName, err) + return fmt.Errorf("failed to parse %s: %v", fileName, err) } for k, v := range certFiles { logging.FromContext(ctx).Infof("Got tsa cert file %s", k)