Protecting Harbor with Zitadel
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
A running Harbor instance (I used the official Helm chart)
- If you want to use PKCE, make sure to use Harbor v2.13.0 or higher.
A running Zitadel instance (I used the official Helm chart).
Step 1: Configure Zitadel
Log in to your Zitadel instance and create a new PKCE application.
- The redirect URL should be
https://<your-harbor-domain>/c/oidc/callback.
- The redirect URL should be
In the
Token Settingstab, enableUser roles inside ID Token.Create a role called
harbor_administrators(or any name you prefer) and assign it to users who should have admin access in Harbor.Go to the
Actiontab 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:
10secondsEnable
Allowed To Fail
Go to the
Flowsection and selectComplement Tokenas theFlow Type.Add triggers:
Pre Userinfo creationandPre access token creation, selecting theflatRolesaction for both triggers.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}Restart Harbor to apply the changes.
Log out of Harbor; you should see a new login button with the name specified in
oidc_name.Log in with a user assigned to the
harbor_administratorsrole 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"