Platform Engineering Starter Kit Series: Securing Argo CD with Ingress & SSO (4)
In Post 3 we installed Argo CD in our GKE cluster and set up a GitOps app-of-apps pattern with a placeholder child app.
Now, we’ll make Argo CD safely accessible on the internet:
- HTTPS with NGINX Ingress + cert-manager (Let’s Encrypt)
- Automatic TLS renewals
- Google OAuth SSO (click-to-login; no more admin password)
- Everything driven by your
.env.<env>files + Makefile targets
0. Starting Point
If you finished Post 3:
git checkout -b post-04-argocd-ingress-ssoIf starting fresh from the Post 3 branch:
git clone -b post-03-gitops-bootstrap https://github.com/<your-username>/platform-starter-kit.git
cd platform-starter-kit
git checkout -b post-04-argocd-ingress-ssoMake sure your GKE dev cluster is running:
# If you destroyed the cluster after Post 3,
# run 'make apply' before make kubeconfig.
make kubeconfig
kubectl get nodes1. What we’re building
- A public hostname (e.g.,
argocd.dev.example.com) that points to the EXTERNAL-IP of the NGINX Ingress Controller’s LoadBalancer Service:
argocd.dev.example.com → <EXTERNAL_IP> - cert-manager gets a Let’s Encrypt TLS cert for that host.
- Argo CD sits behind the ingress and uses Google OAuth for sign-in
1.1 Namespaces & key objects
Cluster
├─ namespace: ingress-nginx
│ ├─ Deployment: ingress-nginx-controller # the router (Ingress Controller)
│ └─ Service: ingress-nginx-controller # type=LoadBalancer → EXTERNAL-IP
│
├─ namespace: argocd
│ ├─ Deployment: argocd-server # your app
│ ├─ Service: argocd-server # ClusterIP (internal)
│ └─ Ingress: argocd # host rules → argocd-server:80
│
└─ namespace: cert-manager
├─ Deployments: cert-manager, cainjector, webhook
└─ ClusterIssuer: letsencrypt # ACME (Let’s Encrypt)1.2 Hostname & DNS (per environment)
- Host:
ARGOCD_HOST(e.g.,argocd.dev.example.com) - DNS:
argocd.dev.example.com → <EXTERNAL_IP>ofingress-nginx-controller(LoadBalancer) - Dev-only alternative:
argocd.<EXTERNAL_IP>.sslip.io(ornip.io) — no DNS record required
1.3 Traffic flow (5 steps)
- The NGINX Ingress Controller gets a public EXTERNAL-IP from its LoadBalancer Service in
ingress-nginx. - You point DNS (
argocd.dev.example.com) → that EXTERNAL-IP (or use magic DNS for dev). - In the argocd namespace, an Ingress resource declares: “for host
ARGOCD_HOST, route/→argocd-server:80”. - The controller reads those rules and reverse-proxies traffic to the app’s Service/Pods.
- cert-manager watches the Ingress, runs an HTTP-01 challenge (a Let’s Encrypt/ACME domain-ownership check served over plain HTTP via NGINX), and on success stores the TLS cert as Secret
argocd-tlsin theargocdnamespace.
1.4 Current State (end of Post 3)
argocdnamespace with Argo CD installed (access via port-forwarding)- No public ingress, no DNS, no certs, no SSO
1.5 Desired State (end of Post 4)
ingress-nginxnamespace with NGINX Ingress Controller (public EXTERNAL-IP)cert-managernamespace with ClusterIssuer (Let’s Encrypt)argocdnamespace with:
- Ingress forARGOCD_HOST+ TLS (argocd-tls)
- Google OAuth SSO (login with your Google account)
1.6 Env-driven inputs (from .env.<env>)
ENV(e.g.,dev,staging,prod)BASE_DOMAIN(e.g.,example.com) andARGOCD_HOST(e.g.,argocd.dev.example.com)ACME_EMAIL(Let’s Encrypt contact)GOOGLE_OAUTH_CLIENT_ID/GOOGLE_OAUTH_CLIENT_SECRET
2. Install NGINX Ingress
Install the controller first to obtain the EXTERNAL-IP you’ll use for DNS or magic DNS.
kubectl create namespace ingress-nginx || true
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.ingressClassResource.name=nginx \
--set controller.ingressClass=nginx \
--set controller.watchIngressWithoutClass=true
# Save this IP (you’ll use it for DNS or magic DNS)
kubectl -n ingress-nginx get svc ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'; echo3. DNS: with a domain vs. without a domain
Pick one path for dev:
A) You have a domain (recommended)
Create an A record that points your Argo CD host to the NGINX Ingress EXTERNAL-IP.
- Get the IP:
# Get the EXTERNAL-IP (already installed in step 2)
kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'; echo2. In your DNS provider (zone: example.com) add:
- Name/Host:
argocd.dev - Type:
A - Value:
<EXTERNAL_IP># Result from the above command - TTL: 30 min — 1 hr (shorter while testing)
- Where to click:
- GoDaddy: Domain -> DNS -> “Add New Record” -> Type A
- Cloudflare: DNS -> Records -> “Add record” -> Type A
Using Cloudflare? Set the record to DNS-only (no proxy) during Let’s Encrypt HTTP-01 validation.
If your DNS zone is
dev.example.com(delegated subdomain), use Name:argocd(the zone already includesdev).If your provider gives you a load balancer hostname instead of an IP, you can use a CNAME record:
— Type:CNAME, Name:argocd.dev, Value:<lb-hostname>
- Verify:
# if dig isn't installed
# sudo apt install bind9-dnsutils
dig +short argocd.dev.example.com
# should print the same <EXTERNAL_IP>B) No domain yet (dev-only shortcut)
Use a “magic DNS” hostname that embeds the IP (no DNS changes needed):
# Put this in your .env.dev after you know the EXTERNAL_IP
ARGOCD_HOST=argocd.<EXTERNAL_IP>.sslip.io
# e.g., ARGOCD_HOST=argocd.34.123.45.67.sslip.io- Services like
sslip.io/nip.ioauto-resolveargocd.<IP>.<service>→<IP>. - Works with Let’s Encrypt HTTP-01.
- Dev/testing only — switch to path A for staging/prod and update
ARGOCD_HOST.
4. Environment variables (.env)
You’ll reference these values when rendering your Argo CD ingress/SSO config and when creating the ClusterIssuer.
Notes
- Define
ENVfirst in each.env.<env>; other vars (likeCLUSTER_NAME,ARGOCD_HOST) reference it. - If you’re using a real domain,
ARGOCD_HOSTfollows a stable pattern (e.g.,argocd.dev.example.com). - If you’re using magic DNS for dev (e.g.,
sslip.io), overrideARGOCD_HOSTafter you have the Ingress EXTERNAL-IP.
.env.sample
# Platform Starter Kit - Environment template (Post 4+)
# Copy this to: .env.<env> (dev|staging|prod) and fill in real values.
# ⚠️ Do NOT commit .env.* files; commit only .env.sample
# --- GCP / GKE (from earlier posts)
PROJECT_ID=your-gcp-project-id
REGION=your-region # e.g., northamerica-northeast1
ZONE=your-zone # e.g., northamerica-northeast1-a
CLUSTER_NAME=psk-<env>-gke # e.g., psk-dev-gke
# --- Artifact Registry (from earlier posts)
REGISTRY_LOCATION=your-region
REGISTRY_REPO=apps-<env> # e.g., apps-dev, apps-staging, apps
# --- Domain & host (new in Post 4)
BASE_DOMAIN=example.com
HOST_PREFIX=argocd
# For non-prod we derive: argocd.<env>.example.com
# (ENV is set in your per-env file)
ARGOCD_HOST=${HOST_PREFIX}.${ENV}.${BASE_DOMAIN}
# For prod you can override in .env.prod to drop ENV:
# ARGOCD_HOST=${HOST_PREFIX}.${BASE_DOMAIN}
ARGOCD_ADMIN_EMAIL=you@example.com
# --- ACME (Let’s Encrypt) notifications (new in Post 4)
ACME_EMAIL=your-email@example.com
# --- Google OAuth (OIDC) for Argo CD (new in Post 4)
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=Then set per-env:
.env.dev
PROJECT_ID=platform-starter-kit
REGION=northamerica-northeast1
ZONE=northamerica-northeast1-a
CLUSTER_NAME=psk-dev-gke
REGISTRY_LOCATION=northamerica-northeast1
REGISTRY_REPO=apps-dev
ENV=dev
BASE_DOMAIN=example.com
# ARGOCD_HOST becomes argocd.dev.example.com
ACME_EMAIL=you@example.com
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=....env.staging (omitted)
ENV=staging
BASE_DOMAIN=example.com
# ARGOCD_HOST -> argocd.staging.example.com.env.prod (omitted)
ENV=prod
BASE_DOMAIN=example.com
ARGOCD_HOST=${HOST_PREFIX}.${BASE_DOMAIN} # argocd.example.comIf you use delegated subdomains per env (e.g.,
dev.example.com), set:
.env.dev:BASE_DOMAIN=dev.example.comand keepARGOCD_HOST=${HOST_PREFIX}.${BASE_DOMAIN}(record name will be justargocd).
5. Install Cert-Manager (Let’s Encrypt)
We’ll install cert-manager to automate TLS certificates from Let’s Encrypt.
kubectl create namespace cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
# check latest at https://github.com/cert-manager/cert-manager/releases
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.18.2 \
--set crds.enabled=true \
--set "config.featureGates.ACMEHTTP01IngressPathTypeExact=false"Verify:
kubectl get pods -n cert-managerAll pods should be Running.
6. Configure a ClusterIssuer (Let’s Encrypt)
We’ll create a ClusterIssuer to automatically issue TLS certs from Let’s Encrypt.
Sensitive values like your email will be pulled from .env.$(ENV).
6.1 Makefile Target (env-render then apply)
# At the top of your Makefile, Set Make’s shell to bash so env loading works
SHELL := /usr/bin/env bash
.PHONY: issuer-apply
issuer-apply: ## Apply ClusterIssuer (env-rendered)
@set -a && source .env.$(ENV) && \
envsubst < argocd/cert-manager-clusterissuer.yaml | kubectl apply -f -6.2 Create
platform-starter-kit/argocd/cert-manager-clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v2.api.letsencrypt.org/directory
email: ${ACME_EMAIL}
privateKeySecretRef:
name: letsencrypt-private-key
solvers:
- http01:
ingress:
class: nginxRun it:
make issuer-apply7. Enable Ingress for Argo CD (TLS + Google SSO)
Keep your base helm/argocd/values.yaml from Post 3. Add an env-templated overlay that injects ARGOCD_HOST, cert-manager annotation, and OIDC settings.
helm/argocd/values.ingress-sso.tmpl.yaml
server:
service:
type: ClusterIP
ingress:
enabled: true
ingressClassName: nginx
hostname: ${ARGOCD_HOST}
tls: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt
configs:
cm:
url: https://${ARGOCD_HOST}
oidc.config: |
name: Google
issuer: https://accounts.google.com
clientID: ${GOOGLE_OAUTH_CLIENT_ID}
clientSecret: $oidc.google.clientSecret
requestedScopes: ["openid", "profile", "email"]
secret:
extra:
oidc.google.clientSecret: ${GOOGLE_OAUTH_CLIENT_SECRET}
# Example RBAC: grant admin to a specific email; adjust to your needs
rbac:
policy.csv: |
g, user:${ARGOCD_ADMIN_EMAIL}, role:admin
scopes: "[email]"Add Makefile helpers:
ARGOCD_OVERLAY=.rendered/argocd-values.$(ENV).yaml
render-argocd: ## Render Argo CD overlay from env
@mkdir -p .rendered
set -a && source .env.$(ENV) && envsubst < helm/argocd/values.ingress-sso.tmpl.yaml > $(ARGOCD_OVERLAY)
@echo "Rendered: $(ARGOCD_OVERLAY)"
argocd-upgrade: render-argocd ## Upgrade Argo CD with ingress/SSO overlay
helm repo add argo https://argoproj.github.io/argo-helm >/dev/null 2>&1 || true
helm repo update
helm upgrade --install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--version 8.2.7 \
-f helm/argocd/values.yaml \
-f $(ARGOCD_OVERLAY)Apply & verify:
make argocd-upgrade
kubectl -n argocd get pods
kubectl -n argocd get ingressCertificate issuance is automatic once DNS (or magic DNS) resolves your
ARGOCD_HOSTto the Ingress EXTERNAL-IP. cert-manager performs an HTTP-01 challenge via NGINX; on success it writes Secretargocd-tlsin theargocdnamespace and your Ingress serves HTTPS.
7. Request the TLS Certificate
Once DNS resolves, Cert-Manager will automatically issue the cert for argocd.dev.example.com using the ClusterIssuer.
Check:
kubectl describe certificate -n argocdYou should see a valid certificate issued.
If the certificate isn’t issued within ~2 minutes, check kubectl describe challenge -A for ACME errors.
8. Enable Google OAuth SSO (Basic)
8.1 Create a Google OAuth client
Go to Google Cloud Console -> APIs & Services -> OAuth consent screen -> click Get started
- Branding -> Get started -> fill App name & Support email -> Save.
- Audience -> Get started -> External (Testing) -> Add test users -> add your Google account email.
- Clients -> Create Client -> OAuth client ID
- Application Type: Web application
- Authorized JavaScript origins: leave empty
- Authorized redirect URIs — add one per environment you’ll use:
# Dev
https://argocd.dev.example.com/auth/callback
# Staging
https://argocd.staging.example.com/auth/callback
# Prod
https://argocd.example.com/auth/callback
# Dev-only magic DNS example (replace with your IP)
https://argocd.<EXTERNAL_IP>.sslip.io/auth/callback8.2 Put the credentials in your env (don’t commit secrets)
After you click Create, copy the Client ID and Client Secret into your .env.dev:
GOOGLE_OAUTH_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=xxxxxxxxxxxx8.3 Use env values in your Helm overlay
helm/argocd/values.ingress-sso.tmpl.yaml
configs:
cm:
url: https://${ARGOCD_HOST}
oidc.config: |
name: Google
issuer: https://accounts.google.com
clientID: ${GOOGLE_OAUTH_CLIENT_ID}
clientSecret: $oidc.google.clientSecret
requestedScopes: ["openid", "profile", "email"]
secret:
extra:
oidc.google.clientSecret: ${GOOGLE_OAUTH_CLIENT_SECRET}8.4 Re-deploy:
make argocd-upgradeAfter logging in via https://argocd.dev.example.com, you should see a Login via Google button.
8.5 Quick verify (URL, cert, login)
# Argo CD URL (should match your domain)
kubectl -n argocd get cm argocd-cm -o jsonpath='{.data.url}'; echo
# Ingress host (should be the same host, without https://)
kubectl -n argocd get ingress argocd-server -o jsonpath='{.spec.rules[0].host}'; echo
# Certificate status (should be Ready=True)
kubectl -n argocd get certificate
kubectl -n argocd describe certificate argocd-server-tls | sed -n '1,120p'Debug Tip kubectl describe certificate prints a lot of output (events, conditions, etc.).
You only need to confirm that the Conditions section shows Ready: True.
If you don’t want to scroll forever, you can trim the output like this:
kubectl -n argocd describe certificate argocd-server-tls | sed -n '1,120p'This just shows the first 120 lines, which usually includes everything important.
9. Commit & Push
# add ignore rule
printf '\n# Rendered files (contain secrets)\n.rendered/\n*.rendered.yaml\n' >> .gitignore
git add .
git commit -m "Post 04: Add Argo CD Ingress, TLS via Cert-Manager, and Google OAuth SSO"
git push -u origin post-04-argocd-ingress-sso10. (Optional) Pause Here? Tidy Up Costs
GKE bills even when idle. If you’re not moving straight to the next post, tear everything down:
# Destroys the GKE cluster and all in-cluster things (Argo CD, ingress, cert-manager, etc.)
make destroy ENV=devIn Post 5, we’ll add a single
make upcommand that rebuilds the cluster + add-ons in one shot when you’re ready to continue.
