fix: route zitadel connections through stable base domain (#1847)

This commit is contained in:
Zack Pollard
2026-07-23 18:18:03 +00:00
committed by GitHub
parent 3be1819e9f
commit 53b6c5fb07
12 changed files with 45 additions and 23 deletions
+1
View File
@@ -17,6 +17,7 @@ export TF_VAR_zitadel_github_client_secret="op://tf/GITHUB_OAUTH_APP_IMMICH_ZITA
export TF_VAR_futo_internal_zitadel_github_client_id="op://tf/IMMICH_GITHUB_OAUTH_APP_INTERNAL_FUTO_ZITADEL_CLIENT_ID/password"
export TF_VAR_futo_internal_zitadel_github_client_secret="op://tf/IMMICH_GITHUB_OAUTH_APP_INTERNAL_FUTO_ZITADEL_CLIENT_SECRET/password"
export TF_VAR_futo_zitadel_profile_json="op://tf/FUTO_ZITADEL_PROFILE_JSON/password"
export TF_VAR_futo_zitadel_base_domain="op://tf/FUTO_ZITADEL_BASE_DOMAIN/password"
export TF_VAR_zitadel_gitlab_client_id="op://tf/GITLAB_OAUTH_APP_FUTO_ZITADEL_CLIENT_ID/password"
export TF_VAR_zitadel_gitlab_client_secret="op://tf/GITLAB_OAUTH_APP_FUTO_ZITADEL_CLIENT_SECRET/password"
export TF_VAR_zitadel_gitlab_issuer="op://tf/GITLAB_OAUTH_APP_FUTO_ZITADEL_ISSUER/password"
@@ -32,6 +32,7 @@ before Terraform can run. Do this in the ZITADEL Cloud console and 1Password:
seeds stub items in `yucca_tf_${env}_manual` with `REPLACE_ME` passwords
for:
- `CUSTOMER_ZITADEL_DOMAIN`
- `CUSTOMER_ZITADEL_BASE_DOMAIN`
- `CUSTOMER_ZITADEL_PROFILE_JSON`
- `CUSTOMER_ZITADEL_SMTP_HOST`
- `CUSTOMER_ZITADEL_SMTP_USER`
@@ -39,9 +40,17 @@ before Terraform can run. Do this in the ZITADEL Cloud console and 1Password:
- `CUSTOMER_ZITADEL_SMTP_SENDER_ADDRESS`
Replace each stub password in `yucca_tf_${env}_manual` with the real value
(`CUSTOMER_ZITADEL_DOMAIN` = the instance URL `<name>.zitadel.cloud`, profile
JSON from step 3, SMTP credentials for the chosen provider, and sender
address e.g. `no-reply@futo.cloud`).
(`CUSTOMER_ZITADEL_DOMAIN` = the public-facing auth URL — prod: the
`auth.futo.cloud` vanity domain, dev/staging: the generated
`<name>.zitadel.cloud` URL — consumed by app-side config, not this module;
`CUSTOMER_ZITADEL_BASE_DOMAIN` = the generated `<name>.zitadel.cloud` instance
host as a **bare hostname** (no `https://`, no trailing slash) in every env,
which this module both connects through (provider + hosted login) and targets
with the `auth.futo.cloud` CNAME, so the connection never depends on the
mutable vanity domain. Stored bare to match the internal instance's
`FUTO_ZITADEL_BASE_DOMAIN`. Profile JSON from step 3, SMTP
credentials for the chosen provider, and sender address e.g.
`no-reply@futo.cloud`).
Re-apply `shared/1password/futo-account` so the copy-secrets module
mirrors each value into the `yucca_tf_${env}` vault that this module
reads from via `data "onepassword_item"` lookups at plan/apply time. No
@@ -2,9 +2,13 @@ data "onepassword_vault" "tf_env" {
name = "yucca_tf_${var.env}"
}
data "onepassword_item" "customer_zitadel_domain" {
# NOTE: CUSTOMER_ZITADEL_DOMAIN (the public auth.futo.cloud vanity domain) is
# intentionally not read here — this module connects to the instance via the
# stable <name>.zitadel.cloud base domain below. The vanity domain remains the
# public-facing auth URL consumed by app-side config.
data "onepassword_item" "customer_zitadel_base_domain" {
vault = data.onepassword_vault.tf_env.uuid
title = "CUSTOMER_ZITADEL_DOMAIN"
title = "CUSTOMER_ZITADEL_BASE_DOMAIN"
}
data "onepassword_item" "customer_zitadel_profile_json" {
@@ -13,22 +13,20 @@ data "cloudflare_zone" "futo_cloud" {
}
}
locals {
# CUSTOMER_ZITADEL_DOMAIN is stored as a URL (https://host/); Cloudflare needs
# a bare hostname as CNAME content.
customer_zitadel_host = trimsuffix(trimprefix(data.onepassword_item.customer_zitadel_domain.password, "https://"), "/")
}
# Prod only: auth.futo.cloud -> the prod ZITADEL Cloud instance host from
# CUSTOMER_ZITADEL_DOMAIN. dev/staging use the generated <name>.zitadel.cloud URL
# directly with no custom domain. DNS-only so ZITADEL terminates TLS.
# Prod only: auth.futo.cloud -> the prod ZITADEL Cloud instance's base
# <name>.zitadel.cloud host from CUSTOMER_ZITADEL_BASE_DOMAIN, which is stored as
# a bare hostname (no scheme) — exactly what a CNAME wants. This is NOT the
# auth.futo.cloud vanity domain that CUSTOMER_ZITADEL_DOMAIN holds; pointing the
# record at the vanity domain would make the CNAME reference itself. dev/staging
# have no custom domain and use the generated URL directly. DNS-only so ZITADEL
# terminates TLS.
resource "cloudflare_dns_record" "customer_auth" {
count = var.env == "prod" ? 1 : 0
zone_id = data.cloudflare_zone.futo_cloud.id
name = "auth.futo.cloud"
type = "CNAME"
content = local.customer_zitadel_host
content = data.onepassword_item.customer_zitadel_base_domain.password
ttl = 1
proxied = false
}
@@ -12,7 +12,7 @@ resource "null_resource" "hosted_login_translations" {
triggers = {
translations_hash = filemd5(local.hosted_login_translations_file)
script_hash = filemd5(local.hosted_login_translations_script)
domain = data.onepassword_item.customer_zitadel_domain.password
domain = data.onepassword_item.customer_zitadel_base_domain.password
locale = local.hosted_login_translations_locale
}
@@ -20,7 +20,8 @@ resource "null_resource" "hosted_login_translations" {
command = local.hosted_login_translations_script
environment = {
ZITADEL_DOMAIN = data.onepassword_item.customer_zitadel_domain.password
# Connect to the API via the stable base domain, not the vanity domain.
ZITADEL_DOMAIN = data.onepassword_item.customer_zitadel_base_domain.password
ZITADEL_PROFILE_JSON = data.onepassword_item.customer_zitadel_profile_json.password
TRANSLATIONS_FILE = local.hosted_login_translations_file
LOCALE = local.hosted_login_translations_locale
@@ -1,5 +1,7 @@
# Connect via the stable <name>.zitadel.cloud base domain, not the auth.futo.cloud
# vanity domain (CUSTOMER_ZITADEL_DOMAIN) which is an alias that could change.
provider "zitadel" {
domain = data.onepassword_item.customer_zitadel_domain.password
domain = data.onepassword_item.customer_zitadel_base_domain.password
insecure = false
jwt_profile_json = data.onepassword_item.customer_zitadel_profile_json.password
}
@@ -82,7 +82,7 @@ resource "cloudflare_worker_version" "zitadel_actions" {
}]
bindings = [
{ name = "ZITADEL_DOMAIN", type = "plain_text", text = "auth.internal.futo.org" },
{ name = "ZITADEL_DOMAIN", type = "plain_text", text = var.futo_zitadel_base_domain },
{ name = "GITHUB_IDP_ID", type = "plain_text", text = zitadel_idp_github.github.id },
{ name = "GITLAB_IDP_ID", type = "plain_text", text = zitadel_idp_gitlab_self_hosted.gitlab.id },
{ name = "ZITADEL_TOKEN", type = "secret_text", text = zitadel_personal_access_token.zitadel_actions.token },
@@ -53,7 +53,7 @@ resource "zitadel_default_privacy_policy" "default" {
locals {
hosted_login_translations_file = "${path.module}/translations/en.json"
hosted_login_translations_script = "${path.module}/scripts/set-hosted-login-translations.sh"
hosted_login_translations_domain = "auth.internal.futo.org"
hosted_login_translations_domain = var.futo_zitadel_base_domain
hosted_login_translations_locale = "en"
}
@@ -54,7 +54,7 @@ resource "terraform_data" "idp_link" {
idp_id = each.value.idp_id
external_user_id = each.value.external_user_id
user_name = each.value.user_name
domain = "auth.internal.futo.org"
domain = var.futo_zitadel_base_domain
token = zitadel_personal_access_token.zitadel_actions.token
}
@@ -47,7 +47,7 @@ resource "cloudflare_worker_version" "outline_role_sync" {
bindings = [
{ name = "OUTLINE_BASE_URL", type = "plain_text", text = "https://outline.immich.cloud" },
{ name = "ZITADEL_BASE_URL", type = "plain_text", text = "https://auth.internal.futo.org" },
{ name = "ZITADEL_BASE_URL", type = "plain_text", text = "https://${var.futo_zitadel_base_domain}" },
{ name = "ZITADEL_OUTLINE_PROJECT_ID", type = "plain_text", text = zitadel_project.projects["Outline"].id },
{ name = "ZITADEL_OUTLINE_PROJECT_ROLES", type = "plain_text", text = join(",", local.outline_role_keys) },
{ name = "ZITADEL_SERVICE_ACCOUNT_TOKEN", type = "secret_text", text = zitadel_personal_access_token.outline_role_sync.token },
@@ -1,5 +1,7 @@
# Connect via the stable <name>.zitadel.cloud base domain, not the
# auth.internal.futo.org custom domain which is a mutable alias.
provider "zitadel" {
domain = "auth.internal.futo.org"
domain = var.futo_zitadel_base_domain
insecure = false
jwt_profile_json = var.futo_zitadel_profile_json
}
@@ -8,6 +8,11 @@ variable "op_connect_token" {
variable "zitadel_profile_json" {}
variable "futo_zitadel_profile_json" {}
# The stable <name>.zitadel.cloud base host for the FUTO internal instance. Used
# for all API connections (provider, hosted-login + idp-link scripts) instead of
# the auth.internal.futo.org custom domain, which is a mutable alias.
variable "futo_zitadel_base_domain" {}
variable "users_data_file_path" {
description = "The path to the JSON file containing user data. This path should be resolvable from the Terragrunt execution directory or be an absolute path."
type = string