> For the complete documentation index, see [llms.txt](https://docs-lunar.earthly.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-lunar.earthly.dev/docs/authorization/ci-identity.md).

# CI Job Identity

Authenticate CI jobs to Lunar Hub with the OIDC token their CI platform issues, on GitHub Actions and GitLab CI.

Put the OIDC token issued by the job's Git platform in `LUNAR_HUB_TOKEN` to authenticate without storing the Hub service token in each repository. See [Authorization](/docs/authorization.md) for the permission model. GitHub Actions and GitLab CI are supported; jobs on other CI platforms keep the service token.

{% tabs %}
{% tab title="GitHub Actions" %}

```yaml
jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      id-token: write      # lets the job request its OIDC token
      contents: read
    steps:
      - uses: actions/checkout@v5
      - run: |
          export LUNAR_HUB_TOKEN="$(curl -fsS \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=hub.example.com" | jq -r .value)"
          lunar ci-tracer run -- make test
        env:
          LUNAR_CI_TYPE: github
          LUNAR_HUB_HOST: hub.example.com
```

Set `audience=` to the bare Hub host in `HUB_AUTH_AUDIENCE`. When using the [`earthly/lunar-ci-tracer`](/install/ci-tracer/github-actions-managed.md) action, request the OIDC token first and pass it in `env:`.
{% endtab %}

{% tab title="GitLab CI" %}

```yaml
job:
  variables:
    LUNAR_HUB_HOST: hub.example.com
  id_tokens:
    LUNAR_HUB_TOKEN:
      aud: hub.example.com
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - >-
      lunar collect
      --component "$CI_SERVER_HOST/$CI_PROJECT_PATH"
      --sha "$CI_COMMIT_SHA"
      --pr "$CI_MERGE_REQUEST_IID"
      .build.image_digest "$DIGEST"
    - >-
      lunar policy ok-pr
      --pr "$CI_MERGE_REQUEST_IID"
      "$CI_SERVER_HOST/$CI_PROJECT_PATH"
      "$CI_COMMIT_SHA"
```

GitLab writes the token into `LUNAR_HUB_TOKEN`. The CI Tracer does not yet run on GitLab, but CLI commands such as [`lunar collect`](/docs/lunar-cli.md#lunar-collect) can use it.
{% endtab %}
{% endtabs %}

## What the job may do

The job can read and collect for its repository and commit or pull request, and create a [bypass](/docs/lunar-cli.md#bypassing-a-block) for that same commit, which it must name with `--sha`. It cannot target other repositories or commits, or use operator-only permissions. Once configuration is installed, only the protected branch it was pulled from can pull it. During first bootstrap, any protected branch of the repository pinned by `HUB_LUNAR_CONFIG_REPO` may install it.

The configuration repository's own job, on that protected branch, is the exception to "other repositories or commits": it may read and collect for any component, and run collectors and catalogers anywhere. It still cannot read secrets or use any other operator-only permission. An out-of-band collect it makes for another component is recorded as that component's, on the component's tracked branch, with no ref of its own. See [Configuration repository administrators](/docs/authorization.md#config-repo-administrators).

### Protected and unprotected refs

A commit can be on several refs at once, so a job on an unprotected branch can write for a commit the default branch also points at. The token says whether the job's ref is protected, and the Hub leaves records written from an unprotected ref out of the Component JSON it builds for a commit on the default branch. Those records still count for the branch or pull request they were written from, and records written with the service token or by a person are never excluded.

Scheduled (`cron`) collections from an unprotected ref are excluded everywhere rather than only on the default branch, because a `cron` record applies to the whole component rather than to one commit.

### Pull requests from forked repositories

A CI job cannot write data for a pull or merge request whose source is a forked repository, even when its Git platform gives the job the original repository's identity. The Hub refuses the write with `PermissionDenied`; reads are unaffected. That covers GitHub private repositories with [Send write tokens to workflows from pull requests](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#enabling-workflows-for-forks-of-private-repositories) enabled, and GitLab fork merge requests whose pipeline runs in the parent project. Every write from a GitHub `pull_request_target` job is refused too, since that event always carries the original repository's identity and its token names no pull request — use `pull_request` for jobs that collect.

So a fork pull request gets no data from CI collectors. A policy that reads it reports no data, and if the check is required to merge, the request waits for a bypass. Hub-side collectors, which run from the webhook rather than in the job, still collect for it.

{% hint style="warning" %}
**The service token is exempt**, because it holds every permission. Never execute fork-controlled content in a job that carries it. The same goes for the configuration repository's pipeline on its configuration branch: `pull_request_target` is refused there, but `workflow_run` and `issue_comment` runs carry that identity too, and it reaches every component.
{% endhint %}

## Limits

* Catalog the repository before switching its jobs. A token for an uncataloged repository authenticates, but Hub-wide tracer requests return `PermissionDenied` and component reads return `NotFound`.
* A Hub session lasts `HUB_AUTH_CI_SESSION_TTL` (default 6h) and the tracer, started at the top of the job, does not renew it, so a traced job longer than that loses Hub access for its tail. Operators with such jobs raise the TTL. Standalone `lunar collect` steps are unaffected: each invocation exchanges the token in `LUNAR_HUB_TOKEN` again, and on GitHub Actions a step can request a fresh OIDC token at any time.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs-lunar.earthly.dev/docs/authorization/ci-identity.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
