Skip to content

Let your pipeline prove who it is instead of holding a password. GitHub Actions, GitLab CI and Bitbucket Pipelines each mint a short-lived, signed OIDC token for a running job that says which repository, branch and pipeline it belongs to. You tell BehindGate which repository, or which whole organization, may deploy which app, once, and the pipeline needs no secret at all.

Compared with a stored deploy token, nothing is left lying around to leak, nothing has to be rotated, and the deploy history records the branch that published rather than the name of a credential.

How it works

  1. Your job asks its CI system for an OIDC token.
  2. bg-deploy exchanges that token at BehindGate for a deploy token that lives 15 minutes.
  3. The deploy proceeds exactly as it would with a stored token.

bg-deploy does steps 1 and 2 for you. There is nothing extra to install.

Trust the repository

In the dashboard, open Settings → CI trusts and add one:

FieldWhat to put
ProviderGitHub Actions, GitLab CI or Bitbucket Pipelines
TrustOne repository, or every repository in an organization, group or workspace
Repositoryowner/repo on GitHub, the project path on GitLab, the repository UUID on Bitbucket
Organization / group / workspaceThe owner, when you trust all of it. Bitbucket also needs its workspace UUID for a single repository, because its repository UUIDs say nothing about where they live.
BranchOptional. Leave it empty to allow any branch, or name one to allow only that.
EnvironmentOptional. The deployment environment the job must be running in.
ResourcesThe sites, or the individual apps, this trust may reach
PermissionsWhat it may do with them: read and deploy, and optionally create and delete apps

Conditions are matched exactly. There is no wildcard syntax, because a wildcard is how people accidentally trust a repository they did not mean to. Leaving a field empty is how you say "any".

A trust carries no secret. You can create it, look at it, and delete it freely; deleting it also cuts off any deploy token already exchanged under it, immediately.

Trusting a whole organization

Naming an organization instead of a repository is the right choice when many repositories publish to the same place, or when you would otherwise be adding a trust every time someone creates a repository. Be clear about what it means: every repository in that organization gets everything the trust grants, including repositories created after you set it up. How wide that is depends on who can create repositories in it.

Pairing it with a branch is usually worth it. A trust on the whole organization restricted to main is a much narrower thing than one that accepts any branch, because a branch is something anyone with push access can make.

On GitLab, a group includes its subgroups: trusting acme covers acme/web/site. Name the subgroup instead if that is the level you mean.

GitHub Actions

Grant the job permission to request a token, and point bg-deploy at your deploy endpoint. The endpoint is shown next to the trust in the dashboard.

yaml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: bg-deploy -y --tag sha=$GITHUB_SHA ./dist
        env:
          BEHINDGATE_URL: https://app.behindgate.com/api/deploy/releases

permissions: id-token: write is what lets the job request an OIDC token; without it GitHub refuses and bg-deploy says so. Requesting the token is all the permission grants; it does not give the job any other access.

GitLab CI

GitLab hands the token to the job through a variable you declare, so name it BEHINDGATE_OIDC_TOKEN and set its audience to your BehindGate host.

yaml
deploy:
  stage: deploy
  id_tokens:
    BEHINDGATE_OIDC_TOKEN:
      aud: https://app.behindgate.com
  variables:
    BEHINDGATE_URL: https://app.behindgate.com/api/deploy/releases
  script:
    - npm ci && npm run build
    - bg-deploy -y --tag sha=$CI_COMMIT_SHA ./dist

The aud value must be exactly the host shown with the trust. GitLab.com is supported; self-managed GitLab instances are not yet.

Bitbucket Pipelines

Turn OIDC on for the step; Bitbucket then provides the token itself.

yaml
pipelines:
  branches:
    main:
      - step:
          oidc: true
          script:
            - npm ci && npm run build
            - export BEHINDGATE_URL=https://app.behindgate.com/api/deploy/releases
            - bg-deploy -y --tag sha=$BITBUCKET_COMMIT ./dist

Bitbucket identifies everything by UUID rather than by name, so the trust asks for your workspace UUID, plus the repository UUID when you are trusting a single repository. Both are in the Bitbucket workspace and repository settings. Using the UUID means renaming the repository cannot silently move the trust somewhere else. To trust the whole workspace, give the workspace UUID and leave the repository empty.

Preview deploys, one app per pull request

A trust can cover a whole site rather than named apps. The pipeline then creates its own app when a pull request opens, deploys to it on every push, and deletes it when the pull request closes. Nobody has to create anything in the dashboard, and the preview is private behind the site's sign-in like everything else, which is what makes it safe for work that is not public yet.

Set the trust up with Resources → Whole sites, pick the site, and tick Create apps and Delete apps on the permissions step.

yaml
name: Preview
on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

jobs:
  preview:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: bg-deploy -y --create-app --site-url https://docs.example.com/preview/pr-${{ github.event.number }} ./dist
        env:
          BEHINDGATE_URL: https://app.behindgate.com/api/deploy/releases

  teardown:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - run: bg-deploy --site-url https://docs.example.com/preview/pr-${{ github.event.number }} --delete-app
        env:
          BEHINDGATE_URL: https://app.behindgate.com/api/deploy/releases

--site-url says what to deploy to as the URL it serves on: the host picks the site, the path picks the app. With --create-app it stands the app up on the first run and finds it on later ones, so the same command works for the whole life of the pull request. --delete-app succeeds when there is nothing to delete, so a teardown that runs twice is not a failure.

Creating is opt-in on purpose. Without --create-app a path that serves no app is an error, which is what stops a mistyped path, or a pr-${{ github.event.number }} that expanded to nothing, from quietly minting an app and reporting success.

Note the two URLs do different jobs. BEHINDGATE_URL is the BehindGate API this pipeline talks to; --site-url is the site being deployed to. They are never the same value.

What the pipeline can and cannot do with that trust:

  • It can create, deploy to and delete apps in the site you named, and nothing in any other site. A site-wide trust is a real grant over that site: it can mount an app at any free path, including the site root, so give it to a site you are happy for the pipeline to own.
  • It can only delete apps it created itself. Anything you created by hand, the pipeline can deploy to but not remove.
  • A site the trust does not name does not appear in its listing and reads as missing, so the credential cannot be used to survey the rest of the workspace.

If you want a pipeline that can only ever touch what you already made, choose Specific apps instead and leave Create apps off. That trust can deploy the apps you picked and nothing else.

When it does not work

bg-deploy exits 2 and says which part failed.

MessageWhat to check
no CI identity was foundThe job is not requesting a token. On GitHub add permissions: id-token: write; on GitLab declare the id_tokens block; on Bitbucket set oidc: true.
this pipeline is not trusted to deployThe verified token does not satisfy any trust. Check the repository or owner in the trust, and the branch if the trust names one. On GitLab, check that aud matches the host exactly.
the deploy endpoint cannot be read from a tokenSet BEHINDGATE_URL (or pass --url). Without a stored token there is nothing to carry the endpoint.
this credential may not app createThe trust does not hold Create apps, or it names specific apps rather than a whole site, so there is nowhere to put a new one.
this credential may not app deleteThe trust does not hold Delete apps. Tick it alongside Create apps if the pipeline tears its previews down.
this app was not created by this credentialThe app at that path was created by hand or by another trust. A pipeline only ever deletes what it created.
more than one trust matchesTwo trusts in the workspace both cover this pipeline, for instance one on the repository and one on its whole organization. Name the one you mean with --trust <id> or BEHINDGATE_TRUST_ID; the id is on the trust in Settings.
this trust covers more than one siteThe trust names several sites, so the job has to say which. Pass --site-url (or BEHINDGATE_SITE_URL) with the URL the site serves on.
this trust does not cover that siteThe host in --site-url is not a site this trust names. The same message covers a host that does not exist at all, so check the spelling as well as the trust. A custom domain that has not finished provisioning is not yet resolvable by host.
--site-url is for a CI trustBEHINDGATE_TOKEN is set, and a deploy token already names the one site and app it deploys to. Drop --site-url, or unset the token to authenticate as the job.
no app serves …The path in --site-url names no app. Check it for a typo; if the app really is meant to be created on the fly, pass --create-app and give the trust the Create apps permission.

A deploy that runs on a branch the trust does not name is refused, not silently redirected. That is the point of naming the branch.

Keeping a deploy token as well

Nothing here removes deploy tokens. They are still the right answer for a laptop, a cron job on your own server, or a CI system that does not issue OIDC tokens. See Deploy with the CLI. If BEHINDGATE_TOKEN is set, bg-deploy uses it and never asks the CI system for anything.