JWT job token memoized with incorrect expiration when generated before job timeout is set
Summary
The JWT job token (encoded_jwt) is memoized on the build object. When pre_assign_runner_checks runs before the job transitions to running, it can trigger token generation via variable serialization code paths. At this point, update_timeout_state hasn't run yet (it runs during pending->running transition), so timeout_value may be nil, resulting in a JWT with only ~6 minutes TTL (60s default + 5min leeway) instead of the job's actual timeout.
This particularly affects jobs with needs: dependencies, as they are more likely to trigger the dependency validation code path.
Steps to reproduce
- Create a parent pipeline with a job that triggers a child pipeline:
# .gitlab-ci.yml
setup:
script: echo "Setup done"
trigger-child:
trigger:
include: child.yml
strategy: depend- Create a child pipeline with a job that has
needs:dependencies and a long timeout:
# child.yml
child-job:
image: ruby
needs:
- pipeline: $CI_UPSTREAM_PIPELINE_ID
job: setup
script: ruby -e "require 'base64'; puts Base64.encode64(ENV['CI_JOB_TOKEN'])"
timeout: 1h- Run the pipeline and copy the encoded token output
- Decode and check the JWT expiration in a Rails console:
jwt = ::Ci::JobToken::Jwt.decode(Base64.decode64(s)).instance_variable_get(:@jwt)
puts (Time.at(jwt.payload["exp"]) - Time.now) / 60Example Project
Any project with child pipelines using needs: dependencies from parent pipeline jobs.
What is the current bug behavior?
The JWT token expires in ~6 minutes regardless of the job's configured timeout. This causes:
- Jobs to fail with 403 errors after ~6 minutes
after_scriptnot running- Artifacts failing to upload
- Jobs getting stuck in
runningstate in the UI
What is the expected correct behavior?
The JWT token should have an expiration matching the job's actual timeout plus leeway (e.g., ~66 minutes for a 1-hour timeout job).
Relevant logs and/or screenshots
The token generation is triggered via these code paths:
has_valid_build_dependencies?->processable.simple_variables_without_dependencies->#scoped_variablessecrets_provider_not_found?(EE) ->#variables_encompassing_secrets_configs->#scoped_variables
And #scoped_variables calls job.try(:token) unconditionally, which memoizes the JWT before the timeout is set.
Relevant code:
- Token memoization: https://gitlab.com/gitlab-org/gitlab/blob/e2e1b2911f407fdd98e6313d603bca6b9002c6eb/app/models/ci/build.rb#L1376-1381
- Pre-assign runner checks: https://gitlab.com/gitlab-org/gitlab/-/blob/e2e1b2911f407fdd98e6313d603bca6b9002c6eb/app/services/ci/register_job_service.rb#L408-417
- Token access in scoped_variables: https://gitlab.com/gitlab-org/gitlab/blob/e2e1b2911f407fdd98e6313d603bca6b9002c6eb/lib/gitlab/ci/variables/builder.rb#L292
Output of checks
This issue was identified on GitLab.com.
Possible fixes
Clear the encoded_jwt memoization in present_build! (which runs after the job has transitioned to running and timeout is set), ensuring the token is regenerated with the correct expiration.
Fixed in !220980 (merged)
Patch release information for backports
If the bug fix needs to be backported in a patch release to a version under the maintenance policy, please follow the steps on the patch release runbook for GitLab engineers.
Refer to the internal "Release Information" dashboard for information about the next patch release, including the targeted versions, expected release date, and current status.
High-severity bug remediation
To remediate high-severity issues requiring an internal release for single-tenant SaaS instances, refer to the internal release process for engineers.