Auth & Pod config
Which secret does git use? Credential initialization
auth-service-accountcredential-initializationtekton-credential-annotationbasic-auth-secretssh-auth-secretdockerconfigjson-secretServiceAccount build-bot → secrets
apiVersion: v1kind: ServiceAccountmetadata:name: build-botsecrets:- 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
[credential]helper = store[credential "https://github.com"]username = ci-bot
https://ci-bot:****@github.com
Host gitlab.comHostName gitlab.comIdentityFile ~/.ssh/id_gitlab-deploy-keyPort 22Host git.corp.internalHostName git.corp.internalIdentityFile ~/.ssh/id_corp-git-keyPort 2222
-----BEGIN OPENSSH PRIVATE KEY----- …
-----BEGIN OPENSSH PRIVATE KEY----- …
gitlab.com ssh-ed25519 AAAAC3Nz…
{"auths": {"https://ghcr.io": { "auth": "****" },"quay.io": { "auth": "****" }}}
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.
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-specsRole deployer — click cells to grant / revoke
| get | list | watch | create | patch | update | delete | |
|---|---|---|---|---|---|---|---|
| deployments | |||||||
| services | |||||||
| configmaps | |||||||
| secrets | |||||||
| pods |
kind: PipelineRunspec:pipelineRef: { name: ship }taskRunTemplate:serviceAccountName: pipeline-admin
kind: Role # bound to SA deployer by a RoleBindingmetadata: { 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-initializationTaskRun Pod: who can reach the registry token
# Secret ghcr-push, annotated tekton.dev/docker-0, listed on# the ServiceAccount. Nothing in the Task mentions it.kind: PipelineRunspec:taskRunTemplate:serviceAccountName: build-bot
$ npm test+ env # a post-install script dumps the environmentHOME=/tekton/homePATH=/usr/local/bin:/usr/bin✓ 42 passing
trade-offs
| approach | who sees it | in env? | in API objects? | log leak path | use for |
|---|---|---|---|---|---|
| Annotated SA creds | every step of every TaskRun using the SA | no (files in $HOME) | no | only if a step cats the file | git clone / image push for tools that read ~/.gitconfig or config.json |
| Secret workspace | tasks that bind it; one step if isolated | no (read-only files) | no (only the Secret name) | only if a step cats the file | default choice: explicit, per-task, auditable |
| env secretKeyRef | steps that declare it (all, via stepTemplate) | yes, inherited by child processes | no (Pod shows the reference) | env, set -x, crash dumps | tools that only take an env var |
| Plain param | wherever it's referenced | depends | yes: PipelineRun, TaskRun, Pod spec, Results | very likely | never |
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-specswinsPipelineRun · taskRunSpecs[build].podTemplate
thenPipelineRun · taskRunTemplate.podTemplate
fallbackconfig-defaults · default-pod-template
# effective podTemplate for TaskRun ship-run-buildnodeSelector:replaced default, templatekubernetes.io/arch: arm64tolerations:template- key: dedicatedvalue: cieffect: NoScheduleaffinity:specnodeAffinity: { … zone in [eu-west-1a] }securityContext:defaultrunAsNonRoot: truerunAsUser: 65532fsGroup: 65532imagePullSecrets:default- name: corp-registryenv:- name: HTTP_PROXYvalue: http://proxy.corp:3128default- name: LOG_LEVELvalue: debugspec- name: NO_PROXYvalue: .svc,.cluster.localtemplatepriorityClassName: ci-highreplaced templatehostAliases:spec- ip: 10.0.0.5hostnames: [registry.corp]dnsConfig:defaultoptions: [{ 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.
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.
Running unprivileged, building images without a daemon
pod-security-admissionpod-security-contextset-security-contextdaemonless-buildadmission check, container by container
| container | allowPrivilegeEscalation != false | capabilities.drop != [ALL] | runAsUser = 0 |
|---|---|---|---|
| prepare(init, Tekton) | ✗ deny | ✗ deny | ✓ |
| place-scripts(init, Tekton) | ✗ deny | ✗ deny | ✓ |
| step-build(your step) | ✗ deny | ✗ deny | ✗ deny |
# namespace labelpod-security.kubernetes.io/enforce: restricted# config-feature-flagsset-security-context: "false"# step-build securityContextsecurityContext:runAsUser: 0seccompProfile: { type: RuntimeDefault }
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.
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.
Compute resources: steps run in sequence, requests add up
step-compute-resourcesCPU reserved by the scheduler vs used by the step that's running
steps:- name: clonecomputeResources: { requests: { cpu: 200m } }- name: buildcomputeResources: { requests: { cpu: 1500m } }- name: testcomputeResources: { requests: { cpu: 800m } }---kind: LimitRangespec: { 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.
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- 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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