08

Auth & Pod config

A TaskRun ends up as a Pod, and that Pod runs as a ServiceAccount. The SA's annotated Secrets become the git and registry credentials your steps see. Its RBAC bindings decide what kubectl inside a step is allowed to do. The pod template decides which nodes the Pod lands on, which user it runs as, and whether a hardened namespace admits it at all. This chapter goes through each of these settings and what goes wrong when they are too loose or too tight.
Secrets listed on the TaskRun's ServiceAccount and annotated with tekton.dev/git-N or tekton.dev/docker-N are rendered into config files before the first step runs. The tool then picks the matching entry: git by protocol and host, ssh by Host and port, docker by registry.
step talks to

ServiceAccount build-bot → secrets

apiVersion: v1
kind: ServiceAccount
metadata:
name: build-bot
secrets:
- name: github-token
- name: gitlab-deploy-key
- name: corp-git-key
- name: ghcr-push
- name: quay-pull

written before step 1 → copied to each step's $HOME

~/.gitconfig
[credential]
helper = store
[credential "https://github.com"]
username = ci-bot
~/.git-credentials
https://ci-bot:****@github.com
~/.ssh/config
Host gitlab.com
HostName gitlab.com
IdentityFile ~/.ssh/id_gitlab-deploy-key
Port 22
Host git.corp.internal
HostName git.corp.internal
IdentityFile ~/.ssh/id_corp-git-key
Port 2222
~/.ssh/id_gitlab-deploy-key
-----BEGIN OPENSSH PRIVATE KEY----- …
~/.ssh/id_corp-git-key
-----BEGIN OPENSSH PRIVATE KEY----- …
~/.ssh/known_hosts
gitlab.com ssh-ed25519 AAAAC3Nz…
~/.docker/config.json
{
"auths": {
"https://ghcr.io": { "auth": "****" },
"quay.io": { "auth": "****" }
}
}
$ git clone git@github.com:acme/app.git
git@github.com: Permission denied (publickey).

github-token holds an https token for github.com; ssh never looks at ~/.git-credentials. Use the https URL or add an ssh-auth secret.

matched secret
none
outcome
denied
secrets in every step
5
all attached ones, used or not
files written
7

Try: clone git@github.com:… — the https token doesn't help ssh. Then pick the corp repo on port 2222 and untick corp-git-key. Notice that the files panel always holds every attached secret: a step that only clones still gets the registry push token.

RBAC: which tasks get which ServiceAccount

tekton-rbacauth-least-privilegetask-run-templatetask-run-specs
The SA token is mounted into the Pod, so a step running kubectl acts as that SA. taskRunTemplate sets one SA for every TaskRun of a PipelineRun. taskRunSpecs overrides it for a single pipeline task. Use that to keep cluster rights in the one task that needs them.
clonetaskRunTemplate
git clone (talks to GitHub, not the API server)
sa: pipeline-admin
need 0missing 0excess 35
can read every Secret in the namespace
buildtaskRunTemplate
build + push image (talks to the registry)
sa: pipeline-admin
need 0missing 0excess 35
can read every Secret in the namespace
deploytaskRunTemplate
kubectl apply -f k8s/ (Deployment web, Service web)
sa: pipeline-admin
need 4missing 0excess 31
can read every Secret in the namespace

Role deployer — click cells to grant / revoke

getlistwatchcreatepatchupdatedelete
deployments
services
configmaps
secrets
pods
deploy $ kubectl apply -f k8s/ # as system:serviceaccount:ci:pipeline-admin
deployment.apps/web configured
service/web configured
deploy
Succeeded
excess grants
101
granted but never used, all tasks
tasks that can read Secrets
3
pods with a deploy-capable token
3
PipelineRun: who runs each TaskRun
kind: PipelineRun
spec:
pipelineRef: { name: ship }
taskRunTemplate:
serviceAccountName: pipeline-admin
Role generated from the grid (yellow = beyond what deploy needs)
kind: Role # bound to SA deployer by a RoleBinding
metadata: { name: deployer, namespace: ci }
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "create", "patch", "update", "delete"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "create", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]

Try: set the template SA to default and watch deploy get Forbidden. Then tick "override deploy via taskRunSpecs" with deployer and press "least privilege": clone and build hold no API rights, deploy holds exactly four. Untick "objects already exist" and see the needed verb change from patch to create.

Getting a secret into a step: four ways, four exposures

secret-workspacesecret-env-refcredential-initialization
Credentials can arrive as files written from SA secrets, as a Secret bound to a workspace, as an env var from a secretKeyRef, or (please don't) as a plain param. They differ in which steps can read the secret and where it can leak.

TaskRun Pod: who can reach the registry token

step-clone
git clone
reads ~/.docker/config.json
step-test
npm test (third-party code)
reads ~/.docker/config.json
step-push
push image (needs the token)
reads ~/.docker/config.json
what you write
# Secret ghcr-push, annotated tekton.dev/docker-0, listed on
# the ServiceAccount. Nothing in the Task mentions it.
kind: PipelineRun
spec:
taskRunTemplate:
serviceAccountName: build-bot
step-test logs (Tekton does not redact secrets)
$ npm test
+ env # a post-install script dumps the environment
HOME=/tekton/home
PATH=/usr/local/bin:/usr/bin
✓ 42 passing
steps with access
3 / 3
only push needs it
value in API objects
no
kubectl get taskrun -o yaml
leaked to logs
no
in process env
no

trade-offs

approachwho sees itin env?in API objects?log leak pathuse for
Annotated SA credsevery step of every TaskRun using the SAno (files in $HOME)noonly if a step cats the filegit clone / image push for tools that read ~/.gitconfig or config.json
Secret workspacetasks that bind it; one step if isolatedno (read-only files)no (only the Secret name)only if a step cats the filedefault choice: explicit, per-task, auditable
env secretKeyRefsteps that declare it (all, via stepTemplate)yes, inherited by child processesno (Pod shows the reference)env, set -x, crash dumpstools that only take an env var
Plain paramwherever it's referenceddependsyes: PipelineRun, TaskRun, Pod spec, Resultsvery likelynever

Try: with "test step prints its env" on, compare env secretKeyRef in stepTemplate (token in the test logs) with a secret workspace isolated to the push step (one step can read it, nothing in env). Annotated creds reach all three steps.

podTemplate: three layers, merged field by field

pod-templatedefault-pod-templateimage-pull-secretstask-run-specs
Tekton merges up to three pod templates. The cluster default comes from config-defaults, then the PipelineRun's taskRunTemplate, then the taskRunSpecs entry for this pipeline task. A higher layer replaces a whole field. Only env and volumes are merged by name.
defaulttaskRunTemplatetaskRunSpecs

winsPipelineRun · taskRunSpecs[build].podTemplate

thenPipelineRun · taskRunTemplate.podTemplate

fallbackconfig-defaults · default-pod-template

the Pod Tekton creates
# effective podTemplate for TaskRun ship-run-build
nodeSelector:replaced default, template
kubernetes.io/arch: arm64
tolerations:template
- key: dedicated
value: ci
effect: NoSchedule
affinity:spec
nodeAffinity: { … zone in [eu-west-1a] }
securityContext:default
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
imagePullSecrets:default
- name: corp-registry
env:
- name: HTTP_PROXY
value: http://proxy.corp:3128default
- name: LOG_LEVEL
value: debugspec
- name: NO_PROXY
value: .svc,.cluster.localtemplate
priorityClassName: ci-highreplaced template
hostAliases:spec
- ip: 10.0.0.5
hostnames: [registry.corp]
dnsConfig:default
options: [{ name: ndots, value: "2" }]

nodeSelector is replaced, not deep-merged: the default's kubernetes.io/os: linux is gone. Repeat it in the higher layer if you still need it.

fields from default
4
from taskRunTemplate
2
from taskRunSpecs
5
fields replaced
2
whole field, lower layer dropped

Try: on build, untick nodeSelector in taskRunSpecs and see taskRunTemplate's value come back. env merges by name instead: LOG_LEVEL comes from the highest layer that sets it while HTTP_PROXY still flows up from the cluster default. Switch to test, which has no taskRunSpecs entry.

A podTemplate can only set Pod-level things. Per-container settings like a step's securityContext or resources live on the Task (steps, stepTemplate) or in taskRunSpecs (stepSpecs, sidecarSpecs, computeResources).

Running unprivileged, building images without a daemon

pod-security-admissionpod-security-contextset-security-contextdaemonless-build
Label a namespace with the restricted Pod Security profile and every container in the Pod must run as non-root, drop all capabilities, forbid privilege escalation and use a seccomp profile. That includes the init containers Tekton injects. Builders that need a daemon or root don't pass.

admission check, container by container

containerallowPrivilegeEscalation != falsecapabilities.drop != [ALL]runAsUser = 0
prepare(init, Tekton)✗ deny✗ deny✓
place-scripts(init, Tekton)✗ deny✗ deny✓
step-build(your step)✗ deny✗ deny✗ deny
what drives it
# namespace label
pod-security.kubernetes.io/enforce: restricted
# config-feature-flags
set-security-context: "false"
# step-build securityContext
securityContext:
runAsUser: 0
seccompProfile: { type: RuntimeDefault }
$ tkn pr describe ship-run
TaskRun ship-run-build: failed to create task run pod: pods "ship-run-build-pod" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (containers "prepare", "place-scripts", "step-build"), capabilities.drop != [ALL] (containers "prepare", "place-scripts", "step-build"), runAsUser = 0 (containers "step-build")

No daemon, no privileged flag, but it runs as root (uid 0) to unpack the base image filesystem. Google archived the original repo in 2025; maintained forks continue it.

admission
rejected
violations enforced
7
7 at restricted
from Tekton init ctrs
4
build
fails

Try: pick ko under restricted — it is still rejected until you turn on set-security-context, because Tekton's own init containers don't set allowPrivilegeEscalation or drop capabilities (a pod-level securityContext can't set those). Then pick kaniko: drop to baseline, or force a restricted context on the step and watch admission pass but the build break.

Setting a restricted securityContext on a builder that needs root doesn't make it safe. Admission passes and the build fails instead. To get through restricted you need a builder that runs without root or a daemon, or a separate namespace with a looser profile used only for image builds.

Compute resources: steps run in sequence, requests add up

step-compute-resources
Each step is an ordinary container with its own requests and limits. The scheduler doesn't know the steps take turns, so it reserves the sum for the Pod's whole life. A LimitRange in the namespace fills in missing values and rejects containers outside its min and max.

CPU reserved by the scheduler vs used by the step that's running

0m12clone · 10sbuild · 60stest · 30spod request 2500m reserved for all 100s
CPU used while the step runsthat step's own requestsum of all requests = what the scheduler reserves
pod CPU request
2500m
Σ steps (init: max)
admission
ok
pods per 4-CPU node
1
reserved CPU actually used
46%
steps + LimitRange
steps:
- name: clone
computeResources: { requests: { cpu: 200m } }
- name: build
computeResources: { requests: { cpu: 1500m } }
- name: test
computeResources: { requests: { cpu: 800m } }
---
kind: LimitRange
spec: { limits: [{ type: Container,
defaultRequest: { cpu: 1200m }, default: { cpu: 2 },
min: { cpu: 100m }, max: { cpu: 2 } }] }

Try: the steps run one after another, yet the dashed box reserves their sum for the whole run (about 50% wasted here). Set every request to unset: with the LimitRange each step gets 1200m/3 = 400m, without it the Pod is BestEffort. Push build above 2 and the Pod is rejected.

Older Tekton releases zeroed every request except the largest step's to work around this. Current releases leave step requests as written. If you'd rather size the Task as a whole, TaskRun computeResources sets a task-level budget instead of per-step values.
  • takeawayThe ServiceAccount is the identity of a TaskRun. Its secrets reach every step, and its RBAC token works from any step. Give each pipeline task its own SA with taskRunSpecs.
  • takeawayCredentials match on URL. An annotated https token doesn't help an ssh clone, and the wrong port doesn't match either. Prefer Secret workspaces isolated to the step that needs them.
  • takeawayPod templates replace whole fields from the cluster default up to taskRunSpecs. Under Pod Security restricted, turn on set-security-context and use a builder that needs no root.

Key concepts · 20

book chapter 8
  1. ServiceAccount (for a TaskRun)

    The Kubernetes identity a TaskRun's Pod runs as; defaults to the namespace's default SA unless set.

    It carries two things at once: the Secrets that credential initialization turns into git/docker config, and the RBAC token steps use against the API server. Pick it per task, not per cluster.

    spec.serviceAccountNameconfig-defaults: default-service-account
    ↑ see it in the visualization
  2. Credential initialization

    Before the first step, Tekton turns the SA's annotated Secrets into ~/.gitconfig, ~/.git-credentials, ~/.ssh/* and ~/.docker/config.json under /tekton/creds, then copies them into each step's $HOME.

    Tools like git and crane pick credentials up with no Task changes. The flip side: every attached secret lands in every step, whether that step needs it or not.

    /tekton/credsdisable-creds-init feature flag
    ↑ see it in the visualization
  3. tekton.dev/git-* and tekton.dev/docker-* annotations

    Annotations on a Secret that tell Tekton which host (or registry) the credential is for, e.g. tekton.dev/git-0: https://github.com.

    The annotation is the match key. git matches on protocol + host, ssh on Host and port, docker on the registry. An https token does nothing for an ssh URL.

    tekton.dev/git-0: https://github.comtekton.dev/docker-0: https://ghcr.io
    ↑ see it in the visualization
  4. basic-auth Secret

    A kubernetes.io/basic-auth Secret (username + password/token), usable for git over https or a registry.

    For git it becomes a [credential] entry plus a line in ~/.git-credentials used by the store helper. For docker it becomes an auths entry.

    type: kubernetes.io/basic-auth
    ↑ see it in the visualization
  5. ssh-auth Secret

    A kubernetes.io/ssh-auth Secret with ssh-privatekey and optionally known_hosts, for git over ssh.

    It becomes a Host block in ~/.ssh/config pointing at the key. Without known_hosts the server's host key is not pinned, so add it.

    type: kubernetes.io/ssh-authknown_hosts
    ↑ see it in the visualization
  6. dockerconfigjson Secret

    A kubernetes.io/dockerconfigjson Secret holding a full Docker config; attached to the SA, its auths are merged into ~/.docker/config.json.

    No annotation needed. Not the same thing as imagePullSecrets, which the kubelet uses to pull step images.

    type: kubernetes.io/dockerconfigjson
    ↑ see it in the visualization
  7. RBAC Role / RoleBinding

    Kubernetes rules (apiGroups, resources, verbs) bound to a ServiceAccount, which decide what kubectl in a step may do.

    A deploy task's kubectl apply needs get plus create or patch on its objects. Anything more is reach an attacker inherits if a step is compromised.

    kind: Rolekind: RoleBindingkubectl auth can-i
    ↑ see it in the visualization
  8. Least privilege per task

    Give each pipeline task an SA that can do exactly its job: clone and build get no API rights, deploy gets a narrow Role.

    One shared, powerful SA means every step, including third-party test code, holds a token that can deploy or read Secrets.

    ↑ see it in the visualization
  9. taskRunTemplate

    PipelineRun field that sets serviceAccountName and podTemplate for every TaskRun it creates.

    The pipeline-wide default. It replaced the older top-level serviceAccountName / podTemplate fields in the v1 API.

    spec.taskRunTemplate.serviceAccountNamespec.taskRunTemplate.podTemplate
    ↑ see it in the visualization
  10. taskRunSpecs

    PipelineRun field that overrides serviceAccountName, podTemplate, compute resources and more for one named pipeline task.

    This is how you give only the deploy task the deployer SA, or only the build task arm64 nodes.

    spec.taskRunSpecs[].pipelineTaskNamespec.taskRunSpecs[].podTemplate
    ↑ see it in the visualization
  11. Secret as a workspace

    Binding a Secret to a workspace mounts its keys as read-only files in the Task, optionally only in the steps that declare it.

    Explicit and auditable: the Task says it needs a credential, only the bound tasks get it, and nothing lands in env or in API objects.

    workspaces[].secret.secretNamesteps[].workspaces (isolated)
    ↑ see it in the visualization
  12. env from secretKeyRef

    A step env var whose value comes from a Secret key; the Pod spec shows only the reference.

    Convenient for tools that only read env vars, but env is inherited by child processes and easily printed. Tekton does not redact secrets in logs.

    env[].valueFrom.secretKeyRefstepTemplate.env
    ↑ see it in the visualization
  13. podTemplate

    A subset of Pod spec fields (nodeSelector, tolerations, affinity, securityContext, imagePullSecrets, env, hostAliases, dnsConfig, priorityClassName, volumes…) applied to the TaskRun's Pod.

    It's how you control where and how a Task runs without editing the Task: a Task stays portable, the run carries the cluster-specific bits.

    TaskRun spec.podTemplatenodeSelectortolerationshostAliasespriorityClassName
    ↑ see it in the visualization
  14. Default pod template

    The default-pod-template key in the config-defaults ConfigMap, merged under every TaskRun's podTemplate.

    Cluster-wide defaults like proxies, pull secrets or a non-root securityContext. Fields are replaced wholesale by higher layers; env and volumes merge by name.

    config-defaults: default-pod-template
    ↑ see it in the visualization
  15. imagePullSecrets

    Secrets the kubelet uses to pull the step and sidecar images, set on the SA or in podTemplate.

    Pulling the image that runs a step is the kubelet's job. Pushing an image from inside a step uses credential initialization or a workspace.

    podTemplate.imagePullSecretsServiceAccount imagePullSecrets
    ↑ see it in the visualization
  16. securityContext

    Pod- and container-level settings: runAsNonRoot, runAsUser, allowPrivilegeEscalation, capabilities, seccompProfile, privileged.

    Some fields, like allowPrivilegeEscalation and capabilities, only exist per container, so a pod-level securityContext from podTemplate can't make every container compliant by itself.

    steps[].securityContextpodTemplate.securityContext
    ↑ see it in the visualization
  17. Pod Security Admission

    The built-in admission controller that enforces the privileged / baseline / restricted profiles per namespace via labels.

    Under restricted a privileged DinD build won't even get a Pod: the TaskRun fails at creation with a 'violates PodSecurity' message.

    pod-security.kubernetes.io/enforce: restricted
    ↑ see it in the visualization
  18. set-security-context flag

    Feature flag that makes Tekton give its injected init containers (and its own containers) a restricted-compatible securityContext.

    Without it, Tekton's prepare and place-scripts containers fail the restricted profile even when your steps are clean.

    config-feature-flags: set-security-context
    ↑ see it in the visualization
  19. Daemonless image builds (kaniko, buildah, ko)

    Building container images inside a step without a Docker daemon or privileged mode.

    kaniko drops the privileged flag but still runs as root, rootless buildah needs privilege escalation for user namespaces, and ko/jib need nothing special because they never run RUN instructions.

    kanikobuildah --storage-driver=vfskojib
    ↑ see it in the visualization
  20. Step compute resources & LimitRange

    Each step's computeResources requests/limits; a namespace LimitRange supplies defaults and min/max per container.

    Steps run one at a time but are regular containers, so the scheduler reserves the sum of their requests. Tekton splits a LimitRange default request across the steps that don't set one.

    steps[].computeResourcesTaskRun spec.computeResourceskind: LimitRange
    ↑ see it in the visualization