Luke Marshall

GitLab Token With Owner Access to GnuTLS in a Public CI Artifact

GitLab Token With Owner Access to GnuTLS in a Public CI Artifact

Luke Marshall

tl;dr Using TruffleHog Public Monitoring, we found a GitLab token exposed in a public GitLab CI artifact. The token had the api scope and Owner access to 55 projects, including GnuTLS, Nettle, libtasn1, libidn2, and ocserv, giving it a potential path to compromise their software supply chains.

GnuTLS ships in the base image of nearly every major Linux distribution and is used by software including cURL, Wget, CUPS, and rsyslog. We disclosed the token to its owner, who revoked it within a few hours.

Why GnuTLS matters

GnuTLS is an open-source library that implements the SSL, TLS, and DTLS protocols. Its primary use is securing connections across the internet. It ships in the base image of nearly every major Linux distribution, CUPS printing systems, and networking tools such as Wget and cURL.

GnuTLS is a dependency for a broad range of widely deployed software, which is what makes a write-capable token so serious.

What the token could access

The Truffle Security research team identified the token in a public GitLab CI artifact produced by the openconnect/ocserv project. We enumerated the projects associated with the token using GitLab's Projects API.

GET https://gitlab.com/api/v4/projects?membership=true

This endpoint returns the projects where the token's user has membership, together with the user's access level for each project.

GitLab has a set of predefined roles that can be assigned to a project. In the response, the access_level field maps to the role held on that project:

image

Of the 75 projects returned, the token had Owner access on 55. The projects most relevant to the potential supply chain impact were:

On gnutls/gnutls, the token holds access_level: 50, which is Owner.

What the token itself was allowed to do

GitLab personal access tokens have per-project roles and a separate set of global scopes. We enumerated the token's scopes using:

GET https://gitlab.com/api/v4/personal_access_tokens/self

The response contained:

scopes: ["api"]

According to GitLab's documentation, the api scope:

Grants complete read and write access to the API, including all groups and projects, the container registry, the dependency proxy, and the package registry. Also grants complete read and write access to the registry and repository using Git-over-HTTP.

The /self endpoint reveals the api tokens scope.

The combination of the api scope and Owner access to 55 projects made the token highly privileged. We prepared a disclosure report and sent it to the project maintainers, then examined what a potential attack using the token could have looked like.

A potential path to a supply chain compromise

Among the projects the token could access, GnuTLS had the broadest downstream reach. It is a hard dependency for software including cURL, Wget, CUPS, and rsyslog, and is packaged by most Linux distributions.

We enumerated the rules and permissions for every branch in the GnuTLS project. Most protected branches restricted direct pushes. The maintained gnutls_3_7_x release branch had a broader rule that allowed direct pushes from Maintainers.

The default branch, master, restricted direct pushes to a single named user. Changes had to arrive through a merge request with at least one approval and a passing pipeline. The gnutls_3_7_x release branch was different because the token already satisfied its Maintainer requirement.

On this branch, “protected” did not mean “requires review.” It meant “restricts who may push,” and the token satisfied that restriction. No merge request or approval was required before a commit could be pushed directly to the release line. Force pushes were blocked, so the token could not rewrite history, but it could append a new commit.

The default branch was more restrictive, but an attacker holding an Owner token could still open a merge request. Because the Owner role could edit approval rules, protected-branch settings, and project membership, the controls governing that merge request were also within the token's reach.

Branch permissions
Truffle Security Co.
BranchPush allowed toMerge allowed toThis token can push?
gnutls_3_7_xMaintainersDevelopers + MaintainersYes, confirmed
echMaintainersDevelopers + MaintainersYes, by rule
masterSingle user onlyDevelopers + MaintainersNo, merge only
gnutls_3_6_xNo one + single userDevelopers + MaintainersNo, merge only
gnutls_2_x to 3_5_xNo oneNo oneNo, frozen

How the token entered a public artifact

The leak resulted from three parts of the CI configuration in the public openconnect/ocserv project that combined to write the token into a downloadable artifact.

The token was stored as a GitLab CI/CD variable, the standard mechanism for holding a secret. During a pipeline, GitLab masks secret values in job logs by replacing them with [MASKED]. That protection applies to log output, not to files published as artifacts. In this case, the token was written to a file and uploaded in plaintext.

The exposure occurred as follows:

1. A test dumped the environment to a file. The helper script tests/connect-ios-script ran env > apple-ios.$REASON.tmp, writing every variable visible to the runner into an untracked temporary file.

The test dumps all of the CI variables the runner has access to.

  1. The dump included every project-wide CI variable available to the job, including the GitLab token referenced as API_TOKEN, even though the test job did not use it.

API_TOKEN is a project-wide variable, so GitLab injects it into every job, including the test job that dumps the environment.

3. The Fedora job published the file as a public artifact. Its artifacts configuration used untracked: true and when: always, so every run uploaded the temporary environment dump. The artifact expired after one day, but until then it could be downloaded from the public project without authentication.

An untracked upload on a public project means anyone can download the dump, no authentication required.

Because the job log itself stayed clean, nothing in the pipeline hinted that a token was leaking on every run, which is why it went unnoticed for years.

Why the token was present

The API_TOKEN variable was used by gitlab-triage, a tool that automatically manages issues and merge requests according to a set of policies. It can add or remove labels, post comments, close or reopen items, and perform similar project housekeeping. Because it makes these changes through the GitLab API, gitlab-triage requires a token with the api scope.

Exposure window

We used the project’s Git history and scheduled-job activity to reconstruct when the conditions required for the leak were present.

The account owner confirmed that the token became active on January 11, 2026, based on the first successful run of the scheduled job tied to it. Before then, the job used a per-project token that did not have sufficient permissions, so it was not exposing a usable credential.

Truffle Security Co.
March 1st - 2018The Fedora job begins publishing untracked files as public artifacts.
March 1st - 2020The env-dump test lands, exposing every project-wide CI/CD variable as a public artifact (~6 years ago).
July 1st - 2020An API_TOKEN variable is added, but it is not scoped correctly, so the triage job never runs successfully and no usable token is leaked.
January 11th - 2026A correctly scoped, valid API_TOKEN is added. From this point a live, privileged token leaks on every run.
June 17th - 2026TruffleHog surfaces the live token, and we verify access and begin disclosure.
June 17th - 2026The same day, the project owner commits a fix removing the environment dump (commit f3f74de3).

The fix

Within a few hours of our disclosure, the OpenConnect and GnuTLS maintainers shipped a fix. Commit f3f74de3, tests: do not store environment information, removed the env > ...tmp commands from both test helpers. The runner's environment, including secrets injected into it, is no longer written to a file that can be uploaded as an artifact.

https://gitlab.com/openconnect/ocserv/-/commit/f3f74de305a4e70178137b3509175b69bb1559fa

Review for malicious activity

The token has since been revoked. The account owner reviewed activity across the projects associated with the credential and found no commits attributed to the account in GnuTLS, libtasn1, or libidn.

The account did not have commit access to the openconnect/openconnect client project, and the review of ocserv found no new members, tokens, or webhooks created after the token became active. Based on the activity reviewed, the maintainer found no indication that the token had been misused.

Key takeaways

  • Audit CI artifacts as well as build logs. The token was masked in the job log but written into a public artifact in plaintext, so the pipeline logs did not reveal the exposure.

  • A leaked credential belonging to a trusted maintainer can satisfy access controls designed around that maintainer's role. In this case, the token already held the project roles and scope needed to reach sensitive repositories and settings.

  • Review Owner and Maintainer access regularly. Roles that outlive someone's involvement in a project increase the impact of a leaked credential, so stale access should be removed.


infra