Skip to content

feat: add GitLab CI provider support#559

Open
cgroschupp wants to merge 8 commits into
openpubkey:mainfrom
cgroschupp:feature/add-gitlab-ci-support
Open

feat: add GitLab CI provider support#559
cgroschupp wants to merge 8 commits into
openpubkey:mainfrom
cgroschupp:feature/add-gitlab-ci-support

Conversation

@cgroschupp

@cgroschupp cgroschupp commented Jul 1, 2026

Copy link
Copy Markdown

Fixes: #52

openpubkey/verifier.NewFromMany stores provider verifiers in a map keyed by issuer:

https://github.com/openpubkey/openpubkey/blob/main/verifier/verifier.go#L126

Because of that, a verifier list cannot contain two providers with the same issuer. If two entries use the same issuer, NewFromMany rejects them as duplicates.

This becomes a problem for GitLab because both normal interactive GitLab login and GitLab CI tokens use the same issuer:

https://gitlab.com

but they require different verification behavior:

  • normal GitLab login uses the standard GitLab/OIDC verifier
  • GitLab CI uses OpenPubkey's GitLab CI verifier for GQ-bound (GQ256) tokens

A server may need to allow both at the same time, for example:

https://gitlab.com 8d8b7024572c7fd501f64374dec6bba37096783dfcd792b3988104be08cb6923 24h
https://gitlab.com OPENPUBKEY-PKTOKEN:ssh-deploy-prod 24h

Both entries have the same issuer, but represent different accepted audiences/token types.

@EthanHeilman EthanHeilman added the enhancement New feature or request label Jul 1, 2026
@EthanHeilman

Copy link
Copy Markdown
Member

@cgroschupp

Lets say you have an OPKSSH server that accepts both GitLab Login ID Tokens and GitLab-CI ID Tokens. How do you tell the tokens apart? If you use the presence of GQ Signatures to tell them apart, an attacker could simply turn a captured GitLab Login ID Token into a GQ-signed Gitlab-CI ID Tokens and in so doing bind the attackers public key.

@cgroschupp
cgroschupp force-pushed the feature/add-gitlab-ci-support branch from 50cbb2b to 6f426d4 Compare July 14, 2026 20:39
@cgroschupp

Copy link
Copy Markdown
Author

Good point. I agree that the server must not distinguish GitLab Login tokens from GitLab CI tokens solely by the presence of a GQ signature.

The unsafe version would be:

If the token is GQ-signed, treat it as GitLab CI. Otherwise treat it as normal GitLab login.

That is not sufficient, because a normal GitLab Login ID Token and a GitLab CI ID Token share the same issuer:

https://gitlab.com

If the only discriminator is "has GQ signature", then an attacker who obtains a normal GitLab Login ID Token could try to re-wrap / GQ-bind that token and make it look like a GitLab CI-style token. In that case the attacker would be changing the binding to their own public key, while still presenting a GitLab-issued token.

So the distinction has to come from claims that are part of the ID token semantics, not just from the outer token/signature format.

Normal GitLab login

Normal interactive GitLab login is configured with the GitLab OAuth client ID:

https://gitlab.com 8d8b7024572c7fd501f64374dec6bba37096783dfcd792b3988104be08cb6923 24h

Those tokens are expected to have an audience matching that client ID and are verified using the normal GitLab/OIDC verifier.

GitLab CI

GitLab CI is configured with an explicit OpenPubkey PKToken audience:

https://gitlab.com OPENPUBKEY-PKTOKEN:ssh-deploy-prod 24h

Those tokens are expected to have an audience matching that value and are verified using the GitLab CI/GQ-bound verifier.

Because of this, I removed the generic server-side marker:

https://gitlab.com gitlab-ci 24h

That marker was too broad because it selected the GitLab CI verifier without expressing which audience was expected. The server-side config now requires an explicit OPENPUBKEY-PKTOKEN:<aud> value.

So the server should distinguish the two cases by configured issuer + expected audience/token type, not merely by whether the token is GQ-signed.

Concretely, for a server accepting both normal GitLab and GitLab CI, the config should look like:

https://gitlab.com 8d8b7024572c7fd501f64374dec6bba37096783dfcd792b3988104be08cb6923 24h
https://gitlab.com OPENPUBKEY-PKTOKEN:ssh-deploy-prod 24h

The multi-provider wrapper is only there to work around the fact that openpubkey/verifier.NewFromMany allows only one verifier per issuer. It should not be interpreted as "any GQ token from this issuer is GitLab CI". The GitLab CI branch should be tied to the explicit OPENPUBKEY-PKTOKEN:<aud> provider entry.

If the underlying GitLab CI verifier skips client ID / audience validation, opkssh should enforce the configured OPENPUBKEY-PKTOKEN:<aud> audience around that verifier so that the provider file entry is an actual restriction and not only a verifier-selection marker.

@EthanHeilman

Copy link
Copy Markdown
Member

GitLab CI is configured with an explicit OpenPubkey PKToken audience:

Using just the audience here concerns me because the audience can be set to any value. Its almost certainly fine to use only audience, but just to be safe, is there anyway to tell GitLab an GitLab-CI tokens apart, other than the audience?

Like GitLab-CI tokens can the claim ci_config_ref_uri, double check, but I doubt GitLab tokens have that claim. https://github.com/openpubkey/openpubkey/blob/main/docs/pktoken.md#gq-commitment-pk-tokens-gitlab-ci-example

@cgroschupp

Copy link
Copy Markdown
Author

Added an explicit GitLab CI token discriminator before invoking the GitLab CI/GQ verifier.

The GitLab CI verifier in OpenPubkey uses SkipClientIDCheck: true, so the audience from /etc/opk/providers was only selecting the GitLab CI verifier path but was not actually being enforced. This commit wraps the GitLab CI provider verifier and adds a fail-closed pre-check that requires:

  • exact aud match against the configured provider entry, e.g. OPENPUBKEY-PKTOKEN:ssh-deploy-prod
  • GitLab-CI-specific claims:
    • ci_config_ref_uri
    • job_id
    • job_project_path
    • pipeline_id

This avoids treating any GQ-bound token from the GitLab issuer as GitLab CI solely based on the presence of a GQ signature. Normal GitLab login tokens and GitLab CI tokens share the same issuer, so the server now uses the explicitly configured audience plus CI-specific claims to route the token to the GitLab CI verification path.

The claim checks are only used as a fail-closed discriminator. The token is still cryptographically verified by the underlying GitLab CI/GQ verifier afterwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support sshing as a Gitlab-CI workflow in opkssh

2 participants