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 throughenv:and reference the env var quoted ("$NAME"). - TKN-007,
TaskRun/PipelineRunmust setserviceAccountNameto 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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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:dindas 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.ymlfor 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
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 curlfor 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)
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
- Create a new module at
pipeline_check/core/checks/tekton/rules/tknNNN_<name>.pyexporting a top-levelRULE = Rule(...)and acheck(ctx: TektonContext) -> Findingfunction. The orchestrator auto-discoversRULEand callscheckwith theTektonContext. - Add a mapping for the new ID in
pipeline_check/core/standards/data/owasp_cicd_top_10.py(and any other standard that applies). - Drop unsafe/safe snippets at
tests/fixtures/per_check/tekton/TKN-NNN.{unsafe,safe}.ymland add aCheckCaseentry intests/test_per_check_real_examples.py::CASES. - Regenerate this doc: