diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 4fe4e17cf..2956be192 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -74,6 +74,7 @@ import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-a
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
+import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
@@ -218,6 +219,7 @@ declare module "fastify" {
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory;
+ identityTlsCertAuth: TIdentityTlsCertAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOciAuth: TIdentityOciAuthServiceFactory;
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 1d4ad1797..7ead9f84b 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -164,6 +164,9 @@ import {
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate,
+ TIdentityTlsCertAuths,
+ TIdentityTlsCertAuthsInsert,
+ TIdentityTlsCertAuthsUpdate,
TIdentityTokenAuths,
TIdentityTokenAuthsInsert,
TIdentityTokenAuthsUpdate,
@@ -794,6 +797,11 @@ declare module "knex/types/tables" {
TIdentityAlicloudAuthsInsert,
TIdentityAlicloudAuthsUpdate
>;
+ [TableName.IdentityTlsCertAuth]: KnexOriginal.CompositeTableType<
+ TIdentityTlsCertAuths,
+ TIdentityTlsCertAuthsInsert,
+ TIdentityTlsCertAuthsUpdate
+ >;
[TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType<
TIdentityAwsAuths,
TIdentityAwsAuthsInsert,
diff --git a/backend/src/db/migrations/20250624061429_identity-tls-auth.ts b/backend/src/db/migrations/20250624061429_identity-tls-auth.ts
new file mode 100644
index 000000000..3e6dc1af8
--- /dev/null
+++ b/backend/src/db/migrations/20250624061429_identity-tls-auth.ts
@@ -0,0 +1,28 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.IdentityTlsCertAuth))) {
+ await knex.schema.createTable(TableName.IdentityTlsCertAuth, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
+ t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
+ t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
+ t.jsonb("accessTokenTrustedIps").notNullable();
+ t.timestamps(true, true, true);
+ t.uuid("identityId").notNullable().unique();
+ t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
+ t.string("allowedCommonNames").nullable();
+ t.binary("encryptedCaCertificate").notNullable();
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.IdentityTlsCertAuth);
+ await dropOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
+}
diff --git a/backend/src/db/schemas/identity-tls-cert-auths.ts b/backend/src/db/schemas/identity-tls-cert-auths.ts
new file mode 100644
index 000000000..c907ead73
--- /dev/null
+++ b/backend/src/db/schemas/identity-tls-cert-auths.ts
@@ -0,0 +1,27 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const IdentityTlsCertAuthsSchema = z.object({
+ id: z.string().uuid(),
+ accessTokenTTL: z.coerce.number().default(7200),
+ accessTokenMaxTTL: z.coerce.number().default(7200),
+ accessTokenNumUsesLimit: z.coerce.number().default(0),
+ accessTokenTrustedIps: z.unknown(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ identityId: z.string().uuid(),
+ allowedCommonNames: z.string().nullable().optional(),
+ encryptedCaCertificate: zodBuffer
+});
+
+export type TIdentityTlsCertAuths = z.infer;
+export type TIdentityTlsCertAuthsInsert = Omit, TImmutableDBKeys>;
+export type TIdentityTlsCertAuthsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts
index 292551c80..1642c3555 100644
--- a/backend/src/db/schemas/index.ts
+++ b/backend/src/db/schemas/index.ts
@@ -52,6 +52,7 @@ export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";
export * from "./identity-project-memberships";
+export * from "./identity-tls-cert-auths";
export * from "./identity-token-auths";
export * from "./identity-ua-client-secrets";
export * from "./identity-universal-auths";
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index ceba6e370..d47962110 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -86,6 +86,7 @@ export enum TableName {
IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths",
IdentityLdapAuth = "identity_ldap_auths",
+ IdentityTlsCertAuth = "identity_tls_cert_auths",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
@@ -251,6 +252,7 @@ export enum IdentityAuthMethod {
ALICLOUD_AUTH = "alicloud-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
+ TLS_CERT_AUTH = "tls-cert-auth",
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth",
diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index e72b9fa46..a2acb62c4 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -202,6 +202,12 @@ export enum EventType {
REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth",
GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth",
+ LOGIN_IDENTITY_TLS_CERT_AUTH = "login-identity-tls-cert-auth",
+ ADD_IDENTITY_TLS_CERT_AUTH = "add-identity-tls-cert-auth",
+ UPDATE_IDENTITY_TLS_CERT_AUTH = "update-identity-tls-cert-auth",
+ REVOKE_IDENTITY_TLS_CERT_AUTH = "revoke-identity-tls-cert-auth",
+ GET_IDENTITY_TLS_CERT_AUTH = "get-identity-tls-cert-auth",
+
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
@@ -1141,6 +1147,53 @@ interface GetIdentityAliCloudAuthEvent {
};
}
+interface LoginIdentityTlsCertAuthEvent {
+ type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH;
+ metadata: {
+ identityId: string;
+ identityTlsCertAuthId: string;
+ identityAccessTokenId: string;
+ };
+}
+
+interface AddIdentityTlsCertAuthEvent {
+ type: EventType.ADD_IDENTITY_TLS_CERT_AUTH;
+ metadata: {
+ identityId: string;
+ allowedCommonNames: string | null | undefined;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: Array;
+ };
+}
+
+interface DeleteIdentityTlsCertAuthEvent {
+ type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface UpdateIdentityTlsCertAuthEvent {
+ type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH;
+ metadata: {
+ identityId: string;
+ allowedCommonNames: string | null | undefined;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: Array;
+ };
+}
+
+interface GetIdentityTlsCertAuthEvent {
+ type: EventType.GET_IDENTITY_TLS_CERT_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: {
@@ -3358,6 +3411,11 @@ export type Event =
| UpdateIdentityAliCloudAuthEvent
| GetIdentityAliCloudAuthEvent
| DeleteIdentityAliCloudAuthEvent
+ | LoginIdentityTlsCertAuthEvent
+ | AddIdentityTlsCertAuthEvent
+ | UpdateIdentityTlsCertAuthEvent
+ | GetIdentityTlsCertAuthEvent
+ | DeleteIdentityTlsCertAuthEvent
| LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index be460b4b4..7c7a6f654 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -22,6 +22,7 @@ export enum ApiDocsTags {
UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth",
AliCloudAuth = "Alibaba Cloud Auth",
+ TlsCertAuth = "TLS Certificate Auth",
AwsAuth = "AWS Auth",
OciAuth = "OCI Auth",
AzureAuth = "Azure Auth",
@@ -283,6 +284,38 @@ export const ALICLOUD_AUTH = {
}
} as const;
+export const TLS_CERT_AUTH = {
+ LOGIN: {
+ identityId: "The ID of the identity to login."
+ },
+ ATTACH: {
+ identityId: "The ID of the identity to attach the configuration onto.",
+ allowedCommonNames:
+ "The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
+ caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
+ accessTokenTTL: "The lifetime for an access token in seconds.",
+ accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
+ accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
+ accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
+ },
+ UPDATE: {
+ identityId: "The ID of the identity to update the auth method for.",
+ allowedCommonNames:
+ "The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
+ caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
+ accessTokenTTL: "The new lifetime for an access token in seconds.",
+ accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
+ accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
+ accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
+ },
+ RETRIEVE: {
+ identityId: "The ID of the identity to retrieve the auth method for."
+ },
+ REVOKE: {
+ identityId: "The ID of the identity to revoke the auth method for."
+ }
+} as const;
+
export const AWS_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts
index 4fb19e7bb..8db5c16c4 100644
--- a/backend/src/lib/config/env.ts
+++ b/backend/src/lib/config/env.ts
@@ -193,6 +193,9 @@ const envSchema = z
PYLON_API_KEY: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
+ IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default(
+ "x-identity-tls-cert-auth-client-cert"
+ ),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 051d33388..3cf90f13d 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -193,6 +193,8 @@ import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
+import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal";
+import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service";
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
@@ -384,6 +386,7 @@ export const registerRoutes = async (
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db);
+ const identityTlsCertAuthDAL = identityTlsCertAuthDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOciAuthDAL = identityOciAuthDALFactory(db);
@@ -1492,6 +1495,15 @@ export const registerRoutes = async (
permissionService
});
+ const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({
+ identityAccessTokenDAL,
+ identityTlsCertAuthDAL,
+ identityOrgMembershipDAL,
+ licenseService,
+ permissionService,
+ kmsService
+ });
+
const identityAwsAuthService = identityAwsAuthServiceFactory({
identityAccessTokenDAL,
identityAwsAuthDAL,
@@ -1946,6 +1958,7 @@ export const registerRoutes = async (
identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService,
identityOciAuth: identityOciAuthService,
+ identityTlsCertAuth: identityTlsCertAuthService,
identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService,
identityLdapAuth: identityLdapAuthService,
diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
new file mode 100644
index 000000000..40060ad40
--- /dev/null
+++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
@@ -0,0 +1,396 @@
+import crypto from "node:crypto";
+
+import { z } from "zod";
+
+import { IdentityTlsCertAuthsSchema } from "@app/db/schemas";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs";
+import { getConfig } from "@app/lib/config/env";
+import { BadRequestError } from "@app/lib/errors";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
+import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
+
+const validateCommonNames = z
+ .string()
+ .min(1)
+ .trim()
+ .transform((el) =>
+ el
+ .split(",")
+ .map((i) => i.trim())
+ .join(",")
+ );
+
+const validateCaCertificate = (caCert: string) => {
+ if (!caCert) return true;
+ try {
+ // eslint-disable-next-line no-new
+ new crypto.X509Certificate(caCert);
+ return true;
+ } catch (err) {
+ return false;
+ }
+};
+
+export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/login",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.TlsCertAuth],
+ description: "Login with TLS Certificate Auth",
+ body: z.object({
+ identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId)
+ }),
+ response: {
+ 200: z.object({
+ accessToken: z.string(),
+ expiresIn: z.coerce.number(),
+ accessTokenMaxTTL: z.coerce.number(),
+ tokenType: z.literal("Bearer")
+ })
+ }
+ },
+ handler: async (req) => {
+ const appCfg = getConfig();
+ const clientCertificate = req.headers[appCfg.IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY];
+ if (!clientCertificate) {
+ throw new BadRequestError({ message: "Missing TLS certificate in header" });
+ }
+
+ const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } =
+ await server.services.identityTlsCertAuth.login({
+ identityId: req.body.identityId,
+ clientCertificate: clientCertificate as string
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityMembershipOrg?.orgId,
+ event: {
+ type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH,
+ metadata: {
+ identityId: identityTlsCertAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ identityTlsCertAuthId: identityTlsCertAuth.id
+ }
+ }
+ });
+
+ return {
+ accessToken,
+ tokenType: "Bearer" as const,
+ expiresIn: identityTlsCertAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.TlsCertAuth],
+ description: "Attach TLS Certificate Auth configuration onto identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(TLS_CERT_AUTH.ATTACH.identityId)
+ }),
+ body: z
+ .object({
+ allowedCommonNames: validateCommonNames
+ .optional()
+ .nullable()
+ .describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames),
+ caCertificate: z
+ .string()
+ .min(1)
+ .max(10240)
+ .refine(validateCaCertificate, "Invalid CA Certificate.")
+ .describe(TLS_CERT_AUTH.ATTACH.caCertificate),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
+ .describe(TLS_CERT_AUTH.ATTACH.accessTokenTrustedIps),
+ accessTokenTTL: z
+ .number()
+ .int()
+ .min(0)
+ .max(315360000)
+ .default(2592000)
+ .describe(TLS_CERT_AUTH.ATTACH.accessTokenTTL),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .min(1)
+ .max(315360000)
+ .default(2592000)
+ .describe(TLS_CERT_AUTH.ATTACH.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: z
+ .number()
+ .int()
+ .min(0)
+ .default(0)
+ .describe(TLS_CERT_AUTH.ATTACH.accessTokenNumUsesLimit)
+ })
+ .refine(
+ (val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
+ "Access Token TTL cannot be greater than Access Token Max TTL."
+ ),
+ response: {
+ 200: z.object({
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityTlsCertAuth = await server.services.identityTlsCertAuth.attachTlsCertAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body,
+ identityId: req.params.identityId,
+ isActorSuperAdmin: isSuperAdmin(req.auth)
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.ADD_IDENTITY_TLS_CERT_AUTH,
+ metadata: {
+ identityId: identityTlsCertAuth.identityId,
+ allowedCommonNames: identityTlsCertAuth.allowedCommonNames,
+ accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
+ accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit
+ }
+ }
+ });
+
+ return { identityTlsCertAuth };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.TlsCertAuth],
+ description: "Update TLS Certificate Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(TLS_CERT_AUTH.UPDATE.identityId)
+ }),
+ body: z
+ .object({
+ caCertificate: z
+ .string()
+ .min(1)
+ .max(10240)
+ .refine(validateCaCertificate, "Invalid CA Certificate.")
+ .optional()
+ .describe(TLS_CERT_AUTH.UPDATE.caCertificate),
+ allowedCommonNames: validateCommonNames
+ .optional()
+ .nullable()
+ .describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .optional()
+ .describe(TLS_CERT_AUTH.UPDATE.accessTokenTrustedIps),
+ accessTokenTTL: z
+ .number()
+ .int()
+ .min(0)
+ .max(315360000)
+ .optional()
+ .describe(TLS_CERT_AUTH.UPDATE.accessTokenTTL),
+ accessTokenNumUsesLimit: z
+ .number()
+ .int()
+ .min(0)
+ .optional()
+ .describe(TLS_CERT_AUTH.UPDATE.accessTokenNumUsesLimit),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .max(315360000)
+ .min(0)
+ .optional()
+ .describe(TLS_CERT_AUTH.UPDATE.accessTokenMaxTTL)
+ })
+ .refine(
+ (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
+ "Access Token TTL cannot be greater than Access Token Max TTL."
+ ),
+ response: {
+ 200: z.object({
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityTlsCertAuth = await server.services.identityTlsCertAuth.updateTlsCertAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body,
+ identityId: req.params.identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH,
+ metadata: {
+ identityId: identityTlsCertAuth.identityId,
+ allowedCommonNames: identityTlsCertAuth.allowedCommonNames,
+ accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
+ accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit
+ }
+ }
+ });
+
+ return { identityTlsCertAuth };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/identities/:identityId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.TlsCertAuth],
+ description: "Retrieve TLS Certificate Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(TLS_CERT_AUTH.RETRIEVE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema.extend({
+ caCertificate: z.string()
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityTlsCertAuth = await server.services.identityTlsCertAuth.getTlsCertAuth({
+ identityId: req.params.identityId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.GET_IDENTITY_TLS_CERT_AUTH,
+ metadata: {
+ identityId: identityTlsCertAuth.identityId
+ }
+ }
+ });
+ return { identityTlsCertAuth };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.TlsCertAuth],
+ description: "Delete TLS Certificate Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(TLS_CERT_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityTlsCertAuth = await server.services.identityTlsCertAuth.revokeTlsCertAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ identityId: req.params.identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: req.permission.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH,
+ metadata: {
+ identityId: identityTlsCertAuth.identityId
+ }
+ }
+ });
+
+ return { identityTlsCertAuth };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index 2363147b6..e6aa2e83f 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -25,6 +25,7 @@ import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
+import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
@@ -66,6 +67,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAliCloudAuthRouter);
await authRouter.register(registerIdentityAwsAuthRouter);
+ await authRouter.register(registerIdentityTlsCertAuthRouter, { prefix: "/tls-cert-auth" });
await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOciAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter);
diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts
index 879ca9fd3..a1b70b21c 100644
--- a/backend/src/services/identity-access-token/identity-access-token-dal.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts
@@ -45,6 +45,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ `${TableName.Identity}.id`,
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
@@ -61,6 +66,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
+ db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTlsCertAuth).as("accessTokenTrustedIpsTlsCert"),
db.ref("name").withSchema(TableName.Identity)
)
.first();
@@ -79,7 +85,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
- trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap
+ trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap,
+ trustedIpsAccessTlsCertAuth: doc.accessTokenTrustedIpsTlsCert
};
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts
index 7c8944f50..587b7c436 100644
--- a/backend/src/services/identity-access-token/identity-access-token-service.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-service.ts
@@ -201,7 +201,8 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
- [IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth
+ [IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth,
+ [IdentityAuthMethod.TLS_CERT_AUTH]: identityAccessToken.trustedIpsAccessTlsCertAuth
};
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts
new file mode 100644
index 000000000..951077f33
--- /dev/null
+++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-dal.ts
@@ -0,0 +1,10 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify, TOrmify } from "@app/lib/knex";
+
+export type TIdentityTlsCertAuthDALFactory = TOrmify;
+
+export const identityTlsCertAuthDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.IdentityTlsCertAuth);
+ return orm;
+};
diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts
new file mode 100644
index 000000000..11dd312ad
--- /dev/null
+++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts
@@ -0,0 +1,423 @@
+import crypto from "node:crypto";
+
+import { ForbiddenError } from "@casl/ability";
+import jwt from "jsonwebtoken";
+
+import { IdentityAuthMethod } from "@app/db/schemas";
+import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
+import {
+ constructPermissionErrorMessage,
+ validatePrivilegeChangeOperation
+} from "@app/ee/services/permission/permission-fns";
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
+import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
+import { getConfig } from "@app/lib/config/env";
+import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
+import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
+
+import { ActorType, AuthTokenType } from "../auth/auth-type";
+import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
+import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
+import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
+import { TKmsServiceFactory } from "../kms/kms-service";
+import { KmsDataKey } from "../kms/kms-types";
+import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
+import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
+import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
+
+type TIdentityTlsCertAuthServiceFactoryDep = {
+ identityAccessTokenDAL: Pick;
+ identityTlsCertAuthDAL: Pick<
+ TIdentityTlsCertAuthDALFactory,
+ "findOne" | "transaction" | "create" | "updateById" | "delete"
+ >;
+ identityOrgMembershipDAL: Pick;
+ licenseService: Pick;
+ permissionService: Pick;
+ kmsService: Pick;
+};
+
+const parseSubjectDetails = (data: string) => {
+ const values: Record = {};
+ data.split("\n").forEach((el) => {
+ const [key, value] = el.split("=");
+ values[key.trim()] = value.trim();
+ });
+ return values;
+};
+
+export const identityTlsCertAuthServiceFactory = ({
+ identityAccessTokenDAL,
+ identityTlsCertAuthDAL,
+ identityOrgMembershipDAL,
+ licenseService,
+ permissionService,
+ kmsService
+}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
+ const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
+ const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
+ if (!identityTlsCertAuth) {
+ throw new NotFoundError({
+ message: "TLS Certificate auth method not found for identity, did you configure TLS Certificate auth?"
+ });
+ }
+
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
+ identityId: identityTlsCertAuth.identityId
+ });
+
+ if (!identityMembershipOrg) {
+ throw new NotFoundError({
+ message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found`
+ });
+ }
+
+ const { decryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+
+ const caCertificate = decryptor({
+ cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
+ }).toString();
+
+ const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0];
+ if (!leafCertificate) {
+ throw new BadRequestError({ message: "Missing client certificate" });
+ }
+
+ const clientCertificateX509 = new crypto.X509Certificate(leafCertificate);
+ const caCertificateX509 = new crypto.X509Certificate(caCertificate);
+
+ const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey);
+ if (!isValidCertificate)
+ throw new UnauthorizedError({
+ message: "Access denied: Certificate not issued by the provided CA."
+ });
+
+ if (new Date(clientCertificateX509.validTo) < new Date()) {
+ throw new UnauthorizedError({
+ message: "Access denied: Certificate has expired."
+ });
+ }
+
+ if (new Date(clientCertificateX509.validFrom) > new Date()) {
+ throw new UnauthorizedError({
+ message: "Access denied: Certificate not yet valid."
+ });
+ }
+
+ const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
+ if (identityTlsCertAuth.allowedCommonNames) {
+ const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN);
+ if (!isValidCommonName) {
+ throw new UnauthorizedError({
+ message: "Access denied: TLS Certificate Auth common name not allowed."
+ });
+ }
+ }
+
+ // Generate the token
+ const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
+ const newToken = await identityAccessTokenDAL.create(
+ {
+ identityId: identityTlsCertAuth.identityId,
+ isAccessTokenRevoked: false,
+ accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
+ accessTokenNumUses: 0,
+ accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
+ authMethod: IdentityAuthMethod.TLS_CERT_AUTH
+ },
+ tx
+ );
+ return newToken;
+ });
+
+ const appCfg = getConfig();
+ const accessToken = jwt.sign(
+ {
+ identityId: identityTlsCertAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
+ } as TIdentityAccessTokenJwtPayload,
+ appCfg.AUTH_SECRET,
+ Number(identityAccessToken.accessTokenTTL) === 0
+ ? undefined
+ : {
+ expiresIn: Number(identityAccessToken.accessTokenTTL)
+ }
+ );
+
+ return {
+ identityTlsCertAuth,
+ accessToken,
+ identityAccessToken,
+ identityMembershipOrg
+ };
+ };
+
+ const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
+ identityId,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId,
+ isActorSuperAdmin,
+ caCertificate,
+ allowedCommonNames
+ }) => {
+ await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
+
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+
+ if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
+ throw new BadRequestError({
+ message: "Failed to add TLS Certificate Auth to already configured identity"
+ });
+ }
+
+ if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
+ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
+ }
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
+
+ const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
+ const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
+ if (
+ !plan.ipAllowlisting &&
+ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
+ accessTokenTrustedIp.ipAddress !== "::/0"
+ )
+ throw new BadRequestError({
+ message:
+ "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
+ });
+ if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
+ throw new BadRequestError({
+ message: "The IP is not a valid IPv4, IPv6, or CIDR block"
+ });
+ return extractIPDetails(accessTokenTrustedIp.ipAddress);
+ });
+
+ const { encryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+
+ const identityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => {
+ const doc = await identityTlsCertAuthDAL.create(
+ {
+ identityId: identityMembershipOrg.identityId,
+ accessTokenMaxTTL,
+ allowedCommonNames,
+ accessTokenTTL,
+ encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
+ },
+ tx
+ );
+ return doc;
+ });
+ return { ...identityTlsCertAuth, orgId: identityMembershipOrg.orgId };
+ };
+
+ const updateTlsCertAuth: TIdentityTlsCertAuthServiceFactory["updateTlsCertAuth"] = async ({
+ identityId,
+ caCertificate,
+ allowedCommonNames,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId
+ }) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
+ throw new NotFoundError({
+ message: "The identity does not have TLS Certificate Auth attached"
+ });
+ }
+
+ const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
+
+ if (
+ (accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL) > 0 &&
+ (accessTokenTTL || identityTlsCertAuth.accessTokenTTL) >
+ (accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL)
+ ) {
+ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
+ }
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+
+ const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
+ const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
+ if (
+ !plan.ipAllowlisting &&
+ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
+ accessTokenTrustedIp.ipAddress !== "::/0"
+ )
+ throw new BadRequestError({
+ message:
+ "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
+ });
+ if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
+ throw new BadRequestError({
+ message: "The IP is not a valid IPv4, IPv6, or CIDR block"
+ });
+ return extractIPDetails(accessTokenTrustedIp.ipAddress);
+ });
+ const { encryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+
+ const updatedTlsCertAuth = await identityTlsCertAuthDAL.updateById(identityTlsCertAuth.id, {
+ allowedCommonNames,
+ encryptedCaCertificate: caCertificate
+ ? encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob
+ : undefined,
+ accessTokenMaxTTL,
+ accessTokenTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps: reformattedAccessTokenTrustedIps
+ ? JSON.stringify(reformattedAccessTokenTrustedIps)
+ : undefined
+ });
+
+ return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.orgId };
+ };
+
+ const getTlsCertAuth: TIdentityTlsCertAuthServiceFactory["getTlsCertAuth"] = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
+ throw new BadRequestError({
+ message: "The identity does not have TLS Certificate Auth attached"
+ });
+ }
+
+ const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
+ const { decryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: identityMembershipOrg.orgId
+ });
+ let caCertificate = "";
+ if (identityAuth.encryptedCaCertificate) {
+ caCertificate = decryptor({ cipherTextBlob: identityAuth.encryptedCaCertificate }).toString();
+ }
+
+ return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.orgId };
+ };
+
+ const revokeTlsCertAuth: TIdentityTlsCertAuthServiceFactory["revokeTlsCertAuth"] = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
+ throw new BadRequestError({
+ message: "The identity does not have TLS Certificate auth"
+ });
+ }
+ const { permission, membership } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ const permissionBoundary = validatePrivilegeChangeOperation(
+ membership.shouldUseNewPrivilegeSystem,
+ OrgPermissionIdentityActions.RevokeAuth,
+ OrgPermissionSubjects.Identity,
+ permission,
+ rolePermission
+ );
+
+ if (!permissionBoundary.isValid)
+ throw new PermissionBoundaryError({
+ message: constructPermissionErrorMessage(
+ "Failed to revoke TLS Certificate auth of identity with more privileged role",
+ membership.shouldUseNewPrivilegeSystem,
+ OrgPermissionIdentityActions.RevokeAuth,
+ OrgPermissionSubjects.Identity
+ ),
+ details: { missingPermissions: permissionBoundary.missingPermissions }
+ });
+
+ const revokedIdentityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => {
+ const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx);
+ await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx);
+
+ return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityTlsCertAuth;
+ };
+
+ return {
+ login,
+ attachTlsCertAuth,
+ updateTlsCertAuth,
+ getTlsCertAuth,
+ revokeTlsCertAuth
+ };
+};
diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts
new file mode 100644
index 000000000..b7a08276b
--- /dev/null
+++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts
@@ -0,0 +1,49 @@
+import { TIdentityAccessTokens, TIdentityOrgMemberships, TIdentityTlsCertAuths } from "@app/db/schemas";
+import { TProjectPermission } from "@app/lib/types";
+
+export type TLoginTlsCertAuthDTO = {
+ identityId: string;
+ clientCertificate: string;
+};
+
+export type TAttachTlsCertAuthDTO = {
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames?: string | null;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: { ipAddress: string }[];
+ isActorSuperAdmin?: boolean;
+} & Omit;
+
+export type TUpdateTlsCertAuthDTO = {
+ identityId: string;
+ caCertificate?: string;
+ allowedCommonNames?: string | null;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: { ipAddress: string }[];
+} & Omit;
+
+export type TGetTlsCertAuthDTO = {
+ identityId: string;
+} & Omit;
+
+export type TRevokeTlsCertAuthDTO = {
+ identityId: string;
+} & Omit;
+
+export type TIdentityTlsCertAuthServiceFactory = {
+ login: (dto: TLoginTlsCertAuthDTO) => Promise<{
+ identityTlsCertAuth: TIdentityTlsCertAuths;
+ accessToken: string;
+ identityAccessToken: TIdentityAccessTokens;
+ identityMembershipOrg: TIdentityOrgMemberships;
+ }>;
+ attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise;
+ updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise;
+ revokeTlsCertAuth: (dto: TRevokeTlsCertAuthDTO) => Promise;
+ getTlsCertAuth: (dto: TGetTlsCertAuthDTO) => Promise;
+};
diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts
index 3020d9c47..dee87fc49 100644
--- a/backend/src/services/identity/identity-fns.ts
+++ b/backend/src/services/identity/identity-fns.ts
@@ -11,7 +11,8 @@ export const buildAuthMethods = ({
azureId,
tokenId,
jwtId,
- ldapId
+ ldapId,
+ tlsCertId
}: {
uaId?: string;
gcpId?: string;
@@ -24,6 +25,7 @@ export const buildAuthMethods = ({
tokenId?: string;
jwtId?: string;
ldapId?: string;
+ tlsCertId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
@@ -36,6 +38,7 @@ export const buildAuthMethods = ({
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null],
- ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null]
+ ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null],
+ ...[tlsCertId ? IdentityAuthMethod.TLS_CERT_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};
diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts
index 28064c9bb..5ca6cbbc1 100644
--- a/backend/src/services/identity/identity-org-dal.ts
+++ b/backend/src/services/identity/identity-org-dal.ts
@@ -12,6 +12,7 @@ import {
TIdentityOciAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
+ TIdentityTlsCertAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths,
TOrgRoles
@@ -99,7 +100,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityLdapAuth}.identityId`
)
-
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ `${TableName.IdentityOrgMembership}.identityId`,
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
@@ -114,6 +119,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
+ db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth),
db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
);
@@ -238,7 +244,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId",
`${TableName.IdentityLdapAuth}.identityId`
)
-
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ "paginatedIdentity.identityId",
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
.select(
db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"),
@@ -260,7 +270,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
- db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
+ db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
+ db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -306,6 +317,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
azureId,
tokenId,
ldapId,
+ tlsCertId,
createdAt,
updatedAt
}) => ({
@@ -313,7 +325,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
roleId,
identityId,
id,
-
orgId,
createdAt,
updatedAt,
@@ -341,7 +352,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
azureId,
tokenId,
jwtId,
- ldapId
+ ldapId,
+ tlsCertId
})
}
}),
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 590e17763..209c6e62e 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -4,7 +4,7 @@ services:
nginx:
container_name: infisical-dev-nginx
image: nginx
- restart: always
+ restart: "always"
ports:
- 8080:80
- 8443:443
diff --git a/docs/api-reference/endpoints/tls-cert-auth/attach.mdx b/docs/api-reference/endpoints/tls-cert-auth/attach.mdx
new file mode 100644
index 000000000..35c3b87e9
--- /dev/null
+++ b/docs/api-reference/endpoints/tls-cert-auth/attach.mdx
@@ -0,0 +1,4 @@
+---
+title: "Attach"
+openapi: "POST /api/v1/auth/tls-cert-auth/identities/{identityId}"
+---
diff --git a/docs/api-reference/endpoints/tls-cert-auth/login.mdx b/docs/api-reference/endpoints/tls-cert-auth/login.mdx
new file mode 100644
index 000000000..0069ef1b7
--- /dev/null
+++ b/docs/api-reference/endpoints/tls-cert-auth/login.mdx
@@ -0,0 +1,4 @@
+---
+title: "Login"
+openapi: "POST /api/v1/auth/tls-cert-auth/login"
+---
diff --git a/docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx b/docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx
new file mode 100644
index 000000000..d59b31d11
--- /dev/null
+++ b/docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx
@@ -0,0 +1,4 @@
+---
+title: "Retrieve"
+openapi: "GET /api/v1/auth/tls-cert-auth/identities/{identityId}"
+---
diff --git a/docs/api-reference/endpoints/tls-cert-auth/revoke.mdx b/docs/api-reference/endpoints/tls-cert-auth/revoke.mdx
new file mode 100644
index 000000000..0d3ccda65
--- /dev/null
+++ b/docs/api-reference/endpoints/tls-cert-auth/revoke.mdx
@@ -0,0 +1,4 @@
+---
+title: "Revoke"
+openapi: "DELETE /api/v1/auth/tls-cert-auth/identities/{identityId}"
+---
diff --git a/docs/api-reference/endpoints/tls-cert-auth/update.mdx b/docs/api-reference/endpoints/tls-cert-auth/update.mdx
new file mode 100644
index 000000000..3bb8892ea
--- /dev/null
+++ b/docs/api-reference/endpoints/tls-cert-auth/update.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/auth/tls-cert-auth/identities/{identityId}"
+---
diff --git a/docs/docs.json b/docs/docs.json
index e0d0db73a..ac9d62a7c 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -288,6 +288,7 @@
"documentation/platform/identities/kubernetes-auth",
"documentation/platform/identities/oci-auth",
"documentation/platform/identities/token-auth",
+ "documentation/platform/identities/tls-cert-auth",
"documentation/platform/identities/universal-auth",
{
"group": "OIDC Auth",
@@ -752,6 +753,16 @@
"api-reference/endpoints/alicloud-auth/revoke"
]
},
+ {
+ "group": "TLS Certificate Auth",
+ "pages": [
+ "api-reference/endpoints/tls-cert-auth/login",
+ "api-reference/endpoints/tls-cert-auth/attach",
+ "api-reference/endpoints/tls-cert-auth/retrieve",
+ "api-reference/endpoints/tls-cert-auth/update",
+ "api-reference/endpoints/tls-cert-auth/revoke"
+ ]
+ },
{
"group": "AWS Auth",
"pages": [
diff --git a/docs/documentation/platform/identities/tls-cert-auth.mdx b/docs/documentation/platform/identities/tls-cert-auth.mdx
new file mode 100644
index 000000000..e11d0c06e
--- /dev/null
+++ b/docs/documentation/platform/identities/tls-cert-auth.mdx
@@ -0,0 +1,176 @@
+---
+title: TLS Certificate Auth
+description: "Learn how to authenticate with Infisical using TLS Certificate."
+---
+
+**TLS Certificate Auth** is an authentication method that verifies a user's TLS Client certificate using the provided CA Certificate, allowing secure access to Infisical resources.
+
+## Diagram
+
+The following sequence diagram illustrates the TLS Certificate Auth workflow for authenticating users with Infisical.
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Infisical
+
+ Note over Client,Client: Step 1: Setup your TLS request with the client certificate
+
+ Note over Client,Infisical: Step 2: Login Operation
+ Client->>Infisical: Send request to /api/v1/auth/tls-cert-auth/login
+
+ Note over Infisical: Step 3: Request verification using CA Certificate
+
+ Infisical->>Client: Return short-lived access token
+
+ Note over Client,Infisical: Step 5: Access Infisical API with token
+ Client->>Infisical: Make authenticated requests using the short-lived access token
+```
+
+## Concept
+
+At a high level, Infisical authenticates the client's TLS Certificate by verifying its identity and checking that it meets specific requirements (e.g., it is bound to the allowed common names) at the `/api/v1/auth/tls-cert-auth/login` endpoint. If successful, Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
+
+To be more specific:
+
+1. The client sends a TLS request with the client certificate to Infisical at the `/api/v1/auth/tls-cert-auth/login` endpoint.
+2. Infisical verifies the incoming request using the provided CA certificate.
+3. Infisical checks the user's properties against set criteria such as Allowed Common Names.
+4. If all checks pass, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
+
+
+ Most of the time, the Infisical server will be behind a load balancer or
+ proxy. To propagate the TLS certificate from the load balancer to the
+ instance, you can configure the TLS to send the client certificate as a header
+ that is set as an [environment
+ variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
+
+
+## Guide
+
+In the following steps, we explore how to create and use identities for your workloads and applications on TLS Certificate to
+access the Infisical API using request signing.
+
+
+ **Self-Hosted Users:** Before using TLS Certificate Auth, please review the
+ [Security Requirements for Self-Hosted
+ Deployments](#security-requirements-for-self-hosted-deployments) section below
+ to ensure proper configuration and avoid security vulnerabilities.
+
+
+### Creating an identity
+
+To create an identity, head to your Organization Settings > Access Control > [Identities](https://app.infisical.com/organization/access-management?selectedTab=identities) and press **Create identity**.
+
+
+
+When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
+
+
+
+Input some details for your new identity:
+
+- **Name (required):** A friendly name for the identity.
+- **Role (required):** A role from the [**Organization Roles**](https://app.infisical.com/organization/access-management?selectedTab=roles) tab for the identity to assume. The organization role assigned will determine what organization-level resources this identity can have access to.
+
+Once you've created an identity, you'll be redirected to a page where you can manage the identity.
+
+
+
+Since the identity has been configured with [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) by default, you should reconfigure it to use TLS Certificate Auth instead. To do this, click the cog next to **Universal Auth** and then select **Delete** in the options dropdown.
+
+
+
+
+
+Now create a new TLS Certificate Auth Method.
+
+
+
+Here's some information about each field:
+
+- **CA Certificate:** A PEM encoded CA Certificate used to validate incoming TLS request client certificate.
+- **Allowed Common Names:** A comma separated list of client certificate common names allowed.
+- **Access Token TTL (default is `2592000` equivalent to 30 days):** The lifetime for an access token in seconds. This value will be referenced at renewal time.
+- **Access Token Max TTL (default is `2592000` equivalent to 30 days):** The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
+- **Access Token Max Number of Uses (default is `0`):** The maximum number of times that an access token can be used; a value of `0` implies an infinite number of uses.
+- **Access Token Trusted IPs:** The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
+
+### Adding an identity to a project
+
+In order to allow an identity to access project-level resources such as secrets, you must add it to the relevant projects.
+
+To do this, head over to the project you want to add the identity to and navigate to Project Settings > Access Control > Machine Identities and press **Add Identity**.
+
+
+
+Select the identity you want to add to the project and the project-level role you want it to assume. The project role given to the identity will determine what project-level resources this identity can access.
+
+
+
+### Accessing the Infisical API with the identity
+
+To access the Infisical API as the identity, you need to send a TLS request to `/api/v1/auth/tls-cert-auth/login` endpoint.
+
+Below is an example of how you can authenticate with Infisical using NodeJS.
+
+```javascript
+const fs = require("fs");
+const https = require("https");
+const axios = require("axios");
+
+try {
+ const clientCertificate = fs.readFileSync("client-cert.pem", "utf8");
+ const clientKeyCertificate = fs.readFileSync("client-key.pem", "utf8");
+
+ const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
+ const identityId = "";
+
+ // Create HTTPS agent with client certificate and key
+ const httpsAgent = new https.Agent({
+ cert: clientCertificate,
+ key: clientKeyCertificate,
+ });
+
+ const { data } = await axios.post(
+ `${infisicalUrl}/api/v1/auth/tls-cert-auth/login`,
+ {
+ identityId,
+ },
+ {
+ httpsAgent: httpsAgent, // Pass the HTTPS agent with client cert
+ }
+ );
+
+ console.log("result data: ", data); // access token here
+} catch (err) {
+ console.error(err);
+}
+```
+
+
+ Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted.
+
+ If an identity access token expires, it can no longer access the Infisical API. A new access token should be obtained by performing another login operation.
+
+
+
+## Security Requirements for Self-Hosted Deployments
+
+ALL TLS cert [login](/api-reference/endpoints/tls-cert-auth/login) requests **MUST** go through a load balancer/proxy that verifies certificate ownership:
+
+- **REQUIRED:** Configure your load balancer/proxy to **require a proper TLS handshake with client certificate presentation**
+- **REQUIRED:** Ensure the load balancer **verifies the client possesses the private key** corresponding to the certificate (standard TLS behavior)
+- **NEVER** allow direct connections to Infisical for TLS cert auth - this enables header injection attacks
+- **NEVER** forward certificate headers without requiring proper TLS certificate presentation
+
+### Load Balancer Configuration Examples
+
+- **AWS ALB:** Use mTLS listeners which require client certificate presentation during the TLS handshake
+- **NGINX/HAProxy:** Configure SSL client certificate requirement with proper TLS handshake verification
+
+
+ Infisical will handle the actual certificate validation against the configured
+ CA certificate and determine authentication permissions. The load balancer's
+ role is to ensure certificate ownership, not certificate trust validation.
+
diff --git a/docs/images/platform/identities/identities-tls-cert-auth-create-auth.png b/docs/images/platform/identities/identities-tls-cert-auth-create-auth.png
new file mode 100644
index 000000000..2e66633e7
Binary files /dev/null and b/docs/images/platform/identities/identities-tls-cert-auth-create-auth.png differ
diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx
index f5d1c58d0..90f3b2207 100644
--- a/docs/self-hosting/configuration/envars.mdx
+++ b/docs/self-hosting/configuration/envars.mdx
@@ -32,7 +32,7 @@ Used to configure platform-specific security and operational settings
Specifies the network interface Infisical will bind to when accepting incoming connections.
- By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
+By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
@@ -122,6 +122,7 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
### Redis
+
Redis is used for caching and background tasks. You can use either a standalone Redis instance or a Redis Sentinel setup.
@@ -199,8 +200,17 @@ Without email configuration, Infisical's core functions like sign-up/login and s
connection can not be encrypted then message is not sent.
-
- If this is `true`, Infisical will validate the server's SSL/TLS certificate and reject the connection if the certificate is invalid or not trusted. If set to `false`, the client will accept the server's certificate regardless of its validity, which can be useful in development or testing environments but is not recommended for production use.
+
+ If this is `true`, Infisical will validate the server's SSL/TLS certificate
+ and reject the connection if the certificate is invalid or not trusted. If set
+ to `false`, the client will accept the server's certificate regardless of its
+ validity, which can be useful in development or testing environments but is
+ not recommended for production use.
@@ -211,6 +221,7 @@ Without email configuration, Infisical's core functions like sign-up/login and s
Infisical highly encourages the following variables be used alongside this one for maximum security:
- `SMTP_REQUIRE_TLS=true`
- `SMTP_TLS_REJECT_UNAUTHORIZED=true`
+
@@ -577,6 +588,7 @@ You can configure third-party app connections for re-use across Infisical Projec
The webhook secret configured for payload verification in the GitHub Radar App
+
@@ -771,3 +783,14 @@ If export type is set to `otlp`, you will have to configure a value for `OTEL_EX
The password for authenticating with the telemetry collector.
+
+## Identity Auth Method
+
+
+ The TLS header used to propagate the client certificate from the load balancer
+ to the server.
+
diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx
index cc8d0cef6..b441f5015 100644
--- a/frontend/src/hooks/api/identities/constants.tsx
+++ b/frontend/src/hooks/api/identities/constants.tsx
@@ -11,5 +11,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.OCI_AUTH]: "OCI Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
- [IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
+ [IdentityAuthMethod.JWT_AUTH]: "JWT Auth",
+ [IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth"
};
diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx
index de329d393..c850005cf 100644
--- a/frontend/src/hooks/api/identities/enums.tsx
+++ b/frontend/src/hooks/api/identities/enums.tsx
@@ -9,7 +9,8 @@ export enum IdentityAuthMethod {
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
LDAP_AUTH = "ldap-auth",
- JWT_AUTH = "jwt-auth"
+ JWT_AUTH = "jwt-auth",
+ TLS_CERT_AUTH = "tls-cert-auth"
}
export enum IdentityJwtConfigurationType {
diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx
index 7abf1a015..dfbf574e8 100644
--- a/frontend/src/hooks/api/identities/mutations.tsx
+++ b/frontend/src/hooks/api/identities/mutations.tsx
@@ -14,6 +14,7 @@ import {
AddIdentityLdapAuthDTO,
AddIdentityOciAuthDTO,
AddIdentityOidcAuthDTO,
+ AddIdentityTlsCertAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
ClientSecretData,
@@ -32,6 +33,7 @@ import {
DeleteIdentityLdapAuthDTO,
DeleteIdentityOciAuthDTO,
DeleteIdentityOidcAuthDTO,
+ DeleteIdentityTlsCertAuthDTO,
DeleteIdentityTokenAuthDTO,
DeleteIdentityUniversalAuthClientSecretDTO,
DeleteIdentityUniversalAuthDTO,
@@ -46,6 +48,7 @@ import {
IdentityLdapAuth,
IdentityOciAuth,
IdentityOidcAuth,
+ IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
RevokeTokenDTO,
@@ -60,6 +63,7 @@ import {
UpdateIdentityLdapAuthDTO,
UpdateIdentityOciAuthDTO,
UpdateIdentityOidcAuthDTO,
+ UpdateIdentityTlsCertAuthDTO,
UpdateIdentityTokenAuthDTO,
UpdateIdentityUniversalAuthDTO,
UpdateTokenIdentityTokenAuthDTO
@@ -655,6 +659,107 @@ export const useDeleteIdentityAliCloudAuth = () => {
});
};
+export const useAddIdentityTlsCertAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ allowedCommonNames,
+ caCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.post<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
+ `/api/v1/auth/tls-cert-auth/identities/${identityId}`,
+ {
+ allowedCommonNames,
+ caCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityTlsCertAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
+ });
+ }
+ });
+};
+
+export const useUpdateIdentityTlsCertAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ allowedCommonNames,
+ caCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.patch<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
+ `/api/v1/auth/tls-cert-auth/identities/${identityId}`,
+ {
+ caCertificate,
+ allowedCommonNames,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityTlsCertAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
+ });
+ }
+ });
+};
+
+export const useDeleteIdentityTlsCertAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`);
+ return identityTlsCertAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
+ });
+ }
+ });
+};
+
export const useUpdateIdentityOidcAuth = () => {
const queryClient = useQueryClient();
return useMutation({
diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx
index 806c1f0a4..b59526da1 100644
--- a/frontend/src/hooks/api/identities/queries.tsx
+++ b/frontend/src/hooks/api/identities/queries.tsx
@@ -17,6 +17,7 @@ import {
IdentityMembershipOrg,
IdentityOciAuth,
IdentityOidcAuth,
+ IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
TSearchIdentitiesDTO
@@ -34,6 +35,8 @@ export const identitiesKeys = {
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
+ getIdentityTlsCertAuth: (identityId: string) =>
+ [{ identityId }, "identity-tls-cert-auth"] as const,
getIdentityAliCloudAuth: (identityId: string) =>
[{ identityId }, "identity-alicloud-auth"] as const,
getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const,
@@ -175,6 +178,27 @@ export const useGetIdentityAwsAuth = (
});
};
+export const useGetIdentityTlsCertAuth = (
+ identityId: string,
+ options?: TReactQueryOptions["options"]
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.get<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
+ `/api/v1/auth/tls-cert-auth/identities/${identityId}`
+ );
+ return identityTlsCertAuth;
+ },
+ staleTime: 0,
+ gcTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
export const useGetIdentityOciAuth = (
identityId: string,
options?: TReactQueryOptions["options"]
diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts
index 873338f09..1af1cc24c 100644
--- a/frontend/src/hooks/api/identities/types.ts
+++ b/frontend/src/hooks/api/identities/types.ts
@@ -485,6 +485,47 @@ export type DeleteIdentityKubernetesAuthDTO = {
identityId: string;
};
+export type IdentityTlsCertAuth = {
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityTlsCertAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames?: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityTlsCertAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames?: string | null;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityTlsCertAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
export type CreateIdentityUniversalAuthClientSecretDTO = {
identityId: string;
description?: string;
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
index 0d44bfe64..57663f441 100644
--- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
@@ -17,6 +17,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
import { IdentityOciAuthForm } from "./IdentityOciAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
+import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
@@ -52,6 +53,7 @@ const identityAuthMethods = [
{ label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
{ label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
+ { label: "TLS Certificate Auth", value: IdentityAuthMethod.TLS_CERT_AUTH },
{
label: "JWT Auth",
value: IdentityAuthMethod.JWT_AUTH
@@ -123,6 +125,15 @@ export const IdentityAuthMethodModalContent = ({
/>
)
},
+ [IdentityAuthMethod.TLS_CERT_AUTH]: {
+ render: () => (
+
+ )
+ },
[IdentityAuthMethod.OIDC_AUTH]: {
render: () => (
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx
new file mode 100644
index 000000000..027620d6b
--- /dev/null
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx
@@ -0,0 +1,358 @@
+import { useEffect, useState } from "react";
+import { Controller, useFieldArray, useForm } from "react-hook-form";
+import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ FormControl,
+ IconButton,
+ Input,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+ TextArea
+} from "@app/components/v2";
+import { useOrganization, useSubscription } from "@app/context";
+import {
+ useAddIdentityTlsCertAuth,
+ useGetIdentityTlsCertAuth,
+ useUpdateIdentityTlsCertAuth
+} from "@app/hooks/api";
+import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+import { IdentityFormTab } from "./types";
+
+const schema = z.object({
+ allowedCommonNames: z.string().optional(),
+ caCertificate: z.string().min(1),
+ accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
+ message: "Access Token TTL cannot be greater than 315360000"
+ }),
+ accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
+ message: "Access Token Max TTL cannot be greater than 315360000"
+ }),
+ accessTokenNumUsesLimit: z.string(),
+ accessTokenTrustedIps: z
+ .array(
+ z.object({
+ ipAddress: z.string().max(50)
+ })
+ )
+ .min(1)
+});
+
+export type FormData = z.infer;
+
+type Props = {
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
+ handlePopUpToggle: (
+ popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
+ state?: boolean
+ ) => void;
+ identityId?: string;
+ isUpdate?: boolean;
+};
+
+export const IdentityTlsCertAuthForm = ({
+ handlePopUpOpen,
+ handlePopUpToggle,
+ identityId,
+ isUpdate
+}: Props) => {
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+ const { subscription } = useSubscription();
+
+ const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth();
+ const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth();
+ const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration);
+
+ const { data } = useGetIdentityTlsCertAuth(identityId ?? "", {
+ enabled: isUpdate
+ });
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ caCertificate: "",
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ }
+ });
+
+ const {
+ fields: accessTokenTrustedIpsFields,
+ append: appendAccessTokenTrustedIp,
+ remove: removeAccessTokenTrustedIp
+ } = useFieldArray({ control, name: "accessTokenTrustedIps" });
+
+ useEffect(() => {
+ if (data) {
+ reset({
+ caCertificate: data.caCertificate,
+ allowedCommonNames: data.allowedCommonNames || undefined,
+ accessTokenTTL: String(data.accessTokenTTL),
+ accessTokenMaxTTL: String(data.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
+ accessTokenTrustedIps: data.accessTokenTrustedIps.map(
+ ({ ipAddress, prefix }: IdentityTrustedIp) => {
+ return {
+ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
+ };
+ }
+ )
+ });
+ } else {
+ reset({
+ caCertificate: "",
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ });
+ }
+ }, [data]);
+
+ const onFormSubmit = async ({
+ caCertificate,
+ allowedCommonNames,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }: FormData) => {
+ try {
+ if (!identityId) return;
+
+ if (data) {
+ await updateMutateAsync({
+ organizationId: orgId,
+ caCertificate,
+ allowedCommonNames: allowedCommonNames || null,
+ identityId,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ } else {
+ await addMutateAsync({
+ organizationId: orgId,
+ identityId,
+ caCertificate,
+ allowedCommonNames: allowedCommonNames || undefined,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ }
+
+ handlePopUpToggle("identityAuthMethod", false);
+
+ createNotification({
+ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
+ type: "success"
+ });
+
+ reset();
+ } catch {
+ createNotification({
+ text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
index 8bfbd09b3..ca0a5e6f3 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
@@ -15,6 +15,7 @@ import {
useDeleteIdentityLdapAuth,
useDeleteIdentityOciAuth,
useDeleteIdentityOidcAuth,
+ useDeleteIdentityTlsCertAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
@@ -29,6 +30,7 @@ import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthC
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
+import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
@@ -63,6 +65,7 @@ export const Content = ({
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
+ const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth();
@@ -93,6 +96,10 @@ export const Content = ({
revokeMethod = revokeGcpAuth;
Component = ViewIdentityGcpAuthContent;
break;
+ case IdentityAuthMethod.TLS_CERT_AUTH:
+ revokeMethod = revokeTlsCertAuth;
+ Component = ViewIdentityTlsCertAuthContent;
+ break;
case IdentityAuthMethod.AWS_AUTH:
revokeMethod = revokeAwsAuth;
Component = ViewIdentityAwsAuthContent;
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx
new file mode 100644
index 000000000..7f4ef7fa6
--- /dev/null
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx
@@ -0,0 +1,88 @@
+import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
+import { useGetIdentityTlsCertAuth } from "@app/hooks/api";
+import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm";
+
+import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
+import { ViewAuthMethodProps } from "./types";
+import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
+
+export const ViewIdentityTlsCertAuthContent = ({
+ identityId,
+ handlePopUpToggle,
+ handlePopUpOpen,
+ onDelete,
+ popUp
+}: ViewAuthMethodProps) => {
+ const { data, isPending } = useGetIdentityTlsCertAuth(identityId);
+
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+ );
+ }
+
+ if (popUp.identityAuthMethod.isOpen) {
+ return (
+
+ );
+ }
+
+ return (
+ handlePopUpOpen("identityAuthMethod")}
+ onDelete={onDelete}
+ >
+
+ {data.accessTokenTTL}
+
+
+ {data.accessTokenMaxTTL}
+
+
+ {data.accessTokenNumUsesLimit}
+
+
+ {data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
+
+
+ {data.caCertificate}
}
+ >
+
+
+
+ Reveal
+
+
+
+
+
+ {data.allowedCommonNames
+ ?.split(",")
+ .map((cn) => cn.trim())
+ .join(", ")}
+
+
+ );
+};