Skip to content

Commit 5f02c19

Browse files
committed
Fix Zitadel TLS and email validation
1 parent db77527 commit 5f02c19

4 files changed

Lines changed: 60 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Non-interactive:
4141
```bash
4242
curl -fsSL https://raw.githubusercontent.com/terion-name/terrarium/refs/heads/main/install.sh | bash -s -- \
4343
--non-interactive \
44-
--email admin@example.com \
44+
--email admin@your-domain.tld \
4545
--idp-mode zitadel-self-hosted \
4646
--storage-mode loop \
4747
--yes

ansible/roles/idp_zitadel/templates/terraform-main.tf.j2

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ terraform {
99

1010
provider "zitadel" {
1111
domain = "{{ terrarium_auth_domain }}"
12-
insecure = true
13-
port = "{{ terrarium_zitadel_api_port }}"
1412
jwt_profile_file = "/secrets/admin-sa.json"
1513
}
1614

scripts/terrarium-install.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ function fail(message: string): never {
5151
process.exit(1);
5252
}
5353

54+
const RESERVED_EMAIL_DOMAINS = new Set(["example.com", "example.org", "example.net"]);
55+
5456
function info(message: string): void {
5557
console.log(chalk.cyan(`${PREFIX}: ${message}`));
5658
}
@@ -59,6 +61,19 @@ function success(message: string): void {
5961
console.log(chalk.green(`${PREFIX}: ${message}`));
6062
}
6163

64+
function validateEmail(email: string, fieldName: string): string {
65+
const normalized = email.trim();
66+
const match = normalized.match(/^[^@\s]+@([^@\s]+)$/);
67+
if (!match) {
68+
fail(`${fieldName} must be a valid email address`);
69+
}
70+
const domain = match[1].toLowerCase();
71+
if (RESERVED_EMAIL_DOMAINS.has(domain)) {
72+
fail(`${fieldName} must not use reserved example.* domains because ACME rejects them`);
73+
}
74+
return normalized;
75+
}
76+
6277
function requireRoot(): void {
6378
if (typeof process.getuid === "function" && process.getuid() !== 0) {
6479
fail("run as root");
@@ -193,6 +208,24 @@ async function promptText(message: string, defaultValue = ""): Promise<string> {
193208
});
194209
}
195210

211+
async function promptEmail(message: string, defaultValue = "", fieldName = "email"): Promise<string> {
212+
return await input({
213+
message,
214+
default: defaultValue,
215+
validate: (value) => {
216+
const normalized = value.trim();
217+
const match = normalized.match(/^[^@\s]+@([^@\s]+)$/);
218+
if (!match) {
219+
return "Enter a valid email address";
220+
}
221+
if (RESERVED_EMAIL_DOMAINS.has(match[1].toLowerCase())) {
222+
return "Reserved example.* domains are not accepted";
223+
}
224+
return true;
225+
}
226+
}).then((value) => validateEmail(value, fieldName));
227+
}
228+
196229
async function promptConfirm(message: string, defaultValue: boolean, assumeYes: boolean): Promise<boolean> {
197230
if (assumeYes) {
198231
return true;
@@ -204,7 +237,7 @@ async function interactiveConfig(options: InstallOptions): Promise<void> {
204237
options.publicIp = await detectPublicIp(options.publicIp);
205238
const dashed = dashedIp(options.publicIp);
206239

207-
options.email = options.email || (await promptText("Email for ACME/notifications", `admin@${options.publicIp}.nip.io`));
240+
options.email = options.email || (await promptEmail("Email for ACME/notifications", `admin@${options.publicIp}.nip.io`, "--email"));
208241
options.zitadelAdminEmail = options.zitadelAdminEmail || options.email;
209242

210243
if (!options.domain && !options.manageDomain) {
@@ -237,7 +270,11 @@ async function interactiveConfig(options: InstallOptions): Promise<void> {
237270
options.authDomain ||
238271
(options.domain ? `auth.${options.domain}` : `auth.${dashed}.traefik.me`);
239272
options.authDomain = await promptText("ZITADEL auth domain", options.authDomain);
240-
options.zitadelAdminEmail = await promptText("ZITADEL bootstrap admin email", options.zitadelAdminEmail || options.email);
273+
options.zitadelAdminEmail = await promptEmail(
274+
"ZITADEL bootstrap admin email",
275+
options.zitadelAdminEmail || options.email,
276+
"--zitadel-admin-email"
277+
);
241278
} else {
242279
options.idpMode = "none";
243280
options.authDomain = "";
@@ -314,10 +351,12 @@ function validateNonInteractive(options: InstallOptions): void {
314351
if (!options.email) {
315352
fail("--email is required in non-interactive mode");
316353
}
354+
options.email = validateEmail(options.email, "--email");
317355

318356
if (options.idpMode === "zitadel_self_hosted") {
319357
options.authDomain = options.authDomain || (options.domain ? `auth.${options.domain}` : `auth.${dashed}.traefik.me`);
320358
options.zitadelAdminEmail = options.zitadelAdminEmail || options.email;
359+
options.zitadelAdminEmail = validateEmail(options.zitadelAdminEmail, "--zitadel-admin-email");
321360
} else {
322361
options.authDomain = "";
323362
}

scripts/terrarium-zitadel-sync.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ async function waitForApiReady(stackDir: string): Promise<void> {
5353
throw new Error(`timed out waiting for ZITADEL API readiness: ${lastError}`);
5454
}
5555

56+
async function waitForHttpsDiscovery(authDomain: string): Promise<void> {
57+
let lastError = "";
58+
for (let attempt = 0; attempt < WAIT_ATTEMPTS; attempt += 1) {
59+
const result = await runAllowFailure([
60+
"curl",
61+
"-fsS",
62+
`https://${authDomain}/.well-known/openid-configuration`
63+
]);
64+
if (result.exitCode === 0) {
65+
return;
66+
}
67+
lastError = result.stderr.trim() || result.stdout.trim() || "OIDC discovery is not reachable yet";
68+
await Bun.sleep(WAIT_INTERVAL_MS);
69+
}
70+
throw new Error(`timed out waiting for HTTPS OIDC discovery on ${authDomain}: ${lastError}`);
71+
}
72+
5673
export async function idpSyncCmd(configPath = DEFAULT_CONFIG_PATH): Promise<void> {
5774
const config = loadConfig(configPath, PREFIX);
5875
if (configString(config, "terrarium_idp_mode") !== "zitadel_self_hosted") {
@@ -76,6 +93,7 @@ export async function idpSyncCmd(configPath = DEFAULT_CONFIG_PATH): Promise<void
7693
await waitForFile(`${bootstrapDir}/admin-sa.json`, "bootstrap machine key");
7794
await waitForFile(`${bootstrapDir}/login-client.pat`, "login client PAT");
7895
await waitForApiReady(zitadelDir);
96+
await waitForHttpsDiscovery(authDomain);
7997

8098
const commonArgs = [
8199
"run",

0 commit comments

Comments
 (0)