Skip to content

Commit

Permalink
Add different run modes for the TUF server, allow saving TUF keys as …
Browse files Browse the repository at this point in the history
…a secret (#1214)

* 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 <[email protected]>
  • Loading branch information
bkabrda authored Aug 19, 2024
1 parent c5c73ef commit 3dcbf17
Showing 1 changed file with 123 additions and 28 deletions.
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"
)

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

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

0 comments on commit 3dcbf17

Please sign in to comment.