Skip to content

Tekton provider

Parses Tekton API documents (apiVersion: tekton.dev/*) from .yaml / .yml files on disk, text-only static analysis, no tkn binary, no cluster access. Recognized kinds: Task, ClusterTask, Pipeline, TaskRun, PipelineRun. Documents that don't carry a tekton.dev/* apiVersion are silently skipped, so a directory mixing Tekton with plain Kubernetes manifests is safe to point at.

Producer workflow

pipeline_check --pipeline tekton --tekton-path tekton/

# A single multi-document file works too.
pipeline_check --pipeline tekton --tekton-path tekton/build-task.yaml

All other flags (--output, --severity-threshold, --checks, --standard, …) behave the same as with the other providers.

Tekton-specific checks

  • TKN-003. Tekton substitutes $(params.X) before the shell parses the script, so any unquoted use is a command-injection primitive. The safe pattern is to receive the parameter through env: and reference the env var quoted ("$NAME").
  • TKN-007, TaskRun / PipelineRun must set serviceAccountName to a least-privilege ServiceAccount. The default SA inherits whatever cluster-admin or wildcard role someone later binds to it.

What it covers

16 checks · 2 have an autofix patch (--fix).

Check Title Severity Fix
TAINT-006 Untrusted input flows across tasks via Tekton results HIGH
TKN-001 Tekton step image not pinned to a digest HIGH
TKN-002 Tekton step runs privileged or as root HIGH
TKN-003 Tekton param interpolated unsafely in step script CRITICAL
TKN-004 Tekton Task mounts hostPath or shares host namespaces CRITICAL
TKN-005 Literal secret value in Tekton step env or param default CRITICAL 🔧 fix
TKN-006 Tekton run lacks an explicit timeout LOW
TKN-007 Tekton run uses the default ServiceAccount MEDIUM
TKN-008 Tekton step script pipes remote install or disables TLS HIGH 🔧 fix
TKN-009 Artifacts not signed (no cosign/sigstore step) MEDIUM
TKN-010 No SBOM generated for build artifacts MEDIUM
TKN-011 No SLSA provenance attestation produced MEDIUM
TKN-012 No vulnerability scanning step MEDIUM
TKN-013 Tekton sidecar runs privileged or as root HIGH
TKN-014 Tekton step script runs unpinned package install MEDIUM
TKN-015 Workspace subPath interpolates a Task parameter (path traversal) HIGH

TAINT-006: Untrusted input flows across tasks via Tekton results

HIGH CICD-SEC-4 CICD-SEC-1 ESF-D-INJECTION CWE-78 CWE-829

Detection walks every Pipeline document. Pass 1 looks for tasks whose body's steps[*].script writes to $(results.<X>.path) AND interpolates a $(params.<Y>) reference, recording X as a tainted result for that producer task. Pass 2 walks every task for params: whose value: is $(tasks.<producer>.results.<X>). When (producer, X) matches a tainted result and the consumer's body's steps[*].script references $(params.<consumer-name>) (where consumer-name is the param the result was forwarded into), TAINT-006 fires.

Body resolution: inline taskSpec: blocks are walked directly; taskRef: { name: <X> } references resolve against Task / ClusterTask documents loaded into the same scan, so a Pipeline that splits the producer / consumer task definitions into separate files still trips the rule. bundle: and resolver: (remote OCI / Tekton-resolver-framework references) aren't followed; they require network fetches the scanner deliberately avoids. finally: blocks aren't walked yet.

Known false-positive modes

  • If the producer task runs a sanitiser between the tainted $(params.X) interpolation and the $(results.Y.path) write, the consumer is no longer exploitable but TAINT-006 still fires. Suppress via ignore-file scoped to the consumer task name when this is the deliberate shape; the sanitiser is then load-bearing.

Recommended action

Sanitise the value at the producer task before it lands in $(results.<name>.path). The canonical safe pattern is to copy the $(params.<name>) source into an intermediate shell variable, run a sanitiser (tr -dc 'a-zA-Z0-9 ' for a freeform title), and only then write the cleaned value to the result file. The consumer task should still treat its own param as tainted: surface $(params.<name>) into a quoted shell variable (TITLE="$(params.title)") before interpolating elsewhere. Removing the cross-task results forwarding is the strongest fix; if the value genuinely needs to flow downstream, validate the sanitiser is doing what you think before relying on it.

TKN-001: Tekton step image not pinned to a digest

HIGH CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-829

Applies to Task and ClusterTask kinds. The image must contain @sha256: followed by a 64-char hex digest. Any tag-only reference, including :latest, fails.

Recommended action

Pin every step image to a content-addressable digest (gcr.io/tekton-releases/git-init@sha256:<digest>). Tag-only references (alpine:3.18) and rolling tags (alpine:latest) let a compromised registry update redirect the step at the next pull, with no audit trail in the Task manifest.

TKN-002: Tekton step runs privileged or as root

HIGH CICD-SEC-5 ESF-D-RUNTIME-HARDENING CWE-269 CWE-250

Detection fires on a step with securityContext.privileged: true, securityContext.runAsUser: 0, securityContext.runAsNonRoot: false, securityContext.allowPrivilegeEscalation: true, or no securityContext block at all.

Recommended action

Set securityContext.privileged: false, runAsNonRoot: true, and allowPrivilegeEscalation: false on every step. A privileged step shares the node's kernel namespaces; a malicious or compromised step image then has root on the build node, breaking the boundary between build and cluster.

TKN-003: Tekton param interpolated unsafely in step script

CRITICAL CICD-SEC-4 CICD-SEC-1 ESF-D-CODE-INTEGRITY CWE-78

Fires on any $(params.X) or $(workspaces.X.path) token inside a script: body that isn't already wrapped in double quotes ("$(params.X)"). Doesn't fire on the env-var indirection pattern, which is safe.

Recommended action

Don't interpolate $(params.<name>) directly into the step script:. Tekton substitutes the value before the shell parses it, so a parameter containing ; rm -rf / runs as shell. Receive the parameter through env: (valueFrom: ... or value: $(params.<name>)) and reference the env var quoted in the script ("$NAME"); or pass it as a positional argument to a shell function.

TKN-004: Tekton Task mounts hostPath or shares host namespaces

CRITICAL CICD-SEC-5 ESF-D-RUNTIME-HARDENING CWE-250 CWE-668

Checks spec.volumes[].hostPath (legacy v1beta1 form), spec.workspaces[].volumeClaimTemplate.spec.storageClassName == 'hostpath', and spec.podTemplate host-namespace flags.

Recommended action

Use Tekton workspaces: backed by emptyDir or persistentVolumeClaim instead of hostPath. Drop hostNetwork: true / hostPID: true / hostIPC: true on the Task's podTemplate. A hostPath mount of /var/run/docker.sock or / lets the build break out of the pod and act as the underlying node.

TKN-005: Literal secret value in Tekton step env or param default

CRITICAL 🔧 autofix CICD-SEC-6 CICD-SEC-7 ESF-D-SECRETS CWE-798

Strong matches: AWS access keys, GitHub PATs, JWTs. Weak match: env var name suggests a secret (*_TOKEN, *_KEY, *PASSWORD, *SECRET) and the value is a non-empty literal rather than a $(params.X) / valueFrom reference.

Recommended action

Mount secrets via env.valueFrom.secretKeyRef (or a volumes: Secret mount) instead of writing the value into env.value or params[].default. Task manifests are committed to git and cluster-readable; literal values leak through normal access paths.

TKN-006: Tekton run lacks an explicit timeout

LOW CICD-SEC-9 ESF-D-RUNTIME-HARDENING CWE-400

Applies to PipelineRun, TaskRun, and Pipeline. For Pipelines, the rule looks for spec.tasks[].timeout as evidence of intent. Task / ClusterTask themselves don't carry a timeout, the timeout lives on the concrete run.

Recommended action

Set spec.timeouts.pipeline (or spec.timeout on a TaskRun) on every PipelineRun and TaskRun. A misbehaving step otherwise pins a build pod for the cluster's default timeout (1h). For long jobs, set a generous explicit value (2h, 6h) rather than leaving it implicit.

TKN-007: Tekton run uses the default ServiceAccount

MEDIUM CICD-SEC-2 ESF-D-IAM CWE-250 CWE-732

An explicit serviceAccountName: default setting is treated the same as omission.

Recommended action

Set spec.serviceAccountName on every TaskRun and PipelineRun to a least-privilege ServiceAccount that carries only the secrets and RBAC the run actually needs. Falling back to the namespace's default SA grants access to whatever cluster-admin or wildcard role someone later binds to default, a privilege-escalation surface that should never be load-bearing for build pods.

TKN-008: Tekton step script pipes remote install or disables TLS

HIGH 🔧 autofix CICD-SEC-3 ESF-S-VERIFY-DEPS ESF-D-COMMS-INTEGRITY CWE-494 CWE-829 CWE-295

Uses the cross-provider CURL_PIPE_RE and TLS_BYPASS_RE regexes so detection is consistent with the GHA / GitLab / CircleCI / Cloud Build providers.

Recommended action

Replace curl ... | sh with a download-then-verify-then-execute pattern. Drop TLS-bypass flags (curl -k, git config http.sslverify false); install the missing CA into the step image instead. Both forms let an attacker controlling DNS / a transparent proxy substitute the script the step runs.

TKN-009: Artifacts not signed (no cosign/sigstore step)

MEDIUM CICD-SEC-9 ESF-D-SIGN-ARTIFACTS CWE-345

Detection mirrors GHA-006 / BK-009 / CC-006, the shared signing-token catalog (cosign, sigstore, slsa-github-generator, slsa-framework, notation-sign) is searched across every string in the Task / Pipeline document. The rule only fires on artifact-producing Tasks (those that invoke docker build / docker push / buildah / kaniko / helm upgrade / aws s3 sync / etc.) so lint-only Tasks don't trip it.

Recommended action

Add a signing step to the Task, either a dedicated cosign sign step after the build, or use the official cosign Tekton catalog Task as a referenced step. The Task should sign by digest (cosign sign --yes <repo>@sha256:<digest>) so a re-pushed tag can't bypass the signature.

TKN-010: No SBOM generated for build artifacts

MEDIUM CICD-SEC-9 ESF-S-SBOM CWE-1357

An SBOM (CycloneDX or SPDX) records every component baked into the build. Without one, post-incident triage can't answer did this CVE ship? for a given artifact. Detection uses the shared SBOM-token catalog: syft, cyclonedx, cdxgen, spdx-tools, microsoft/sbom-tool. Fires only on artifact-producing Tasks.

Recommended action

Add an SBOM-generation step. syft <artifact> -o cyclonedx-json > $(workspaces.output.path)/sbom.json runs in the official syft Tekton catalog Task. cyclonedx-cli and cdxgen are alternatives. Publish the SBOM as a Workspace result so downstream Tasks can consume it.

TKN-011: No SLSA provenance attestation produced

MEDIUM CICD-SEC-9 ESF-S-PROVENANCE ESF-D-SIGN-ARTIFACTS CWE-345

Provenance generation is distinct from signing. A signed artifact proves who published it; a provenance attestation proves where / how it was built. Tekton Chains is the Tekton-native answer, once enabled on the cluster, every TaskRun's outputs are signed and attested without per-Task wiring. Detection uses the shared provenance-token catalog (slsa-framework, cosign attest, in-toto, attest-build-provenance, witness run). Tasks produced by tekton-chains pass on the cosign attest match.

Recommended action

After the build step, run cosign attest --predicate slsa.json --type slsaprovenance <ref> (or use the tekton-chains controller, which signs and attests every TaskRun automatically when configured). Publish the attestation alongside the artifact so consumers can verify how it was built, not just who signed it.

TKN-012: No vulnerability scanning step

MEDIUM CICD-SEC-9 ESF-S-VULN-MGMT CWE-1104

Vulnerability scanning sits at a different layer from signing and SBOM. It answers does this artifact ship a known CVE? rather than can we verify what it is?. Detection uses the shared vuln-scan-token catalog: trivy, grype, snyk, npm-audit, pip-audit, osv-scanner, govulncheck, anchore, codeql-action, semgrep, bandit, checkov, tfsec, dependency-check. Walks every Task / Pipeline / *Run document; passes if any document includes a scanner reference.

Recommended action

Add a vulnerability scanner step. trivy fs $(workspaces.src.path) for source / filesystem; trivy image <ref> for container images. The official Tekton catalog ships trivy-scanner and grype-scanner Tasks if you'd rather reference one. Fail the step on findings above a chosen severity so a regression blocks the merge instead of shipping.

TKN-013: Tekton sidecar runs privileged or as root

HIGH CICD-SEC-5 ESF-D-RUNTIME-HARDENING CWE-269 CWE-250

TKN-002 hardens the spec.steps list. Tekton's spec.sidecars list runs alongside the steps in the same pod, but a sidecar's container image and command come from a separate place in the manifest, so a Task with hardened steps and a privileged sidecar (a common pattern when wrapping docker:dind) leaves the same kernel-namespace gap TKN-002 was meant to close. The detection mirrors TKN-002: fires on a sidecar with securityContext.privileged: true, runAsUser: 0, runAsNonRoot: false, allowPrivilegeEscalation: true, or no securityContext block at all.

Known false-positive modes

  • Tasks that genuinely need docker:dind as a sidecar, e.g. building images inside the cluster without giving the step itself host-Docker access. The replacement pattern is Kaniko or BuildKit running as the step itself, with no privileged sidecar; if neither is viable, ignore TKN-013 in .pipeline-check-ignore.yml for the affected Task.

Recommended action

Set securityContext.privileged: false, runAsNonRoot: true, and allowPrivilegeEscalation: false on every sidecar in spec.sidecars. A privileged sidecar is the same escape vector as a privileged step, it shares the pod's network and kernel namespaces, and a compromised sidecar image owns the entire TaskRun's execution surface.

TKN-014: Tekton step script runs unpinned package install

MEDIUM CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-829 CWE-1357

Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. Same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket / Azure DevOps / Jenkins / CircleCI / Cloud Build / Buildkite / Drone. Tekton was a gap; this closes it. Only Task and ClusterTask documents are scanned because that's where Tekton step scripts live.

Known false-positive modes

  • Bootstrap-stage installs that intentionally pull latest (apt-get install -y curl for a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific step name.

Recommended action

Pin every package install to a lockfile or a checksum-verified version. npm ci (not npm install), yarn install --frozen-lockfile, pip install -r requirements.txt --require-hashes, bundle install --frozen. Don't use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (TKN-008 covers the TLS subset; this rule covers the lockfile subset).

TKN-015: Workspace subPath interpolates a Task parameter (path traversal)

HIGH CICD-SEC-4 CICD-SEC-5 ESF-D-CODE-INTEGRITY ESF-D-RUNTIME-HARDENING CWE-22 CWE-73

Tekton's $(params.x) substitution is performed on every string field of the resolved TaskRun body, including a step-level workspace binding's subPath. TKN-003 catches the same parameter being interpolated into a step's script body; TKN-015 catches the complementary file-system breakout vector that script-only detection misses, the value never appears in a shell command, only in the volume-mount config.

The detection scans the step-level workspaces: list (spec.steps[*].workspaces[*].subPath) for any $(params.<name>) reference. $(workspaces.x.path) expansions are unaffected because those are not pusher-controlled.

Known false-positive modes

  • Some teams use a parameter to select between a small set of allowed sub-paths and rely on a step pre-check to reject anything off-list. The rule has no way to see that pre-check; suppress on the specific step name when this is the deliberate shape.

Recommended action

Pin every workspace subPath: to a static literal that your team controls. subPath: build/output is fine; subPath: $(params.target_dir) is not, because a parameter-driven sub-path lets an attacker break out of the workspace and write into a sibling directory of the shared volume. Tekton resolves $(params.x) substitution in workspace bindings before the volume mount happens, so ../../../etc lands as a real path. If you genuinely need a runtime-chosen sub-path, sanitise the parameter with a step-level pre-check (case against an allow-list, reject anything containing ..) and pass the validated value through a result rather than the raw parameter.


Adding a new Tekton check

  1. Create a new module at pipeline_check/core/checks/tekton/rules/tknNNN_<name>.py exporting a top-level RULE = Rule(...) and a check(ctx: TektonContext) -> Finding function. The orchestrator auto-discovers RULE and calls check with the TektonContext.
  2. Add a mapping for the new ID in pipeline_check/core/standards/data/owasp_cicd_top_10.py (and any other standard that applies).
  3. Drop unsafe/safe snippets at tests/fixtures/per_check/tekton/TKN-NNN.{unsafe,safe}.yml and add a CheckCase entry in tests/test_per_check_real_examples.py::CASES.
  4. Regenerate this doc:
python scripts/gen_provider_docs.py tekton