Skip to content

Examples on how to use federated identity of Harbor/8gears to push/pull images without secrets from GitHubAction/GitlabCI and Kubernetes.

License

Notifications You must be signed in to change notification settings

container-registry/harbor-workload-identity-federation

Repository files navigation

Workload Identity Federation Examples to Push/Pull container Images Without Secrets

Repository with examples demonstrating how to use Harbor/8gears Container Registry with Workload Identity Federation, eliminating the need for static secrets in CI/CD pipelines and Kubernetes.

Overview

Workload Identity Federation allows Harbor to authenticate clients using short-lived JWTs instead of static robot account secrets. By establishing a trust relationship with an external Identity Provider (like GitHub Actions, GitLab CI, or Kubernetes), Harbor can validate tokens and map them to internal robot accounts based on specific claims.

Benefits

  • No static secrets: Eliminate the operational burden of managing pull secrets
  • Enhanced security: Use ephemeral, workload-specific credentials
  • Simplified rotation: No secret rotation required since tokens are short-lived
  • Audit trail: Better traceability of which workload accessed the registry

Supported Identity Providers

  • GitHub Actions
  • GitLab CI
  • Kubernetes 1.33 (via Service Account tokens)
  • FluxCD
  • Forgejo Actions (TBD)

GitHub Actions Example

This example demonstrates how to authenticate to Harbor from a GitHub Actions workflow using OIDC tokens.

Prerequisites

  1. Harbor Setup: Configure a Federated Identity Provider in Harbor:

    • OpenID Configuration URL: https://token.actions.githubusercontent.com/.well-known/openid-configuration
    • JWKS URI: Automatically discovered
    • Issuer: Automatically discovered

    Harbor Federated IDP Setup

  2. Robot Account: Create a federated robot account in Harbor with claim rules matching your GitHub repository:

    • iss: https://token.actions.githubusercontent.com
    • aud: <your-registry-domain> (e.g., macfly4200.8gears.ch) β€” The audience can be any string, but using your registry domain is recommended. This ensures tokens are scoped specifically to your registry and prevents token reuse across different services.
    • repository: <owner>/<repo> (e.g., myorg/myrepo)

Workflow Configuration

See a successful run example.

name: Create Image and Push Using federated IDP

on:
  workflow_dispatch:

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # Required for OIDC JWT
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Get OIDC token
        id: oidc
        run: |
          echo "Requesting OIDC token"
          RESPONSE=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
          "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=<your-registry-domain>")

          TOKEN=$(echo "$RESPONSE" | jq -r '.value')
          echo "TOKEN=$TOKEN" >> $GITHUB_ENV

      - name: Build and Push Image
        run: |
          echo $TOKEN | docker login -u not-relevant --password-stdin <your-registry-domain>
          docker build -t <your-registry-domain>/library/image:${{ github.sha }} .
          docker push <your-registry-domain>/library/image:${{ github.sha }}

      - name: Pull Image from Registry
        run: |
          echo $TOKEN | docker login -u not-relevant --password-stdin <your-registry-domain>
          docker pull <your-registry-domain>/library/hello-world:latest

Key Points

  1. Permissions: The workflow must have id-token: write permission to request OIDC tokens.

  2. Audience: The audience parameter in the token request must match the audience configured in your Harbor Federated Identity Provider (typically your registry domain).

  3. Username: The username for docker login is not used for authentication (can be any value like not-relevant). Authentication is based solely on the JWT token.

  4. Token Claims: GitHub Actions OIDC tokens include claims such as:

    • iss: Issuer (always https://token.actions.githubusercontent.com)
    • aud: Audience (your registry domain)
    • sub: Subject (e.g., repo:owner/repo:ref:refs/heads/main)
    • repository: Repository name
    • repository_owner: Repository owner
    • actor: User who triggered the workflow
    • ref: Git reference
    • sha: Commit SHA

Debugging JWT Tokens

To inspect the JWT token contents during workflow execution:

- name: Debug JWT Token
  run: |
    echo "=== JWT Header ==="
    echo "$TOKEN" | cut -d'.' -f1 | base64 -d 2>/dev/null | jq .
    echo ""
    echo "=== JWT Payload ==="
    echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .

Example JWT Token

Here's an example of what a GitHub Actions OIDC token looks like:

JWT Header:

{
  "alg": "RS256",
  "kid": "38826b17-6a30-5f9b-b169-8beb8202f723",
  "typ": "JWT",
  "x5t": "ykNaY4qM_ta4k2TgZOCEYLkcYlA"
}

JWT Payload:

{
  "actor": "Vad1mo",
  "actor_id": "1492007",
  "aud": "macfly4200.8gears.ch",
  "base_ref": "",
  "check_run_id": "56363837828",
  "event_name": "workflow_dispatch",
  "exp": 1764090768,
  "head_ref": "",
  "iat": 1764090468,
  "iss": "https://token.actions.githubusercontent.com",
  "job_workflow_ref": "container-registry/federated-idp-examples/.github/workflows/example_1.yml@refs/heads/main",
  "job_workflow_sha": "15a5ebfa3fb5ddf10c4b4250e14496bec7f03a56",
  "jti": "6cf8862b-b832-4372-9998-22026ecd21a1",
  "nbf": 1764090168,
  "ref": "refs/heads/main",
  "ref_protected": "false",
  "ref_type": "branch",
  "repository": "container-registry/federated-idp-examples",
  "repository_id": "1104004353",
  "repository_owner": "container-registry",
  "repository_owner_id": "46576199",
  "repository_visibility": "public",
  "run_attempt": "1",
  "run_id": "19677834613",
  "run_number": "4",
  "runner_environment": "github-hosted",
  "sha": "15a5ebfa3fb5ddf10c4b4250e14496bec7f03a56",
  "sub": "repo:container-registry/federated-idp-examples:ref:refs/heads/main",
  "workflow": "Create Image and Push Using federated IDP",
  "workflow_ref": "container-registry/federated-idp-examples/.github/workflows/example_1.yml@refs/heads/main",
  "workflow_sha": "15a5ebfa3fb5ddf10c4b4250e14496bec7f03a56"
}

Key claims for Harbor claim rules:

Claim Description Example Value
iss Token issuer https://token.actions.githubusercontent.com
aud Target audience (your registry) macfly4200.8gears.ch
sub Subject identifier repo:container-registry/federated-idp-examples:ref:refs/heads/main
repository Full repository name container-registry/federated-idp-examples
repository_owner Organization or user container-registry
ref Git reference refs/heads/main
actor User who triggered the workflow Vad1mo

GitLab CI Example

This example demonstrates how to authenticate to Harbor from a GitLab CI pipeline using OIDC tokens.

Prerequisites

  1. Harbor Setup: Configure a Federated Identity Provider in Harbor:

    • OpenID Configuration URL: https://gitlab.com/.well-known/openid-configuration
    • JWKS URI: Automatically discovered
    • Issuer: Automatically discovered
  2. Robot Account: Create a federated robot account in Harbor with claim rules matching your GitLab project:

    • iss: https://gitlab.com
    • aud: <your-registry-domain> (e.g., macfly4200.8gears.ch)
    • project_path: <namespace>/<project> (e.g., 8gears/container-registry/harbor-workload-identity-federation)

Pipeline Configuration

stages:
  - build

build-and-push:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  id_tokens:
    ID_TOKEN:
      aud: <your-registry-domain>  # πŸ‘ˆ audience claim matching your registry
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  script:
    # Login to registry using JWT token
    - echo "$ID_TOKEN" | docker login -u not-relevant --password-stdin <your-registry-domain>

    # Build and push image
    - docker build -t <your-registry-domain>/library/image:$CI_COMMIT_SHA .
    - docker push <your-registry-domain>/library/image:$CI_COMMIT_SHA
  rules:
    - when: manual

Key Points

  1. id_tokens: GitLab CI uses the id_tokens keyword to request OIDC tokens. The token is automatically available as $ID_TOKEN.

  2. Audience: The aud field under id_tokens must match the audience configured in your Harbor Federated Identity Provider.

  3. Username: The username for docker login is not used for authentication (can be any value like not-relevant). Authentication is based solely on the JWT token.

  4. Token Claims: GitLab CI OIDC tokens include claims such as:

    • iss: Issuer (always https://gitlab.com for gitlab.com)
    • aud: Audience (your registry domain)
    • sub: Subject (e.g., project_path:8gears/container-registry/harbor-workload-identity-federation:ref_type:branch:ref:main)
    • project_path: Full project path
    • namespace_path: Group/namespace path
    • ref: Git reference
    • user_login: User who triggered the pipeline

Example JWT Token

Here's an example of what a GitLab CI OIDC token looks like:

JWT Header:

{
  "kid": "4i3sFE7sxqNPOT7FdvcGA1ZVGGI_r-tsDXnEuYT4ZqE",
  "typ": "JWT",
  "alg": "RS256"
}

** GitLab JWT Payload:**

{
  "project_id": "76366029",
  "project_path": "8gears/container-registry/harbor-workload-identity-federation",
  "namespace_id": "1087575",
  "namespace_path": "8gears/container-registry",
  "user_id": "907142",
  "user_login": "vad1mo",
  "user_email": "vaba@gmail.com",
  "user_access_level": "owner",
  "pipeline_id": "2179265456",
  "pipeline_source": "push",
  "job_id": "12217223061",
  "ref": "main",
  "ref_type": "branch",
  "ref_path": "refs/heads/main",
  "ref_protected": "true",
  "runner_id": 47561171,
  "runner_environment": "self-hosted",
  "sha": "de7a8b6e3892321ac9d2305f26332dcdc775f1c2",
  "project_visibility": "public",
  "ci_config_ref_uri": "gitlab.com/8gears/container-registry/harbor-workload-identity-federation//.gitlab-ci.yml@refs/heads/main",
  "ci_config_sha": "de7a8b6e3892321ac9d2305f26332dcdc775f1c2",
  "jti": "aeb1491b-8988-49b5-ada0-2e372973b18e",
  "iat": 1764098080,
  "nbf": 1764098075,
  "exp": 1764101680,
  "iss": "https://gitlab.com",
  "sub": "project_path:8gears/container-registry/harbor-workload-identity-federation:ref_type:branch:ref:main",
  "aud": "macfly4200.8gears.ch"
}

Key claims for Harbor claim rules:

Claim Description Example Value
iss Token issuer https://gitlab.com
aud Target audience (your registry) macfly4200.8gears.ch
sub Subject identifier project_path:8gears/container-registry/harbor-workload-identity-federation:ref_type:branch:ref:main
project_path Full project path 8gears/container-registry/harbor-workload-identity-federation
namespace_path Group/namespace path 8gears/container-registry
ref Git reference main
user_login User who triggered the pipeline vad1mo

How It Works

  1. Token Request: The CI/CD platform (GitHub Actions, GitLab CI) provides an OIDC token to the job. GitHub uses environment variables and a curl request, while GitLab provides the token directly via id_tokens.

  2. Token Validation: Harbor validates the JWT by:

    • Verifying the signature using the provider's JWKS
    • Checking the iss (issuer) claim matches the configured provider
    • Validating exp (expiration) and aud (audience) claims
  3. Robot Account Matching: Harbor matches the token's claims against configured claim rules to identify the appropriate robot account.

  4. Authorization: Once matched, the request is authorized based on the robot account's permissions.


Kubernetes (k3s/k3d) Setup

This section describes how to set up a local k3s/k3d cluster with Kubernetes Image Credential Provider to pull images using Service Account tokens (Workload Identity Federation).

Prerequisites

  • Docker installed
  • k3d installed (brew install k3d or see k3d.io)
  • kubectl installed
  • The credential-provider-plugin binary for your architecture (linux-amd64 or linux-arm64)

1. Create k3d Configuration

Create k3d-config.yaml:

apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
  name: credential-provider-test
servers: 1
agents: 0
image: rancher/k3s:v1.34.2-k3s1
volumes:
  # Mount the credential provider binary
  - volume: /path/to/linux-arm64/credential-provider-plugin:/var/lib/rancher/credentialprovider/bin/credential-provider-plugin
    nodeFilters:
      - all
  # Mount the credential provider config
  - volume: /path/to/k8s_credential_provider_config.yaml:/var/lib/rancher/credentialprovider/config.yaml
    nodeFilters:
      - all
options:
  k3s:
    extraArgs:
      - arg: --disable=traefik
        nodeFilters:
          - server:*
      # Allow your registry as an audience for service account tokens
      - arg: --kube-apiserver-arg=api-audiences=https://kubernetes.default.svc.cluster.local,<your-registry-domain>
        nodeFilters:
          - server:*
      # Enable feature gates for credential providers with SA tokens
      - arg: --kube-apiserver-arg=feature-gates=ServiceAccountNodeAudienceRestriction=true
        nodeFilters:
          - server:*
      - arg: --kubelet-arg=feature-gates=KubeletServiceAccountTokenForCredentialProviders=true
        nodeFilters:
          - server:*

2. Create Credential Provider Configuration

Create k8s_credential_provider_config.yaml:

kind: CredentialProviderConfig
apiVersion: kubelet.config.k8s.io/v1
providers:
  - name: credential-provider-plugin
    apiVersion: credentialprovider.kubelet.k8s.io/v1
    tokenAttributes:
      requireServiceAccount: true
      serviceAccountTokenAudience: "<your-registry-domain>"
      cacheType: Token
    matchImages:
      - "<your-registry-domain>"
    defaultCacheDuration: "1h"

3. Create the Cluster

# Create the cluster
k3d cluster create --config k3d-config.yaml

# Get kubeconfig
k3d kubeconfig get credential-provider-test > kubeconfig.yaml
export KUBECONFIG=kubeconfig.yaml

4. Export JWKS for Harbor

Export the cluster's JWKS keys for configuring Harbor's Federated IDP:

# Get the OIDC discovery document
kubectl get --raw /.well-known/openid-configuration | jq .

# Get the JWKS keys
kubectl get --raw /openid/v1/jwks | jq . > jwks.json

5. Configure Harbor Federated IDP

  1. In Harbor, go to Administration β†’ Robot Accounts β†’ Federated Identity Providers
  2. Create a new Federated IDP with:
    • Issuer: https://kubernetes.default.svc.cluster.local
    • JWKS: Paste the contents of jwks.json
  3. Create a robot account with claim rules:
    • iss == https://kubernetes.default.svc.cluster.local
    • aud == <your-registry-domain>
    • kubernetes.io.namespace == <namespace> (optional, for namespace-scoped access)

6. Create Service Account and Test Pod

# Create a service account
kubectl create serviceaccount test-pull

# Create a test pod with projected volume for the audience
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: test-harbor
  namespace: default
spec:
  serviceAccountName: test-pull
  containers:
  - name: test-harbor
    image: <your-registry-domain>/library/hello-world:latest
    volumeMounts:
    - name: harbor-token
      mountPath: /var/run/secrets/harbor
      readOnly: true
  volumes:
  - name: harbor-token
    projected:
      sources:
      - serviceAccountToken:
          audience: <your-registry-domain>
          expirationSeconds: 3600
          path: token
EOF

Key Points

  1. Feature Gates: Both ServiceAccountNodeAudienceRestriction (API server) and KubeletServiceAccountTokenForCredentialProviders (kubelet) must be enabled.

  2. Projected Volume: The pod MUST have a projected volume with the audience matching your registry. Without this, kubelet cannot request tokens for that audience.

  3. Credential Provider Plugin: The credential-provider-plugin binary receives the SA token from kubelet and returns it as credentials for the registry.

  4. JWKS Rotation: If you recreate the cluster, new signing keys are generated. You must update Harbor's Federated IDP with the new JWKS.

Example JWT Token

A Kubernetes Service Account token for image pulls looks like:

{
  "aud": ["macfly4200.8gears.ch"],
  "exp": 1764255017,
  "iat": 1764251417,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "5e8572b6-4e27-4039-8fc5-dcdd85ade4e1",
  "kubernetes.io": {
    "namespace": "default",
    "serviceaccount": {
      "name": "test-pull",
      "uid": "238dbbbe-4d7c-4efe-bd03-957c547c8daa"
    }
  },
  "nbf": 1764251417,
  "sub": "system:serviceaccount:default:test-pull"
}

Key claims for Harbor claim rules:

Claim Description Example Value
iss Token issuer https://kubernetes.default.svc.cluster.local
aud Target audience (your registry) macfly4200.8gears.ch
sub Subject identifier system:serviceaccount:default:test-pull
kubernetes.io.namespace Kubernetes namespace default
kubernetes.io.serviceaccount.name Service account name test-pull

Security Considerations

  • Tokens are short-lived (typically 5-10 minutes)
  • Each pipeline/workflow run gets a unique token
  • Claims provide fine-grained control over which workflows can access which resources
  • No secrets need to be stored in CI/CD settings

References

GitHub Actions

GitLab CI

About

Examples on how to use federated identity of Harbor/8gears to push/pull images without secrets from GitHubAction/GitlabCI and Kubernetes.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published