Sitemap

Platform Engineering Starter Kit Series: Securing Argo CD with Ingress & SSO (4)

9 min readAug 23, 2025

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-sso

If 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-sso

Make 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 nodes

1. 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> of ingress-nginx-controller (LoadBalancer)
  • Dev-only alternative: argocd.<EXTERNAL_IP>.sslip.io (or nip.io) — no DNS record required

1.3 Traffic flow (5 steps)

  1. The NGINX Ingress Controller gets a public EXTERNAL-IP from its LoadBalancer Service in ingress-nginx.
  2. You point DNS (argocd.dev.example.com) → that EXTERNAL-IP (or use magic DNS for dev).
  3. In the argocd namespace, an Ingress resource declares: “for host ARGOCD_HOST, route /argocd-server:80”.
  4. The controller reads those rules and reverse-proxies traffic to the app’s Service/Pods.
  5. 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-tls in the argocd namespace.

1.4 Current State (end of Post 3)

  • argocd namespace 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-nginx namespace with NGINX Ingress Controller (public EXTERNAL-IP)
  • cert-manager namespace with ClusterIssuer (Let’s Encrypt)
  • argocd namespace with:
    - Ingress for ARGOCD_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) and ARGOCD_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}'; echo

3. 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.

  1. 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}'; echo

2. 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.
Press enter or click to view image in full size

If your DNS zone is dev.example.com (delegated subdomain), use Name: argocd (the zone already includes dev).

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.io auto-resolve argocd.<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 ENV first in each .env.<env>; other vars (like CLUSTER_NAME, ARGOCD_HOST) reference it.
  • If you’re using a real domain, ARGOCD_HOST follows a stable pattern (e.g., argocd.dev.example.com).
  • If you’re using magic DNS for dev (e.g., sslip.io), override ARGOCD_HOST after 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.com

If you use delegated subdomains per env (e.g., dev.example.com), set:
.env.dev: BASE_DOMAIN=dev.example.com and keep ARGOCD_HOST=${HOST_PREFIX}.${BASE_DOMAIN} (record name will be just argocd).

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-manager

All 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: nginx

Run it:

make issuer-apply

7. 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 ingress

Certificate issuance is automatic once DNS (or magic DNS) resolves your ARGOCD_HOST to the Ingress EXTERNAL-IP. cert-manager performs an HTTP-01 challenge via NGINX; on success it writes Secret argocd-tls in the argocd namespace 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 argocd

You 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

Press enter or click to view image in full size
  • 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/callback
Press enter or click to view image in full size

8.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=xxxxxxxxxxxx

8.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-upgrade

After 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-sso

Post 4 branch on GitHub

10. (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=dev

In Post 5, we’ll add a single make up command that rebuilds the cluster + add-ons in one shot when you’re ready to continue.

--

--

Jimin
Jimin

Written by Jimin

DevOps engineer and tech enthusiast. Sharing tech insights to simplify the complex. Let's connect on LinkedIn! https://www.linkedin.com/in/byun-jimin/

No responses yet