Argo CD GitLab Authentication: A Complete GitOps-Friendly Setup

Jun 10, 20266 min read

Argo CD can use GitLab as an authentication provider, so users log in with their GitLab accounts instead of local Argo CD users. That gives you centralized identity, cleaner access control, and one less password database waiting to become a security incident with a logo.

The setup uses OIDC through Dex, stores the GitLab OAuth credentials as encrypted Kubernetes secrets with SOPS and age, and lets Argo CD decrypt them with KSOPS during reconciliation.

How authentication works

Argo CD does not talk to GitLab directly for user login. It uses Dex as an identity broker.

The flow looks like this:

User -> Argo CD -> Dex -> GitLab -> Dex -> Argo CD -> RBAC

The short version:

  • The user opens Argo CD.
  • Argo CD redirects the user to Dex.
  • Dex redirects the user to GitLab.
  • GitLab authenticates the user and returns OIDC claims.
  • Argo CD reads those claims and applies RBAC rules.

Authentication tells Argo CD who the user is. RBAC tells Argo CD what damage they are allowed to do. Ideally, not much.

Create a GitLab OAuth application

First, create an OAuth application in GitLab.

In GitLab, go to your user, group, or admin application settings and create a new application with these values:

Name: Argo CD Redirect URI: https://argocd.example.com/api/dex/callback Scopes: openid, read_user

GitLab gives you two important values:

Application ID Secret

In the Argo CD Dex config, these become:

dex.gitlab.clientID: "<gitlab-application-id>" dex.gitlab.clientSecret: "<gitlab-application-secret>" dex.gitlab.redirectURI: "https://argocd.example.com/api/dex/callback"

Do not commit the secret in plain YAML. That is not GitOps. That is just leaving your keys under the doormat and calling it automation.

Encrypt the Argo CD secret with SOPS and age

Use SOPS with age to encrypt the Kubernetes secret before committing it.

Generate an age key if you do not have one yet:

age-keygen -o age.key

Store the public key in .sops.yaml:

creation_rules: - encrypted_regex: '^(data|stringData)$' age: age1examplepublickeyreplacewithyourrealpublickey

Create argocd-secret.yml:

apiVersion: v1 kind: Secret metadata: name: argocd-secret namespace: argocd type: Opaque stringData: dex.gitlab.clientID: "<gitlab-application-id>" dex.gitlab.clientSecret: "<gitlab-application-secret>" dex.gitlab.redirectURI: "https://argocd.example.com/api/dex/callback"

Encrypt it:

sops --encrypt argocd-secret.yml > argocd-secret.sops.yml

Then remove the unencrypted file:

rm argocd-secret.yml

Yes, really remove it. Git has a long memory and an even longer blame history.

Add KSOPS to the Argo CD repo server

Argo CD does not decrypt SOPS files by default. To make this work with Kustomize, install the KSOPS plugin in the Argo CD repo server.

The plugin lives here:

https://github.com/viaduct-ai/kustomize-sops

If you deploy Argo CD with Helm, add this to your values:

repoServer: volumeMounts: - name: custom-tools mountPath: /usr/local/bin/kustomize subPath: kustomize - name: custom-tools mountPath: /usr/local/bin/ksops subPath: ksops volumes: - name: custom-tools emptyDir: {} initContainers: - name: install-ksops image: viaductoss/ksops:v4.5.1 command: [ "/usr/local/bin/ksops", "install", "--with-kustomize", "/custom-tools", ] volumeMounts: - mountPath: /custom-tools name: custom-tools

This installs ksops and a Kustomize binary with plugin support into the repo server container.

Provide the age private key to Argo CD

The repo server needs the age private key to decrypt the SOPS file.

Create a Kubernetes secret from your age key:

kubectl -n argocd create secret generic sops-age-key \ --from-literal=key="$(cat age.key)"

Expose it to the repo server as SOPS_AGE_KEY:

repoServer: env: - name: SOPS_AGE_KEY valueFrom: secretKeyRef: name: sops-age-key key: key volumeMounts: - name: custom-tools mountPath: /usr/local/bin/kustomize subPath: kustomize - name: custom-tools mountPath: /usr/local/bin/ksops subPath: ksops volumes: - name: custom-tools emptyDir: {}

Protect this key properly. Anyone with the private key can decrypt the secrets. Kubernetes secrets are base64 encoded, not magically protected by tiny YAML elves.

Create the KSOPS generator

Create secret-generator.yml:

apiVersion: viaduct.ai/v1 kind: ksops metadata: name: argocd-secret-generator annotations: config.kubernetes.io/function: | exec: path: ksops files: - argocd-secret.sops.yml

This tells Kustomize to call ksops and decrypt argocd-secret.sops.yml during the build.

Reference the generator in Kustomize

In kustomization.yml, add the generator:

apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization generators: - secret-generator.yml

When Argo CD renders this application, Kustomize runs the KSOPS generator and produces the decrypted argocd-secret resource.

Configure Dex in Argo CD

Now configure Argo CD to use GitLab through Dex.

In Helm values, configure argocd-cm like this:

configs: cm: kustomize.buildOptions: "--enable-alpha-plugins --enable-exec" dex.config: | connectors: - type: gitlab id: gitlab name: GitLab config: baseURL: https://gitlab.example.com clientID: $dex.gitlab.clientID clientSecret: $dex.gitlab.clientSecret

The $dex.gitlab.* values are loaded from argocd-secret. That is why the secret names must match exactly.

The Kustomize build options matter:

kustomize.buildOptions: "--enable-alpha-plugins --enable-exec"

This allows Kustomize to execute the KSOPS plugin.

Do not enable exec plugins for repositories you do not trust. Otherwise, you have invented remote command execution with extra YAML. Security teams love that. Briefly.

Configure Argo CD RBAC

Authentication is not authorization. After GitLab login works, configure Argo CD RBAC.

For simple user mapping by email:

helm: valuesObject: configs: rbac: policy.csv: | g, alice@example.com, role:admin g, bob@example.com, role:readonly scopes: "[email,groups]"

This maps:

alice@example.com -> role:admin bob@example.com -> role:readonly

For larger teams, prefer GitLab groups instead of individual users. User by user RBAC starts cute and ends as spreadsheet archaeology.

Apply the configuration

If Argo CD is managed by another Argo CD application, commit the files and let Argo CD sync them.

If you are applying locally for a test, run:

kustomize build --enable-alpha-plugins --enable-exec . | kubectl apply -f -

Then restart the Argo CD components if needed:

kubectl -n argocd rollout restart deployment argocd-server kubectl -n argocd rollout restart deployment argocd-dex-server kubectl -n argocd rollout restart deployment argocd-repo-server

Verify the setup

Check that the secret exists:

kubectl -n argocd get secret argocd-secret

Check the Dex logs:

kubectl -n argocd logs deploy/argocd-dex-server

Check the Argo CD server logs:

kubectl -n argocd logs deploy/argocd-server

Check the repo server if decryption fails:

kubectl -n argocd logs deploy/argocd-repo-server

Open Argo CD in the browser:

https://argocd.example.com

You should see a GitLab login option. After login, the user should land in Argo CD with the expected role.

Troubleshoot common failures

If GitLab returns a redirect error, check the redirect URI.

It must match exactly:

https://argocd.example.com/api/dex/callback

If login works but RBAC does not, check which claim Argo CD receives. The email in policy.csv must match the user claim.

If the secret does not render, check the repo server pod:

kubectl -n argocd describe pod -l app.kubernetes.io/name=argocd-repo-server

Common causes:

  • KSOPS is not installed in the repo server.
  • -enable-alpha-plugins --enable-exec is missing.
  • The age private key is not available in the repo server environment.
  • SOPS_AGE_KEY is missing or points to the wrong Kubernetes secret key.
  • The encrypted file name does not match secret-generator.yml.
  • The secret keys do not match $dex.gitlab.clientID$dex.gitlab.clientSecret, or $dex.gitlab.redirectURI.

You can test decryption locally if you have the age key:

SOPS_AGE_KEY="$(cat age.key)" sops --decrypt argocd-secret.sops.yml

Security notes

Keep the boring security parts boring. Boring is good. Boring means nobody is explaining an outage in a meeting with too many directors.

  • Encrypt the GitLab OAuth secret with SOPS.
  • Protect the age private key.
  • Limit who can change Argo CD applications that use Kustomize exec plugins.
  • Use GitLab groups for larger teams.
  • Rotate the GitLab OAuth secret periodically.
  • Audit role:admin assignments.
  • Avoid giving admin access to users because they asked nicely in Slack.

Final result

With this setup, GitLab becomes the login provider for Argo CD, secrets stay encrypted in Git, and Argo CD RBAC controls what authenticated users can do.

The moving parts are not complicated:

GitLab OAuth app SOPS encrypted argocd-secret KSOPS in repo server Dex GitLab connector Argo CD RBAC policy

That is a reasonable amount of YAML for centralized authentication. Not small, obviously. This is Kubernetes. We measure configuration in geological layers.

RELATED POSTS
Maciej Łopalewski
Maciej Łopalewski
Senior Software Engineer

Reciprocal Rank Fusion on free Elasticsearch: licensing, workarounds, and the OpenSearch alternative

Jun 03, 20268 min read
Article image
Michał Miler
Michał Miler
Senior Software Engineer

How to Deploy Payload CMS on AWS Amplify with MongoDB Atlas for Free

May 27, 202611 min read
Article image
Michał Miler
Michał Miler
Senior Software Engineer

Keep Your Next.js App Warm on AWS Amplify Using Lambda + EventBridge

May 25, 20265 min read
Article image