Argo CD runs in the cluster and reconciles the GitOps repositories (homelabs and homelabs-private). Install the in-cluster manifests, then register applications through app-of-apps. Every private Git source needs repository credentials.
Install#
kubectl apply -k argocd
kubectl -n argocd get podsReach the UI#
Service argocd-server stays ClusterIP. Publish it through a Cloudflare Tunnel Public Hostname to the in-cluster Service.
Dex is a shared IdP (Grafana, kubectl, Argo CD). This homelab uses a personal GitHub account, so Dex cannot filter GitHub users by org, and Google cannot restrict to one Gmail. Any GitHub or Google user who completes Dex can get an Argo CD session; RBAC then shows an empty apps list. That is authorization, not a login gate.
Put Cloudflare Access on the Argo CD hostname only, allow-list the same address as github.admin_email in clusters/<cluster>/config/cluster.yaml. Leave Access off on Dex so discovery, JWKS, and the Dex callback stay reachable.
| Field | Value |
|---|---|
| Type | Published application (HTTP) |
| Hostname | argocd.<your-domain> |
| Origin | http://argocd-server.argocd.svc.cluster.local:80 |
| Cloudflare Access | On — Allow, include Emails: github.admin_email |
Cloudflare terminates TLS. Application argocd-sso sets argocd-cmd-params-cm server.insecure: "true" so Argo CD serves HTTP on the origin and does not 307-loop back to HTTPS. Restart argocd-server after that ConfigMap changes.
Visit https://argocd.<your-domain> first so Access sets a session cookie, then use GitHub through Dex. The button can say GitHub; the OIDC issuer is still Dex at https://dex.<your-domain>. After GitHub approves, Dex redirects to https://argocd.<your-domain>/auth/callback (Access must already have allowed you, or the OAuth code is lost). Local admin password login is off (admin.enabled: "false"). Anyone not on the Access allow-list never reaches Argo CD.
The argocd CLI uses the same hostname:
argocd login argocd.<your-domain> --ssoBreak-glass is kubeconfig: argocd login --core on a context that can reach namespace argocd, or kubectl -n argocd. Do not store an Argo CD UI password in Vault.
GitHub login (Dex)#
Argo CD is a confidential Dex static client (id: argocd, public: false) with secretEnv: ARGOCD_CLIENT_SECRET. The redirect URI is https://argocd.<your-domain>/auth/callback. Public homelabs/dex/values.yaml does not declare this client; production wiring lives in overlays/dex/values.yaml, next to Grafana.
Seed the client secret on the existing Dex KV. Patch, do not put, so GitHub, Google, and Grafana keys stay:
vault kv patch secret/homelab/dex ARGOCD_CLIENT_SECRET='<random>'ESO materializes Secret dex-argocd in namespace dex (Dex envVars) and Secret argocd-oidc in namespace argocd (clientSecret: $argocd-oidc:clientSecret). Both ExternalSecrets live in homelabs-private. The Argo CD Secret is dedicated; it does not replace bootstrap argocd-secret. Label app.kubernetes.io/part-of: argocd on argocd-oidc is required for that $secret:key substitution.
Application argocd-sso GitOps OIDC and RBAC from clusters/<cluster>/manifests/argocd only (no public homelabs/argocd tree). Sync Application dex first so ExternalSecret dex-argocd under manifests/oidc exists, then argocd-sso. argocd-cm sets url: https://argocd.<your-domain>, oidc.config, and admin.enabled: "false".
RBAC is default deny (policy.default empty) as defense in depth. Only github.admin_email receives role:admin. Do not treat that empty apps list as the login gate; Cloudflare Access on the Argo CD hostname is what stops other GitHub and Google users from opening the UI.
On the Dex static client, set secretEnv to the environment variable name, with no $. Dex does not expand secret: $VAR on staticClients; that form makes Dex treat the dollar-sign string as the secret (invalid_client_secret). Same rule as Grafana; see Dex.
Local admin disabled#
Password login is off. argocd-cm sets admin.enabled: "false" and policy.csv does not map local user admin to role:admin.
After the UI rejects admin plus password, delete the bootstrap secret if it is still present:
kubectl -n argocd delete secret argocd-initial-admin-secret --ignore-not-foundBreak-glass stays kubeconfig (argocd login --core or kubectl). Restart argocd-server if the login form still offers a password after sync.
Repository authentication#
Argo CD needs credentials for every private Git repo used as a source. Without them, apps show ComparisonError: authentication required: Repository not found.
- Create a GitHub Personal Access Token (classic) with
reposcope. - For each private repo, create a repository secret. Replace placeholders:
kubectl create secret generic SECRET_NAME \
-n argocd \
--from-literal=type=git \
--from-literal=url=REPO_URL \
--from-literal=password=YOUR_GITHUB_TOKEN \
--from-literal=username=YOUR_GITHUB_USERNAME
kubectl label secret SECRET_NAME -n argocd argocd.argoproj.io/secret-type=repositoryRepos used by this setup:
| Secret name | Repo URL |
|---|---|
homelabs-private-repo | https://github.com/<owner>/homelabs-private.git |
website-repo | https://github.com/<owner>/website.git |
When adding an application that uses another private repo, extend the AppProject (argocd/projects/homelab.yaml) and create a matching repository secret.
Rotate an expired GitHub token#
When the PAT expires, applications fail with authentication errors. Recreate the secret (example for homelabs-private):
NEW_GITHUB_PAT=ghp_xxx
kubectl create secret generic homelabs-private-repo \
-n argocd \
--from-literal=type=git \
--from-literal=url=https://github.com/<owner>/homelabs-private.git \
--from-literal=password="${NEW_GITHUB_PAT}" \
--from-literal=username=YOUR_GITHUB_USERNAME \
-o yaml --dry-run=client | kubectl apply -f -The argocd.argoproj.io/secret-type=repository label is only needed on first create.
Single token for all repos under an owner#
Use a repository credentials secret for every repo under https://github.com/<owner>:
export NEW_GITHUB_PAT=ghp_xxx
export GITHUB_USERNAME=your-github-username
kubectl create secret generic github-repo-creds \
-n argocd \
--from-literal=url=https://github.com/<owner> \
--from-literal=username="${GITHUB_USERNAME}" \
--from-literal=password="${NEW_GITHUB_PAT}" \
-o yaml --dry-run=client | kubectl apply -f -
kubectl label secret github-repo-creds -n argocd argocd.argoproj.io/secret-type=repo-creds --overwriteWebsite image pull secret (GHCR)#
If the website image is private, the pull secret lives in homelabs-private/clusters/<cluster>/overlays/website/secret.yaml. Generate a base64 Docker config with a PAT that has read:packages:
echo -n '{"auths":{"ghcr.io":{"username":"USERNAME","password":"YOUR_GITHUB_PAT","auth":"'$(echo -n "USERNAME:YOUR_GITHUB_PAT" | base64 -w0)'"}}}' | base64 -w0On macOS, omit -w0 from base64. Paste the output into secret.yaml under data.dockerconfigjson.
Disable Git submodules (if needed)#
If a repo uses submodules (e.g. Hugo theme), disable submodule fetch in Argo CD:
kubectl patch configmap argocd-cmd-params-cm -n argocd --type merge -p '{"data":{"reposerver.enable.git.submodule":"false"}}'
kubectl rollout restart deployment argocd-repo-server -n argocdDeploy applications#
After repository authentication is configured, apply AppProject resources, then deploy app-of-apps (it discovers Application resources under clusters/<cluster>/argocd/applications/):
kubectl apply -k argocdkubectl apply -f clusters/<cluster>/argocd/app-of-apps.yamlDelete an application#
Remove the app definition from Git: clusters/<cluster>/argocd/applications/<app-name>.yaml, the app repo and destination entries from argocd/projects/homelab.yaml, and the optional overlay at clusters/<cluster>/overlays/<app-name>/. Then follow Argo CD app deletion.
Application tracking model#
This setup uses app-of-apps with <cluster>-apps creating child applications. Sync <cluster>-apps when changing files under clusters/<cluster>/argocd/applications/.