Skip to content

9. Repositories and credentials

Argo CD needs to read your repositories and write to your clusters. Both are configured as Kubernetes Secrets with particular labels, and both are worth setting up properly, because the shortcuts here become the security findings later.

A labelled Secret in Argo CD’s namespace:

apiVersion: v1
kind: Secret
metadata:
name: repo-manifests
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: https://github.com/acme/manifests.git
username: not-used
password: ghp_xxxxxxxxxxxx

The label is what makes Argo CD find it. Without the label it is an ordinary Secret and nothing happens — which is the most common cause of “Argo CD cannot authenticate to my repository” when the credentials are obviously correct.

You can also add repositories through the UI or CLI, which creates exactly this Secret:

Terminal window
argocd repo add https://github.com/acme/manifests.git \
--username git --password "$TOKEN"
stringData:
type: git
url: git@github.com:acme/manifests.git
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

Two things people get wrong:

The URL form must be the SSH one. git@github.com:acme/manifests.git, not the HTTPS URL. Argo CD picks the transport from the URL.

Host keys must be known. Argo CD verifies them against the argocd-ssh-known-hosts-cm ConfigMap, which ships with the major providers pre-populated. A self-hosted GitLab or Gitea needs adding:

Terminal window
ssh-keyscan git.internal.example.com >> known_hosts
kubectl create cm argocd-ssh-known-hosts-cm --from-file=ssh_known_hosts=known_hosts \
--dry-run=client -o yaml | kubectl apply -f -

There is an insecureIgnoreHostKey option. Do not use it. It removes the only protection against a machine-in-the-middle serving you different manifests, in a system whose entire job is to apply manifests to production.

A personal access token belongs to a person, expires when they leave, and usually carries far more access than needed. A GitHub App belongs to the organisation:

stringData:
type: git
url: https://github.com/acme/manifests.git
githubAppID: "123456"
githubAppInstallationID: "7891011"
githubAppPrivateKey: |
-----BEGIN RSA PRIVATE KEY-----
...

Why it is better: scoped to specific repositories, read-only if you configure it so, not tied to a person, and higher rate limits. For an organisation of any size this is the right answer, and it is a smaller change than it looks.

GitLab’s equivalent is a deploy token or a group access token; Bitbucket has app passwords. The principle holds everywhere: the credential should belong to the system, be scoped to what it reads, and be read-only.

That last word matters. Argo CD never needs write access to your manifest repository. If your credential can push, that is unnecessary risk — unless you are using the Image Updater’s git write-back mode, which is exactly the trade-off chapter 12 examines.

Fifty repositories in one organisation should not be fifty Secrets. A credential template matches by URL prefix:

apiVersion: v1
kind: Secret
metadata:
name: creds-acme
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repo-creds # ← creds, not repository
stringData:
url: https://github.com/acme
githubAppID: "123456"
githubAppInstallationID: "7891011"
githubAppPrivateKey: |
...

Any repository URL starting with https://github.com/acme uses these credentials, with no per-repository Secret at all. The longest matching prefix wins, so you can have a general template and a specific override.

This matters for ApplicationSets, where Applications are generated for repositories that did not exist when you configured Argo CD. Without a credential template, a generated Application pointing at a new repository fails to authenticate and there is nowhere obvious to look.

# a Helm repository
stringData:
type: helm
name: bitnami
url: https://charts.bitnami.com/bitnami
# an OCI registry
stringData:
type: helm
name: acme-charts
url: ghcr.io/acme/charts # NO oci:// prefix here
enableOCI: "true"
username: ...
password: ...

Note the asymmetry that catches everyone: the Secret’s url has no oci:// prefix, but the Application’s repoURL does. oci://ghcr.io/acme/charts in the Application, ghcr.io/acme/charts in the Secret. Getting this wrong produces an authentication error that says nothing about the prefix.

type: helm is required for Helm repositories. Without it Argo CD treats the URL as git and fails confusingly.

The other half, and the one with the larger blast radius.

Terminal window
# the usual way — reads your kubeconfig and creates the Secret
argocd cluster add prod-eu --name prod-eu

What that command actually does is worth knowing, because people are often surprised:

1. creates a ServiceAccount `argocd-manager` in
kube-system on the TARGET cluster
2. binds it to the `cluster-admin` ClusterRole
3. takes its token
4. stores it in a labelled Secret in Argo CD's namespace

Step 2 is cluster-admin, by default. Argo CD in one cluster now has full administrative access to another. That is convenient and it is a serious decision, made by a one-line command that does not warn you.

The alternatives:

Scope it down. Create your own ServiceAccount with only the permissions Argo CD needs, and register that:

Terminal window
argocd cluster add prod-eu --service-account argocd-limited \
--system-namespace argocd

This works well when Argo CD manages a known set of resource kinds. It works badly when it manages everything, which is why most people leave it as cluster-admin and compensate with AppProject restrictions instead (chapter 10).

Or do not centralise at all. Run an Argo CD per cluster, each managing only itself with in-cluster credentials, so there are no cross-cluster credentials anywhere. Chapter 10 argues this is underrated.

https://kubernetes.default.svc is a built-in destination meaning “the cluster Argo CD runs in”, using its own ServiceAccount rather than a stored credential. You can disable it (cluster.inClusterEnabled: false) if Argo CD should manage only remote clusters — worth doing on a dedicated management cluster, because it removes an entire target.

Be clear about this: Argo CD’s namespace contains credentials to every repository and every cluster it manages. Anyone who can read Secrets in that namespace — or exec into a pod there, or read an etcd backup — has all of it.

Practical consequences:

Lock down the namespace. Kubernetes RBAC on argocd should be as tight as on kube-system. In particular, get secrets in that namespace is equivalent to admin on every managed cluster.

Encrypt etcd at rest. These Secrets are base64 in etcd otherwise — the etcd chapter covers EncryptionConfiguration and why existing Secrets must be rewritten after you enable it.

Prefer short-lived credentials where you can. A GitHub App token is minted per request and expires in an hour, which is meaningfully better than a personal access token sitting in a Secret for two years.

Consider external secret storage. Vault with the External Secrets Operator can populate these Secrets from outside the cluster, so the durable copy is not in Kubernetes at all. Chapter 12 is about that pattern in general.

The uncomfortable observation, worth stating plainly: Argo CD’s own credentials are the biggest static-credential problem in a GitOps setup, and it is the one the GitOps model does not solve for you. You moved deployment credentials out of CI and concentrated them here instead. That is a genuine improvement — one hardened namespace beats every build runner — but it is a concentration, not an elimination.

Since these are just Secrets, they can be managed declaratively — with the obvious caveat that the credential itself must not be in git in plaintext. This is exactly chapter 12’s problem, and the usual answer is the External Secrets Operator generating the labelled Secret from Vault or a cloud secret manager, so the shape is in git and the value is not.

  • Repositories are labelled Secrets (argocd.argoproj.io/secret-type: repository). A missing label is the most common “it will not authenticate” cause.
  • SSH needs the SSH-form URL and a known host key in argocd-ssh-known-hosts-cm. Never use insecureIgnoreHostKey.
  • Prefer a GitHub App (or deploy/group token) over a personal access token: organisation-owned, repository-scoped, short-lived, higher rate limits.
  • Argo CD never needs write access to your manifest repository unless you deliberately adopt Image Updater’s git write-back.
  • Credential templates (secret-type: repo-creds) match by longest URL prefix and are essential for ApplicationSets generating Applications for repositories you have not configured.
  • OCI: oci:// prefix in the Application, no prefix in the Secret.
  • argocd cluster add grants cluster-admin on the target cluster by default. Scope it down, or run one Argo CD per cluster.
  • Argo CD’s namespace holds credentials to everything it manages. Treat get secrets there as equivalent to admin everywhere. Encrypt etcd, prefer short-lived credentials, consider external secret storage.
  • GitOps concentrates deployment credentials rather than eliminating them — a real improvement over credentials on every build runner, but not a solved problem.

Next: multi-cluster and multi-tenancy — AppProjects, RBAC, SSO, sync windows, and the blast-radius question.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source