Skip to content

Keycloak Outbound Email + GitHub Social Login Implementation Plan

Keycloak Outbound Email + GitHub Social Login Implementation Plan

Section titled “Keycloak Outbound Email + GitHub Social Login 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: Give the fzymgc Keycloak realm outbound email (self-service password reset) and a GitHub social login option, reusing the cluster’s existing shared Mailgun transport.

Architecture: Two Terraform PRs across two separate HCP Terraform workspaces — tf/vault grants the tf/keycloak workspace read access to the shared cluster/smtp secret, then tf/keycloak adds an smtp_server block to the realm and a dedicated keycloak_oidc_github_identity_provider resource. A manual, out-of-band step (GitHub OAuth App creation — no creation API exists) seeds three new Vault fields both PRs depend on.

Tech Stack: Terraform (keycloak/keycloak ~> 5.0, hashicorp/vault ~> 5.9), HCP Terraform (Agent execution, GitOps merge-triggered), HashiCorp Vault (KV v2).

Design spec: docs/engineering/specs/2026-07-02-keycloak-email-github-login-design.md Design bead: hl-d0md

Model intent (for plan-to-beads): Tasks 1, 3, 4, and 5 are mechanical, fully-specified HCL/Markdown diffs already verified against the real Terraform provider schema → model:haiku. Task 2 (manual GitHub OAuth App creation + Vault field seeding) and Task 6 (PR sequencing — confirming an HCP Terraform run actually applied before merging the dependent PR, then driving post-apply functional verification) require human/operator judgment and reading external state rather than following a fixed diff → model:sonnet.


All commands below were run and verified against this exact repo checkout (terraform init -backend=false + terraform fmt -check + terraform validate in both tf/vault and tf/keycloak, plus rumdl check on the docs edit) — every code block in this plan is confirmed syntactically and referentially valid against the real provider schemas, not just the Terraform Registry docs. terraform validate does not need live Vault credentials (it doesn’t evaluate data source values, only references) — safe to run at any point in this plan. A full terraform plan does need Vault credentials (vault login) and, for Tasks 4–5, the Task 2 Vault fields already seeded — that gate is called out explicitly where it matters.

PR split (do not combine): Task 1 is its own PR (PR-A, tf/vault). Tasks 3–5 are a second PR (PR-B, tf/keycloak + docs). PR-A must merge and apply before PR-B mergestf/keycloak’s new cluster/smtp data source read (Task 4) will 403 otherwise. Task 2 (manual) has no PR; it must complete before PR-B’s plan/apply (not before PR-B’s code review).


Task 1: Grant tf/keycloak read access to the shared SMTP secret (PR-A)

Section titled “Task 1: Grant tf/keycloak read access to the shared SMTP secret (PR-A)”

Files:

  • Modify: tf/vault/policy-keycloak.tf

  • Step 1: Add the read-only grant for cluster/smtp

Add this block immediately before the closing EOT (after the existing sys/auth/oidc block), matching the file’s existing KV v2 pairing convention (every secret/data/... grant paired with the matching secret/metadata/... grant, per the file’s own header comment):

path "auth/oidc/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "sys/auth/oidc" {
capabilities = ["create", "read", "update", "delete", "sudo"]
}
# Shared Mailgun SMTP transport (hl-d0md): tf/keycloak reads host/port/
# username/password/use_tls to configure the realm's smtp_server block,
# reusing the same transport tf/authentik's pod env already consumes (via
# ExternalSecrets/ESO, not Terraform — this is a new Terraform-level read).
path "secret/data/fzymgc-house/cluster/smtp" {
capabilities = ["read"]
}
path "secret/metadata/fzymgc-house/cluster/smtp" {
capabilities = ["read"]
}
EOT
}
  • Step 2: Format and validate
Terminal window
cd tf/vault
terraform fmt -check -diff .
terraform validate

Expected: fmt prints nothing (exit 0); validate prints Success! The configuration is valid.

  • Step 3: Commit

Use the commit-commands:commit skill. Suggested message:

feat(vault): grant tf/keycloak read access to shared SMTP secret [hl-d0md]
tf/keycloak needs to read host/port/username/password/use_tls from
cluster/smtp to configure the realm's smtp_server block (companion
tf/keycloak PR). Read-only — tf/keycloak never writes this secret.
  • Step 4: Open PR-A and merge it before continuing to Task 3

Do not proceed to Task 3 in a way that gets merged before this PR applies — see Task 6 for how to confirm the apply succeeded.


Task 2: Manual prerequisites — GitHub OAuth App + Vault fields (no PR)

Section titled “Task 2: Manual prerequisites — GitHub OAuth App + Vault fields (no PR)”

This task has no Terraform files. It must complete before Task 4/5’s terraform plan (their new data source reads would error on a missing map key otherwise) — it can happen any time before then, including in parallel with Task 1’s PR review.

  • Step 1: Create the GitHub OAuth App

Go to https://github.com/settings/developers → OAuth Apps → New OAuth App, and fill in:

FieldValue
Application namefzymgc.house Keycloak
Homepage URLhttps://id.fzymgc.house/
Authorization callback URLhttps://id.fzymgc.house/realms/fzymgc/broker/github/endpoint

Click Register application, then Generate a new client secret. Copy both the Client ID and the Client Secret immediately — the secret is shown once.

The callback URL must be exactly right: Keycloak’s identity-provider convention is {realm_issuer}/broker/{alias}/endpoint, and Task 5 pins alias = "github" explicitly so this URL never silently drifts.

  • Step 2: Seed the three new Vault fields

vault kv patch merges new fields into the existing KV v2 secret without touching terraform_username/terraform_password (requires vault login first — see docs/operations/vault.md):

Terminal window
vault kv patch secret/fzymgc-house/cluster/keycloak \
email_from="keycloak@fzymgc.house" \
github_client_id="<Client ID from Step 1>" \
github_client_secret="<Client Secret from Step 1>"

Expected output: Success! Data written to: secret/data/fzymgc-house/cluster/keycloak

  • Step 3: Verify the fields landed
Terminal window
vault kv get -field=email_from secret/fzymgc-house/cluster/keycloak
vault kv get -field=github_client_id secret/fzymgc-house/cluster/keycloak

Expected: each command prints the value you just set and exits 0. (Don’t print github_client_secret to a terminal you don’t control — its presence can be confirmed the same way if needed.)


Task 3: Document the cluster/keycloak secret (part of PR-B)

Section titled “Task 3: Document the cluster/keycloak secret (part of PR-B)”

Files:

  • Modify: docs/reference/secrets.md

cluster/keycloak has no row in this table today, even though terraform_username/terraform_password are already in use (tf/keycloak/terraform.tf) — this task documents the existing fields alongside the three new ones.

  • Step 1: Add the new table row

Insert immediately after the existing cluster/smtp row:

| `secret/fzymgc-house/cluster/smtp` | Shared Mailgun SMTP transport | `host`, `port`, `username`, `password`, `use_tls` |
| `secret/fzymgc-house/cluster/keycloak` | Keycloak realm admin (Terraform provider auth) + outbound email + GitHub social login (`hl-d0md`) | `terraform_username`, `terraform_password` (admin-cli auth for the `keycloak` Terraform provider), `email_from` (per-app sender; SMTP transport at `cluster/smtp`), `github_client_id`, `github_client_secret` (GitHub OAuth App — manually created, see `docs/engineering/specs/2026-07-02-keycloak-email-github-login-design.md`) |
  • Step 2: Lint
Terminal window
rumdl check docs/reference/secrets.md

Expected: Success: No issues found in 1 file

  • Step 3: Commit
docs(secrets): document cluster/keycloak secret fields [hl-d0md]
cluster/keycloak had no table row despite terraform_username/password
already being live; adds that plus the 3 new email/GitHub-IdP fields.

Task 4: Add SMTP configuration to the realm (part of PR-B)

Section titled “Task 4: Add SMTP configuration to the realm (part of PR-B)”

Files:

  • Create: tf/keycloak/smtp.tf
  • Modify: tf/keycloak/main.tf
  • Step 1: Create the SMTP data source
# tf/keycloak/smtp.tf — Realm outbound email (hl-d0md).
#
# Reuses the shared Mailgun SMTP transport already proven by Authentik
# (docs/engineering/specs/2026-06-26-authentik-outbound-email-design.md).
# Unlike Authentik's env-injected consumption, this is a NEW Terraform-to-Vault
# read: the tf/keycloak HCP workspace was granted read access to cluster/smtp
# in a companion tf/vault PR, which must apply before this one (see the design
# spec's Rollout section).
data "vault_kv_secret_v2" "smtp" {
mount = "secret"
name = "fzymgc-house/cluster/smtp"
}
  • Step 2: Wire the realm’s smtp_server block

In tf/keycloak/main.tf, replace:

# Registration of NEW end-users stays off here (Phase 4 owns the
# registration flow). This realm flag is unrelated to OAuth client DCR.
registration_allowed = false
}

with:

# Registration of NEW end-users stays off here (Phase 4 owns the
# registration flow). This realm flag is unrelated to OAuth client DCR.
registration_allowed = false
# Self-service "Forgot password" (hl-d0md) — safe to enable now that the
# realm can send email via the smtp_server block below.
reset_password_allowed = true
smtp_server {
host = data.vault_kv_secret_v2.smtp.data["host"]
port = data.vault_kv_secret_v2.smtp.data["port"]
from = data.vault_kv_secret_v2.keycloak.data["email_from"]
starttls = tobool(data.vault_kv_secret_v2.smtp.data["use_tls"])
auth {
username = data.vault_kv_secret_v2.smtp.data["username"]
password = data.vault_kv_secret_v2.smtp.data["password"]
}
}
}

(data.vault_kv_secret_v2.keycloak is already defined in tf/keycloak/vault.tf — a same-module cross-file reference, same pattern terraform.tf already uses for the keycloak provider block.)

  • Step 3: Format and validate
Terminal window
cd tf/keycloak
terraform fmt -check -diff .
terraform validate

Expected: fmt prints nothing; validate prints Success! The configuration is valid, but there were some validation warnings as shown above. (The warnings are pre-existing vault_kv_secret_v2 deprecation notices unrelated to this change — every other file in this module already triggers them.)

  • Step 4: Commit
feat(keycloak): configure realm SMTP + enable password reset [hl-d0md]
Adds an smtp_server block sourced from the shared cluster/smtp Mailgun
transport (same one Authentik already uses) plus a new per-app
email_from field on cluster/keycloak. Flips reset_password_allowed on
now that the realm can actually send the reset email. Requires the
companion tf/vault cluster/smtp read grant (PR-A) to already be
applied, and the email_from Vault field to already be seeded (Task 2)
— otherwise terraform plan errors on a missing map key.

Task 5: Add the GitHub identity provider (part of PR-B)

Section titled “Task 5: Add the GitHub identity provider (part of PR-B)”

Files:

  • Create: tf/keycloak/github_idp.tf

  • Step 1: Create the resource

# tf/keycloak/github_idp.tf — GitHub social login (hl-d0md).
#
# link_only = false is a deliberate choice: a GitHub identity with no existing
# Keycloak account auto-creates one via the default first-broker-login flow.
# This is intentionally a bigger surface than registration_allowed = false
# gates (that flag only closes the direct registration form) — see the design
# spec's Risks section.
#
# The GitHub OAuth App itself is a manual, out-of-band artifact (GitHub's
# classic OAuth Apps have no creation API) — see
# docs/engineering/specs/2026-07-02-keycloak-email-github-login-design.md for
# the callback URL and required Vault fields.
resource "keycloak_oidc_github_identity_provider" "github" {
realm = keycloak_realm.fzymgc.id
alias = "github" # pins the /broker/github/endpoint callback URL explicitly
client_id = data.vault_kv_secret_v2.keycloak.data["github_client_id"]
client_secret = data.vault_kv_secret_v2.keycloak.data["github_client_secret"]
trust_email = true
sync_mode = "IMPORT"
link_only = false
}
  • Step 2: Format and validate
Terminal window
cd tf/keycloak
terraform fmt -check -diff .
terraform validate

Expected: same as Task 4 Step 3 (clean fmt, validate succeeds with only the pre-existing deprecation warnings).

  • Step 3: Commit
feat(keycloak): add GitHub social identity provider [hl-d0md]
keycloak_oidc_github_identity_provider with link_only = false — a
GitHub login with no matching Keycloak account auto-creates one via
the default first-broker-login flow (explicit, confirmed decision; see
the design spec's Risks table). Requires the github_client_id/secret
Vault fields (Task 2) to already be seeded before plan/apply.

Files: none — this task is process/verification only.

  • Step 1: Open PR-A (Task 1) first
Terminal window
gh pr create --title "feat(vault): grant tf/keycloak read access to shared SMTP secret [hl-d0md]" --body "Part 1/2 of hl-d0md. Must merge+apply before the tf/keycloak PR."
  • Step 2: Confirm PR-A applied in HCP Terraform before merging PR-B

Use the terraform skill’s gateway script (or the MCP tools per tf/CLAUDE.md — read-only operations only, never trigger a run manually):

Terminal window
${CLAUDE_PLUGIN_ROOT}/skills/terraform/scripts/terraform_mcp.py workspace-status main-cluster-vault

Expected: the latest run shows status: applied and includes the cluster/smtp policy diff.

  • Step 3: Open PR-B (Tasks 3–5) only after Step 2 confirms
Terminal window
gh pr create --title "feat(keycloak): outbound email + GitHub social login [hl-d0md]" --body "Part 2/2 of hl-d0md. Requires PR-A (tf/vault cluster/smtp grant) already applied, and the manual Vault fields from Task 2 already seeded."
  • Step 4: Post-apply functional checks (after PR-B applies)
  • Login page → Forgot password for an existing user → confirm an email arrives from the configured email_from address.
  • Login page shows a GitHub button; authenticate with a GitHub identity that has no matching Keycloak user → confirm a new account is auto-created (link_only = false).
  • As an existing Keycloak user, link a GitHub identity via Account Console → Linked Accounts (confirms linking still works alongside auto-create).