Skip to content

Commit

Permalink
Add support for experiemental hermetic execution mode
Browse files Browse the repository at this point in the history
This PR adds supoprt for an experimental hermetic execution mode. If users specify this on their TaskRun, then all user containers are run without network access.
Any containers created or injected by tekton (init containers or sidecar containers) are not affected.

This PR also adds an integration test to make sure that network access isn't available when hermetic mode is enabled.

Relevant TEP: https://github.com/tektoncd/community/blob/main/teps/0025-hermekton.md
  • Loading branch information
Priya Wadhwa committed May 18, 2021
1 parent 6016ccc commit e481b80
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 230 deletions.
26 changes: 26 additions & 0 deletions cmd/entrypoint/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"syscall"

"github.com/tektoncd/pipeline/pkg/entrypoint"
"github.com/tektoncd/pipeline/pkg/pod"
)

// TODO(jasonhall): Test that original exit code is propagated and that
Expand Down Expand Up @@ -58,6 +59,31 @@ func (rr *realRunner) Run(ctx context.Context, args ...string) error {
// main process and all children
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

if os.Getenv("TEKTON_RESOURCE_NAME") == "" && os.Getenv(pod.HermeticEnvVar) == "1" {
cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNS |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWUSER

cmd.SysProcAttr.GidMappingsEnableSetgroups = true
cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: 0,
// Map all users
Size: 4294967295,
},
}
cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: 0,
// Map all groups
Size: 4294967295,
},
}
}

// Start defined command
if err := cmd.Start(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
Expand Down
498 changes: 269 additions & 229 deletions pkg/apis/pipeline/v1beta1/openapi_generated.go

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions pkg/apis/pipeline/v1beta1/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,11 @@
"description": "Description is a user-facing description of the task that may be used to populate a UI.",
"type": "string"
},
"executionMode": {
"description": "ExecutionMode specifies the execution mode to run user-specified containers in (tekton injected containers will not run with this policy)",
"default": {},
"$ref": "#/definitions/v1beta1.ExecutionMode"
},
"metadata": {
"default": {},
"$ref": "#/definitions/v1beta1.PipelineTaskMetadata"
Expand Down Expand Up @@ -571,6 +576,14 @@
}
}
},
"v1beta1.ExecutionMode": {
"type": "object",
"properties": {
"hermetic": {
"type": "boolean"
}
}
},
"v1beta1.InternalTaskModifier": {
"description": "InternalTaskModifier implements TaskModifier for resources that are built-in to Tekton Pipelines.",
"type": "object",
Expand Down Expand Up @@ -2178,6 +2191,11 @@
"description": "TaskRunSpec defines the desired state of TaskRun",
"type": "object",
"properties": {
"executionMode": {
"description": "ExecutionMode specifies the execution mode to run user-specified containers in (tekton injected containers will not run with this policy)",
"default": {},
"$ref": "#/definitions/v1beta1.ExecutionMode"
},
"params": {
"type": "array",
"items": {
Expand Down Expand Up @@ -2400,6 +2418,11 @@
"description": "Description is a user-facing description of the task that may be used to populate a UI.",
"type": "string"
},
"executionMode": {
"description": "ExecutionMode specifies the execution mode to run user-specified containers in (tekton injected containers will not run with this policy)",
"default": {},
"$ref": "#/definitions/v1beta1.ExecutionMode"
},
"params": {
"description": "Params is a list of input parameters required to run the task. Params must be supplied as inputs in TaskRuns unless they declare a default value.",
"type": "array",
Expand Down
9 changes: 9 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ type TaskSpec struct {

// Results are values that this Task can output
Results []TaskResult `json:"results,omitempty"`

// ExecutionMode specifies the execution mode to run user-specified containers in
// (tekton injected containers will not run with this policy)
// +optional
ExecutionMode ExecutionMode `json:"executionMode,omitempty"`
}

// TaskResult used to describe the results of a task
Expand Down Expand Up @@ -198,3 +203,7 @@ const (
// ClusterTaskKind indicates that task type has a cluster scope.
ClusterTaskKind TaskKind = "ClusterTask"
)

type ExecutionMode struct {
Hermetic bool `json:"hermetic,omitempty"`
}
4 changes: 4 additions & 0 deletions pkg/apis/pipeline/v1beta1/taskrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ type TaskRunSpec struct {
// Workspaces is a list of WorkspaceBindings from volumes to workspaces.
// +optional
Workspaces []WorkspaceBinding `json:"workspaces,omitempty"`
// ExecutionMode specifies the execution mode to run user-specified containers in
// (tekton injected containers will not run with this policy)
// +optional
ExecutionMode ExecutionMode `json:"executionMode,omitempty"`
}

// TaskRunSpecStatus defines the taskrun spec status the user can provide
Expand Down
12 changes: 12 additions & 0 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const (

// TaskRunLabelKey is the name of the label added to the Pod to identify the TaskRun
TaskRunLabelKey = pipeline.GroupName + pipeline.TaskRunLabelKey

// HermeticEnvVar is the env var we set in containers to indicate they should be run hermetically
HermeticEnvVar = "HERMETIC"
)

// These are effectively const, but Go doesn't have such an annotation.
Expand Down Expand Up @@ -168,6 +171,15 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
}
}

// Add env var if hermetic execution was requested
if taskRun.Spec.ExecutionMode.Hermetic {
for i, s := range stepContainers {
// Add it at the end so it overrides
env := append(s.Env, corev1.EnvVar{Name: HermeticEnvVar, Value: "1"})
stepContainers[i].Env = env
}
}

// Add implicit volume mounts to each step, unless the step specifies
// its own volume mount at that path.
for i, s := range stepContainers {
Expand Down
2 changes: 1 addition & 1 deletion test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ go test ./...
# Integration tests (against your current kube cluster)
go test -v -count=1 -tags=e2e -timeout=20m ./test

#conformance tests (against your current kube cluster)
# Conformance tests (against your current kube cluster)
go test -v -count=1 -tags=conformance -timeout=10m ./test
```

Expand Down
88 changes: 88 additions & 0 deletions test/hermetic_taskrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// +build e2e

/*
Copyright 2021 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package test

import (
"context"
"testing"
"time"

"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestHermeticTaskRun(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

c, namespace := setup(ctx, t)
t.Parallel()
defer tearDown(ctx, t, c, namespace)

// first, run the task run with hermetic=false to prove that it succeeds
regularTaskRunName := "not-hermetic"
regularTaskRun := taskRun(regularTaskRunName, namespace, false)
t.Logf("Creating TaskRun %s, hermetic=false", regularTaskRunName)
if _, err := c.TaskRunClient.Create(ctx, regularTaskRun, metav1.CreateOptions{}); err != nil {
t.Fatalf("Failed to create TaskRun `%s`: %s", regularTaskRunName, err)
}
if err := WaitForTaskRunState(ctx, c, regularTaskRunName, Succeed(regularTaskRunName), "TaskRunCompleted"); err != nil {
t.Fatalf("Error waiting for TaskRun %s to finish: %s", regularTaskRunName, err)
}

// now, run the task mode with hermetic mode
// it should fail, since it shouldn't be able to access any network
hermeticTaskRunName := "hermetic-should-fail"
hermeticTaskRun := taskRun(hermeticTaskRunName, namespace, true)
t.Logf("Creating TaskRun %s, hermetic=true", hermeticTaskRunName)
if _, err := c.TaskRunClient.Create(ctx, hermeticTaskRun, metav1.CreateOptions{}); err != nil {
t.Fatalf("Failed to create TaskRun `%s`: %s", regularTaskRun.Name, err)
}
if err := WaitForTaskRunState(ctx, c, hermeticTaskRunName, Failed(hermeticTaskRunName), "Failed"); err != nil {
t.Fatalf("Error waiting for TaskRun %s to fail: %s", hermeticTaskRunName, err)
}
}

func taskRun(name, namespace string, hermetic bool) *v1beta1.TaskRun {
return &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
Spec: v1beta1.TaskRunSpec{
ExecutionMode: v1beta1.ExecutionMode{
Hermetic: hermetic,
},
Timeout: &metav1.Duration{Duration: time.Minute},
TaskSpec: &v1beta1.TaskSpec{
Steps: []v1beta1.Step{
{
Container: corev1.Container{
Name: "access-network",
Image: "ubuntu",
},
Script: `#!/bin/bash
set -ex
apt-get update
apt-get install -y curl`,
},
},
},
},
}
}

0 comments on commit e481b80

Please sign in to comment.