Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tf/deployment/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 12 additions & 3 deletions tf/deployment/modules/scoped/zitadel/customer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,25 @@ 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`
- `CUSTOMER_ZITADEL_SMTP_PASSWORD`
- `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
Expand Down
8 changes: 6 additions & 2 deletions tf/deployment/modules/scoped/zitadel/customer/data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
18 changes: 8 additions & 10 deletions tf/deployment/modules/scoped/zitadel/customer/dns.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ 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
}

provisioner "local-exec" {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion tf/deployment/modules/shared/zitadel/cloud/actions-v2.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion tf/deployment/modules/shared/zitadel/cloud/branding.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
2 changes: 1 addition & 1 deletion tf/deployment/modules/shared/zitadel/cloud/idp-links.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
4 changes: 3 additions & 1 deletion tf/deployment/modules/shared/zitadel/cloud/providers.tf
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions tf/deployment/modules/shared/zitadel/cloud/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading