Skip to content

Commit

Permalink
[TEP-0089] Modify entrypoint to sign the results.
Browse files Browse the repository at this point in the history
Breaking down PR tektoncd#4759 originally proposed by @pxp928 to address TEP-0089
according @lumjjb suggestions.
Plan for breaking down PR is
PR 1.1: api
PR 1.2: entrypointer (+cmd line + test/entrypointer)
Entrypoint takes results and signs the results (termination message).
PR 1.3: reconciler + pod + cmd/controller + integration tests
Controller will verify the signed result.
This commit corresponds to 1.2 above.
  • Loading branch information
jagathprakash authored and tekton-robot committed Oct 28, 2022
1 parent 1b941d3 commit b7d8693
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 4 deletions.
6 changes: 6 additions & 0 deletions cmd/entrypoint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ The following flags are available:
same value as `{{stdout_path}}` so both streams are copied to the same
file. However, there is no ordering guarantee on data copied from both
streams.
- `-enable_spire`: If set will enable signing of the results by SPIRE. Signing
results by SPIRE ensures that no process other than the current process can
tamper the results and go undetected.
- `-spire_socket_path`: This flag makes sense only when enable_spire is set.
When enable_spire is set, spire_socket_path is used to point to the
SPIRE agent socket for SPIFFE workload API.

Any extra positional arguments are passed to the original entrypoint command.

Expand Down
13 changes: 13 additions & 0 deletions cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"github.com/tektoncd/pipeline/pkg/credentials/dockercreds"
"github.com/tektoncd/pipeline/pkg/credentials/gitcreds"
"github.com/tektoncd/pipeline/pkg/entrypoint"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/spire/config"
"github.com/tektoncd/pipeline/pkg/termination"
)

Expand All @@ -51,6 +53,8 @@ var (
onError = flag.String("on_error", "", "Set to \"continue\" to ignore an error and continue when a container terminates with a non-zero exit code."+
" Set to \"stopAndFail\" to declare a failure with a step error and stop executing the rest of the steps.")
stepMetadataDir = flag.String("step_metadata_dir", "", "If specified, create directory to store the step metadata e.g. /tekton/steps/<step-name>/")
enableSpire = flag.Bool("enable_spire", false, "If specified by configmap, this enables spire signing and verification")
socketPath = flag.String("spire_socket_path", "unix:///spiffe-workload-api/spire-agent.sock", "Experimental: The SPIRE agent socket for SPIFFE workload API.")
)

const (
Expand Down Expand Up @@ -131,6 +135,14 @@ func main() {
}
}

var spireWorkloadAPI spire.EntrypointerAPIClient
if enableSpire != nil && *enableSpire && socketPath != nil && *socketPath != "" {
spireConfig := config.SpireConfig{
SocketPath: *socketPath,
}
spireWorkloadAPI = spire.NewEntrypointerAPIClient(&spireConfig)
}

e := entrypoint.Entrypointer{
Command: append(cmd, commandArgs...),
WaitFiles: strings.Split(*waitFiles, ","),
Expand All @@ -148,6 +160,7 @@ func main() {
BreakpointOnFailure: *breakpointOnFailure,
OnError: *onError,
StepMetadataDir: *stepMetadataDir,
SpireWorkloadAPI: spireWorkloadAPI,
}

// Copy any creds injected by the controller into the $HOME directory of the current
Expand Down
25 changes: 22 additions & 3 deletions pkg/entrypoint/entrypointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/termination"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -80,6 +81,10 @@ type Entrypointer struct {
OnError string
// StepMetadataDir is the directory for a step where the step related metadata can be stored
StepMetadataDir string
// SpireWorkloadAPI connects to spire and does obtains SVID based on taskrun
SpireWorkloadAPI spire.EntrypointerAPIClient
// ResultsDirectory is the directory to find results, defaults to pipeline.DefaultResultPath
ResultsDirectory string
}

// Waiter encapsulates waiting for files to exist.
Expand Down Expand Up @@ -136,13 +141,14 @@ func (e Entrypointer) Go() error {
ResultType: v1beta1.InternalTektonResultType,
})

ctx := context.Background()
var err error

if e.Timeout != nil && *e.Timeout < time.Duration(0) {
err = fmt.Errorf("negative timeout specified")
}

if err == nil {
ctx := context.Background()
var cancel context.CancelFunc
if e.Timeout != nil && *e.Timeout != time.Duration(0) {
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
Expand Down Expand Up @@ -184,15 +190,19 @@ func (e Entrypointer) Go() error {
// strings.Split(..) with an empty string returns an array that contains one element, an empty string.
// This creates an error when trying to open the result folder as a file.
if len(e.Results) >= 1 && e.Results[0] != "" {
if err := e.readResultsFromDisk(pipeline.DefaultResultPath); err != nil {
resultPath := pipeline.DefaultResultPath
if e.ResultsDirectory != "" {
resultPath = e.ResultsDirectory
}
if err := e.readResultsFromDisk(ctx, resultPath); err != nil {
logger.Fatalf("Error while handling results: %s", err)
}
}

return err
}

func (e Entrypointer) readResultsFromDisk(resultDir string) error {
func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) error {
output := []v1beta1.PipelineResourceResult{}
for _, resultFile := range e.Results {
if resultFile == "" {
Expand All @@ -211,6 +221,15 @@ func (e Entrypointer) readResultsFromDisk(resultDir string) error {
ResultType: v1beta1.TaskRunResultType,
})
}

if e.SpireWorkloadAPI != nil {
signed, err := e.SpireWorkloadAPI.Sign(ctx, output)
if err != nil {
return err
}
output = append(output, signed...)
}

// push output to termination path
if len(output) != 0 {
if err := termination.WriteMessage(e.TerminationPath, output); err != nil {
Expand Down
219 changes: 218 additions & 1 deletion pkg/entrypoint/entrypointer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ import (
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/termination"
"github.com/tektoncd/pipeline/test/diff"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/logging"
)

Expand Down Expand Up @@ -284,6 +287,7 @@ func TestReadResultsFromDisk(t *testing.T) {
},
} {
t.Run(c.desc, func(t *testing.T) {
ctx := context.Background()
terminationPath := "termination"
if terminationFile, err := ioutil.TempFile("", "termination"); err != nil {
t.Fatalf("unexpected error creating temporary termination file: %v", err)
Expand Down Expand Up @@ -314,7 +318,7 @@ func TestReadResultsFromDisk(t *testing.T) {
Results: resultsFilePath,
TerminationPath: terminationPath,
}
if err := e.readResultsFromDisk(""); err != nil {
if err := e.readResultsFromDisk(ctx, ""); err != nil {
t.Fatal(err)
}
msg, err := ioutil.ReadFile(terminationPath)
Expand Down Expand Up @@ -434,6 +438,167 @@ func TestEntrypointer_OnError(t *testing.T) {
}
}

func TestEntrypointerResults(t *testing.T) {
for _, c := range []struct {
desc, entrypoint, postFile, stepDir, stepDirLink string
waitFiles, args []string
resultsToWrite map[string]string
resultsOverride []string
breakpointOnFailure bool
sign bool
signVerify bool
}{{
desc: "do nothing",
}, {
desc: "no results",
entrypoint: "echo",
}, {
desc: "write single result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
},
}, {
desc: "write multiple result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
"bar": "def",
},
}, {
// These next two tests show that if not results are defined in the entrypointer, then no signature is produced
// indicating that no signature was created. However, it is important to note that results were defined,
// but no results were created, that signature is still produced.
desc: "no results signed",
entrypoint: "echo",
sign: true,
signVerify: false,
}, {
desc: "defined results but no results produced signed",
entrypoint: "echo",
resultsOverride: []string{"foo"},
sign: true,
signVerify: true,
}, {
desc: "write single result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
},
sign: true,
signVerify: true,
}, {
desc: "write multiple result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
"bar": "def",
},
sign: true,
signVerify: true,
}, {
desc: "write n/m results",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
},
resultsOverride: []string{"foo", "bar"},
sign: true,
signVerify: true,
}} {
t.Run(c.desc, func(t *testing.T) {
ctx := context.Background()
fw, fpw := &fakeWaiter{}, &fakePostWriter{}
var fr Runner = &fakeRunner{}
timeout := time.Duration(0)
terminationPath := "termination"
if terminationFile, err := ioutil.TempFile("", "termination"); err != nil {
t.Fatalf("unexpected error creating temporary termination file: %v", err)
} else {
terminationPath = terminationFile.Name()
defer os.Remove(terminationFile.Name())
}

resultsDir := createTmpDir(t, "results")
var results []string
if c.resultsToWrite != nil {
tmpResultsToWrite := map[string]string{}
for k, v := range c.resultsToWrite {
resultFile := path.Join(resultsDir, k)
tmpResultsToWrite[resultFile] = v
results = append(results, k)
}

fr = &fakeResultsWriter{
resultsToWrite: tmpResultsToWrite,
}
}

signClient, verifyClient, tr := getMockSpireClient(ctx)
if !c.sign {
signClient = nil
}

if c.resultsOverride != nil {
results = c.resultsOverride
}

err := Entrypointer{
Command: append([]string{c.entrypoint}, c.args...),
WaitFiles: c.waitFiles,
PostFile: c.postFile,
Waiter: fw,
Runner: fr,
PostWriter: fpw,
Results: results,
ResultsDirectory: resultsDir,
TerminationPath: terminationPath,
Timeout: &timeout,
BreakpointOnFailure: c.breakpointOnFailure,
StepMetadataDir: c.stepDir,
SpireWorkloadAPI: signClient,
}.Go()
if err != nil {
t.Fatalf("Entrypointer failed: %v", err)
}

fileContents, err := ioutil.ReadFile(terminationPath)
if err == nil {
resultCheck := map[string]bool{}
var entries []v1beta1.PipelineResourceResult
if err := json.Unmarshal(fileContents, &entries); err != nil {
t.Fatalf("failed to unmarshal results: %v", err)
}

for _, result := range entries {
if _, ok := c.resultsToWrite[result.Key]; ok {
if c.resultsToWrite[result.Key] == result.Value {
resultCheck[result.Key] = true
} else {
t.Errorf("expected result (%v) to have value %v, got %v", result.Key, result.Value, c.resultsToWrite[result.Key])
}
}
}

if len(resultCheck) != len(c.resultsToWrite) {
t.Error("number of results matching did not add up")
}

// Check signature
verified := verifyClient.VerifyTaskRunResults(ctx, entries, tr) == nil
if verified != c.signVerify {
t.Errorf("expected signature verify result %v, got %v", c.signVerify, verified)
}
} else if !os.IsNotExist(err) {
t.Error("Wanted termination file written, got nil")
}
if err := os.Remove(terminationPath); err != nil {
t.Errorf("Could not remove termination path: %s", err)
}
})
}
}

type fakeWaiter struct{ waited []string }

func (f *fakeWaiter) Wait(file string, _ bool, _ bool) error {
Expand Down Expand Up @@ -503,3 +668,55 @@ func (f *fakeExitErrorRunner) Run(ctx context.Context, args ...string) error {
f.args = &args
return exec.Command("ls", "/bogus/path").Run()
}

type fakeResultsWriter struct {
args *[]string
resultsToWrite map[string]string
}

func (f *fakeResultsWriter) Run(ctx context.Context, args ...string) error {
f.args = &args
for k, v := range f.resultsToWrite {
err := ioutil.WriteFile(k, []byte(v), 0666)
if err != nil {
return err
}
}
return nil
}

func createTmpDir(t *testing.T, name string) string {
tmpDir, err := ioutil.TempDir("", name)
if err != nil {
t.Fatalf("unexpected error creating temporary dir: %v", err)
}
return tmpDir
}

func getMockSpireClient(ctx context.Context) (spire.EntrypointerAPIClient, spire.ControllerAPIClient, *v1beta1.TaskRun) {
tr := &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Name: "taskrun-example",
Namespace: "foo",
},
Spec: v1beta1.TaskRunSpec{
TaskRef: &v1beta1.TaskRef{
Name: "taskname",
APIVersion: "a1",
},
ServiceAccountName: "test-sa",
},
}

sc := &spire.MockClient{}

_ = sc.CreateEntries(ctx, tr, nil, 10000)

// bootstrap with about 20 calls to sign which should be enough for testing
id := sc.GetIdentity(tr)
for i := 0; i < 20; i++ {
sc.SignIdentities = append(sc.SignIdentities, id)
}

return sc, sc, tr
}

0 comments on commit b7d8693

Please sign in to comment.