Skip to content

Commit

Permalink
Add more secure SecurityContext to injected pod containers
Browse files Browse the repository at this point in the history
This commit adds a SecurityContext to containers injected into TaskRun pods to allow them
to run in namespaces with "restricted" pod security admission policies.
This includes both init containers and the sidecar container which extracts results.
See https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted.

It assumes that a TaskRun is meant to run on Windows if and only if it has a podTemplate
with a nodeSelector containing "kubernetes.io/os: windows".
(See https://kubernetes.io/docs/concepts/windows/user-guide/.)

This functionality is guarded behind a feature flag that defaults to false,
as it may not work on all Kubernetes implementations.
  • Loading branch information
lbernick committed May 8, 2023
1 parent 77c1698 commit e5dbd7d
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 72 deletions.
4 changes: 4 additions & 0 deletions config/config-feature-flags.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ data:
# If set to "none", then Tekton will not have non-falsifiable provenance.
# This is an experimental feature and thus should still be considered an alpha feature.
enforce-nonfalsifiablity: "none"
# Setting this flag to "true" will limit privileges for containers injected by Tekton into TaskRuns.
# This allows TaskRuns to run in namespaces with "restricted" pod security standards.
# Not all Kubernetes implementations support this option.
set-security-context: "false"
8 changes: 8 additions & 0 deletions docs/additional-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ to better fit specific usecases.
Out-of-the-box, Tekton Pipelines Controller is configured for relatively small-scale deployments but there have several options for configuring Pipelines' performance are available. See the [Performance Configuration](tekton-controller-performance-configuration.md) document which describes how to change the default ThreadsPerController, QPS and Burst settings to meet your requirements.
## Running TaskRuns and PipelineRuns with restricted pod security standards
To allow TaskRuns and PipelineRuns to run in namespaces with [restricted pod security standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/),
set the "set-security-context" feature flag to "true" in the [feature-flags configMap](#customizing-the-pipelines-controller-behavior). This configuration option applies a [SecurityContext](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
to any containers injected into TaskRuns by the Pipelines controller. This SecurityContext may not be supported in all Kubernetes implementations (for example, OpenShift).
**Note**: running TaskRuns and PipelineRuns in the "tekton-pipelines" namespace is discouraged.
## Platform Support
The Tekton project provides support for running on x86 Linux Kubernetes nodes.
Expand Down
7 changes: 7 additions & 0 deletions pkg/apis/config/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const (
DefaultResultExtractionMethod = ResultExtractionMethodTerminationMessage
// DefaultMaxResultSize is the default value in bytes for the size of a result
DefaultMaxResultSize = 4096
// DefaultSetSecurityContext is the default value for "set-security-context"
DefaultSetSecurityContext = false

disableAffinityAssistantKey = "disable-affinity-assistant"
disableCredsInitKey = "disable-creds-init"
Expand All @@ -90,6 +92,7 @@ const (
enableProvenanceInStatus = "enable-provenance-in-status"
resultExtractionMethod = "results-from"
maxResultSize = "max-result-size"
setSecurityContextKey = "set-security-context"
)

// FeatureFlags holds the features configurations
Expand All @@ -116,6 +119,7 @@ type FeatureFlags struct {
EnableProvenanceInStatus bool
ResultExtractionMethod string
MaxResultSize int
SetSecurityContext bool
}

// GetFeatureFlagsConfigName returns the name of the configmap containing all
Expand Down Expand Up @@ -179,6 +183,9 @@ func NewFeatureFlagsFromMap(cfgMap map[string]string) (*FeatureFlags, error) {
if err := setEnforceNonFalsifiability(cfgMap, tc.EnableAPIFields, &tc.EnforceNonfalsifiability); err != nil {
return nil, err
}
if err := setFeature(setSecurityContextKey, DefaultSetSecurityContext, &tc.SetSecurityContext); err != nil {
return nil, err
}

// Given that they are alpha features, Tekton Bundles and Custom Tasks should be switched on if
// enable-api-fields is "alpha". If enable-api-fields is not "alpha" then fall back to the value of
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/config/feature_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
EnableProvenanceInStatus: config.DefaultEnableProvenanceInStatus,
ResultExtractionMethod: config.DefaultResultExtractionMethod,
MaxResultSize: config.DefaultMaxResultSize,
SetSecurityContext: config.DefaultSetSecurityContext,
},
fileName: config.GetFeatureFlagsConfigName(),
},
Expand All @@ -68,6 +69,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
EnableProvenanceInStatus: true,
ResultExtractionMethod: "termination-message",
MaxResultSize: 4096,
SetSecurityContext: true,
},
fileName: "feature-flags-all-flags-set",
},
Expand All @@ -87,6 +89,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
VerificationNoMatchPolicy: config.DefaultNoMatchPolicyConfig,
ResultExtractionMethod: config.DefaultResultExtractionMethod,
MaxResultSize: config.DefaultMaxResultSize,
SetSecurityContext: config.DefaultSetSecurityContext,
},
fileName: "feature-flags-enable-api-fields-overrides-bundles-and-custom-tasks",
},
Expand All @@ -104,6 +107,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
VerificationNoMatchPolicy: config.DefaultNoMatchPolicyConfig,
ResultExtractionMethod: config.DefaultResultExtractionMethod,
MaxResultSize: config.DefaultMaxResultSize,
SetSecurityContext: config.DefaultSetSecurityContext,
},
fileName: "feature-flags-bundles-and-custom-tasks",
},
Expand All @@ -121,6 +125,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
VerificationNoMatchPolicy: config.DefaultNoMatchPolicyConfig,
ResultExtractionMethod: config.DefaultResultExtractionMethod,
MaxResultSize: config.DefaultMaxResultSize,
SetSecurityContext: config.DefaultSetSecurityContext,
},
fileName: "feature-flags-beta-api-fields",
},
Expand All @@ -134,6 +139,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
AwaitSidecarReadiness: config.DefaultAwaitSidecarReadiness,
ResultExtractionMethod: config.DefaultResultExtractionMethod,
MaxResultSize: config.DefaultMaxResultSize,
SetSecurityContext: config.DefaultSetSecurityContext,
},
fileName: "feature-flags-enforce-nonfalsifiability-spire",
},
Expand All @@ -145,6 +151,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) {
AwaitSidecarReadiness: config.DefaultAwaitSidecarReadiness,
ResultExtractionMethod: config.ResultExtractionMethodSidecarLogs,
MaxResultSize: 8192,
SetSecurityContext: config.DefaultSetSecurityContext,
},
fileName: "feature-flags-results-via-sidecar-logs",
},
Expand Down Expand Up @@ -175,6 +182,7 @@ func TestNewFeatureFlagsFromEmptyConfigMap(t *testing.T) {
EnableProvenanceInStatus: config.DefaultEnableProvenanceInStatus,
ResultExtractionMethod: config.DefaultResultExtractionMethod,
MaxResultSize: config.DefaultMaxResultSize,
SetSecurityContext: config.DefaultSetSecurityContext,
}
verifyConfigFileWithExpectedFeatureFlagsConfig(t, FeatureFlagsConfigEmptyName, expectedConfig)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/config/testdata/feature-flags-all-flags-set.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ data:
enforce-nonfalsifiability: "spire"
trusted-resources-verification-no-match-policy: "fail"
enable-provenance-in-status: "true"
set-security-context: "true"
67 changes: 58 additions & 9 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ const (

// SpiffeCsiDriver is the CSI storage plugin needed for injection of SPIFFE workload api.
SpiffeCsiDriver = "csi.spiffe.io"

// osSelectorLabel is the label Kubernetes uses for OS-specific workloads (https://kubernetes.io/docs/reference/labels-annotations-taints/#kubernetes-io-os)
osSelectorLabel = "kubernetes.io/os"
)

// These are effectively const, but Go doesn't have such an annotation.
Expand Down Expand Up @@ -99,6 +102,27 @@ var (

// MaxActiveDeadlineSeconds is a maximum permitted value to be used for a task with no timeout
MaxActiveDeadlineSeconds = int64(math.MaxInt32)

// Used in security context of pod init containers
allowPrivilegeEscalation = false
runAsNonRoot = true

// The following security contexts allow init containers to run in namespaces
// with "restricted" pod security admission
// See https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
linuxSecurityContext = &corev1.SecurityContext{
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
RunAsNonRoot: &runAsNonRoot,
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
}
windowsSecurityContext = &corev1.SecurityContext{
RunAsNonRoot: &runAsNonRoot,
}
)

// Builder exposes options to configure Pod construction from TaskSpecs/Runs.
Expand Down Expand Up @@ -127,6 +151,7 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
defaultForbiddenEnv := config.FromContextOrDefaults(ctx).Defaults.DefaultForbiddenEnv
alphaAPIEnabled := featureFlags.EnableAPIFields == config.AlphaAPIFields
sidecarLogsResultsEnabled := config.FromContextOrDefaults(ctx).FeatureFlags.ResultExtractionMethod == config.ResultExtractionMethodSidecarLogs
setSecurityContext := config.FromContextOrDefaults(ctx).FeatureFlags.SetSecurityContext

// Add our implicit volumes first, so they can be overridden by the user if they prefer.
volumes = append(volumes, implicitVolumes...)
Expand Down Expand Up @@ -161,9 +186,10 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
if alphaAPIEnabled && taskRun.Spec.ComputeResources != nil {
tasklevel.ApplyTaskLevelComputeResources(steps, taskRun.Spec.ComputeResources)
}
windows := usesWindows(taskRun)
if sidecarLogsResultsEnabled && taskSpec.Results != nil {
// create a results sidecar
resultsSidecar := createResultsSidecar(taskSpec, b.Images.SidecarLogResultsImage)
resultsSidecar := createResultsSidecar(taskSpec, b.Images.SidecarLogResultsImage, setSecurityContext, windows)
taskSpec.Sidecars = append(taskSpec.Sidecars, resultsSidecar)
commonExtraEntrypointArgs = append(commonExtraEntrypointArgs, "-result_from", config.ResultExtractionMethodSidecarLogs)
}
Expand All @@ -173,15 +199,15 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
}

initContainers = []corev1.Container{
entrypointInitContainer(b.Images.EntrypointImage, steps),
entrypointInitContainer(b.Images.EntrypointImage, steps, setSecurityContext, windows),
}

// Convert any steps with Script to command+args.
// If any are found, append an init container to initialize scripts.
if alphaAPIEnabled {
scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, b.Images.ShellImageWin, steps, sidecars, taskRun.Spec.Debug)
scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, b.Images.ShellImageWin, steps, sidecars, taskRun.Spec.Debug, setSecurityContext)
} else {
scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, "", steps, sidecars, nil)
scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, "", steps, sidecars, nil, setSecurityContext)
}

if scriptsInit != nil {
Expand All @@ -192,7 +218,7 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
volumes = append(volumes, debugScriptsVolume, debugInfoVolume)
}
// Initialize any workingDirs under /workspace.
if workingDirInit := workingDirInit(b.Images.WorkingDirInitImage, stepContainers); workingDirInit != nil {
if workingDirInit := workingDirInit(b.Images.WorkingDirInitImage, stepContainers, setSecurityContext, windows); workingDirInit != nil {
initContainers = append(initContainers, *workingDirInit)
}

Expand Down Expand Up @@ -497,16 +523,20 @@ func runVolume(i int) corev1.Volume {
}
}

// entrypointInitContainer generates a few init containers based of a set of command (in images) and volumes to run
// entrypointInitContainer generates a few init containers based of a set of command (in images), volumes to run, and whether the pod will run on a windows node
// This should effectively merge multiple command and volumes together.
func entrypointInitContainer(image string, steps []v1beta1.Step) corev1.Container {
func entrypointInitContainer(image string, steps []v1beta1.Step, setSecurityContext, windows bool) corev1.Container {
// Invoke the entrypoint binary in "cp mode" to copy itself
// into the correct location for later steps and initialize steps folder
command := []string{"/ko-app/entrypoint", "init", "/ko-app/entrypoint", entrypointBinary}
for i, s := range steps {
command = append(command, StepName(s.Name, i))
}
volumeMounts := []corev1.VolumeMount{binMount, internalStepsMount}
securityContext := linuxSecurityContext
if windows {
securityContext = windowsSecurityContext
}

// Rewrite steps with entrypoint binary. Append the entrypoint init
// container to place the entrypoint binary. Also add timeout flags
Expand All @@ -521,20 +551,39 @@ func entrypointInitContainer(image string, steps []v1beta1.Step) corev1.Containe
Command: command,
VolumeMounts: volumeMounts,
}
if setSecurityContext {
prepareInitContainer.SecurityContext = securityContext
}
return prepareInitContainer
}

// createResultsSidecar creates a sidecar that will run the sidecarlogresults binary.
func createResultsSidecar(taskSpec v1beta1.TaskSpec, image string) v1beta1.Sidecar {
func createResultsSidecar(taskSpec v1beta1.TaskSpec, image string, setSecurityContext, windows bool) v1beta1.Sidecar {
names := make([]string, 0, len(taskSpec.Results))
for _, r := range taskSpec.Results {
names = append(names, r.Name)
}
securityContext := linuxSecurityContext
if windows {
securityContext = windowsSecurityContext
}
resultsStr := strings.Join(names, ",")
command := []string{"/ko-app/sidecarlogresults", "-results-dir", pipeline.DefaultResultPath, "-result-names", resultsStr}
return v1beta1.Sidecar{
sidecar := v1beta1.Sidecar{
Name: pipeline.ReservedResultsSidecarName,
Image: image,
Command: command,
}
if setSecurityContext {
sidecar.SecurityContext = securityContext
}
return sidecar
}

func usesWindows(tr *v1beta1.TaskRun) bool {
if tr.Spec.PodTemplate == nil || tr.Spec.PodTemplate.NodeSelector == nil {
return false
}
osSelector := tr.Spec.PodTemplate.NodeSelector[osSelectorLabel]
return osSelector == "windows"
}
Loading

0 comments on commit e5dbd7d

Please sign in to comment.