Skip to content

CI/CD Pipelines

Registry Reads with Dex Client Credentials

If your organization uses OIDC (Dex) for human users and you want CI/CD pipelines to authenticate without needing kubectl or a ServiceAccount, use the Dex client credentials grant instead. This is the recommended approach because it preserves separation of duties: OIDC stays read-only for registry access, while Kubernetes RBAC governs create, update, and delete permissions.

Enable client credentials support in your Helm values and register a dedicated Dex static client for the pipeline:

dex:
  config:
    oauth2:
      grantTypes:
        - authorization_code
        - client_credentials
    staticClients:
      - id: opendepot
        name: OpenDepot
        secretEnv: OPENDEPOT_DEX_CLIENT_SECRET
        redirectURIs:
          - https://opendepot.defdev.io/...
      - id: ci-pipeline
        name: CI Pipeline
        secretEnv: OPENDEPOT_CC_CLIENT_SECRET
        grantTypes:
          - client_credentials
server:
  oidc:
    enabled: true
    allowClientCredentials: true

Create a GroupBinding to authorize the pipeline client:

apiVersion: opendepot.defdev.io/v1alpha1
kind: GroupBinding
metadata:
  name: ci-pipeline-binding
  namespace: opendepot-system
spec:
  expression: '"client:ci-pipeline" in groups'
  moduleResources:
    - "*"
  providerResources:
    - "*"

GitHub Actions Example

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Get Dex CC token for OpenDepot registry
        id: opendepot-token
        env:
          CC_CLIENT_SECRET: ${{ secrets.OPENDEPOT_CC_CLIENT_SECRET }}
        run: |
          TOKEN=$(curl -sf -X POST https://dex.defdev.io/dex/token \
            -d grant_type=client_credentials \
            -d client_id=ci-pipeline \
            -d "client_secret=${CC_CLIENT_SECRET}" \
            -d scope=openid \
            | jq -r '.access_token')
          echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

      - name: Write .tofurc
        run: |
          export TF_TOKEN_OPENDEPOT_DEFDEV_IO="${{ steps.opendepot-token.outputs.token }}"
          cat > ~/.tofurc <<EOF
          host "opendepot.defdev.io" {
            services = {
              "providers.v1" = "https://opendepot.defdev.io/opendepot/providers/v1/"
            }
          }
          EOF

      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1

      - run: tofu init

The CC token is short-lived (TTL controlled by Dex) and scoped to read-only operations via the GroupBinding. No kubectl access or cluster kubeconfig is required — only the Dex token endpoint must be reachable from the runner.

For full configuration details see Client Credentials (Machine-to-Machine). For a side-by-side comparison of all supported authentication methods and their access-control mechanisms, see the Authentication Comparison table.

Registry Reads: SA Fallback with OIDC

Last resort — exhaust other options first

SA fallback bypasses GroupBinding entirely. When a ServiceAccount token is used, the SA's Kubernetes RBAC governs access instead of the centralized GroupBinding model your OIDC setup provides. This means:

  • Human users and pipeline tokens follow different access control paths, which increases audit surface.
  • A leaked pipeline token grants direct cluster API access, not just registry access.
  • SA-token access is invisible to GroupBinding audit logs.

Before enabling SA fallback, consider whether one of these fits your use case instead:

  • Publishing modules only? → Use the GitOps approach. No pipeline credentials needed at all — Argo CD handles cluster auth, developers push to Git.
  • Reading the registry from pipelines without cluster access? → Use Dex Client Credentials. Pipelines get a Dex-issued token scoped to GroupBinding, with no Kubernetes API exposure.

SA fallback is appropriate when your pipeline must interact with the Kubernetes API directly for reasons beyond registry access and you have already ruled out the above options.

When your organization uses OIDC for human users, CI/CD pipelines still need to run tofu init and download providers from the registry. By default OIDC and bearer-token modes are mutually exclusive, which would require pipelines to use a separate credential mechanism. The ServiceAccount fallback removes this constraint.

Enable SA Fallback in Your Helm Values

server:
  oidc:
    enabled: true
    allowServiceAccountFallback: true

This lets K8s SA tokens authenticate alongside OIDC JWTs. SA tokens bypass GroupBinding and rely on Kubernetes RBAC directly.

Create an SA and Bind Registry-Reader RBAC

apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-registry-reader
  namespace: my-ci-namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: opendepot-registry-reader
  namespace: opendepot-system
rules:
- apiGroups: ["opendepot.defdev.io"]
  resources: ["modules", "versions", "providers"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: opendepot-registry-reader-binding
  namespace: opendepot-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: opendepot-registry-reader
subjects:
- kind: ServiceAccount
  name: ci-registry-reader
  namespace: my-ci-namespace

GitHub Actions Example

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # required for OIDC to your cloud provider
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<ACCOUNT>:role/my-role
          aws-region: us-west-2

      - name: Configure kubeconfig for EKS
        run: aws eks update-kubeconfig --name my-cluster --region us-west-2

      - name: Get SA token for OpenDepot registry
        id: opendepot-token
        run: |
          TOKEN=$(kubectl create token ci-registry-reader \
            -n my-ci-namespace \
            --duration=15m)
          echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

      - name: Write .tofurc
        run: |
          export TF_TOKEN_OPENDEPOT_DEFDEV_IO="${{ steps.opendepot-token.outputs.token }}"
          cat > ~/.tofurc <<EOF
          host "opendepot.defdev.io" {
            services = {
              "providers.v1" = "https://opendepot.defdev.io/opendepot/providers/v1/"
            }
          }
          EOF

      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1

      - run: tofu init

The SA token is short-lived (15 minutes) and scoped to read-only registry operations via the RBAC defined above. No Dex client credentials are needed.

This approach uses kubectl create token to authenticate as the dedicated ci-registry-reader SA, keeping the pipeline's registry access strictly bounded to the RBAC above — regardless of how broad the runner's cloud IAM role is. If your runner's cloud IAM role already has appropriate K8s RBAC configured, you can simplify by using the provider token directly instead of creating an SA token (see Managed Cluster Tokens).

Push-Based Workflows

For private modules you control, bypass the Depot entirely and create Module resources directly from your CI/CD pipeline:

apiVersion: opendepot.defdev.io/v1alpha1
kind: Module
metadata:
  name: terraform-aws-eks
  namespace: opendepot-system
spec:
  moduleConfig:
    name: terraform-aws-eks
    provider: aws
    repoOwner: terraform-aws-modules
    repoUrl: https://github.com/terraform-aws-modules/terraform-aws-eks
    fileFormat: zip
    immutable: true
    storageConfig:
      s3:
        bucket: opendepot-modules
        region: us-west-2
    githubClientConfig:
      useAuthenticatedClient: true
  versions:
    - version: "21.10.1"
    - version: "21.11.0"
    - version: "21.12.0"

GitHub Actions Example:

name: Publish Module Version

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<AWS_ACCOUNT_ID>:role/opendepot-github-actions-role
          aws-region: us-west-2

      - name: Setup kubeconfig
        run: aws eks update-kubeconfig --name my-cluster --region us-west-2

      - name: Publish module version
        run: |
          kubectl apply -f - <<EOF
          apiVersion: opendepot.defdev.io/v1alpha1
          kind: Module
          metadata:
            name: my-module
            namespace: opendepot-system
          spec:
            moduleConfig:
              name: my-module
              provider: aws
              repoOwner: my-org
              repoUrl: https://github.com/my-org/terraform-aws-my-module
              fileFormat: zip
              storageConfig:
                s3:
                  bucket: opendepot-modules
                  region: us-west-2
            versions:
              - version: ${{ github.event.release.tag_name }}
          EOF

The Module controller creates the Version resource, and the Version controller fetches the archive from GitHub and uploads it to storage — no manual archive upload needed.

Adding Versions to an Existing Module

To publish a new version of a module that already exists in OpenDepot, append the version to the spec.versions list. Existing versions are preserved — the Module controller only creates Version resources for entries it hasn't seen before.

Using kubectl patch (quick):

kubectl patch module terraform-aws-eks -n opendepot-system \
  --type json -p '[{"op":"add","path":"/spec/versions/-","value":{"version":"21.13.0"}}]'

Using kubectl apply (declarative):

Include all existing versions alongside the new one. The Module controller is idempotent — it won't re-create versions that already exist.

apiVersion: opendepot.defdev.io/v1alpha1
kind: Module
metadata:
  name: terraform-aws-eks
  namespace: opendepot-system
spec:
  moduleConfig:
    name: terraform-aws-eks
    provider: aws
    repoOwner: terraform-aws-modules
    repoUrl: https://github.com/terraform-aws-modules/terraform-aws-eks
    fileFormat: zip
    storageConfig:
      s3:
        bucket: opendepot-modules
        region: us-west-2
  versions:
    - version: "21.10.1"
    - version: "21.11.0"
    - version: "21.12.0"
    - version: "21.13.0"   # new version

GitHub Actions Example (Append on Release):

- name: Add version to existing module
  run: |
    VERSION=${{ github.event.release.tag_name }}
    kubectl patch module my-module -n opendepot-system \
      --type json \
      -p "[{\"op\":\"add\",\"path\":\"/spec/versions/-\",\"value\":{\"version\":\"${VERSION}\"}}]"

Removing a version: Remove the entry from spec.versions and re-apply. The Module controller garbage-collects orphaned Version resources. If versionHistoryLimit is set, older versions are automatically pruned when the limit is exceeded.

For day-2 operations such as force re-sync, inline Version configs, provider lifecycle actions, vulnerability scanning runbooks, and pre-signed URL tuning, see Registry Operations.

For canonical configuration reference: