Build YAML Misconfiguration
Detect and prevent security misconfigurations in GitLab CI and Tekton pipeline definitions. Misconfigured build YAML is a common source of secret leakage, privilege escalation, and supply chain compromise.
GitLab CI (.gitlab-ci.yml)
Secret Exposure
- Never hardcode secrets in
.gitlab-ci.yml,variables:, orscript:blocks. Use CI/CD protected variables or an external vault. - Mask and protect variables: set
masked: trueandprotected: trueon sensitive CI/CD variables. Masked variables are redacted from job logs. - Never echo secrets: avoid
echo $SECRET,printenv,env, orset -xin scripts that handle credentials. Debug output is stored in job logs. - Restrict variable scope: use
protected: trueto limit variables to protected branches/tags only. Use environment scoping to restrict which jobs see which secrets. - Artifacts and caches: never include files containing secrets (
.env, credentials, tokens) inartifacts:orcache:paths. These are stored and downloadable.
yaml# BAD -- secret visible in logs and available on all branches variables: DB_PASSWORD: "hunter2" # GOOD -- use CI/CD protected variable (set in GitLab UI) # Reference as $DB_PASSWORD in scripts; set masked + protected in settings
Runner and Execution Isolation
- Never use shared runners for secret-heavy jobs without understanding the trust boundary. Prefer project-specific or group runners for sensitive pipelines.
- Never use
privileged: truein runner config unless absolutely required (e.g., container builds). Privileged runners can escape to the host. - Use
dockerorkubernetesexecutor overshellexecutor. Shell executors share the host filesystem and can leak state between jobs. - Tag sensitive jobs to run only on hardened, dedicated runners.
yaml# BAD -- no runner restriction, runs on any available shared runner deploy-production: script: - ./deploy.sh # GOOD -- pinned to a dedicated runner deploy-production: tags: - production-deploy - privileged-denied script: - ./deploy.sh
Pipeline Permissions and Trust
- Protected branches: ensure deploy and release jobs use
only: refs: [main]orrules:with branch restrictions. Never allow feature branches to trigger production deploys. - Merge request pipelines: use
rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'to prevent MR pipelines from accessing protected variables. allow_failure: true: never set this on security scanning jobs (SAST, SCA, secret detection). Failures in security gates must block the pipeline.when: manual: use for production deploys to enforce human approval. Never auto-deploy to production from CI.
yaml# BAD -- security scan can fail silently sast: allow_failure: true script: - run-sast-scanner # GOOD -- security scan blocks pipeline on failure sast: allow_failure: false script: - run-sast-scanner rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Container Image Builds in CI
- Never use
docker:dindwith--privilegedwithout understanding the risk. Prefer Kaniko, Buildah, or Podman for unprivileged image builds. - Pin base images by digest, not just tag. Tags are mutable.
- Do not mount the Docker socket (
/var/run/docker.sock) in CI jobs. - Scan built images before pushing to registry.
yaml# BAD -- privileged DinD with mutable tag build-image: image: docker:latest services: - docker:dind variables: DOCKER_HOST: tcp://docker:2375 script: - docker build -t myapp . # GOOD -- unprivileged build with pinned image build-image: image: quay.io/buildah/stable@sha256:<digest> script: - buildah bud --layers -t myapp . - buildah push myapp $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Include and Extends Risks
- Audit remote includes:
include: remote:pulls YAML from external URLs. If the remote changes, your pipeline changes. Pin to a specific ref or commit. - Audit cross-project includes:
include: project:pulls from other GitLab projects. Ensure the source project has proper access controls. - Review inherited configuration:
extends:andinclude:can overridevariables:,script:, andrules:in unexpected ways. Verify the final merged configuration.
yaml# RISKY -- remote include with no version pinning include: - remote: 'https://example.com/ci-templates/build.yml' # BETTER -- pinned project include include: - project: 'my-group/ci-templates' ref: 'v2.1.0' file: '/templates/build.yml'
Tekton Pipelines
Secret Exposure
- Never inline secrets in Task or Pipeline YAML. Use Kubernetes Secrets referenced via
secretKeyRefinenv:or mounted as volumes. - Never log secrets in steps: avoid
echo,printenv,caton mounted secret files inscript:blocks. Tekton step logs are stored and accessible. - Workspace binding: when binding secrets as workspaces, ensure the workspace is not shared with untrusted tasks in the same pipeline.
- Results: never write secrets to Tekton
results. Results are stored in the TaskRun/PipelineRun status and visible to anyone with read access.
yaml# BAD -- secret hardcoded in task steps: - name: deploy image: registry.redhat.io/ubi9/ubi-minimal script: | curl -H "Authorization: Bearer my-secret-token" https://api.example.com # GOOD -- secret from Kubernetes Secret steps: - name: deploy image: registry.redhat.io/ubi9/ubi-minimal env: - name: API_TOKEN valueFrom: secretKeyRef: name: deploy-creds key: token script: | curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com
Step Image Security
- Pin images by digest: never use
:latestor mutable tags for step images. A compromised tag can inject malicious code into your pipeline. - Use trusted registries: prefer
registry.redhat.ioor your internal registry. Avoid pulling from untrusted public registries in production pipelines. - Minimal images: use UBI minimal or distroless images. Avoid full OS images with unnecessary tools that expand the attack surface.
yaml# BAD -- mutable tag from public registry steps: - name: build image: ubuntu:latest # GOOD -- pinned digest from trusted registry steps: - name: build image: registry.redhat.io/ubi9/ubi-minimal@sha256:<digest>
Task and Pipeline Permissions
- Run steps as non-root: set
securityContext.runAsNonRoot: trueandrunAsUseron steps. Never run build steps as root unless absolutely required. - Drop capabilities: set
securityContext.capabilities.drop: ["ALL"]on step containers. - Do not use
privileged: truein step security contexts. - Service account scoping: bind the minimum RBAC to the service account running the PipelineRun. Never use the
pipelineservice account with cluster-admin privileges. - Limit workspace access: use
readOnly: trueon workspace bindings when the task only needs to read.
yaml# BAD -- running as root with no restrictions steps: - name: build image: registry.redhat.io/ubi9/ubi-minimal script: | make build # GOOD -- non-root with dropped capabilities steps: - name: build image: registry.redhat.io/ubi9/ubi-minimal securityContext: runAsNonRoot: true runAsUser: 1001 capabilities: drop: ["ALL"] script: | make build
Pipeline Structure Risks
- Untrusted task references:
taskRefwithresolver: bundlesorresolver: gitcan pull tasks from external sources. Pin to specific digests or commits. - Parameter injection: if pipeline parameters are passed into
script:blocks via$(params.*), treat them as untrusted input. Validate and quote them. - When expressions: do not use
whenexpressions as security controls. They skip tasks but do not enforce authorization. - Finally tasks: use
finally:tasks for cleanup (removing credentials, revoking tokens) but never assume they will always run (node failure, timeout).
yaml# BAD -- unpinned external task bundle taskRef: resolver: bundles params: - name: bundle value: quay.io/someone/my-task:latest # GOOD -- pinned by digest taskRef: resolver: bundles params: - name: bundle value: quay.io/myorg/my-task@sha256:<digest>
Tekton Chains and Provenance
- Enable Tekton Chains for automatic signing and attestation of TaskRun/PipelineRun results.
- Verify signatures on built artifacts before promotion to production registries.
- Store attestations alongside artifacts for auditability.
Containerfile / Dockerfile Hardening
Container image definitions are typically referenced or inlined in CI YAML. Misconfigurations here produce insecure images that run in production.
Run as Non-Root
- Never leave
USERunset -- the default is root. Always add aUSERdirective before the finalCMD/ENTRYPOINT. - Create a dedicated user in the build stage.
dockerfile# BAD -- runs as root by default FROM registry.redhat.io/ubi9/ubi-minimal COPY app /app CMD ["/app/server"] # GOOD -- explicit non-root user FROM registry.redhat.io/ubi9/ubi-minimal RUN microdnf install -y shadow-utils && \ useradd -r -u 1001 appuser && \ microdnf clean all COPY --chown=appuser:appuser app /app USER appuser CMD ["/app/server"]
Secrets in Image Layers
- Never
COPYorADDsecrets (keys, credentials,.envfiles) into the image. Every layer is extractable. - Never use
ARGorENVfor secrets -- they are visible indocker inspectand image history. - Use BuildKit secret mounts for build-time secrets. They are not persisted in layers.
dockerfile# BAD -- secret baked into image layer COPY .env /app/.env ENV API_KEY=sk-secret-value # GOOD -- BuildKit secret mount (not persisted in layer) RUN --mount=type=secret,id=api_key \ export API_KEY=$(cat /run/secrets/api_key) && \ ./configure --with-key="$API_KEY"
Base Image Hygiene
- Pin base images by digest, not mutable tags.
:latestor:9can change without notice. - Use trusted registries: prefer
registry.redhat.ioor your internal registry. - Use minimal base images: UBI minimal, micro, or distroless. Fewer packages = smaller attack surface.
- Remove package managers from the final image when possible.
dockerfile# BAD -- mutable tag, full OS image FROM ubuntu:latest # GOOD -- pinned digest, minimal trusted image FROM registry.redhat.io/ubi9/ubi-minimal@sha256:<digest>
Multi-Stage Builds
- Separate build and runtime stages. Build tools, compilers, and test dependencies must not ship in the final image.
- Copy only the built artifact into the runtime stage.
dockerfile# GOOD -- multi-stage: build tools stay in builder stage FROM registry.redhat.io/ubi9/ubi:latest AS builder RUN dnf install -y gcc make && dnf clean all COPY . /src RUN cd /src && make FROM registry.redhat.io/ubi9/ubi-minimal@sha256:<digest> COPY --from=builder /src/bin/app /app/server USER 1001 CMD ["/app/server"]
Build Context Leakage
- Always use
.dockerignore(or.containerignore). Without it, the entire directory (including.git,.env, credentials, IDE configs) is sent to the build daemon. - Explicitly exclude sensitive paths.
text# .dockerignore .git .env *.pem *.key credentials/ node_modules/
Unnecessary Capabilities and Ports
- Do not
EXPOSEports you don't use. WhileEXPOSEis documentation only, it signals intent and misconfigured orchestrators may open them. - Do not install
sudo,su, or setuid binaries in the final image. - Set
HEALTHCHECKso orchestrators can detect unhealthy containers.
dockerfile# GOOD -- only expose what's needed, add healthcheck EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://localhost:8080/healthz || exit 1
Read-Only and Immutability
- Design images to run with a read-only root filesystem at runtime. Write only to explicitly mounted volumes or
tmpfs. - Avoid writing to the container filesystem in
CMD/ENTRYPOINTscripts.
Common Containerfile Misconfigurations
| Misconfiguration | Risk | Fix |
|---|---|---|
No USER directive | Container runs as root | Add USER <non-root> before CMD |
COPY .env or COPY *.key | Secrets baked into extractable layers | Use BuildKit --mount=type=secret |
ENV SECRET_KEY=value | Secret visible in docker inspect | Pass at runtime via orchestrator |
FROM image:latest | Mutable tag; supply chain risk | Pin by @sha256:<digest> |
No .dockerignore | .git, credentials sent to build context | Create .dockerignore with exclusions |
| Single-stage with build tools | Compilers, test deps ship to production | Use multi-stage builds |
RUN chmod 777 | World-writable files in image | Use specific permissions (chmod 755) |
No HEALTHCHECK | Orchestrator cannot detect unhealthy state | Add HEALTHCHECK instruction |
sudo or setuid binaries installed | Privilege escalation path | Remove from final image |
Common Misconfigurations (Both Platforms)
| Misconfiguration | Risk | Fix |
|---|---|---|
| Hardcoded secrets in YAML | Credential leakage via repo access or logs | Use vault/CI variables/K8s Secrets |
allow_failure: true on security jobs | Security findings silently ignored | Set allow_failure: false |
Mutable image tags (:latest) | Supply chain compromise via tag poisoning | Pin by digest |
| Privileged containers/runners | Container escape to host | Use unprivileged builds (Buildah/Kaniko) |
| Unscoped variables/secrets | Secrets available to untrusted branches/jobs | Scope to protected branches/environments |
Debug output (set -x, printenv) | Secrets printed to job logs | Remove debug flags; use masked variables |
| Unpinned remote includes/task refs | External changes alter your pipeline | Pin to version/commit/digest |
| Missing security scan gates | Vulnerable code reaches production | Add SAST/SCA/image scanning as blocking steps |
| Auto-deploy to production | No human review before deploy | Require manual approval gate |
| Shell executor (GitLab) | Job-to-job state leakage on shared host | Use Docker/Kubernetes executor |
No USER in Containerfile | Container runs as root in production | Add non-root USER directive |
| Secrets in image layers | Credentials extractable from image | Use BuildKit secret mounts |
No .dockerignore | Sensitive files leak into build context | Create .dockerignore with exclusions |
| Single-stage build | Build tools ship to production | Use multi-stage builds |
Implementation Checklist
- No secrets hardcoded in pipeline YAML; all credentials from vault or protected variables
- Sensitive variables masked and scoped to protected branches/environments
- Security scan jobs set to block pipeline on failure
- All step/job images pinned by digest from trusted registries
- Containers run as non-root with dropped capabilities; no privileged mode
- Remote includes and task references pinned to specific versions/digests
- Production deploys require manual approval
- No debug output (
set -x,printenv,env) in jobs that handle secrets - Pipeline parameters validated before use in scripts
- Tekton Chains enabled for provenance and signing (where applicable)
- Containerfiles set explicit non-root
USERbefore CMD - No secrets in image layers,
ARG, orENV; use BuildKit secret mounts -
.dockerignore/.containerignoreexcludes.git,.env, keys, credentials - Multi-stage builds separate build tools from runtime image
- Base images from trusted registries, pinned by digest
Test Plan
- Audit all
.gitlab-ci.ymland Tekton Task/Pipeline YAML for patterns in the table above. - Verify that protected variables are not accessible from unprotected branches.
- Confirm security scan jobs block merge on failure.
- Test that pinned images resolve to the expected digest and are from trusted registries.
- Review runner/service account RBAC for least privilege.


