mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-06 06:12:16 +00:00
feat(login): add optional TLS termination support (#11663)
# Which Problems Are Solved The login container can only serve HTTP. In deployments where a reverse proxy is not used, operators need the login container to terminate TLS itself. Closes #11473 # How the Problems Are Solved The container now supports optional HTTPS via three environment variables that match the pattern already used by the backend server: `ZITADEL_TLS_ENABLED`, `ZITADEL_TLS_CERTPATH`, and `ZITADEL_TLS_KEYPATH`. When `ZITADEL_TLS_ENABLED` is not set or set to `false`, behavior is identical to before. When set to `true`, the server reads the provided certificate and key files and serves HTTPS using the lower-level `getRequestHandlers` API from Next.js. The Docker healthcheck was updated to probe the correct scheme automatically based on the same environment variable. # Additional Changes Added a dockerized integration test that verifies both HTTP and HTTPS modes using testcontainers. # Additional Context The TLS path uses `getRequestHandlers` from `next/dist/server/lib/start-server` instead of `startServer` because the latter rejects self-signed certificates in production mode. This is the same internal API that `startServer` itself calls. --------- Co-authored-by: Copilot <[email protected]>
This commit is contained in:
co-authored by
Copilot
parent
0261536243
commit
c8abf6de74
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
|
||||
import * as https from "node:https";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { generateCertificates } from "../ca/utils/tls.ts";
|
||||
|
||||
const TEST_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const OUTPUT_DIR = path.join(TEST_DIR, "output");
|
||||
const CERTS_DIR = path.join(OUTPUT_DIR, "certs");
|
||||
const LOGIN_APP_DIR = path.join(TEST_DIR, "../..");
|
||||
|
||||
/**
|
||||
* Integration tests for TLS support in the login application.
|
||||
*
|
||||
* These tests verify that the login container can serve traffic over HTTPS
|
||||
* when configured with ZITADEL_TLS_ENABLED, ZITADEL_TLS_CERTPATH, and
|
||||
* ZITADEL_TLS_KEYPATH environment variables. This is essential for deployments
|
||||
* where the login application must terminate TLS itself rather than relying on
|
||||
* a reverse proxy.
|
||||
*
|
||||
* The test suite builds the login container image and runs it in two modes:
|
||||
* HTTP (default) and HTTPS (with TLS enabled). For HTTPS mode, a self-signed
|
||||
* certificate is generated and mounted into the container. The tests verify
|
||||
* that the application starts correctly and responds to health checks in both
|
||||
* modes.
|
||||
*/
|
||||
describe("Login Container TLS Support", () => {
|
||||
let loginImage: GenericContainer;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.rmSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(CERTS_DIR, { recursive: true });
|
||||
|
||||
const certs = generateCertificates({
|
||||
serverCN: "localhost",
|
||||
dns: ["localhost"],
|
||||
ips: ["127.0.0.1"],
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(CERTS_DIR, "server.crt"), certs.server.cert);
|
||||
fs.writeFileSync(path.join(CERTS_DIR, "server.key"), certs.server.key);
|
||||
|
||||
loginImage = await GenericContainer.fromDockerfile(LOGIN_APP_DIR).build();
|
||||
});
|
||||
|
||||
describe("when TLS is not enabled", () => {
|
||||
let container: StartedTestContainer;
|
||||
let appUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
container = await loginImage
|
||||
.withExposedPorts(3000)
|
||||
.withWaitStrategy(Wait.forHttp("/ui/v2/login/healthy", 3000))
|
||||
.start();
|
||||
|
||||
appUrl = `http://${container.getHost()}:${container.getMappedPort(3000)}`;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await container?.stop();
|
||||
});
|
||||
|
||||
it("returns 200 from healthy endpoint", async () => {
|
||||
const response = await fetch(`${appUrl}/ui/v2/login/healthy`);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when TLS is enabled", () => {
|
||||
let container: StartedTestContainer;
|
||||
let appUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
container = await loginImage
|
||||
.withExposedPorts(3000)
|
||||
.withEnvironment({
|
||||
ZITADEL_TLS_ENABLED: "true",
|
||||
ZITADEL_TLS_CERTPATH: "/certs/server.crt",
|
||||
ZITADEL_TLS_KEYPATH: "/certs/server.key",
|
||||
})
|
||||
.withCopyFilesToContainer([
|
||||
{
|
||||
source: path.join(CERTS_DIR, "server.crt"),
|
||||
target: "/certs/server.crt",
|
||||
},
|
||||
{
|
||||
source: path.join(CERTS_DIR, "server.key"),
|
||||
target: "/certs/server.key",
|
||||
},
|
||||
])
|
||||
.withWaitStrategy(Wait.forLogMessage(/Ready on https/))
|
||||
.start();
|
||||
|
||||
appUrl = `https://${container.getHost()}:${container.getMappedPort(3000)}`;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await container?.stop();
|
||||
});
|
||||
|
||||
it("returns 200 from healthy endpoint over HTTPS", async () => {
|
||||
const response = await new Promise<https.IncomingMessage>((resolve, reject) => {
|
||||
https
|
||||
.get(`${appUrl}/ui/v2/login/healthy`, { rejectUnauthorized: false }, (res) => {
|
||||
res.resume();
|
||||
resolve(res);
|
||||
})
|
||||
.on("error", reject);
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user