December 16, 2025·4 min read
This beginner's guide covers how to configure ArgoCD to sync applications from private GitHub repositories and pull private container images from GitHub Container Registry (GHCR). For production environments, consider using External Secrets Operator or Sealed Secrets to manage credentials declaratively rather than imperatively
Prerequisites
- ArgoCD installed on your Kubernetes cluster
- A GitHub Personal Access Token (PAT) with the following scopes:
repo- Full control of private repositoriesread:packages- Read packages from GHCRwrite:packages- Push packages to GHCR (for CI/CD)
Part 1: ArgoCD Git Repository Access
ArgoCD needs credentials to clone private repositories. Create a Kubernetes secret with your GitHub username and PAT.
Create the Secret
apiVersion: v1
kind: Secret
metadata:
name: github-repo-creds
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repo-creds
type: Opaque
stringData:
url: https://github.com/YOUR_USERNAME/
type: git
username: YOUR_USERNAME
password: YOUR_GITHUB_PATKey points:
- Use
repo-credsas the secret type for prefix matching (applies to all repos under that URL) - The
usernamefield should be your actual GitHub username - The
passwordfield is your PAT - Include the trailing slash on the URL
Apply and Restart
kubectl apply -f github-repo-creds.yaml
kubectl rollout restart deployment argocd-repo-server -n argocdVerify
Check that your application syncs:
kubectl get application -n argocdYou should see Synced status instead of Unknown.
Part 2: Private Container Image Pulls
If your container images are also private in GHCR, Kubernetes needs an imagePullSecret to authenticate when pulling images.
Create the Docker Registry Secret
Run this for each namespace where you deploy applications:
kubectl create secret docker-registry ghcr-pull-secret \
--namespace YOUR_NAMESPACE \
--docker-server=ghcr.io \
--docker-username=YOUR_USERNAME \
--docker-password=YOUR_GITHUB_PAT \
--docker-email=noreply@github.comReference in Your Helm Chart
Add to your values.yaml:
imagePullSecrets:
- name: ghcr-pull-secretUpdate your deployment template to use it:
spec:
template:
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"Storing the PAT Securely
Instead of hardcoding the PAT, store it in a secret manager like Azure Key Vault and retrieve it when creating secrets:
PAT=$(az keyvault secret show --vault-name YOUR_VAULT --name github-pat --query value -o tsv)
kubectl create secret docker-registry ghcr-pull-secret \
--namespace dev \
--docker-server=ghcr.io \
--docker-username=YOUR_USERNAME \
--docker-password="$PAT" \
--docker-email=noreply@github.comSummary
| Secret | Purpose | Namespace |
|---|---|---|
github-repo-creds |
ArgoCD clones private repos | argocd |
ghcr-pull-secret |
Kubernetes pulls private images | Each deployment namespace |
Both secrets use the same PAT with your GitHub username. The repo-creds type enables prefix matching, so one secret covers all repositories under your GitHub account.
Production Approach: External Secrets Operator
The imperative approach above works, but for production you want secrets managed declaratively through GitOps. External Secrets Operator automatically syncs secrets from Azure Key Vault (or AWS Secrets Manager, HashiCorp Vault, etc.) into Kubernetes secrets.
Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespaceConfigure Azure Key Vault Access
First, create a SecretStore that connects to your Key Vault. This example uses Azure Workload Identity:
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: azure-keyvault
spec:
provider:
azurekv:
authType: WorkloadIdentity
vaultUrl: "https://YOUR_VAULT.vault.azure.net"
serviceAccountRef:
name: external-secrets-sa
namespace: external-secretsDeclarative GitHub Credentials for ArgoCD
Now define an ExternalSecret that pulls the PAT from Key Vault and creates the ArgoCD secret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: github-repo-creds
namespace: argocd
spec:
refreshInterval: 1h
secretStoreRef:
name: azure-keyvault
kind: ClusterSecretStore
target:
name: github-repo-creds
template:
metadata:
labels:
argocd.argoproj.io/secret-type: repo-creds
data:
type: git
url: https://github.com/YOUR_USERNAME/
username: YOUR_USERNAME
password: "{{ .github_pat }}"
data:
- secretKey: github_pat
remoteRef:
key: github-patDeclarative GHCR Pull Secret
Similarly for the image pull secret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: ghcr-pull-secret
namespace: dev
spec:
refreshInterval: 1h
secretStoreRef:
name: azure-keyvault
kind: ClusterSecretStore
target:
name: ghcr-pull-secret
template:
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: |
{
"auths": {
"ghcr.io": {
"username": "YOUR_USERNAME",
"password": "{{ .github_pat }}",
"auth": "{{ printf "%s:%s" "YOUR_USERNAME" .github_pat | b64enc }}"
}
}
}
data:
- secretKey: github_pat
remoteRef:
key: github-patBenefits of This Approach
- GitOps-friendly - Secret definitions live in git, actual values stay in Key Vault
- Automatic rotation - Change the PAT in Key Vault, secrets update automatically
- Audit trail - Key Vault logs all secret access
- No imperative commands - Everything is declarative YAML
- Single source of truth - One PAT in Key Vault, referenced everywhere
Alternative: Sealed Secrets
If you don't have a secret manager, Sealed Secrets lets you encrypt secrets so they can be committed to git:
# Install kubeseal CLI and controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# Encrypt a secret
kubectl create secret generic github-repo-creds \
--namespace argocd \
--from-literal=password=YOUR_PAT \
--dry-run=client -o yaml | kubeseal -o yaml > sealed-secret.yamlThe sealed secret can be safely committed to git - only your cluster can decrypt it.
Which Approach Should You Use?
| Approach | Best For |
|---|---|
Manual kubectl create |
Learning, development, quick setup |
| External Secrets Operator | Production with existing secret manager |
| Sealed Secrets | Production without external secret manager |
For most production Kubernetes environments, External Secrets Operator with your cloud provider's secret manager is the recommended approach.
Enjoyed this post? Give it a clap!
Comments