mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved The Login V2 readiness endpoint `/ui/v2/login/ready` performs an authenticated `SettingsService.GetGeneralSettings` call on every invocation. This only affects self-hosted Login V2 deployments that probe the endpoint periodically, for example via the Helm chart's readiness and startup probes, the Docker `HEALTHCHECK`, or a custom HTTP probe. In those setups every login replica continuously issues authenticated API calls that pass through the access-log interceptor and are treated as regular authenticated requests of the instance, although they are only probe traffic. The login hosted by ZITADEL Cloud is not affected, and neither are self-hosted deployments that do not probe `/ready`. Independent of the counting, validating credentials on every probe is unusual: readiness probes are meant to report whether a replica can serve traffic, while configuration errors such as invalid credentials are better caught once at startup. # How the Problems Are Solved The readiness endpoint now only checks that the ZITADEL API is reachable and ready, using the API's unauthenticated `/debug/ready` endpoint. That endpoint is served outside the access-log and quota interceptors, so probe traffic is no longer recorded. `CUSTOM_REQUEST_HEADERS` are applied to the readiness request as they are to gRPC requests. The credential check moves to process startup: `register()` in `instrumentation.ts` calls the new `verifyApiCredentials()` once, using the same auth and transport path as real requests. If the API rejects the credentials (`Unauthenticated`/`PermissionDenied`) or no credentials are configured, the process exits with a clear error so misconfiguration is visible immediately. If the API is not reachable at startup, a warning is logged and the process continues; reachability remains the job of the readiness probe and `ZITADEL_API_AWAITINITIALCONN`. Unit tests cover the readiness route, the credential check result mapping, and the startup behavior of `register()`. No changes to the Helm chart or Dockerfile are required. Self-hosted deployments pick up the fix with the next login image; until then, the probe interval can be increased to reduce the number of calls.
100 lines
4.2 KiB
TypeScript
100 lines
4.2 KiB
TypeScript
import { hasLoginClientKey, hasServiceUserToken, hasSystemUserCredentials } from "@/lib/deployment";
|
|
import { createLogger } from "@/lib/logger";
|
|
import { createServiceForHost } from "@/lib/service";
|
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
import { Client } from "@zitadel/client";
|
|
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
|
|
|
const logger = createLogger("startup");
|
|
|
|
/** Upper bound for the startup RPC so a hanging API cannot block process startup. */
|
|
const CREDENTIAL_CHECK_TIMEOUT_MS = 10_000;
|
|
|
|
/** gRPC codes that mean the API received the request but rejected the credentials. */
|
|
const CREDENTIAL_ERROR_CODES: ReadonlySet<Code> = new Set([Code.Unauthenticated, Code.PermissionDenied]);
|
|
|
|
export type CredentialCheckResult =
|
|
/** The API accepted the configured credentials. */
|
|
| "ok"
|
|
/** The API rejected the configured credentials. */
|
|
| "rejected"
|
|
/** Credentials are configured but could not be loaded or signed (e.g. missing or malformed key file). */
|
|
| "invalid"
|
|
/** No credentials are configured at all. */
|
|
| "missing"
|
|
/** The API could not be reached (or did not answer in time), so the credentials could not be verified. */
|
|
| "unreachable"
|
|
/** No check was performed: ZITADEL_API_URL is not set, or it does not resolve to an instance. */
|
|
| "skipped";
|
|
|
|
/** Results that indicate a configuration error the process cannot recover from. */
|
|
export const FATAL_CREDENTIAL_CHECK_RESULTS: ReadonlySet<CredentialCheckResult> = new Set<CredentialCheckResult>([
|
|
"rejected",
|
|
"invalid",
|
|
"missing",
|
|
]);
|
|
|
|
/**
|
|
* Verifies once, at process startup, that the configured API credentials are
|
|
* accepted by the ZITADEL API, using the same auth and transport path as real
|
|
* requests.
|
|
*
|
|
* This deliberately runs at startup and not in the readiness probe: probes are
|
|
* executed every few seconds per replica and every authenticated API call is
|
|
* recorded as a billable request of the instance. Verifying once per process
|
|
* catches misconfigured credentials early (fail fast) without generating
|
|
* continuous authenticated traffic.
|
|
*
|
|
* The check is scoped to the instance that ZITADEL_API_URL resolves to. In
|
|
* multi-tenant deployments, where the instance is only known per request, the
|
|
* API answers with NotFound and the check is skipped.
|
|
*
|
|
* The function never throws; the caller decides what to do with the result.
|
|
*/
|
|
export async function verifyApiCredentials(): Promise<CredentialCheckResult> {
|
|
const apiUrl = process.env.ZITADEL_API_URL;
|
|
if (!apiUrl) {
|
|
logger.warn("ZITADEL_API_URL is not set, skipping API credential check");
|
|
return "skipped";
|
|
}
|
|
|
|
if (!hasSystemUserCredentials() && !hasLoginClientKey() && !hasServiceUserToken()) {
|
|
logger.error(
|
|
"No API credentials configured. Set ZITADEL_LOGINCLIENT_KEYFILE, system user credentials (AUDIENCE, SYSTEM_USER_ID, SYSTEM_USER_PRIVATE_KEY or SYSTEM_USER_PRIVATE_KEY_FILE), or ZITADEL_SERVICE_USER_TOKEN",
|
|
);
|
|
return "missing";
|
|
}
|
|
|
|
// Loading and signing the credentials happens locally, before any request is
|
|
// sent. Failures here are configuration errors, not connectivity problems.
|
|
let settingsService: Client<typeof SettingsService>;
|
|
try {
|
|
settingsService = await createServiceForHost(SettingsService, { baseUrl: apiUrl });
|
|
} catch (e) {
|
|
logger.error("The configured API credentials could not be loaded", { apiUrl, error: e });
|
|
return "invalid";
|
|
}
|
|
|
|
try {
|
|
await settingsService.getGeneralSettings({}, { timeoutMs: CREDENTIAL_CHECK_TIMEOUT_MS });
|
|
logger.info("API credentials verified", { apiUrl });
|
|
return "ok";
|
|
} catch (e) {
|
|
if (e instanceof ConnectError) {
|
|
if (CREDENTIAL_ERROR_CODES.has(e.code)) {
|
|
logger.error("The ZITADEL API rejected the configured credentials", { apiUrl, error: e });
|
|
return "rejected";
|
|
}
|
|
if (e.code === Code.NotFound) {
|
|
logger.warn(
|
|
"ZITADEL_API_URL does not resolve to an instance, skipping API credential check (expected for multi-tenant deployments)",
|
|
{ apiUrl, error: e },
|
|
);
|
|
return "skipped";
|
|
}
|
|
}
|
|
logger.warn("Could not verify API credentials because the ZITADEL API is not reachable", { apiUrl, error: e });
|
|
return "unreachable";
|
|
}
|
|
}
|