Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add different run modes for the TUF server, allow saving TUF keys as a secret #1214

Merged
merged 4 commits into from
Aug 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 123 additions & 28 deletions cmd/tuf/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main

import (
"bytes"
"context"
"flag"
"fmt"
"net/http"
Expand All @@ -33,13 +34,24 @@ import (
"sigs.k8s.io/release-utils/version"
)

const (
modeInit = "init"
modeInitAndServe = "init-and-serve"
modeServe = "serve"
modeInitNoOverwrite = "init-no-overwrite"
)
Comment on lines +37 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were using Cobra, I'd suggest using subcommands, but I don't want to pull in that dependency randomly.


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 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")
)

func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) {
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracting this is a nice win anyway, though it's unfortunate that we have 4 string-type arguments next to each other. Oh well.

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)
trimDir := strings.TrimSuffix(certsDir, "/")
tufFiles, err := os.ReadDir(trimDir)
if err != nil {
logging.FromContext(ctx).Fatalf("failed to read dir %s: %v", *dir, err)
return fmt.Errorf("failed to read dir %s: %v", trimDir, err)
}
trimDir := strings.TrimSuffix(*dir, "/")
files := map[string][]byte{}
for _, file := range tufFiles {
if !file.IsDir() {
Expand All @@ -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: %v", fileName, err)
}
// If it's a TSA file, we need to split it into multiple TUF
// targets.
Expand All @@ -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: %v", fileName, err)
}
for k, v := range certFiles {
logging.FromContext(ctx).Infof("Got tsa cert file %s", k)
Expand All @@ -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.
Expand All @@ -140,26 +149,112 @@ 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, read all their files and save them in the secret
if keysSecretName != "" {
keyFiles, err := os.ReadDir(filepath.Join(dir, "keys"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we pack the keys directory into data["repository"] already, along with the rest of dir?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't. If you look at how this is done:

if err := repo.CompressFS(os.DirFS(dir), &compressed, map[string]bool{"keys": true, "staged": true}); err != nil

and how the CompressFS function works, it will actually intentionally skip the keys and staged directories because of how it is called.

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
}
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Right now, it seems like we're creating in a temp directory and then moving to the final destination. What about having the repo.CreateRepo take targetDir as an argument, and not needing to do this unpack?

At that point, the code could also look like:

if noK8s {
  // We're done
  return nil
}

I suppose one advantage of the copy approach is that you won't end up with a half-initialized directory if you crash partway through....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were two reasons why I didn't add the extra argument, even though we considered it:

  • Exactly as you said, I think it's good to not end up with a potentially half-initialized directory.
  • Adding the new argument would change the public API of repo.CreateRepo. While I think that the modules from this repo might not be depended on very much, I wanted to prevent API breakage (but actually talking about this right now, I'm curious if that's ok - for future contributions).

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
Comment on lines +206 to +208
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming there's no use case for InitNoOverwriteAndServe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't have such a usecase right now and I always try to keep new stuff at minimum. If we ever see a need for it in the future, it will be easy to add it in a fully backwards compatible way.

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)
}

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
}

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)
}
}

// 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 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)
/* #nosec G114 */
if err := http.ListenAndServe(":8080", nil); err != nil { //nolint: gosec
panic(err)
}
}
}
Loading