Blog

Protecting Harbor with Zitadel

· Mattia Müggler

What is Harbor?

Harbor is an open-source container image registry that can also function as a chart or image proxy. This is especially useful when you want to cache images from Docker Hub or other registries. My main reason for using Harbor was to bypass the rate limits imposed by Docker Hub.

What is Zitadel?

Zitadel is an open-source identity and access management (IAM) solution that provides authentication and authorization services. It supports multiple authentication methods, including OAuth2, OpenID Connect and so on. Zitadel allows you to manage user identities, roles, and permissions for your applications and services.

How to Protect Harbor with Zitadel

To secure Harbor with Zitadel, you need to configure Harbor to use Zitadel as an external authentication provider. My goal was to achieve this by updating the Harbor configuration file and setting up Zitadel as an OAuth2 provider. However, a few additional steps were required to make it work.

Prerequisites

Step 1: Configure Zitadel

  1. Log in to your Zitadel instance and create a new PKCE application.

    • The redirect URL should be https://<your-harbor-domain>/c/oidc/callback.
  2. In the Token Settings tab, enable User roles inside ID Token.

  3. Create a role called harbor_administrators (or any name you prefer) and assign it to users who should have admin access in Harbor.

  4. Go to the Action tab and create a new action:

    • Name: flatRoles (the name must match the function name)

    • Code:

       1function flatRoles(ctx, api) {
       2    if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) {
       3        return;
       4    }
       5
       6    let grants = [];
       7    ctx.v1.user.grants.grants.forEach(claim => {
       8        claim.roles.forEach(role => {
       9            grants.push(role)  
      10        })
      11    });
      12
      13    api.v1.claims.setClaim('groups', grants)
      14}
      
    • Timeout: 10 seconds

    • Enable Allowed To Fail

  5. Go to the Flow section and select Complement Token as the Flow Type.

  6. Add triggers: Pre Userinfo creation and Pre access token creation, selecting the flatRoles action for both triggers.

  7. Update your Harbor configuration with the following userSettings:

    Make sure the JSON is valid. If you get errors, check for issues like comments (//) which aren’t valid in JSON.

    1{
    2  "auth_mode": "oidc_auth", // must be set to oidc_auth to enable OIDC authentication
    3  "oidc_name": "Zitadel", // display name for the OIDC provider
    4  "oidc_endpoint": "https://zitadel.domain.com", // issuer of your Zitadel instance
    5  "oidc_client_id": "335927275759403852", // client ID of your PKCE application we created earlier
    6  "oidc_scope": "openid,profile,email,offline_access", // scopes to request
    7  "oidc_groups_claim": "groups", // claim in the ID token that contains the user's groups
    8  "oidc_admin_group": "harbor_administrators" // group (which we created earlier) that will be mapped to Harbor administrators
    9}
    
  8. Restart Harbor to apply the changes.

  9. Log out of Harbor; you should see a new login button with the name specified in oidc_name.

  10. Log in with a user assigned to the harbor_administrators role to gain admin access.

That’s it! Your Harbor instance is now using Zitadel as IdP, allowing you to manage user access and permissions centrally.


Troubleshooting

To inspect your JWT token, you can use Auth-Playground. To check the group claim, temporarily add https://auth-playground.makefermion.com/oauth2/ as a redirect URL in your PKCE application on Zitadel.

Settings example:

  • Authorize URL: https://zitadel.domain.com/oauth/v2/authorize
  • Redirect URL: https://auth-playground.makefermion.com/oauth2
  • Client-ID: <your-client-id-of-the-pkce-application>
  • Client Secret: leave empty (not needed for PKCE)
  • Response type: code
  • Code Challenge: <your-code-challange> (ignore the placeholder on the site)
  • Code Challenge Method: S256
  • Scopes: openid profile email offline_access
  • Custom attributes: state=<random-string>

After logging in, you’ll be redirected back to Auth-Playground. Copy the code shown in the pop-up and exchange it for a token:

1curl --request POST \
2  --url 'https://zitadel.domain.com/oauth/v2/token' \
3  --header 'Content-Type: application/x-www-form-urlencoded' \
4  --data-urlencode 'client_id=<your-client-id>' \
5  --data-urlencode 'code_verifier=<original-code-verifier>' \
6  --data-urlencode 'code=<code-from-popup>' \
7  --data-urlencode 'grant_type=authorization_code' \
8  --data-urlencode 'redirect_uri=https://auth-playground.makefermion.com/oauth2'

Generate Code Challenge and State

Generate a code verifier and challenge with:

1CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | tr -d '\n')
2echo "CODE_VERIFIER: $CODE_VERIFIER"
3
4CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -binary -sha256 | openssl base64 | tr -d '=+/')
5echo "CODE_CHALLENGE: $CODE_CHALLENGE"
6
7STATE=$(openssl rand -base64 32 | tr -d '=+/' | tr -d '\n')
8echo "STATE: $STATE"

References