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.
Before you start
Section titled “Before you start”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 merges — tf/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
cd tf/vaultterraform fmt -check -diff .terraform validateExpected: 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 fromcluster/smtp to configure the realm's smtp_server block (companiontf/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:
| Field | Value |
|---|---|
| Application name | fzymgc.house Keycloak |
| Homepage URL | https://id.fzymgc.house/ |
| Authorization callback URL | https://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 pinsalias = "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):
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
vault kv get -field=email_from secret/fzymgc-house/cluster/keycloakvault kv get -field=github_client_id secret/fzymgc-house/cluster/keycloakExpected: 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
rumdl check docs/reference/secrets.mdExpected: 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/passwordalready 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_serverblock
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
cd tf/keycloakterraform fmt -check -diff .terraform validateExpected: 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 Mailguntransport (same one Authentik already uses) plus a new per-appemail_from field on cluster/keycloak. Flips reset_password_allowed onnow that the realm can actually send the reset email. Requires thecompanion tf/vault cluster/smtp read grant (PR-A) to already beapplied, 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
cd tf/keycloakterraform fmt -check -diff .terraform validateExpected: 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 — aGitHub login with no matching Keycloak account auto-creates one viathe default first-broker-login flow (explicit, confirmed decision; seethe design spec's Risks table). Requires the github_client_id/secretVault fields (Task 2) to already be seeded before plan/apply.Task 6: Sequence and verify both PRs
Section titled “Task 6: Sequence and verify both PRs”Files: none — this task is process/verification only.
- Step 1: Open PR-A (Task 1) first
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):
${CLAUDE_PLUGIN_ROOT}/skills/terraform/scripts/terraform_mcp.py workspace-status main-cluster-vaultExpected: 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
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_fromaddress. - 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).