From 58bc84abad62c3b0aed64954a5f030d571bb78d8 Mon Sep 17 00:00:00 2001 From: Daniel Genis Date: Fri, 10 Jul 2026 16:06:25 +0200 Subject: [PATCH 1/4] feat: support Google IAP on the GKE ingress via BackendConfig --- helm/optio/templates/backend-config.yaml | 12 ++++++++++++ helm/optio/templates/iap-secret.yaml | 16 ++++++++++++++++ helm/optio/values.yaml | 12 ++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 helm/optio/templates/iap-secret.yaml diff --git a/helm/optio/templates/backend-config.yaml b/helm/optio/templates/backend-config.yaml index 740d7019..533c339d 100644 --- a/helm/optio/templates/backend-config.yaml +++ b/helm/optio/templates/backend-config.yaml @@ -8,6 +8,12 @@ metadata: {{- include "optio.labels" . | nindent 4 }} spec: timeoutSec: 3600 + {{- if and .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} + iap: + enabled: true + oauthclientCredentials: + secretName: {{ .Release.Name }}-iap-oauth + {{- end }} {{- if .Values.ingress.gke.cloudArmorPolicy }} securityPolicy: name: {{ .Values.ingress.gke.cloudArmorPolicy | quote }} @@ -25,6 +31,12 @@ metadata: labels: {{- include "optio.labels" . | nindent 4 }} spec: + {{- if and .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} + iap: + enabled: true + oauthclientCredentials: + secretName: {{ .Release.Name }}-iap-oauth + {{- end }} {{- if .Values.ingress.gke.cloudArmorPolicy }} securityPolicy: name: {{ .Values.ingress.gke.cloudArmorPolicy | quote }} diff --git a/helm/optio/templates/iap-secret.yaml b/helm/optio/templates/iap-secret.yaml new file mode 100644 index 00000000..4f3e770c --- /dev/null +++ b/helm/optio/templates/iap-secret.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.ingress.enabled .Values.ingress.gke .Values.ingress.gke.enabled .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} +{{- if or (not .Values.auth.google.clientId) (not .Values.auth.google.clientSecret) }} +{{- fail "ingress.gke.iap.enabled requires auth.google.clientId and auth.google.clientSecret" }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-iap-oauth + namespace: {{ .Values.namespace }} + labels: + {{- include "optio.labels" . | nindent 4 }} +type: Opaque +stringData: + client_id: {{ .Values.auth.google.clientId | quote }} + client_secret: {{ .Values.auth.google.clientSecret | quote }} +{{- end }} diff --git a/helm/optio/values.yaml b/helm/optio/values.yaml index e2778051..788969f8 100644 --- a/helm/optio/values.yaml +++ b/helm/optio/values.yaml @@ -400,6 +400,18 @@ ingress: enabled: false staticIpName: "" cloudArmorPolicy: "" + # Google Identity-Aware Proxy. When enabled, every request through the + # GKE ingress must carry a Google identity before reaching optio pods. + # Reuses the OAuth client from auth.google.clientId/clientSecret. + # NOTE: blocks external webhook callers (/api/webhooks, /api/hooks, Slack). + # One-time GCP setup: + # 1. Add redirect URI to the OAuth client: + # https://iap.googleapis.com/v1/oauth/clientIds/:handleRedirect + # 2. Grant access: + # gcloud iap web add-iam-policy-binding \ + # --member=domain: --role=roles/iap.httpsResourceAccessor + iap: + enabled: false managedCertificate: enabled: false domains: [] From 97401c1611d6bc356ae9ac387159fdfe4efe845d Mon Sep 17 00:00:00 2001 From: Daniel Genis Date: Fri, 10 Jul 2026 16:07:01 +0200 Subject: [PATCH 2/4] docs: document IAP ingress option and design --- .../plans/2026-07-10-iap-ingress.md | 269 ++++++++++++++++++ .../specs/2026-07-10-iap-ingress-design.md | 101 +++++++ helm/optio/values.production.yaml | 6 + 3 files changed, 376 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-10-iap-ingress.md create mode 100644 docs/superpowers/specs/2026-07-10-iap-ingress-design.md diff --git a/docs/superpowers/plans/2026-07-10-iap-ingress.md b/docs/superpowers/plans/2026-07-10-iap-ingress.md new file mode 100644 index 00000000..42b77556 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-iap-ingress.md @@ -0,0 +1,269 @@ +# Google IAP at the GKE Ingress Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enable Google Identity-Aware Proxy on the existing GKE ingress so every request to optio.gynzy.dev must carry a gynzy Google identity before reaching optio pods. + +**Architecture:** A single new helm value `ingress.gke.iap.enabled` gates two changes: a small dedicated secret (`-iap-oauth`, keys `client_id`/`client_secret` as IAP requires) rendered from the existing `auth.google.*` values, and an `iap` block on both existing BackendConfigs. No new runtime components; optio's own login is untouched. + +**Tech Stack:** Helm chart (`helm/optio/`), GKE BackendConfig CRD, Google IAP. + +Spec: `docs/superpowers/specs/2026-07-10-iap-ingress-design.md` + +## Global Constraints + +- IAP covers **both** web and api backends. No bypass for `/api/webhooks/*`, `/api/hooks/*`, or Slack — external webhook callers are knowingly blocked. +- The IAP secret's keys must be exactly `client_id` and `client_secret` (GKE requirement). +- Guard all template accesses with `and` chains so old values files without the `iap` key still render (nil-safe). +- No app code changes. No GitHub workflow changes. + +--- + +### Task 1: Chart changes — IAP value, secret template, BackendConfig blocks + +**Files:** + +- Modify: `helm/optio/values.yaml` (ingress.gke block, ~line 397) +- Create: `helm/optio/templates/iap-secret.yaml` +- Modify: `helm/optio/templates/backend-config.yaml` + +**Interfaces:** + +- Consumes: existing values `auth.google.clientId`, `auth.google.clientSecret`, `ingress.gke.enabled`. +- Produces: value `ingress.gke.iap.enabled` (bool, default false); secret `{{ .Release.Name }}-iap-oauth`; `iap` block on both BackendConfigs referencing that secret. + +- [ ] **Step 1: Capture the failing render check** + +Run (from repo root): + +```bash +helm template optio helm/optio \ + --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ + --set publicUrl=https://optio.gynzy.dev \ + --set auth.google.clientId=test-id \ + --set auth.google.clientSecret=test-secret \ + --set ingress.enabled=true \ + --set ingress.gke.enabled=true \ + --set ingress.gke.iap.enabled=true \ + | grep -A4 "iap:" +``` + +Expected: FAIL — `--set ingress.gke.iap.enabled=true` renders nothing (no `iap:` output; grep exits 1). + +- [ ] **Step 2: Add the value to `helm/optio/values.yaml`** + +In the `ingress.gke` block (after `cloudArmorPolicy: ""`), add: + +```yaml +# Google Identity-Aware Proxy. When enabled, every request through the +# GKE ingress must carry a Google identity before reaching optio pods. +# Reuses the OAuth client from auth.google.clientId/clientSecret. +# NOTE: blocks external webhook callers (/api/webhooks, /api/hooks, Slack). +# One-time GCP setup: +# 1. Add redirect URI to the OAuth client: +# https://iap.googleapis.com/v1/oauth/clientIds/:handleRedirect +# 2. Grant access: +# gcloud iap web add-iam-policy-binding \ +# --member=domain: --role=roles/iap.httpsResourceAccessor +iap: + enabled: false +``` + +- [ ] **Step 3: Create `helm/optio/templates/iap-secret.yaml`** + +```yaml +{{- if and .Values.ingress.enabled .Values.ingress.gke .Values.ingress.gke.enabled .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} +{{- if or (not .Values.auth.google.clientId) (not .Values.auth.google.clientSecret) }} +{{- fail "ingress.gke.iap.enabled requires auth.google.clientId and auth.google.clientSecret" }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-iap-oauth + namespace: {{ .Values.namespace }} + labels: + {{- include "optio.labels" . | nindent 4 }} +type: Opaque +stringData: + client_id: {{ .Values.auth.google.clientId | quote }} + client_secret: {{ .Values.auth.google.clientSecret | quote }} +{{- end }} +``` + +- [ ] **Step 4: Add the `iap` block to both BackendConfigs in `helm/optio/templates/backend-config.yaml`** + +Insert this block into **both** BackendConfig specs — in the api config directly under `timeoutSec: 3600`, and in the web config directly under `spec:`: + +```yaml + {{- if and .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} + iap: + enabled: true + oauthclientCredentials: + secretName: {{ .Release.Name }}-iap-oauth + {{- end }} +``` + +- [ ] **Step 5: Verify enabled render** + +Re-run the Step 1 command. Expected: two `iap:` blocks, each with `enabled: true` and `secretName: optio-iap-oauth`. Also verify the secret: + +```bash +helm template optio helm/optio \ + --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ + --set publicUrl=https://optio.gynzy.dev \ + --set auth.google.clientId=test-id \ + --set auth.google.clientSecret=test-secret \ + --set ingress.enabled=true \ + --set ingress.gke.enabled=true \ + --set ingress.gke.iap.enabled=true \ + --show-only templates/iap-secret.yaml +``` + +Expected: secret `optio-iap-oauth` with `client_id: "test-id"` and `client_secret: "test-secret"`. + +- [ ] **Step 6: Verify disabled + guard renders** + +```bash +# Disabled (default): no iap anywhere +helm template optio helm/optio \ + --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ + --set publicUrl=https://optio.gynzy.dev \ + --set auth.google.clientId=test-id \ + --set auth.google.clientSecret=test-secret \ + --set ingress.enabled=true \ + --set ingress.gke.enabled=true \ + | grep "iap" ; echo "exit=$?" + +# IAP enabled without google creds: must fail with the message above +helm template optio helm/optio \ + --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ + --set publicUrl=https://optio.gynzy.dev \ + --set auth.github.clientId=test-id \ + --set auth.github.clientSecret=test-secret \ + --set ingress.enabled=true \ + --set ingress.gke.enabled=true \ + --set ingress.gke.iap.enabled=true +``` + +Expected: first command prints nothing / `exit=1`; second fails with `ingress.gke.iap.enabled requires auth.google.clientId and auth.google.clientSecret`. + +- [ ] **Step 7: Lint** + +```bash +helm lint helm/optio --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 +``` + +Expected: `1 chart(s) linted, 0 chart(s) failed`. + +- [ ] **Step 8: Commit** + +```bash +git add helm/optio/values.yaml helm/optio/templates/iap-secret.yaml helm/optio/templates/backend-config.yaml +git commit -m "feat: support Google IAP on the GKE ingress via BackendConfig" +``` + +--- + +### Task 2: Document in values.production.yaml + +**Files:** + +- Modify: `helm/optio/values.production.yaml` (ingress block, ~line 94) + +**Interfaces:** + +- Consumes: `ingress.gke.iap.enabled` from Task 1. +- Produces: documentation only. + +- [ ] **Step 1: Add the IAP block (disabled) with setup notes** + +In `values.production.yaml`, inside `ingress.gke` after `cloudArmorPolicy: optio`, add: + +```yaml +# Google IAP: gate the whole site behind Google sign-in at the load +# balancer. Requires auth.google.clientId/clientSecret. Blocks external +# webhook callers. See ingress.gke.iap comments in values.yaml for the +# one-time GCP setup (OAuth redirect URI + IAM grant). +iap: + enabled: false +``` + +- [ ] **Step 2: Verify production values still render** + +```bash +helm template optio helm/optio -f helm/optio/values.production.yaml \ + --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ + --set publicUrl=https://optio.example.com \ + --set auth.google.clientId=test-id \ + --set auth.google.clientSecret=test-secret \ + --set externalDatabase.url=postgres://u:p@h:5432/optio \ + --set externalRedis.url=redis://h:6379 > /dev/null && echo OK +``` + +Expected: `OK`. + +- [ ] **Step 3: Commit (include spec + plan docs)** + +```bash +git add helm/optio/values.production.yaml docs/superpowers/specs/2026-07-10-iap-ingress-design.md docs/superpowers/plans/2026-07-10-iap-ingress.md +git commit -m "docs: document IAP ingress option and design" +``` + +--- + +### Task 3: Production rollout (interactive — run with the operator, not a subagent) + +**Files:** none (cluster + GCP operations). Context: `gke_gh-runners_europe-west4_gh-runners-2023`, namespace `optio`, GCP project `gh-runners`. + +- [ ] **Step 1: Get the OAuth client id from the cluster** + +```bash +kubectl --context gke_gh-runners_europe-west4_gh-runners-2023 -n optio \ + get secret optio-config -o jsonpath='{.data.GOOGLE_OAUTH_CLIENT_ID}' | base64 -d +``` + +- [ ] **Step 2: Add the IAP redirect URI to that OAuth client** + +In GCP Console (project `gh-runners`) → APIs & Services → Credentials → the OAuth client from Step 1 → add authorized redirect URI: + +``` +https://iap.googleapis.com/v1/oauth/clientIds/:handleRedirect +``` + +- [ ] **Step 3: Grant IAP access to gynzy.com** + +```bash +gcloud iap web add-iam-policy-binding --project=gh-runners \ + --member=domain:gynzy.com --role=roles/iap.httpsResourceAccessor +``` + +- [ ] **Step 4: Deploy** + +Upgrade the release with IAP on, using whatever values flow prod normally uses, e.g.: + +```bash +helm upgrade optio helm/optio -n optio --reuse-values \ + --kube-context gke_gh-runners_europe-west4_gh-runners-2023 \ + --set ingress.gke.iap.enabled=true +``` + +- [ ] **Step 5: Verify** + +```bash +# Backend configs picked up IAP +kubectl --context gke_gh-runners_europe-west4_gh-runners-2023 -n optio \ + get backendconfig -o yaml | grep -B2 -A3 "iap:" + +# Anonymous request redirects to Google sign-in (allow a few minutes for LB propagation) +curl -sI https://optio.gynzy.dev | head -5 +``` + +Expected: both BackendConfigs show `iap.enabled: true`; curl returns `302` to `accounts.google.com`. Then in a browser with a gynzy account: sign in through Google, confirm optio loads and a running task's log stream (WebSocket) works. + +- [ ] **Step 6: Rollback plan (only if broken)** + +```bash +helm upgrade optio helm/optio -n optio --reuse-values \ + --kube-context gke_gh-runners_europe-west4_gh-runners-2023 \ + --set ingress.gke.iap.enabled=false +``` diff --git a/docs/superpowers/specs/2026-07-10-iap-ingress-design.md b/docs/superpowers/specs/2026-07-10-iap-ingress-design.md new file mode 100644 index 00000000..c7545a90 --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-iap-ingress-design.md @@ -0,0 +1,101 @@ +# Google IAP at the Ingress — Design + +Date: 2026-07-10 +Status: Approved + +## Goal + +Put a Google identity layer (IAP) in front of `optio.gynzy.dev` at the load +balancer, before any traffic reaches optio pods. Optio's own application login +stays as-is (it will be disabled later, separately). + +## Context + +Production runs the chart's GKE-native ingress (GCLB) with paths `/*` → web, +`/api/*` + `/ws/*` → api, a Cloud Armor policy, and a Google-managed +certificate. The chart already renders `BackendConfig` resources for both +services, and `auth.google.clientId` / `auth.google.clientSecret` values +already exist (they feed optio's own Google login via the `optio-config` +secret). + +## Decisions + +- **Mechanism**: Google Identity-Aware Proxy enabled per backend service via + the existing `BackendConfig` templates. No new proxy components. +- **Coverage**: IAP on **both** web and api backends. External webhook + callers (`/api/webhooks/*`, `/api/hooks/*`, Slack events) are knowingly + blocked — no bypass backend. +- **App auth**: unchanged. IAP is a pure extra gate. +- **OAuth client**: reuse the existing Google OAuth client from + `auth.google.*` values. IAP requires a secret with keys exactly + `client_id` / `client_secret`, so the chart renders a small dedicated + secret from those same values. + +## Chart changes + +1. `values.yaml` — new setting: + + ```yaml + ingress: + gke: + iap: + enabled: false + ``` + +2. New template `templates/iap-secret.yaml` — when + `ingress.enabled && ingress.gke.enabled && ingress.gke.iap.enabled`, + render: + + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: {{ .Release.Name }}-iap-oauth + stringData: + client_id: {{ .Values.auth.google.clientId }} + client_secret: {{ .Values.auth.google.clientSecret }} + ``` + + Fail the render (`fail`) if IAP is enabled but `auth.google.clientId` + is empty. + +3. `templates/backend-config.yaml` — when IAP is enabled, both + BackendConfigs gain: + + ```yaml + iap: + enabled: true + oauthclientCredentials: + secretName: {{ .Release.Name }}-iap-oauth + ``` + +4. `values.production.yaml` — document the `iap` block (left disabled), plus + the manual GCP steps. + +## Manual GCP steps (one-time, outside helm) + +1. Add redirect URI + `https://iap.googleapis.com/v1/oauth/clientIds/:handleRedirect` + to the existing Google OAuth client. +2. Grant `roles/iap.httpsResourceAccessor` to the gynzy.com domain (or a + Google group) on the IAP-protected backend services. + +## Behavior notes + +- GCLB health checks bypass IAP — `/api/health` checks keep working. +- WebSockets (`/ws/*`) work through IAP via the browser session cookie. +- Cloud Armor policy coexists with IAP on the same backend services. +- Agent pods talk to the API via in-cluster service DNS, bypassing the + ingress — unaffected. +- Rollback: `helm upgrade` with `ingress.gke.iap.enabled=false`. + +## Testing + +- `helm template` render checks (no unit-test framework in this chart): IAP + block present on both BackendConfigs when enabled, absent when disabled, + secret rendered with correct keys, render failure when google client id + missing. +- `helm lint` passes. +- Post-deploy verification: anonymous curl to `https://optio.gynzy.dev` + returns a Google sign-in redirect; authenticated gynzy browser session + reaches optio; task log streaming over WS still works. diff --git a/helm/optio/values.production.yaml b/helm/optio/values.production.yaml index ef24b181..419c576c 100644 --- a/helm/optio/values.production.yaml +++ b/helm/optio/values.production.yaml @@ -97,6 +97,12 @@ ingress: enabled: true staticIpName: optio cloudArmorPolicy: optio + # Google IAP: gate the whole site behind Google sign-in at the load + # balancer. Requires auth.google.clientId/clientSecret. Blocks external + # webhook callers. See ingress.gke.iap comments in values.yaml for the + # one-time GCP setup (OAuth redirect URI + IAM grant). + iap: + enabled: false managedCertificate: enabled: true domains: From 44b5bae713952a6b7eca30b700295055180174dd Mon Sep 17 00:00:00 2001 From: Daniel Genis Date: Fri, 10 Jul 2026 16:14:09 +0200 Subject: [PATCH 3/4] docs: drop IAP rollout plan doc --- .../plans/2026-07-10-iap-ingress.md | 269 ------------------ 1 file changed, 269 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-10-iap-ingress.md diff --git a/docs/superpowers/plans/2026-07-10-iap-ingress.md b/docs/superpowers/plans/2026-07-10-iap-ingress.md deleted file mode 100644 index 42b77556..00000000 --- a/docs/superpowers/plans/2026-07-10-iap-ingress.md +++ /dev/null @@ -1,269 +0,0 @@ -# Google IAP at the GKE Ingress Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Enable Google Identity-Aware Proxy on the existing GKE ingress so every request to optio.gynzy.dev must carry a gynzy Google identity before reaching optio pods. - -**Architecture:** A single new helm value `ingress.gke.iap.enabled` gates two changes: a small dedicated secret (`-iap-oauth`, keys `client_id`/`client_secret` as IAP requires) rendered from the existing `auth.google.*` values, and an `iap` block on both existing BackendConfigs. No new runtime components; optio's own login is untouched. - -**Tech Stack:** Helm chart (`helm/optio/`), GKE BackendConfig CRD, Google IAP. - -Spec: `docs/superpowers/specs/2026-07-10-iap-ingress-design.md` - -## Global Constraints - -- IAP covers **both** web and api backends. No bypass for `/api/webhooks/*`, `/api/hooks/*`, or Slack — external webhook callers are knowingly blocked. -- The IAP secret's keys must be exactly `client_id` and `client_secret` (GKE requirement). -- Guard all template accesses with `and` chains so old values files without the `iap` key still render (nil-safe). -- No app code changes. No GitHub workflow changes. - ---- - -### Task 1: Chart changes — IAP value, secret template, BackendConfig blocks - -**Files:** - -- Modify: `helm/optio/values.yaml` (ingress.gke block, ~line 397) -- Create: `helm/optio/templates/iap-secret.yaml` -- Modify: `helm/optio/templates/backend-config.yaml` - -**Interfaces:** - -- Consumes: existing values `auth.google.clientId`, `auth.google.clientSecret`, `ingress.gke.enabled`. -- Produces: value `ingress.gke.iap.enabled` (bool, default false); secret `{{ .Release.Name }}-iap-oauth`; `iap` block on both BackendConfigs referencing that secret. - -- [ ] **Step 1: Capture the failing render check** - -Run (from repo root): - -```bash -helm template optio helm/optio \ - --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ - --set publicUrl=https://optio.gynzy.dev \ - --set auth.google.clientId=test-id \ - --set auth.google.clientSecret=test-secret \ - --set ingress.enabled=true \ - --set ingress.gke.enabled=true \ - --set ingress.gke.iap.enabled=true \ - | grep -A4 "iap:" -``` - -Expected: FAIL — `--set ingress.gke.iap.enabled=true` renders nothing (no `iap:` output; grep exits 1). - -- [ ] **Step 2: Add the value to `helm/optio/values.yaml`** - -In the `ingress.gke` block (after `cloudArmorPolicy: ""`), add: - -```yaml -# Google Identity-Aware Proxy. When enabled, every request through the -# GKE ingress must carry a Google identity before reaching optio pods. -# Reuses the OAuth client from auth.google.clientId/clientSecret. -# NOTE: blocks external webhook callers (/api/webhooks, /api/hooks, Slack). -# One-time GCP setup: -# 1. Add redirect URI to the OAuth client: -# https://iap.googleapis.com/v1/oauth/clientIds/:handleRedirect -# 2. Grant access: -# gcloud iap web add-iam-policy-binding \ -# --member=domain: --role=roles/iap.httpsResourceAccessor -iap: - enabled: false -``` - -- [ ] **Step 3: Create `helm/optio/templates/iap-secret.yaml`** - -```yaml -{{- if and .Values.ingress.enabled .Values.ingress.gke .Values.ingress.gke.enabled .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} -{{- if or (not .Values.auth.google.clientId) (not .Values.auth.google.clientSecret) }} -{{- fail "ingress.gke.iap.enabled requires auth.google.clientId and auth.google.clientSecret" }} -{{- end }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Release.Name }}-iap-oauth - namespace: {{ .Values.namespace }} - labels: - {{- include "optio.labels" . | nindent 4 }} -type: Opaque -stringData: - client_id: {{ .Values.auth.google.clientId | quote }} - client_secret: {{ .Values.auth.google.clientSecret | quote }} -{{- end }} -``` - -- [ ] **Step 4: Add the `iap` block to both BackendConfigs in `helm/optio/templates/backend-config.yaml`** - -Insert this block into **both** BackendConfig specs — in the api config directly under `timeoutSec: 3600`, and in the web config directly under `spec:`: - -```yaml - {{- if and .Values.ingress.gke.iap .Values.ingress.gke.iap.enabled }} - iap: - enabled: true - oauthclientCredentials: - secretName: {{ .Release.Name }}-iap-oauth - {{- end }} -``` - -- [ ] **Step 5: Verify enabled render** - -Re-run the Step 1 command. Expected: two `iap:` blocks, each with `enabled: true` and `secretName: optio-iap-oauth`. Also verify the secret: - -```bash -helm template optio helm/optio \ - --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ - --set publicUrl=https://optio.gynzy.dev \ - --set auth.google.clientId=test-id \ - --set auth.google.clientSecret=test-secret \ - --set ingress.enabled=true \ - --set ingress.gke.enabled=true \ - --set ingress.gke.iap.enabled=true \ - --show-only templates/iap-secret.yaml -``` - -Expected: secret `optio-iap-oauth` with `client_id: "test-id"` and `client_secret: "test-secret"`. - -- [ ] **Step 6: Verify disabled + guard renders** - -```bash -# Disabled (default): no iap anywhere -helm template optio helm/optio \ - --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ - --set publicUrl=https://optio.gynzy.dev \ - --set auth.google.clientId=test-id \ - --set auth.google.clientSecret=test-secret \ - --set ingress.enabled=true \ - --set ingress.gke.enabled=true \ - | grep "iap" ; echo "exit=$?" - -# IAP enabled without google creds: must fail with the message above -helm template optio helm/optio \ - --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ - --set publicUrl=https://optio.gynzy.dev \ - --set auth.github.clientId=test-id \ - --set auth.github.clientSecret=test-secret \ - --set ingress.enabled=true \ - --set ingress.gke.enabled=true \ - --set ingress.gke.iap.enabled=true -``` - -Expected: first command prints nothing / `exit=1`; second fails with `ingress.gke.iap.enabled requires auth.google.clientId and auth.google.clientSecret`. - -- [ ] **Step 7: Lint** - -```bash -helm lint helm/optio --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 -``` - -Expected: `1 chart(s) linted, 0 chart(s) failed`. - -- [ ] **Step 8: Commit** - -```bash -git add helm/optio/values.yaml helm/optio/templates/iap-secret.yaml helm/optio/templates/backend-config.yaml -git commit -m "feat: support Google IAP on the GKE ingress via BackendConfig" -``` - ---- - -### Task 2: Document in values.production.yaml - -**Files:** - -- Modify: `helm/optio/values.production.yaml` (ingress block, ~line 94) - -**Interfaces:** - -- Consumes: `ingress.gke.iap.enabled` from Task 1. -- Produces: documentation only. - -- [ ] **Step 1: Add the IAP block (disabled) with setup notes** - -In `values.production.yaml`, inside `ingress.gke` after `cloudArmorPolicy: optio`, add: - -```yaml -# Google IAP: gate the whole site behind Google sign-in at the load -# balancer. Requires auth.google.clientId/clientSecret. Blocks external -# webhook callers. See ingress.gke.iap comments in values.yaml for the -# one-time GCP setup (OAuth redirect URI + IAM grant). -iap: - enabled: false -``` - -- [ ] **Step 2: Verify production values still render** - -```bash -helm template optio helm/optio -f helm/optio/values.production.yaml \ - --set encryption.key=b8f2a1d94c7e3f605a2b9d8c1e4f7a0312d5c8b6e9f2a4d7c0b3e6f9a2d5c8b1 \ - --set publicUrl=https://optio.example.com \ - --set auth.google.clientId=test-id \ - --set auth.google.clientSecret=test-secret \ - --set externalDatabase.url=postgres://u:p@h:5432/optio \ - --set externalRedis.url=redis://h:6379 > /dev/null && echo OK -``` - -Expected: `OK`. - -- [ ] **Step 3: Commit (include spec + plan docs)** - -```bash -git add helm/optio/values.production.yaml docs/superpowers/specs/2026-07-10-iap-ingress-design.md docs/superpowers/plans/2026-07-10-iap-ingress.md -git commit -m "docs: document IAP ingress option and design" -``` - ---- - -### Task 3: Production rollout (interactive — run with the operator, not a subagent) - -**Files:** none (cluster + GCP operations). Context: `gke_gh-runners_europe-west4_gh-runners-2023`, namespace `optio`, GCP project `gh-runners`. - -- [ ] **Step 1: Get the OAuth client id from the cluster** - -```bash -kubectl --context gke_gh-runners_europe-west4_gh-runners-2023 -n optio \ - get secret optio-config -o jsonpath='{.data.GOOGLE_OAUTH_CLIENT_ID}' | base64 -d -``` - -- [ ] **Step 2: Add the IAP redirect URI to that OAuth client** - -In GCP Console (project `gh-runners`) → APIs & Services → Credentials → the OAuth client from Step 1 → add authorized redirect URI: - -``` -https://iap.googleapis.com/v1/oauth/clientIds/:handleRedirect -``` - -- [ ] **Step 3: Grant IAP access to gynzy.com** - -```bash -gcloud iap web add-iam-policy-binding --project=gh-runners \ - --member=domain:gynzy.com --role=roles/iap.httpsResourceAccessor -``` - -- [ ] **Step 4: Deploy** - -Upgrade the release with IAP on, using whatever values flow prod normally uses, e.g.: - -```bash -helm upgrade optio helm/optio -n optio --reuse-values \ - --kube-context gke_gh-runners_europe-west4_gh-runners-2023 \ - --set ingress.gke.iap.enabled=true -``` - -- [ ] **Step 5: Verify** - -```bash -# Backend configs picked up IAP -kubectl --context gke_gh-runners_europe-west4_gh-runners-2023 -n optio \ - get backendconfig -o yaml | grep -B2 -A3 "iap:" - -# Anonymous request redirects to Google sign-in (allow a few minutes for LB propagation) -curl -sI https://optio.gynzy.dev | head -5 -``` - -Expected: both BackendConfigs show `iap.enabled: true`; curl returns `302` to `accounts.google.com`. Then in a browser with a gynzy account: sign in through Google, confirm optio loads and a running task's log stream (WebSocket) works. - -- [ ] **Step 6: Rollback plan (only if broken)** - -```bash -helm upgrade optio helm/optio -n optio --reuse-values \ - --kube-context gke_gh-runners_europe-west4_gh-runners-2023 \ - --set ingress.gke.iap.enabled=false -``` From c4f478f23ac71787a053b2f9b8ec0caa09ad0b6b Mon Sep 17 00:00:00 2001 From: Daniel Genis Date: Fri, 10 Jul 2026 20:41:53 +0200 Subject: [PATCH 4/4] feat: enable IAP and disable app auth in the prod deploy values --- .github.jsonnet | 2 ++ .github/workflows/Release.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github.jsonnet b/.github.jsonnet index 65abd79c..991af857 100644 --- a/.github.jsonnet +++ b/.github.jsonnet @@ -179,6 +179,7 @@ local release = base.pipeline( enabled: true, staticIpName: 'optio', cloudArmorPolicy: 'optio', + iap: { enabled: true }, managedCertificate: { enabled: true, domains: ['optio.gynzy.dev'], @@ -195,6 +196,7 @@ local release = base.pipeline( }, publicUrl: 'https://optio.gynzy.dev', auth: { + disabled: true, google: { clientId: misc.secret('GOOGLE_OAUTH_CLIENT_ID'), clientSecret: misc.secret('GOOGLE_OAUTH_CLIENT_SECRET'), diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 0f943a67..5de4ff9a 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -100,7 +100,7 @@ "namespace": "optio" "release": "optio" "token": "${{ github.token }}" - "values": "{\"agent\":{\"image\":{\"pullPolicy\":\"IfNotPresent\",\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-agent-base\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"},\"imagePullPolicy\":\"IfNotPresent\"},\"api\":{\"image\":{\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-api\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"}},\"auth\":{\"google\":{\"clientId\":\"${{ secrets.GOOGLE_OAUTH_CLIENT_ID }}\",\"clientSecret\":\"${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}\"}},\"cloudSqlProxy\":{\"enabled\":true,\"instanceConnectionName\":\"gh-runners:europe-west4:optio\"},\"encryption\":{\"key\":\"${{ secrets.ENCRYPTION_KEY }}\"},\"externalDatabase\":{\"url\":\"${{ secrets.EXTERNAL_DATABASE_URL }}\"},\"ingress\":{\"enabled\":true,\"gke\":{\"cloudArmorPolicy\":\"optio\",\"enabled\":true,\"managedCertificate\":{\"domains\":[\"optio.gynzy.dev\"],\"enabled\":true},\"staticIpName\":\"optio\"},\"hosts\":[{\"host\":\"optio.gynzy.dev\",\"paths\":[{\"path\":\"/*\",\"pathType\":\"ImplementationSpecific\",\"service\":\"web\"},{\"path\":\"/api/*\",\"pathType\":\"ImplementationSpecific\",\"service\":\"api\"},{\"path\":\"/ws/*\",\"pathType\":\"ImplementationSpecific\",\"service\":\"api\"}]}]},\"optio\":{\"image\":{\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-optio\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"}},\"postgresql\":{\"enabled\":false},\"publicUrl\":\"https://optio.gynzy.dev\",\"web\":{\"image\":{\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-web\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"}}}" + "values": "{\"agent\":{\"image\":{\"pullPolicy\":\"IfNotPresent\",\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-agent-base\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"},\"imagePullPolicy\":\"IfNotPresent\"},\"api\":{\"image\":{\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-api\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"}},\"auth\":{\"disabled\":true,\"google\":{\"clientId\":\"${{ secrets.GOOGLE_OAUTH_CLIENT_ID }}\",\"clientSecret\":\"${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}\"}},\"cloudSqlProxy\":{\"enabled\":true,\"instanceConnectionName\":\"gh-runners:europe-west4:optio\"},\"encryption\":{\"key\":\"${{ secrets.ENCRYPTION_KEY }}\"},\"externalDatabase\":{\"url\":\"${{ secrets.EXTERNAL_DATABASE_URL }}\"},\"ingress\":{\"enabled\":true,\"gke\":{\"cloudArmorPolicy\":\"optio\",\"enabled\":true,\"iap\":{\"enabled\":true},\"managedCertificate\":{\"domains\":[\"optio.gynzy.dev\"],\"enabled\":true},\"staticIpName\":\"optio\"},\"hosts\":[{\"host\":\"optio.gynzy.dev\",\"paths\":[{\"path\":\"/*\",\"pathType\":\"ImplementationSpecific\",\"service\":\"web\"},{\"path\":\"/api/*\",\"pathType\":\"ImplementationSpecific\",\"service\":\"api\"},{\"path\":\"/ws/*\",\"pathType\":\"ImplementationSpecific\",\"service\":\"api\"}]}]},\"optio\":{\"image\":{\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-optio\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"}},\"postgresql\":{\"enabled\":false},\"publicUrl\":\"https://optio.gynzy.dev\",\"web\":{\"image\":{\"repository\":\"europe-docker.pkg.dev/unicorn-985/private-images/optio-web\",\"tag\":\"deploy-${{ github.event.pull_request.head.sha || github.sha }}\"}}}" "version": "${{ github.sha }}" "wait": "false" - "if": "${{ always() }}"