diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 917caa95a08..0a67636cfde 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -98,6 +98,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 { TIdentitySpiffeAuthServiceFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-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";
@@ -287,6 +288,7 @@ declare module "fastify" {
identityOciAuth: TIdentityOciAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory;
+ identitySpiffeAuth: TIdentitySpiffeAuthServiceFactory;
identityLdapAuth: TIdentityLdapAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 7fa629333a8..4b08f28d84f 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -641,6 +641,11 @@ import {
TIdentityLdapAuthsInsert,
TIdentityLdapAuthsUpdate
} from "@app/db/schemas/identity-ldap-auths";
+import {
+ TIdentitySpiffeAuths,
+ TIdentitySpiffeAuthsInsert,
+ TIdentitySpiffeAuthsUpdate
+} from "@app/db/schemas/identity-spiffe-auths";
import {
TMicrosoftTeamsIntegrations,
TMicrosoftTeamsIntegrationsInsert,
@@ -1070,6 +1075,11 @@ declare module "knex/types/tables" {
TIdentityLdapAuthsInsert,
TIdentityLdapAuthsUpdate
>;
+ [TableName.IdentitySpiffeAuth]: KnexOriginal.CompositeTableType<
+ TIdentitySpiffeAuths,
+ TIdentitySpiffeAuthsInsert,
+ TIdentitySpiffeAuthsUpdate
+ >;
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
diff --git a/backend/src/db/migrations/20260305201413_add-spiffe-machine-auth.ts b/backend/src/db/migrations/20260305201413_add-spiffe-machine-auth.ts
new file mode 100644
index 00000000000..8029d6c26e8
--- /dev/null
+++ b/backend/src/db/migrations/20260305201413_add-spiffe-machine-auth.ts
@@ -0,0 +1,37 @@
+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.IdentitySpiffeAuth))) {
+ await knex.schema.createTable(TableName.IdentitySpiffeAuth, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.uuid("identityId").notNullable().unique();
+ t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
+ t.string("trustDomain").notNullable();
+ t.string("allowedSpiffeIds").notNullable();
+ t.string("allowedAudiences").notNullable();
+ t.string("configurationType").notNullable();
+ t.binary("encryptedCaBundleJwks").nullable();
+ t.string("bundleEndpointUrl").nullable();
+ t.string("bundleEndpointProfile").nullable();
+ t.binary("encryptedBundleEndpointCaCert").nullable();
+ t.binary("encryptedCachedBundleJwks").nullable();
+ t.datetime("cachedBundleLastRefreshedAt").nullable();
+ t.integer("bundleRefreshHintSeconds").defaultTo(300).notNullable();
+ 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);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.IdentitySpiffeAuth);
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.IdentitySpiffeAuth);
+ await dropOnUpdateTrigger(knex, TableName.IdentitySpiffeAuth);
+}
diff --git a/backend/src/db/schemas/identity-spiffe-auths.ts b/backend/src/db/schemas/identity-spiffe-auths.ts
new file mode 100644
index 00000000000..5073ea7f2e2
--- /dev/null
+++ b/backend/src/db/schemas/identity-spiffe-auths.ts
@@ -0,0 +1,36 @@
+// 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 IdentitySpiffeAuthsSchema = z.object({
+ id: z.string().uuid(),
+ identityId: z.string().uuid(),
+ trustDomain: z.string(),
+ allowedSpiffeIds: z.string(),
+ allowedAudiences: z.string(),
+ configurationType: z.string(),
+ encryptedCaBundleJwks: zodBuffer.nullable().optional(),
+ bundleEndpointUrl: z.string().nullable().optional(),
+ bundleEndpointProfile: z.string().nullable().optional(),
+ encryptedBundleEndpointCaCert: zodBuffer.nullable().optional(),
+ encryptedCachedBundleJwks: zodBuffer.nullable().optional(),
+ cachedBundleLastRefreshedAt: z.date().nullable().optional(),
+ bundleRefreshHintSeconds: z.coerce.number().default(300),
+ 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()
+});
+
+export type TIdentitySpiffeAuths = z.infer;
+export type TIdentitySpiffeAuthsInsert = Omit, TImmutableDBKeys>;
+export type TIdentitySpiffeAuthsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts
index 73e0727ce94..39b3b203a9e 100644
--- a/backend/src/db/schemas/index.ts
+++ b/backend/src/db/schemas/index.ts
@@ -72,6 +72,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-spiffe-auths";
export * from "./identity-tls-cert-auths";
export * from "./identity-token-auths";
export * from "./identity-ua-client-secrets";
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index a927eb79e6e..2cacd9447c5 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -103,6 +103,7 @@ export enum TableName {
IdentityJwtAuth = "identity_jwt_auths",
IdentityLdapAuth = "identity_ldap_auths",
IdentityTlsCertAuth = "identity_tls_cert_auths",
+ IdentitySpiffeAuth = "identity_spiffe_auths",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
@@ -343,7 +344,8 @@ export enum IdentityAuthMethod {
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth",
- LDAP_AUTH = "ldap-auth"
+ LDAP_AUTH = "ldap-auth",
+ SPIFFE_AUTH = "spiffe-auth"
}
export enum ProjectType {
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 0cf50028cd8..347efc02588 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -216,6 +216,13 @@ export enum EventType {
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth",
+ LOGIN_IDENTITY_SPIFFE_AUTH = "login-identity-spiffe-auth",
+ ADD_IDENTITY_SPIFFE_AUTH = "add-identity-spiffe-auth",
+ UPDATE_IDENTITY_SPIFFE_AUTH = "update-identity-spiffe-auth",
+ GET_IDENTITY_SPIFFE_AUTH = "get-identity-spiffe-auth",
+ REVOKE_IDENTITY_SPIFFE_AUTH = "revoke-identity-spiffe-auth",
+ REFRESH_IDENTITY_SPIFFE_AUTH_BUNDLE = "refresh-identity-spiffe-auth-bundle",
+
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
@@ -1840,6 +1847,66 @@ interface GetIdentityJwtAuthEvent {
};
}
+interface LoginIdentitySpiffeAuthEvent {
+ type: EventType.LOGIN_IDENTITY_SPIFFE_AUTH;
+ metadata: {
+ identityId: string;
+ identitySpiffeAuthId: string;
+ identityAccessTokenId: string;
+ };
+}
+
+interface AddIdentitySpiffeAuthEvent {
+ type: EventType.ADD_IDENTITY_SPIFFE_AUTH;
+ metadata: {
+ identityId: string;
+ trustDomain: string;
+ allowedSpiffeIds: string;
+ allowedAudiences: string;
+ configurationType: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: Array;
+ };
+}
+
+interface UpdateIdentitySpiffeAuthEvent {
+ type: EventType.UPDATE_IDENTITY_SPIFFE_AUTH;
+ metadata: {
+ identityId: string;
+ trustDomain?: string;
+ allowedSpiffeIds?: string;
+ allowedAudiences?: string;
+ configurationType?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: Array;
+ };
+}
+
+interface DeleteIdentitySpiffeAuthEvent {
+ type: EventType.REVOKE_IDENTITY_SPIFFE_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface GetIdentitySpiffeAuthEvent {
+ type: EventType.GET_IDENTITY_SPIFFE_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface RefreshIdentitySpiffeAuthBundleEvent {
+ type: EventType.REFRESH_IDENTITY_SPIFFE_AUTH_BUNDLE;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
@@ -5171,6 +5238,12 @@ export type Event =
| UpdateIdentityJwtAuthEvent
| GetIdentityJwtAuthEvent
| DeleteIdentityJwtAuthEvent
+ | LoginIdentitySpiffeAuthEvent
+ | AddIdentitySpiffeAuthEvent
+ | UpdateIdentitySpiffeAuthEvent
+ | GetIdentitySpiffeAuthEvent
+ | RefreshIdentitySpiffeAuthBundleEvent
+ | DeleteIdentitySpiffeAuthEvent
| LoginIdentityLdapAuthEvent
| AddIdentityLdapAuthEvent
| UpdateIdentityLdapAuthEvent
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index a022ee23f09..902073d5314 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -32,6 +32,7 @@ export enum ApiDocsTags {
AzureAuth = "Azure Auth",
KubernetesAuth = "Kubernetes Auth",
JwtAuth = "JWT Auth",
+ SpiffeAuth = "SPIFFE Auth",
OidcAuth = "OIDC Auth",
LdapAuth = "LDAP Auth",
Groups = "Groups",
@@ -745,6 +746,61 @@ export const JWT_AUTH = {
}
} as const;
+export const SPIFFE_AUTH = {
+ LOGIN: {
+ identityId: "The ID of the machine identity to login.",
+ jwt: "The JWT-SVID token to authenticate with.",
+ organizationSlug: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
+ },
+ ATTACH: {
+ identityId: "The ID of the machine identity to attach the configuration onto.",
+ trustDomain: "The SPIFFE trust domain (e.g. prod.example.com).",
+ allowedSpiffeIds:
+ "Comma-separated list of allowed SPIFFE ID patterns. Supports picomatch glob patterns (e.g. spiffe://prod.example.com/**).",
+ allowedAudiences: "Comma-separated list of allowed audiences for JWT-SVID validation.",
+ configurationType:
+ "The configuration type for trust bundle management. Must be one of: 'static' (admin uploads JWKS), 'remote' (auto-refresh from SPIRE bundle endpoint).",
+ caBundleJwks:
+ "The JWKS JSON containing public keys for JWT-SVID verification. Required if configurationType is 'static'.",
+ bundleEndpointUrl:
+ "The SPIRE bundle endpoint URL for automatic trust bundle retrieval. Required if configurationType is 'remote'.",
+ bundleEndpointProfile:
+ "The bundle endpoint authentication profile. Must be one of: 'https_web' (standard HTTPS), 'https_spiffe' (mTLS with SPIFFE auth).",
+ bundleEndpointCaCert:
+ "The PEM-encoded CA certificate for verifying the bundle endpoint TLS connection. Required when bundleEndpointProfile is 'https_spiffe'.",
+ bundleRefreshHintSeconds: "The interval in seconds between bundle refresh attempts. Defaults to 300.",
+ accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
+ 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."
+ },
+ UPDATE: {
+ identityId: "The ID of the machine identity to update the auth method for.",
+ trustDomain: "The new SPIFFE trust domain.",
+ allowedSpiffeIds: "The new comma-separated list of allowed SPIFFE ID patterns.",
+ allowedAudiences: "The new comma-separated list of allowed audiences.",
+ configurationType: "The new configuration type for trust bundle management.",
+ caBundleJwks: "The new JWKS JSON containing public keys.",
+ bundleEndpointUrl: "The new SPIRE bundle endpoint URL.",
+ bundleEndpointProfile: "The new bundle endpoint authentication profile.",
+ bundleEndpointCaCert: "The new PEM-encoded CA certificate for the bundle endpoint.",
+ bundleRefreshHintSeconds: "The new interval in seconds between bundle refresh attempts.",
+ accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
+ 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."
+ },
+ RETRIEVE: {
+ identityId: "The ID of the machine identity to retrieve the auth method for."
+ },
+ REVOKE: {
+ identityId: "The ID of the machine identity to revoke the auth method for."
+ },
+ REFRESH: {
+ identityId: "The ID of the machine identity to force-refresh the cached SPIFFE trust bundle for."
+ }
+} as const;
+
export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from."
diff --git a/backend/src/lib/telemetry/metrics.ts b/backend/src/lib/telemetry/metrics.ts
index 0a4efff9aba..2f8516fa8f0 100644
--- a/backend/src/lib/telemetry/metrics.ts
+++ b/backend/src/lib/telemetry/metrics.ts
@@ -23,7 +23,8 @@ export enum AuthAttemptAuthMethod {
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth",
- LDAP_AUTH = "ldap-auth"
+ LDAP_AUTH = "ldap-auth",
+ SPIFFE_AUTH = "spiffe-auth"
}
export enum AuthAttemptAuthResult {
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index ede9548abad..be81ed4db8f 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -287,6 +287,8 @@ import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/ide
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
+import { identitySpiffeAuthDALFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-dal";
+import { identitySpiffeAuthServiceFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-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";
@@ -527,6 +529,7 @@ export const registerRoutes = async (
const identityOciAuthDAL = identityOciAuthDALFactory(db);
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
+ const identitySpiffeAuthDAL = identitySpiffeAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
const identityLdapAuthDAL = identityLdapAuthDALFactory(db);
@@ -1950,6 +1953,17 @@ export const registerRoutes = async (
membershipIdentityDAL
});
+ const identitySpiffeAuthService = identitySpiffeAuthServiceFactory({
+ identityDAL,
+ identitySpiffeAuthDAL,
+ orgDAL,
+ permissionService,
+ identityAccessTokenDAL,
+ licenseService,
+ kmsService,
+ membershipIdentityDAL
+ });
+
const identityLdapAuthService = identityLdapAuthServiceFactory({
identityLdapAuthDAL,
orgDAL,
@@ -2832,6 +2846,7 @@ export const registerRoutes = async (
identityTlsCertAuth: identityTlsCertAuthService,
identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService,
+ identitySpiffeAuth: identitySpiffeAuthService,
identityLdapAuth: identityLdapAuthService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
diff --git a/backend/src/server/routes/v1/identity-spiffe-auth-router.ts b/backend/src/server/routes/v1/identity-spiffe-auth-router.ts
new file mode 100644
index 00000000000..2c105f10410
--- /dev/null
+++ b/backend/src/server/routes/v1/identity-spiffe-auth-router.ts
@@ -0,0 +1,430 @@
+import { z } from "zod";
+
+import { IdentitySpiffeAuthsSchema } from "@app/db/schemas";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { ApiDocsTags, SPIFFE_AUTH } from "@app/lib/api-docs";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { slugSchema } from "@app/server/lib/schemas";
+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 {
+ SpiffeBundleEndpointProfile,
+ SpiffeConfigurationType
+} from "@app/services/identity-spiffe-auth/identity-spiffe-auth-types";
+import {
+ validateSpiffeAllowedAudiencesField,
+ validateSpiffeAllowedIdsField,
+ validateTrustDomain
+} from "@app/services/identity-spiffe-auth/identity-spiffe-auth-validators";
+import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
+
+const IdentitySpiffeAuthResponseSchema = IdentitySpiffeAuthsSchema.omit({
+ encryptedCaBundleJwks: true,
+ encryptedBundleEndpointCaCert: true,
+ encryptedCachedBundleJwks: true
+}).extend({
+ caBundleJwks: z.string(),
+ bundleEndpointCaCert: z.string()
+});
+
+const CommonCreateFields = z.object({
+ trustDomain: validateTrustDomain.describe(SPIFFE_AUTH.ATTACH.trustDomain),
+ allowedSpiffeIds: validateSpiffeAllowedIdsField.describe(SPIFFE_AUTH.ATTACH.allowedSpiffeIds),
+ allowedAudiences: validateSpiffeAllowedAudiencesField.describe(SPIFFE_AUTH.ATTACH.allowedAudiences),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
+ .describe(SPIFFE_AUTH.ATTACH.accessTokenTrustedIps),
+ accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(SPIFFE_AUTH.ATTACH.accessTokenTTL),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .min(0)
+ .max(315360000)
+ .default(2592000)
+ .describe(SPIFFE_AUTH.ATTACH.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(SPIFFE_AUTH.ATTACH.accessTokenNumUsesLimit)
+});
+
+const CommonUpdateFields = z
+ .object({
+ trustDomain: validateTrustDomain.describe(SPIFFE_AUTH.UPDATE.trustDomain),
+ allowedSpiffeIds: validateSpiffeAllowedIdsField.describe(SPIFFE_AUTH.UPDATE.allowedSpiffeIds),
+ allowedAudiences: validateSpiffeAllowedAudiencesField.describe(SPIFFE_AUTH.UPDATE.allowedAudiences),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .describe(SPIFFE_AUTH.UPDATE.accessTokenTrustedIps),
+ accessTokenTTL: z.number().int().min(0).max(315360000).describe(SPIFFE_AUTH.UPDATE.accessTokenTTL),
+ accessTokenMaxTTL: z.number().int().min(0).max(315360000).describe(SPIFFE_AUTH.UPDATE.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: z.number().int().min(0).describe(SPIFFE_AUTH.UPDATE.accessTokenNumUsesLimit)
+ })
+ .partial();
+
+const StaticConfigurationSchema = z.object({
+ configurationType: z.literal(SpiffeConfigurationType.STATIC).describe(SPIFFE_AUTH.ATTACH.configurationType),
+ caBundleJwks: z.string().min(1).describe(SPIFFE_AUTH.ATTACH.caBundleJwks),
+ bundleEndpointUrl: z.string().optional().default(""),
+ bundleEndpointProfile: z.nativeEnum(SpiffeBundleEndpointProfile).optional(),
+ bundleEndpointCaCert: z.string().optional().default(""),
+ bundleRefreshHintSeconds: z.number().int().min(0).optional().default(3600)
+});
+
+const RemoteConfigurationSchema = z.object({
+ configurationType: z.literal(SpiffeConfigurationType.REMOTE).describe(SPIFFE_AUTH.ATTACH.configurationType),
+ caBundleJwks: z.string().optional().default(""),
+ bundleEndpointUrl: z
+ .string()
+ .trim()
+ .url()
+ .refine((url) => url.startsWith("https://"), "Bundle endpoint URL must use HTTPS")
+ .describe(SPIFFE_AUTH.ATTACH.bundleEndpointUrl),
+ bundleEndpointProfile: z
+ .nativeEnum(SpiffeBundleEndpointProfile)
+ .default(SpiffeBundleEndpointProfile.HTTPS_WEB)
+ .describe(SPIFFE_AUTH.ATTACH.bundleEndpointProfile),
+ bundleEndpointCaCert: z.string().optional().default("").describe(SPIFFE_AUTH.ATTACH.bundleEndpointCaCert),
+ bundleRefreshHintSeconds: z.number().int().min(0).default(3600).describe(SPIFFE_AUTH.ATTACH.bundleRefreshHintSeconds)
+});
+
+export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/spiffe-auth/login",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ hide: false,
+ operationId: "loginWithSpiffeAuth",
+ tags: [ApiDocsTags.SpiffeAuth],
+ description: "Login with SPIFFE Auth (JWT-SVID) for machine identity",
+ body: z.object({
+ identityId: z.string().trim().describe(SPIFFE_AUTH.LOGIN.identityId),
+ jwt: z.string().trim().describe(SPIFFE_AUTH.LOGIN.jwt),
+ organizationSlug: slugSchema().optional().describe(SPIFFE_AUTH.LOGIN.organizationSlug)
+ }),
+ response: {
+ 200: z.object({
+ accessToken: z.string(),
+ expiresIn: z.coerce.number(),
+ accessTokenMaxTTL: z.coerce.number(),
+ tokenType: z.literal("Bearer")
+ })
+ }
+ },
+ handler: async (req) => {
+ const { identitySpiffeAuth, accessToken, identityAccessToken, identity } =
+ await server.services.identitySpiffeAuth.login(req.body);
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identity.orgId,
+ event: {
+ type: EventType.LOGIN_IDENTITY_SPIFFE_AUTH,
+ metadata: {
+ identityId: identitySpiffeAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ identitySpiffeAuthId: identitySpiffeAuth.id
+ }
+ }
+ });
+ return {
+ accessToken,
+ tokenType: "Bearer" as const,
+ expiresIn: identitySpiffeAuth.accessTokenTTL,
+ accessTokenMaxTTL: identitySpiffeAuth.accessTokenMaxTTL
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/spiffe-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ operationId: "attachSpiffeAuth",
+ tags: [ApiDocsTags.SpiffeAuth],
+ description: "Attach SPIFFE Auth configuration onto machine identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(SPIFFE_AUTH.ATTACH.identityId)
+ }),
+ body: z.discriminatedUnion("configurationType", [
+ StaticConfigurationSchema.merge(CommonCreateFields),
+ RemoteConfigurationSchema.merge(CommonCreateFields)
+ ]),
+ response: {
+ 200: z.object({
+ identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identitySpiffeAuth = await server.services.identitySpiffeAuth.attachSpiffeAuth({
+ 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: identitySpiffeAuth.orgId,
+ event: {
+ type: EventType.ADD_IDENTITY_SPIFFE_AUTH,
+ metadata: {
+ identityId: identitySpiffeAuth.identityId,
+ trustDomain: identitySpiffeAuth.trustDomain,
+ allowedSpiffeIds: identitySpiffeAuth.allowedSpiffeIds,
+ allowedAudiences: identitySpiffeAuth.allowedAudiences,
+ configurationType: identitySpiffeAuth.configurationType,
+ accessTokenTTL: identitySpiffeAuth.accessTokenTTL,
+ accessTokenMaxTTL: identitySpiffeAuth.accessTokenMaxTTL,
+ accessTokenTrustedIps: identitySpiffeAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ accessTokenNumUsesLimit: identitySpiffeAuth.accessTokenNumUsesLimit
+ }
+ }
+ });
+
+ return {
+ identitySpiffeAuth
+ };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/spiffe-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ operationId: "updateSpiffeAuth",
+ tags: [ApiDocsTags.SpiffeAuth],
+ description: "Update SPIFFE Auth configuration on machine identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(SPIFFE_AUTH.UPDATE.identityId)
+ }),
+ body: z.discriminatedUnion("configurationType", [
+ StaticConfigurationSchema.merge(CommonUpdateFields),
+ RemoteConfigurationSchema.merge(CommonUpdateFields)
+ ]),
+ response: {
+ 200: z.object({
+ identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identitySpiffeAuth = await server.services.identitySpiffeAuth.updateSpiffeAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ identityId: req.params.identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identitySpiffeAuth.orgId,
+ event: {
+ type: EventType.UPDATE_IDENTITY_SPIFFE_AUTH,
+ metadata: {
+ identityId: identitySpiffeAuth.identityId,
+ trustDomain: identitySpiffeAuth.trustDomain,
+ allowedSpiffeIds: identitySpiffeAuth.allowedSpiffeIds,
+ allowedAudiences: identitySpiffeAuth.allowedAudiences,
+ configurationType: identitySpiffeAuth.configurationType,
+ accessTokenTTL: identitySpiffeAuth.accessTokenTTL,
+ accessTokenMaxTTL: identitySpiffeAuth.accessTokenMaxTTL,
+ accessTokenTrustedIps: identitySpiffeAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ accessTokenNumUsesLimit: identitySpiffeAuth.accessTokenNumUsesLimit
+ }
+ }
+ });
+
+ return { identitySpiffeAuth };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/spiffe-auth/identities/:identityId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ operationId: "getSpiffeAuth",
+ tags: [ApiDocsTags.SpiffeAuth],
+ description: "Retrieve SPIFFE Auth configuration on machine identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(SPIFFE_AUTH.RETRIEVE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identitySpiffeAuth = await server.services.identitySpiffeAuth.getSpiffeAuth({
+ 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: identitySpiffeAuth.orgId,
+ event: {
+ type: EventType.GET_IDENTITY_SPIFFE_AUTH,
+ metadata: {
+ identityId: identitySpiffeAuth.identityId
+ }
+ }
+ });
+
+ return { identitySpiffeAuth };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/spiffe-auth/identities/:identityId/refresh-bundle",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ operationId: "refreshSpiffeBundle",
+ tags: [ApiDocsTags.SpiffeAuth],
+ description: "Force-refresh the cached SPIFFE trust bundle for a remote-configured machine identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim().describe(SPIFFE_AUTH.REFRESH.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identitySpiffeAuth = await server.services.identitySpiffeAuth.refreshSpiffeBundle({
+ 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: identitySpiffeAuth.orgId,
+ event: {
+ type: EventType.REFRESH_IDENTITY_SPIFFE_AUTH_BUNDLE,
+ metadata: {
+ identityId: identitySpiffeAuth.identityId
+ }
+ }
+ });
+
+ return { identitySpiffeAuth };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/spiffe-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ hide: false,
+ operationId: "deleteSpiffeAuth",
+ tags: [ApiDocsTags.SpiffeAuth],
+ description: "Delete SPIFFE Auth configuration on machine identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(SPIFFE_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identitySpiffeAuth: IdentitySpiffeAuthResponseSchema.omit({
+ caBundleJwks: true,
+ bundleEndpointCaCert: true
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identitySpiffeAuth = await server.services.identitySpiffeAuth.revokeSpiffeAuth({
+ 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: identitySpiffeAuth.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_SPIFFE_AUTH,
+ metadata: {
+ identityId: identitySpiffeAuth.identityId
+ }
+ }
+ });
+
+ return { identitySpiffeAuth };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index 3d57df4518a..6b69c2c7ea5 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -45,6 +45,7 @@ import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityOrgMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectMembershipRouter } from "./identity-project-membership-router";
import { registerIdentityRouter } from "./identity-router";
+import { registerIdentitySpiffeAuthRouter } from "./identity-spiffe-auth-router";
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
@@ -98,6 +99,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityOciAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter);
+ await authRouter.register(registerIdentitySpiffeAuthRouter);
await authRouter.register(registerIdentityLdapAuthRouter);
},
{ prefix: "/auth" }
diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts
index c264717d0f8..936f7c8c8e5 100644
--- a/backend/src/services/identity-project/identity-project-dal.ts
+++ b/backend/src/services/identity-project/identity-project-dal.ts
@@ -9,12 +9,16 @@ import {
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
+ TIdentityJwtAuths,
TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths,
+ TIdentitySpiffeAuths,
+ TIdentityTlsCertAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths
} from "@app/db/schemas";
+import { TIdentityLdapAuths } from "@app/db/schemas/identity-ldap-auths";
import { DatabaseError } from "@app/lib/errors";
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
@@ -81,6 +85,26 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.Membership}.actorIdentityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentityJwtAuth,
+ `${TableName.Membership}.actorIdentityId`,
+ `${TableName.IdentityJwtAuth}.identityId`
+ )
+ .leftJoin(
+ TableName.IdentityLdapAuth,
+ `${TableName.Membership}.actorIdentityId`,
+ `${TableName.IdentityLdapAuth}.identityId`
+ )
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ `${TableName.Membership}.actorIdentityId`,
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
+ .leftJoin(
+ TableName.IdentitySpiffeAuth,
+ `${TableName.Membership}.actorIdentityId`,
+ `${TableName.IdentitySpiffeAuth}.identityId`
+ )
.select(
db.ref("id").withSchema(TableName.Membership),
@@ -112,7 +136,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
- db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
+ 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("id").as("spiffeId").withSchema(TableName.IdentitySpiffeAuth)
);
const members = sqlNestRelationships({
@@ -127,6 +155,10 @@ export const identityProjectDALFactory = (db: TDbClient) => {
oidcId,
azureId,
tokenId,
+ jwtId,
+ ldapId,
+ tlsCertId,
+ spiffeId,
id,
createdAt,
updatedAt,
@@ -149,7 +181,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
kubernetesId,
oidcId,
azureId,
- tokenId
+ tokenId,
+ jwtId,
+ ldapId,
+ tlsCertId,
+ spiffeId
})
},
project: {
@@ -285,6 +321,26 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`,
`${TableName.IdentityTokenAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentityJwtAuth,
+ `${TableName.Identity}.id`,
+ `${TableName.IdentityJwtAuth}.identityId`
+ )
+ .leftJoin(
+ TableName.IdentityLdapAuth,
+ `${TableName.Identity}.id`,
+ `${TableName.IdentityLdapAuth}.identityId`
+ )
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ `${TableName.Identity}.id`,
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
+ .leftJoin(
+ TableName.IdentitySpiffeAuth,
+ `${TableName.Identity}.id`,
+ `${TableName.IdentitySpiffeAuth}.identityId`
+ )
.select(
db.ref("id").withSchema(TableName.Membership),
@@ -317,7 +373,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
- db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
+ 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("id").as("spiffeId").withSchema(TableName.IdentitySpiffeAuth)
);
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
@@ -349,6 +409,10 @@ export const identityProjectDALFactory = (db: TDbClient) => {
oidcId,
azureId,
tokenId,
+ jwtId,
+ ldapId,
+ tlsCertId,
+ spiffeId,
id,
createdAt,
updatedAt,
@@ -374,7 +438,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
ociId,
oidcId,
azureId,
- tokenId
+ tokenId,
+ jwtId,
+ ldapId,
+ tlsCertId,
+ spiffeId
})
},
// TODO: scott - not sure why these aren't properly typed?
diff --git a/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-dal.ts b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-dal.ts
new file mode 100644
index 00000000000..4d7523469a6
--- /dev/null
+++ b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-dal.ts
@@ -0,0 +1,11 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TIdentitySpiffeAuthDALFactory = ReturnType;
+
+export const identitySpiffeAuthDALFactory = (db: TDbClient) => {
+ const spiffeAuthOrm = ormify(db, TableName.IdentitySpiffeAuth);
+
+ return { ...spiffeAuthOrm };
+};
diff --git a/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-fns.ts b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-fns.ts
new file mode 100644
index 00000000000..022df6d117e
--- /dev/null
+++ b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-fns.ts
@@ -0,0 +1,96 @@
+import https from "https";
+import picomatch from "picomatch";
+import RE2 from "re2";
+
+const SPIFFE_ID_REGEX = new RE2("^spiffe:\\/\\/([^/]+)(\\/.*)?");
+
+export const isValidSpiffeId = (value: string): boolean => {
+ return SPIFFE_ID_REGEX.test(value);
+};
+
+export const extractTrustDomainFromSpiffeId = (spiffeId: string): string => {
+ const match = SPIFFE_ID_REGEX.exec(spiffeId);
+ if (!match) {
+ throw new Error(`Invalid SPIFFE ID: ${spiffeId}`);
+ }
+ return match[1];
+};
+
+export const doesSpiffeIdMatchPattern = (spiffeId: string, patterns: string): boolean => {
+ const patternList = patterns
+ .split(",")
+ .map((p) => p.trim())
+ .filter(Boolean);
+
+ return patternList.some((pattern) => picomatch.isMatch(spiffeId, pattern));
+};
+
+export const findSigningKeyInJwks = (jwksJson: string, kid: string) => {
+ const jwks = JSON.parse(jwksJson) as {
+ keys: Array<{ kid?: string; use?: string; kty: string; [key: string]: unknown }>;
+ };
+
+ if (!jwks.keys || !Array.isArray(jwks.keys)) {
+ throw new Error("Invalid JWKS: missing keys array");
+ }
+
+ const matchingKey = jwks.keys.find((key) => key.kid === kid);
+
+ if (!matchingKey) {
+ throw new Error(`No key found in JWKS matching kid: ${kid}`);
+ }
+
+ return matchingKey;
+};
+
+const BUNDLE_FETCH_TIMEOUT_MS = 10_000;
+const MAX_BUNDLE_SIZE_BYTES = 1_048_576; // 1 MB
+
+export const fetchRemoteBundleJwks = (url: string, caCert?: string): Promise => {
+ const parsedUrl = new URL(url);
+
+ return new Promise((resolve, reject) => {
+ let totalBytes = 0;
+ const chunks: Buffer[] = [];
+
+ const req = https.request(
+ {
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port || 443,
+ path: parsedUrl.pathname + parsedUrl.search,
+ method: "GET",
+ agent: caCert ? new https.Agent({ ca: caCert, rejectUnauthorized: true }) : undefined,
+ timeout: BUNDLE_FETCH_TIMEOUT_MS
+ },
+ (res) => {
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
+ req.destroy(new Error(`Failed to fetch SPIFFE trust bundle: HTTP ${res.statusCode}`));
+ return;
+ }
+
+ const contentLengthHeader = res.headers["content-length"];
+ if (contentLengthHeader && parseInt(contentLengthHeader, 10) > MAX_BUNDLE_SIZE_BYTES) {
+ req.destroy(new Error("SPIFFE trust bundle response exceeds maximum allowed size"));
+ return;
+ }
+
+ res.on("data", (chunk: Buffer) => {
+ totalBytes += chunk.length;
+ if (totalBytes > MAX_BUNDLE_SIZE_BYTES) {
+ req.destroy(new Error("SPIFFE trust bundle response exceeds maximum allowed size"));
+ return;
+ }
+ chunks.push(chunk);
+ });
+
+ res.on("end", () => {
+ resolve(Buffer.concat(chunks).toString());
+ });
+ }
+ );
+
+ req.on("timeout", () => req.destroy(new Error("SPIFFE trust bundle fetch timed out")));
+ req.on("error", reject);
+ req.end();
+ });
+};
diff --git a/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-service.ts b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-service.ts
new file mode 100644
index 00000000000..9ab43767fa8
--- /dev/null
+++ b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-service.ts
@@ -0,0 +1,905 @@
+import { ForbiddenError, subject } from "@casl/ability";
+import { requestContext } from "@fastify/request-context";
+import { decodeProtectedHeader, errors as joseErrors, importJWK, jwtVerify } from "jose";
+
+import {
+ AccessScope,
+ ActionProjectType,
+ IdentityAuthMethod,
+ OrganizationActionScope,
+ TIdentitySpiffeAuthsUpdate
+} 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 { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { getConfig } from "@app/lib/config/env";
+import { crypto } from "@app/lib/crypto";
+import {
+ BadRequestError,
+ ForbiddenRequestError,
+ NotFoundError,
+ PermissionBoundaryError,
+ UnauthorizedError
+} from "@app/lib/errors";
+import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
+import { logger } from "@app/lib/logger";
+import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
+import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
+
+import { ActorType, AuthTokenType } from "../auth/auth-type";
+import { TIdentityDALFactory } from "../identity/identity-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 { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
+import { TOrgDALFactory } from "../org/org-dal";
+import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
+import { TIdentitySpiffeAuthDALFactory } from "./identity-spiffe-auth-dal";
+import {
+ doesSpiffeIdMatchPattern,
+ extractTrustDomainFromSpiffeId,
+ fetchRemoteBundleJwks,
+ findSigningKeyInJwks,
+ isValidSpiffeId
+} from "./identity-spiffe-auth-fns";
+import {
+ SpiffeConfigurationType,
+ TAttachSpiffeAuthDTO,
+ TGetSpiffeAuthDTO,
+ TLoginSpiffeAuthDTO,
+ TRevokeSpiffeAuthDTO,
+ TUpdateSpiffeAuthDTO
+} from "./identity-spiffe-auth-types";
+
+type TIdentitySpiffeAuthServiceFactoryDep = {
+ identityDAL: Pick;
+ identitySpiffeAuthDAL: TIdentitySpiffeAuthDALFactory;
+ membershipIdentityDAL: Pick;
+ identityAccessTokenDAL: Pick;
+ permissionService: Pick;
+ licenseService: Pick;
+ kmsService: Pick;
+ orgDAL: Pick;
+};
+
+export type TIdentitySpiffeAuthServiceFactory = ReturnType;
+
+const verifyJwtSvid = async (jwtValue: string, jwksJson: string) => {
+ const header = decodeProtectedHeader(jwtValue);
+ if (!header.kid) {
+ throw new UnauthorizedError({ message: "JWT missing kid header" });
+ }
+
+ let jwk;
+ try {
+ jwk = findSigningKeyInJwks(jwksJson, header.kid);
+ } catch {
+ throw new UnauthorizedError({ message: `No key found in JWKS matching kid: ${header.kid}` });
+ }
+ const key = await importJWK(jwk, header.alg);
+
+ try {
+ const { payload } = await jwtVerify(jwtValue, key);
+ return { tokenData: payload as Record, kid: header.kid };
+ } catch (error) {
+ if (error instanceof joseErrors.JWTExpired) {
+ throw new UnauthorizedError({ message: "JWT-SVID has expired" });
+ }
+ if (error instanceof joseErrors.JWSSignatureVerificationFailed) {
+ throw new UnauthorizedError({ message: "JWT-SVID signature verification failed" });
+ }
+ throw new UnauthorizedError({ message: "JWT-SVID verification failed" });
+ }
+};
+
+const validateSpiffeClaims = (
+ tokenData: Record,
+ config: { allowedAudiences: string; trustDomain: string; allowedSpiffeIds: string }
+): boolean => {
+ if (!tokenData.aud) return false;
+
+ const tokenAudiences: string[] = Array.isArray(tokenData.aud)
+ ? (tokenData.aud as string[])
+ : [tokenData.aud as string];
+ const allowedAudiences = config.allowedAudiences
+ .split(", ")
+ .map((a) => a.trim())
+ .filter(Boolean);
+
+ if (!tokenAudiences.some((aud) => allowedAudiences.includes(aud))) return false;
+
+ const tokenSub = tokenData.sub as string;
+ if (!tokenSub || !isValidSpiffeId(tokenSub)) return false;
+
+ const tokenTrustDomain = extractTrustDomainFromSpiffeId(tokenSub);
+ if (tokenTrustDomain !== config.trustDomain) return false;
+
+ if (!doesSpiffeIdMatchPattern(tokenSub, config.allowedSpiffeIds)) return false;
+
+ return true;
+};
+
+export const identitySpiffeAuthServiceFactory = ({
+ identityDAL,
+ identitySpiffeAuthDAL,
+ membershipIdentityDAL,
+ permissionService,
+ licenseService,
+ identityAccessTokenDAL,
+ kmsService,
+ orgDAL
+}: TIdentitySpiffeAuthServiceFactoryDep) => {
+ const resolveJwks = async ({
+ config,
+ orgId,
+ forceRefresh
+ }: {
+ config: {
+ id: string;
+ configurationType: string;
+ encryptedCaBundleJwks?: Buffer | null;
+ encryptedCachedBundleJwks?: Buffer | null;
+ bundleEndpointUrl?: string | null;
+ bundleEndpointProfile?: string | null;
+ encryptedBundleEndpointCaCert?: Buffer | null;
+ bundleRefreshHintSeconds?: number | null;
+ cachedBundleLastRefreshedAt?: Date | string | null;
+ };
+ orgId: string;
+ forceRefresh?: boolean;
+ }): Promise<{ jwksJson: string; fromCache: boolean }> => {
+ const { decryptor: orgDataKeyDecryptor, encryptor: orgDataKeyEncryptor } =
+ await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId
+ });
+
+ if (config.configurationType === SpiffeConfigurationType.STATIC) {
+ if (!config.encryptedCaBundleJwks) {
+ throw new BadRequestError({ message: "Static SPIFFE auth has no CA bundle JWKS configured" });
+ }
+
+ return {
+ jwksJson: orgDataKeyDecryptor({ cipherTextBlob: config.encryptedCaBundleJwks }).toString(),
+ fromCache: false
+ };
+ }
+
+ // Remote configuration: check cache freshness
+ if (!forceRefresh && config.encryptedCachedBundleJwks) {
+ const refreshIntervalMs = (config.bundleRefreshHintSeconds || 3600) * 1000;
+ const lastRefreshed = config.cachedBundleLastRefreshedAt
+ ? new Date(config.cachedBundleLastRefreshedAt).getTime()
+ : 0;
+
+ if (Date.now() - lastRefreshed < refreshIntervalMs) {
+ return {
+ jwksJson: orgDataKeyDecryptor({ cipherTextBlob: config.encryptedCachedBundleJwks }).toString(),
+ fromCache: true
+ };
+ }
+ }
+
+ // Fetch fresh bundle
+ if (!config.bundleEndpointUrl) {
+ throw new BadRequestError({ message: "Remote SPIFFE auth has no bundle endpoint URL configured" });
+ }
+
+ await blockLocalAndPrivateIpAddresses(config.bundleEndpointUrl);
+
+ let caCert: string | undefined;
+ if (config.bundleEndpointProfile === "https_spiffe" && config.encryptedBundleEndpointCaCert) {
+ caCert = orgDataKeyDecryptor({ cipherTextBlob: config.encryptedBundleEndpointCaCert }).toString();
+ }
+
+ let bundleJson: string;
+ try {
+ bundleJson = await fetchRemoteBundleJwks(config.bundleEndpointUrl, caCert);
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : "Unknown error";
+ throw new BadRequestError({ message: `Failed to fetch SPIFFE trust bundle from remote endpoint: ${msg}` });
+ }
+
+ const { cipherTextBlob: encryptedCachedBundleJwks } = orgDataKeyEncryptor({
+ plainText: Buffer.from(bundleJson)
+ });
+
+ await identitySpiffeAuthDAL.updateById(config.id, {
+ encryptedCachedBundleJwks,
+ cachedBundleLastRefreshedAt: new Date()
+ });
+
+ logger.info(`SPIFFE auth: refreshed JWKS bundle for config ${config.id}`);
+
+ return { jwksJson: bundleJson, fromCache: false };
+ };
+
+ const login = async ({ identityId, jwt: jwtValue, organizationSlug }: TLoginSpiffeAuthDTO) => {
+ const appCfg = getConfig();
+ const identitySpiffeAuth = await identitySpiffeAuthDAL.findOne({ identityId });
+ if (!identitySpiffeAuth) {
+ throw new NotFoundError({
+ message: "SPIFFE auth method not found for identity, did you configure SPIFFE auth?"
+ });
+ }
+
+ const identity = await identityDAL.findById(identitySpiffeAuth.identityId);
+ if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
+
+ const org = await orgDAL.findById(identity.orgId);
+ const isSubOrgIdentity = Boolean(org.rootOrgId);
+ let subOrganizationId = isSubOrgIdentity ? org.id : null;
+
+ try {
+ // Resolve JWKS (lazy fetch for remote, decrypt for static)
+ let { jwksJson, fromCache } = await resolveJwks({ config: identitySpiffeAuth, orgId: identity.orgId });
+
+ let tokenData: Record;
+ try {
+ ({ tokenData } = await verifyJwtSvid(jwtValue, jwksJson));
+ } catch (verifyError) {
+ // Kid-miss retry: if we used a cached JWKS and the kid wasn't found, force-refresh once
+ if (fromCache && verifyError instanceof Error && verifyError.message.includes("No key found in JWKS")) {
+ ({ jwksJson, fromCache } = await resolveJwks({
+ config: identitySpiffeAuth,
+ orgId: identity.orgId,
+ forceRefresh: true
+ }));
+ ({ tokenData } = await verifyJwtSvid(jwtValue, jwksJson));
+ } else {
+ throw verifyError;
+ }
+ }
+
+ if (!validateSpiffeClaims(tokenData, identitySpiffeAuth)) {
+ throw new UnauthorizedError({ message: "Access denied" });
+ }
+
+ // Sub-org resolution
+ if (organizationSlug) {
+ if (!isSubOrgIdentity) {
+ const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: organizationSlug });
+ if (!subOrg) {
+ throw new NotFoundError({ message: `Sub organization with slug ${organizationSlug} not found` });
+ }
+
+ const subOrgMembership = await orgDAL.findEffectiveOrgMembership({
+ actorType: ActorType.IDENTITY,
+ actorId: identity.id,
+ orgId: subOrg.id
+ });
+
+ if (!subOrgMembership) {
+ throw new UnauthorizedError({
+ message: `Identity not authorized to access sub organization ${organizationSlug}`
+ });
+ }
+
+ subOrganizationId = subOrg.id;
+ }
+ }
+
+ const identityAccessToken = await identitySpiffeAuthDAL.transaction(async (tx) => {
+ await membershipIdentityDAL.update(
+ identity.projectId
+ ? {
+ scope: AccessScope.Project,
+ scopeOrgId: identity.orgId,
+ scopeProjectId: identity.projectId,
+ actorIdentityId: identity.id
+ }
+ : {
+ scope: AccessScope.Organization,
+ scopeOrgId: identity.orgId,
+ actorIdentityId: identity.id
+ },
+ {
+ lastLoginAuthMethod: IdentityAuthMethod.SPIFFE_AUTH,
+ lastLoginTime: new Date()
+ },
+ tx
+ );
+ const newToken = await identityAccessTokenDAL.create(
+ {
+ identityId: identitySpiffeAuth.identityId,
+ isAccessTokenRevoked: false,
+ accessTokenTTL: identitySpiffeAuth.accessTokenTTL,
+ accessTokenMaxTTL: identitySpiffeAuth.accessTokenMaxTTL,
+ accessTokenNumUses: 0,
+ accessTokenNumUsesLimit: identitySpiffeAuth.accessTokenNumUsesLimit,
+ authMethod: IdentityAuthMethod.SPIFFE_AUTH,
+ subOrganizationId
+ },
+ tx
+ );
+
+ return newToken;
+ });
+
+ let expireyOptions: { expiresIn: number } | undefined;
+ const accessTokenTTL = Number(identityAccessToken.accessTokenTTL);
+ if (accessTokenTTL > 0) {
+ expireyOptions = { expiresIn: accessTokenTTL };
+ }
+
+ const accessToken = crypto.jwt().sign(
+ {
+ identityId: identitySpiffeAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
+ } as TIdentityAccessTokenJwtPayload,
+ appCfg.AUTH_SECRET,
+ expireyOptions
+ );
+
+ if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
+ authAttemptCounter.add(1, {
+ "infisical.identity.id": identitySpiffeAuth.identityId,
+ "infisical.identity.name": identity.name,
+ "infisical.organization.id": org.id,
+ "infisical.organization.name": org.name,
+ "infisical.identity.auth_method": AuthAttemptAuthMethod.SPIFFE_AUTH,
+ "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
+ "client.address": requestContext.get("ip"),
+ "user_agent.original": requestContext.get("userAgent")
+ });
+ }
+
+ return { accessToken, identitySpiffeAuth, identityAccessToken, identity };
+ } catch (error) {
+ if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
+ authAttemptCounter.add(1, {
+ "infisical.identity.id": identitySpiffeAuth.identityId,
+ "infisical.identity.name": identity.name,
+ "infisical.organization.id": org.id,
+ "infisical.organization.name": org.name,
+ "infisical.identity.auth_method": AuthAttemptAuthMethod.SPIFFE_AUTH,
+ "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
+ "client.address": requestContext.get("ip"),
+ "user_agent.original": requestContext.get("userAgent")
+ });
+ }
+ throw error;
+ }
+ };
+
+ const attachSpiffeAuth = async ({
+ identityId,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId,
+ isActorSuperAdmin
+ }: TAttachSpiffeAuthDTO) => {
+ await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
+
+ const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({
+ scopeData: {
+ scope: AccessScope.Organization,
+ orgId: actorOrgId
+ },
+ identityId
+ });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+ if (identityMembershipOrg.identity.orgId !== actorOrgId) {
+ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
+ }
+ if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.SPIFFE_AUTH)) {
+ throw new BadRequestError({
+ message: "Failed to add SPIFFE Auth to already configured identity"
+ });
+ }
+
+ if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
+ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
+ }
+
+ if (identityMembershipOrg.identity.projectId) {
+ const { permission } = await permissionService.getProjectPermission({
+ actionProjectType: ActionProjectType.Any,
+ actor,
+ actorId,
+ projectId: identityMembershipOrg.identity.projectId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionIdentityActions.Create,
+ subject(ProjectPermissionSub.Identity, { identityId })
+ );
+ } else {
+ const { permission } = await permissionService.getOrgPermission({
+ scope: OrganizationActionScope.Any,
+ actor,
+ actorId,
+ orgId: identityMembershipOrg.scopeOrgId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ OrgPermissionIdentityActions.Create,
+ OrgPermissionSubjects.Identity
+ );
+ }
+
+ const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
+ 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);
+ });
+
+ if (bundleEndpointUrl) {
+ await blockLocalAndPrivateIpAddresses(bundleEndpointUrl);
+ }
+
+ const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: actorOrgId
+ });
+
+ const encryptedCaBundleJwks = caBundleJwks
+ ? orgDataKeyEncryptor({ plainText: Buffer.from(caBundleJwks) }).cipherTextBlob
+ : null;
+
+ const encryptedBundleEndpointCaCert = bundleEndpointCaCert
+ ? orgDataKeyEncryptor({ plainText: Buffer.from(bundleEndpointCaCert) }).cipherTextBlob
+ : null;
+
+ const identitySpiffeAuth = await identitySpiffeAuthDAL.transaction(async (tx) => {
+ const doc = await identitySpiffeAuthDAL.create(
+ {
+ identityId: identityMembershipOrg.identity.id,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ encryptedCaBundleJwks,
+ bundleEndpointUrl: bundleEndpointUrl || null,
+ bundleEndpointProfile: bundleEndpointProfile || null,
+ encryptedBundleEndpointCaCert,
+ bundleRefreshHintSeconds: bundleRefreshHintSeconds || 300,
+ accessTokenMaxTTL,
+ accessTokenTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
+ },
+ tx
+ );
+
+ return doc;
+ });
+
+ return {
+ ...identitySpiffeAuth,
+ orgId: identityMembershipOrg.scopeOrgId,
+ caBundleJwks: caBundleJwks || "",
+ bundleEndpointCaCert: bundleEndpointCaCert || ""
+ };
+ };
+
+ const updateSpiffeAuth = async ({
+ identityId,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId
+ }: TUpdateSpiffeAuthDTO) => {
+ const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({
+ scopeData: {
+ scope: AccessScope.Organization,
+ orgId: actorOrgId
+ },
+ identityId
+ });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+ if (identityMembershipOrg.identity.orgId !== actorOrgId) {
+ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
+ }
+
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.SPIFFE_AUTH)) {
+ throw new BadRequestError({ message: "Failed to update SPIFFE Auth" });
+ }
+
+ const identitySpiffeAuth = await identitySpiffeAuthDAL.findOne({ identityId });
+
+ if (
+ (accessTokenMaxTTL || identitySpiffeAuth.accessTokenMaxTTL) > 0 &&
+ (accessTokenTTL || identitySpiffeAuth.accessTokenTTL) >
+ (accessTokenMaxTTL || identitySpiffeAuth.accessTokenMaxTTL)
+ ) {
+ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
+ }
+
+ if (identityMembershipOrg.identity.projectId) {
+ const { permission } = await permissionService.getProjectPermission({
+ actionProjectType: ActionProjectType.Any,
+ actor,
+ actorId,
+ projectId: identityMembershipOrg.identity.projectId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionIdentityActions.Edit,
+ subject(ProjectPermissionSub.Identity, { identityId })
+ );
+ } else {
+ const { permission } = await permissionService.getOrgPermission({
+ scope: OrganizationActionScope.Any,
+ actor,
+ actorId,
+ orgId: identityMembershipOrg.scopeOrgId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+ }
+
+ const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
+ 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);
+ });
+
+ if (bundleEndpointUrl) {
+ await blockLocalAndPrivateIpAddresses(bundleEndpointUrl);
+ }
+
+ const updateQuery: TIdentitySpiffeAuthsUpdate = {
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleRefreshHintSeconds,
+ accessTokenMaxTTL,
+ accessTokenTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps: reformattedAccessTokenTrustedIps
+ ? JSON.stringify(reformattedAccessTokenTrustedIps)
+ : undefined
+ };
+
+ const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
+ await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: actorOrgId
+ });
+
+ if (caBundleJwks !== undefined) {
+ updateQuery.encryptedCaBundleJwks = caBundleJwks
+ ? orgDataKeyEncryptor({ plainText: Buffer.from(caBundleJwks) }).cipherTextBlob
+ : null;
+ }
+
+ if (bundleEndpointCaCert !== undefined) {
+ updateQuery.encryptedBundleEndpointCaCert = bundleEndpointCaCert
+ ? orgDataKeyEncryptor({ plainText: Buffer.from(bundleEndpointCaCert) }).cipherTextBlob
+ : null;
+ }
+
+ const updatedSpiffeAuth = await identitySpiffeAuthDAL.updateById(identitySpiffeAuth.id, updateQuery);
+
+ const decryptedCaBundleJwks = updatedSpiffeAuth.encryptedCaBundleJwks
+ ? orgDataKeyDecryptor({ cipherTextBlob: updatedSpiffeAuth.encryptedCaBundleJwks }).toString()
+ : "";
+ const decryptedBundleEndpointCaCert = updatedSpiffeAuth.encryptedBundleEndpointCaCert
+ ? orgDataKeyDecryptor({ cipherTextBlob: updatedSpiffeAuth.encryptedBundleEndpointCaCert }).toString()
+ : "";
+
+ return {
+ ...updatedSpiffeAuth,
+ orgId: identityMembershipOrg.scopeOrgId,
+ caBundleJwks: decryptedCaBundleJwks,
+ bundleEndpointCaCert: decryptedBundleEndpointCaCert
+ };
+ };
+
+ const getSpiffeAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetSpiffeAuthDTO) => {
+ const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({
+ scopeData: {
+ scope: AccessScope.Organization,
+ orgId: actorOrgId
+ },
+ identityId
+ });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+ if (identityMembershipOrg.identity.orgId !== actorOrgId) {
+ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
+ }
+
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.SPIFFE_AUTH)) {
+ throw new BadRequestError({ message: "The identity does not have SPIFFE Auth attached" });
+ }
+
+ if (identityMembershipOrg.identity.projectId) {
+ const { permission } = await permissionService.getProjectPermission({
+ actionProjectType: ActionProjectType.Any,
+ actor,
+ actorId,
+ projectId: identityMembershipOrg.identity.projectId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionIdentityActions.Read,
+ subject(ProjectPermissionSub.Identity, { identityId })
+ );
+ } else {
+ const { permission } = await permissionService.getOrgPermission({
+ scope: OrganizationActionScope.Any,
+ actor,
+ actorId,
+ orgId: identityMembershipOrg.scopeOrgId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
+ }
+
+ const identitySpiffeAuth = await identitySpiffeAuthDAL.findOne({ identityId });
+
+ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: actorOrgId
+ });
+
+ const decryptedCaBundleJwks = identitySpiffeAuth.encryptedCaBundleJwks
+ ? orgDataKeyDecryptor({ cipherTextBlob: identitySpiffeAuth.encryptedCaBundleJwks }).toString()
+ : "";
+ const decryptedBundleEndpointCaCert = identitySpiffeAuth.encryptedBundleEndpointCaCert
+ ? orgDataKeyDecryptor({ cipherTextBlob: identitySpiffeAuth.encryptedBundleEndpointCaCert }).toString()
+ : "";
+
+ return {
+ ...identitySpiffeAuth,
+ orgId: identityMembershipOrg.scopeOrgId,
+ caBundleJwks: decryptedCaBundleJwks,
+ bundleEndpointCaCert: decryptedBundleEndpointCaCert
+ };
+ };
+
+ const revokeSpiffeAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeSpiffeAuthDTO) => {
+ const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({
+ scopeData: {
+ scope: AccessScope.Organization,
+ orgId: actorOrgId
+ },
+ identityId
+ });
+ if (!identityMembershipOrg) {
+ throw new NotFoundError({ message: "Failed to find identity" });
+ }
+ if (identityMembershipOrg.identity.orgId !== actorOrgId) {
+ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
+ }
+
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.SPIFFE_AUTH)) {
+ throw new BadRequestError({ message: "The identity does not have SPIFFE auth" });
+ }
+
+ if (identityMembershipOrg.identity.projectId) {
+ const { permission } = await permissionService.getProjectPermission({
+ actionProjectType: ActionProjectType.Any,
+ actor,
+ actorId,
+ projectId: identityMembershipOrg.identity.projectId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionIdentityActions.RevokeAuth,
+ subject(ProjectPermissionSub.Identity, { identityId })
+ );
+ } else {
+ const { permission } = await permissionService.getOrgPermission({
+ scope: OrganizationActionScope.Any,
+ actor,
+ actorId,
+ orgId: identityMembershipOrg.scopeOrgId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission({
+ scope: OrganizationActionScope.Any,
+ actor: ActorType.IDENTITY,
+ actorId: identityMembershipOrg.identity.id,
+ orgId: identityMembershipOrg.scopeOrgId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
+ const permissionBoundary = validatePrivilegeChangeOperation(
+ shouldUseNewPrivilegeSystem,
+ OrgPermissionIdentityActions.RevokeAuth,
+ OrgPermissionSubjects.Identity,
+ permission,
+ rolePermission
+ );
+ if (!permissionBoundary.isValid)
+ throw new PermissionBoundaryError({
+ message: constructPermissionErrorMessage(
+ "Failed to revoke SPIFFE auth of identity with more privileged role",
+ shouldUseNewPrivilegeSystem,
+ OrgPermissionIdentityActions.RevokeAuth,
+ OrgPermissionSubjects.Identity
+ ),
+ details: { missingPermissions: permissionBoundary.missingPermissions }
+ });
+ }
+
+ const revokedIdentitySpiffeAuth = await identitySpiffeAuthDAL.transaction(async (tx) => {
+ const deletedSpiffeAuth = await identitySpiffeAuthDAL.delete({ identityId }, tx);
+ await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.SPIFFE_AUTH }, tx);
+
+ return { ...deletedSpiffeAuth?.[0], orgId: identityMembershipOrg.scopeOrgId };
+ });
+
+ return revokedIdentitySpiffeAuth;
+ };
+
+ const refreshSpiffeBundle = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TGetSpiffeAuthDTO) => {
+ const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({
+ scopeData: {
+ scope: AccessScope.Organization,
+ orgId: actorOrgId
+ },
+ identityId
+ });
+ if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
+ if (identityMembershipOrg.identity.orgId !== actorOrgId) {
+ throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
+ }
+
+ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.SPIFFE_AUTH)) {
+ throw new BadRequestError({ message: "The identity does not have SPIFFE Auth attached" });
+ }
+
+ if (identityMembershipOrg.identity.projectId) {
+ const { permission } = await permissionService.getProjectPermission({
+ actionProjectType: ActionProjectType.Any,
+ actor,
+ actorId,
+ projectId: identityMembershipOrg.identity.projectId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionIdentityActions.Edit,
+ subject(ProjectPermissionSub.Identity, { identityId })
+ );
+ } else {
+ const { permission } = await permissionService.getOrgPermission({
+ scope: OrganizationActionScope.Any,
+ actor,
+ actorId,
+ orgId: identityMembershipOrg.scopeOrgId,
+ actorAuthMethod,
+ actorOrgId
+ });
+
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
+ }
+
+ const identitySpiffeAuth = await identitySpiffeAuthDAL.findOne({ identityId });
+
+ if (identitySpiffeAuth.configurationType !== SpiffeConfigurationType.REMOTE) {
+ throw new BadRequestError({
+ message: "Bundle refresh is only applicable to identities with remote SPIFFE Auth configuration"
+ });
+ }
+
+ await resolveJwks({ config: identitySpiffeAuth, orgId: identityMembershipOrg.scopeOrgId, forceRefresh: true });
+
+ const refreshed = await identitySpiffeAuthDAL.findOne({ identityId });
+
+ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.Organization,
+ orgId: actorOrgId
+ });
+
+ const decryptedCaBundleJwks = refreshed.encryptedCaBundleJwks
+ ? orgDataKeyDecryptor({ cipherTextBlob: refreshed.encryptedCaBundleJwks }).toString()
+ : "";
+ const decryptedBundleEndpointCaCert = refreshed.encryptedBundleEndpointCaCert
+ ? orgDataKeyDecryptor({ cipherTextBlob: refreshed.encryptedBundleEndpointCaCert }).toString()
+ : "";
+
+ return {
+ ...refreshed,
+ orgId: identityMembershipOrg.scopeOrgId,
+ caBundleJwks: decryptedCaBundleJwks,
+ bundleEndpointCaCert: decryptedBundleEndpointCaCert
+ };
+ };
+
+ return {
+ login,
+ attachSpiffeAuth,
+ updateSpiffeAuth,
+ getSpiffeAuth,
+ revokeSpiffeAuth,
+ refreshSpiffeBundle
+ };
+};
diff --git a/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-types.ts b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-types.ts
new file mode 100644
index 00000000000..e75c5f7faeb
--- /dev/null
+++ b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-types.ts
@@ -0,0 +1,60 @@
+import { TProjectPermission } from "@app/lib/types";
+
+export enum SpiffeConfigurationType {
+ STATIC = "static",
+ REMOTE = "remote"
+}
+
+export enum SpiffeBundleEndpointProfile {
+ HTTPS_WEB = "https_web",
+ HTTPS_SPIFFE = "https_spiffe"
+}
+
+export type TLoginSpiffeAuthDTO = {
+ identityId: string;
+ jwt: string;
+ organizationSlug?: string;
+};
+
+export type TAttachSpiffeAuthDTO = {
+ identityId: string;
+ trustDomain: string;
+ allowedSpiffeIds: string;
+ allowedAudiences: string;
+ configurationType: SpiffeConfigurationType;
+ caBundleJwks?: string;
+ bundleEndpointUrl?: string;
+ bundleEndpointProfile?: SpiffeBundleEndpointProfile;
+ bundleEndpointCaCert?: string;
+ bundleRefreshHintSeconds?: number;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: { ipAddress: string }[];
+ isActorSuperAdmin?: boolean;
+} & Omit;
+
+export type TUpdateSpiffeAuthDTO = {
+ identityId: string;
+ trustDomain?: string;
+ allowedSpiffeIds?: string;
+ allowedAudiences?: string;
+ configurationType?: SpiffeConfigurationType;
+ caBundleJwks?: string;
+ bundleEndpointUrl?: string;
+ bundleEndpointProfile?: SpiffeBundleEndpointProfile;
+ bundleEndpointCaCert?: string;
+ bundleRefreshHintSeconds?: number;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: { ipAddress: string }[];
+} & Omit;
+
+export type TGetSpiffeAuthDTO = {
+ identityId: string;
+} & Omit;
+
+export type TRevokeSpiffeAuthDTO = {
+ identityId: string;
+} & Omit;
diff --git a/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-validators.ts b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-validators.ts
new file mode 100644
index 00000000000..cffcc6c8a7b
--- /dev/null
+++ b/backend/src/services/identity-spiffe-auth/identity-spiffe-auth-validators.ts
@@ -0,0 +1,34 @@
+import RE2 from "re2";
+import { z } from "zod";
+
+export const validateSpiffeAllowedAudiencesField = z
+ .string()
+ .trim()
+ .min(1, "At least one audience is required")
+ .transform((data) => {
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .filter(Boolean)
+ .join(", ");
+ });
+
+export const validateSpiffeAllowedIdsField = z
+ .string()
+ .trim()
+ .min(1, "At least one SPIFFE ID pattern is required")
+ .transform((data) => {
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .filter(Boolean)
+ .join(", ");
+ });
+
+const TRUST_DOMAIN_REGEX = new RE2("^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$");
+
+export const validateTrustDomain = z
+ .string()
+ .trim()
+ .min(1, "Trust domain is required")
+ .refine((val) => TRUST_DOMAIN_REGEX.test(val), "Invalid trust domain format");
diff --git a/backend/src/services/identity-v2/identity-dal.ts b/backend/src/services/identity-v2/identity-dal.ts
index 72c9a16c85e..7e594227f9c 100644
--- a/backend/src/services/identity-v2/identity-dal.ts
+++ b/backend/src/services/identity-v2/identity-dal.ts
@@ -43,6 +43,7 @@ export const identityV2DALFactory = (db: TDbClient) => {
)
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
+ .leftJoin(TableName.IdentitySpiffeAuth, `${TableName.Identity}.id`, `${TableName.IdentitySpiffeAuth}.identityId`)
.where(`${TableName.Identity}.id`, identityId)
.where(`${TableName.Identity}.orgId`, scopeData.orgId)
.where((qb) => {
@@ -68,7 +69,8 @@ export const identityV2DALFactory = (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("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth),
+ db.ref("id").as("spiffeId").withSchema(TableName.IdentitySpiffeAuth)
);
if (!doc) return doc;
@@ -89,7 +91,8 @@ export const identityV2DALFactory = (db: TDbClient) => {
jwtId,
ociId,
ldapId,
- tlsCertId
+ tlsCertId,
+ spiffeId
} = el;
return {
...IdentitiesSchema.parse(el),
@@ -105,7 +108,8 @@ export const identityV2DALFactory = (db: TDbClient) => {
jwtId,
ldapId,
ociId,
- tlsCertId
+ tlsCertId,
+ spiffeId
})
};
},
diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts
index a2e4995ca4d..5a07f383b53 100644
--- a/backend/src/services/identity/identity-dal.ts
+++ b/backend/src/services/identity/identity-dal.ts
@@ -21,7 +21,8 @@ export const identityDALFactory = (db: TDbClient) => {
[IdentityAuthMethod.OCI_AUTH]: TableName.IdentityOciAuth,
[IdentityAuthMethod.OIDC_AUTH]: TableName.IdentityOidcAuth,
[IdentityAuthMethod.JWT_AUTH]: TableName.IdentityJwtAuth,
- [IdentityAuthMethod.LDAP_AUTH]: TableName.IdentityLdapAuth
+ [IdentityAuthMethod.LDAP_AUTH]: TableName.IdentityLdapAuth,
+ [IdentityAuthMethod.SPIFFE_AUTH]: TableName.IdentitySpiffeAuth
} as const;
const tableName = authMethodToTableName[authMethod];
if (!tableName) return;
diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts
index dee87fc4965..189948d01ca 100644
--- a/backend/src/services/identity/identity-fns.ts
+++ b/backend/src/services/identity/identity-fns.ts
@@ -12,7 +12,8 @@ export const buildAuthMethods = ({
tokenId,
jwtId,
ldapId,
- tlsCertId
+ tlsCertId,
+ spiffeId
}: {
uaId?: string;
gcpId?: string;
@@ -26,6 +27,7 @@ export const buildAuthMethods = ({
jwtId?: string;
ldapId?: string;
tlsCertId?: string;
+ spiffeId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
@@ -39,6 +41,7 @@ export const buildAuthMethods = ({
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null],
...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null],
- ...[tlsCertId ? IdentityAuthMethod.TLS_CERT_AUTH : null]
+ ...[tlsCertId ? IdentityAuthMethod.TLS_CERT_AUTH : null],
+ ...[spiffeId ? IdentityAuthMethod.SPIFFE_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 117195ca718..893fcaa69ad 100644
--- a/backend/src/services/identity/identity-org-dal.ts
+++ b/backend/src/services/identity/identity-org-dal.ts
@@ -12,6 +12,7 @@ import {
TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths,
+ TIdentitySpiffeAuths,
TIdentityTlsCertAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths,
@@ -106,6 +107,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.Membership}.actorIdentityId`,
`${TableName.IdentityTlsCertAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentitySpiffeAuth,
+ `${TableName.Membership}.actorIdentityId`,
+ `${TableName.IdentitySpiffeAuth}.identityId`
+ )
.select(
selectAllTableCols(TableName.Membership),
@@ -121,6 +127,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
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("id").as("spiffeId").withSchema(TableName.IdentitySpiffeAuth),
db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
);
@@ -251,6 +258,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityTlsCertAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentitySpiffeAuth,
+ "paginatedIdentity.actorIdentityId",
+ `${TableName.IdentitySpiffeAuth}.identityId`
+ )
.select(
db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema(TableName.MembershipRole),
@@ -276,7 +288,8 @@ 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("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth),
+ db.ref("id").as("spiffeId").withSchema(TableName.IdentitySpiffeAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.Role))
@@ -323,6 +336,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
tokenId,
ldapId,
tlsCertId,
+ spiffeId,
createdAt,
updatedAt,
lastLoginAuthMethod,
@@ -363,7 +377,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
tokenId,
jwtId,
ldapId,
- tlsCertId
+ tlsCertId,
+ spiffeId
})
}
}),
@@ -504,6 +519,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.Membership}.actorIdentityId`,
`${TableName.IdentityLdapAuth}.identityId`
)
+ .leftJoin(
+ TableName.IdentitySpiffeAuth,
+ `${TableName.Membership}.actorIdentityId`,
+ `${TableName.IdentitySpiffeAuth}.identityId`
+ )
.select(
db.ref("id").withSchema(TableName.Membership),
db.ref("total_count").withSchema("searchedIdentities"),
@@ -529,7 +549,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("spiffeId").withSchema(TableName.IdentitySpiffeAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.Role))
@@ -587,6 +608,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
azureId,
tokenId,
ldapId,
+ spiffeId,
createdAt,
updatedAt,
lastLoginTime,
@@ -627,7 +649,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
azureId,
tokenId,
jwtId,
- ldapId
+ ldapId,
+ spiffeId
})
}
}),
diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts
index 5b8490eb9d4..042fe1566e8 100644
--- a/backend/src/services/membership-identity/membership-identity-dal.ts
+++ b/backend/src/services/membership-identity/membership-identity-dal.ts
@@ -81,6 +81,11 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
)
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
+ .leftJoin(
+ TableName.IdentitySpiffeAuth,
+ `${TableName.Identity}.id`,
+ `${TableName.IdentitySpiffeAuth}.identityId`
+ )
.select(selectAllTableCols(TableName.Membership))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
@@ -120,7 +125,8 @@ export const membershipIdentityDALFactory = (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("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth),
+ db.ref("id").as("spiffeId").withSchema(TableName.IdentitySpiffeAuth)
);
const data = sqlNestRelationships({
@@ -144,7 +150,8 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
jwtId,
ociId,
ldapId,
- tlsCertId
+ tlsCertId,
+ spiffeId
} = el;
return {
...MembershipsSchema.parse(el),
@@ -166,7 +173,8 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
jwtId,
ldapId,
ociId,
- tlsCertId
+ tlsCertId,
+ spiffeId
})
}
};
diff --git a/docs/docs.json b/docs/docs.json
index b27e58ae26d..2cba814a231 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -280,6 +280,7 @@
"documentation/platform/identities/kubernetes-auth",
"documentation/platform/identities/oci-auth",
"documentation/platform/identities/token-auth",
+ "documentation/platform/identities/spiffe-auth",
"documentation/platform/identities/tls-cert-auth",
"documentation/platform/identities/universal-auth",
{
diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx
index 964a3ad7c7f..fdd88fbe637 100644
--- a/docs/documentation/platform/identities/machine-identities.mdx
+++ b/docs/documentation/platform/identities/machine-identities.mdx
@@ -59,6 +59,7 @@ To interact with various resources in Infisical, Machine Identities can authenti
- [Azure Auth](/documentation/platform/identities/azure-auth): An Azure-native authentication method for Azure resources (e.g. Azure VMs, Azure App Services, Azure Functions, Azure Kubernetes Service, etc.).
- [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.).
- [OIDC Auth](/documentation/platform/identities/oidc-auth): A platform-agnostic, JWT-based authentication method for workloads using an OpenID Connect identity provider.
+- [SPIFFE Auth](/documentation/platform/identities/spiffe-auth): A SPIFFE-native authentication method for workloads using JWT-SVIDs issued by SPIRE.
## Identity Lockout
diff --git a/docs/documentation/platform/identities/spiffe-auth.mdx b/docs/documentation/platform/identities/spiffe-auth.mdx
new file mode 100644
index 00000000000..602ba1566f5
--- /dev/null
+++ b/docs/documentation/platform/identities/spiffe-auth.mdx
@@ -0,0 +1,230 @@
+---
+title: SPIFFE Auth
+description: "Learn how to authenticate with Infisical using SPIFFE JWT-SVIDs."
+---
+
+**SPIFFE Auth** is a SPIFFE-native authentication method that validates JWT-SVIDs issued by [SPIRE](https://spiffe.io/docs/latest/spire-about/) or other SPIFFE-compliant workload identity providers, allowing workloads with SPIFFE identities to securely authenticate with Infisical.
+
+## Diagram
+
+The following sequence diagram illustrates the SPIFFE Auth workflow for authenticating with Infisical.
+
+```mermaid
+sequenceDiagram
+ participant Client as Client Workload
+ participant SPIRE as SPIRE Agent
+ participant Infisical as Infisical
+
+ Client->>SPIRE: Step 1: Request JWT-SVID
+ SPIRE-->>Client: Return signed JWT-SVID
+
+ Note over Client,Infisical: Step 2: Login Operation
+ Client->>Infisical: Send JWT-SVID to /api/v1/auth/spiffe-auth/login
+
+ Note over Infisical: Step 3: JWT-SVID Validation
+ Infisical->>Infisical: Verify signature using configured JWKS (static or remote)
+ Infisical->>Infisical: Validate trust domain, SPIFFE ID, and audience
+
+ Note over Infisical: Step 4: Token Generation
+ 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 a workload by verifying its JWT-SVID and checking that it meets specific requirements (e.g. its SPIFFE ID matches an allowed pattern, it belongs to the expected trust domain) at the `/api/v1/auth/spiffe-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 workload requests a JWT-SVID from its local SPIRE Agent (via the SPIFFE Workload API).
+2. The JWT-SVID is sent to Infisical at the `/api/v1/auth/spiffe-auth/login` endpoint.
+3. Infisical verifies the JWT-SVID signature using either:
+ - Pre-configured JWKS (Static configuration)
+ - JWKS fetched from a SPIRE bundle endpoint (Remote configuration)
+4. Infisical validates that the token's trust domain matches the configured trust domain, the SPIFFE ID (`sub` claim) matches at least one of the allowed SPIFFE ID patterns, and the audience (`aud` claim) is in the allowed audiences list.
+5. If all checks pass, Infisical returns a short-lived access token that the workload can use to make authenticated requests to the Infisical API.
+
+
+ For Remote configuration, Infisical needs network-level access to the configured SPIRE bundle endpoint. The trust bundle is fetched on-demand at login time and cached. If the cache is older than the configured refresh interval (default: 1 hour), Infisical fetches a fresh bundle before verifying the JWT-SVID. You can also force an immediate refresh via the API without waiting for the cache to expire.
+
+
+## Guide
+
+In the following steps, we explore how to create and use identities to access the Infisical API using the SPIFFE authentication method.
+
+
+
+ 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/access-controls/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 SPIFFE Auth instead. To do this, click the cog next to **Universal Auth** and then select **Delete** in the options dropdown.
+
+ 
+
+ 
+
+ Now create a new SPIFFE Auth Method.
+
+ Restrict access by properly configuring the trust domain, allowed SPIFFE IDs, and allowed audiences.
+
+ Here's some information about each field:
+
+ **Configuration Type:**
+ - **Static:** You provide the SPIRE JWKS JSON directly. Best for environments where the trust bundle is managed externally or does not change frequently.
+ - **Remote:** Infisical automatically fetches and caches the trust bundle from a SPIRE bundle endpoint. Best for dynamic environments where keys rotate regularly.
+
+ **Static configuration fields:**
+ - **CA Bundle JWKS (required):** The JWKS JSON containing the public keys used to verify JWT-SVID signatures. You can obtain this from your SPIRE server's bundle endpoint or via `spire-server bundle show -format jwks`.
+
+ **Remote configuration fields:**
+ - **Bundle Endpoint URL (required):** The URL of the SPIRE bundle endpoint (e.g. `https://spire-server:8443`). Must use HTTPS. Infisical fetches the trust bundle on-demand at login time and caches it for the configured refresh interval.
+ - **Bundle Endpoint Profile:** The authentication profile for the bundle endpoint.
+ - `HTTPS Web` — Standard HTTPS with publicly trusted certificates.
+ - `HTTPS SPIFFE` — HTTPS with a custom CA certificate for mTLS-secured endpoints.
+ - **Bundle Endpoint CA Certificate:** The PEM-encoded CA certificate for verifying the bundle endpoint TLS connection. Required when using the `HTTPS SPIFFE` profile.
+ - **Bundle Refresh Hint (seconds):** How long the cached trust bundle is considered fresh before Infisical re-fetches it on the next login. Defaults to `3600` (1 hour). You can force an immediate refresh at any time using the [refresh bundle API endpoint](/api-reference/endpoints/machine-identities/spiffe-auth/refresh-bundle).
+
+ **Common fields for both configurations:**
+ - **Trust Domain (required):** The SPIFFE trust domain that authenticating workloads must belong to (e.g. `example.org` or `prod.example.com`).
+ - **Allowed SPIFFE IDs (required):** A comma-separated list of SPIFFE ID patterns that are permitted to authenticate. Supports [picomatch](https://github.com/micromatch/picomatch) glob patterns (e.g. `spiffe://example.org/ns/production/**`, `spiffe://example.org/ns/*/sa/my-service`).
+ - **Allowed Audiences (required):** A comma-separated list of allowed audience values. The JWT-SVID's `aud` claim must contain at least one of these values.
+ - **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.
+
+
+
+ To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
+
+ To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
+
+ Next, select the identity you want to add to the project and the project-level role you want to allow it to assume. The project role assigned will determine what project-level resources this identity can have access to.
+
+ 
+
+ 
+
+
+ To access the Infisical API as the identity, you need to obtain a JWT-SVID from your SPIRE Agent that meets the validation requirements configured in the previous step.
+
+ The JWT-SVID must:
+ - Be signed by a key present in the configured JWKS (static or remote).
+ - Have a `sub` claim containing a valid SPIFFE ID that matches one of the allowed SPIFFE ID patterns.
+ - Have an `aud` claim containing at least one of the allowed audiences.
+ - Belong to the configured trust domain.
+
+ Once you have a valid JWT-SVID, use it to authenticate with Infisical at the `/api/v1/auth/spiffe-auth/login` endpoint.
+
+
+ The following example uses Node.js, but you can use any language that supports the SPIFFE Workload API or can obtain a JWT-SVID from your SPIRE Agent.
+
+ ```javascript
+ const axios = require("axios");
+
+ // Obtain JWT-SVID from SPIRE Agent via the Workload API,
+ // or from your SPIFFE-compatible identity provider.
+ const jwtSvid = "";
+
+ const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
+ const identityId = "";
+
+ const { data } = await axios.post(
+ `${infisicalUrl}/api/v1/auth/spiffe-auth/login`,
+ {
+ identityId,
+ jwt: jwtSvid,
+ }
+ );
+
+ console.log("Access token:", data.accessToken);
+ // Use data.accessToken for subsequent Infisical API requests
+ ```
+
+
+
+ If your workload uses the [go-spiffe](https://github.com/spiffe/go-spiffe) library, you can fetch a JWT-SVID directly from the Workload API.
+
+ ```go
+ package main
+
+ import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/spiffe/go-spiffe/v2/workloadapi"
+ )
+
+ func main() {
+ ctx := context.Background()
+
+ // Connect to the SPIRE Agent Workload API
+ client, err := workloadapi.New(ctx, workloadapi.WithAddr("unix:///tmp/spire-agent/public/api.sock"))
+ if err != nil {
+ panic(err)
+ }
+ defer client.Close()
+
+ // Fetch a JWT-SVID with the required audience
+ svid, err := client.FetchJWTSVID(ctx, workloadapi.JWTSVIDParams{
+ Audience: "infisical",
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // Authenticate with Infisical
+ payload, _ := json.Marshal(map[string]string{
+ "identityId": "",
+ "jwt": svid.Marshal(),
+ })
+
+ resp, err := http.Post(
+ "https://app.infisical.com/api/v1/auth/spiffe-auth/login",
+ "application/json",
+ bytes.NewReader(payload),
+ )
+ if err != nil {
+ panic(err)
+ }
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+ fmt.Println("Response:", string(body))
+ }
+ ```
+
+
+
+ We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using SPIFFE Auth as they handle the authentication process for you.
+
+
+
+ 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 `2592000` seconds (30 days) which can be adjusted in the configuration.
+
+ If an identity access token exceeds its max TTL or maximum number of uses, it can no longer authenticate with the Infisical API. In this case,
+ a new access token should be obtained by performing another login operation with a valid JWT-SVID.
+
+
+
diff --git a/docs/snippets/MachineAuthenticationBrowser.jsx b/docs/snippets/MachineAuthenticationBrowser.jsx
index 52149384664..6cb673dc526 100644
--- a/docs/snippets/MachineAuthenticationBrowser.jsx
+++ b/docs/snippets/MachineAuthenticationBrowser.jsx
@@ -25,7 +25,8 @@ export const MachineAuthenticationBrowser = () => {
{"name": "OIDC Auth for SPIRE", "slug": "oidc-auth-spire", "path": "/documentation/platform/identities/oidc-auth/spire", "description": "Learn how to authenticate workloads using SPIFFE/SPIRE OIDC.", "category": "Token-based"},
{"name": "TLS Certificate Auth", "slug": "tls-cert-auth", "path": "/documentation/platform/identities/tls-cert-auth", "description": "Learn how to authenticate machines using TLS client certificates.", "category": "Certificate-based"},
{"name": "LDAP Auth", "slug": "ldap-auth-general", "path": "/documentation/platform/identities/ldap-auth/general", "description": "Learn how to authenticate machines using LDAP credentials.", "category": "Directory-based"},
- {"name": "LDAP Auth for JumpCloud", "slug": "ldap-auth-jumpcloud", "path": "/documentation/platform/identities/ldap-auth/jumpcloud", "description": "Learn how to authenticate machines using JumpCloud LDAP.", "category": "Directory-based"}
+ {"name": "LDAP Auth for JumpCloud", "slug": "ldap-auth-jumpcloud", "path": "/documentation/platform/identities/ldap-auth/jumpcloud", "description": "Learn how to authenticate machines using JumpCloud LDAP.", "category": "Directory-based"},
+ {"name": "SPIFFE Auth", "slug": "spiffe-auth", "path": "/documentation/platform/identities/spiffe-auth", "description": "Learn how to authenticate workloads using SPIFFE JWT-SVIDs issued by SPIRE.", "category": "Certificate-based"}
].sort(function(a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx
index b441f5015d6..66338f538a1 100644
--- a/frontend/src/hooks/api/identities/constants.tsx
+++ b/frontend/src/hooks/api/identities/constants.tsx
@@ -12,5 +12,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth",
- [IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth"
+ [IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth",
+ [IdentityAuthMethod.SPIFFE_AUTH]: "SPIFFE Auth"
};
diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx
index c850005cf8f..22c9ca1dc97 100644
--- a/frontend/src/hooks/api/identities/enums.tsx
+++ b/frontend/src/hooks/api/identities/enums.tsx
@@ -10,10 +10,21 @@ export enum IdentityAuthMethod {
OIDC_AUTH = "oidc-auth",
LDAP_AUTH = "ldap-auth",
JWT_AUTH = "jwt-auth",
- TLS_CERT_AUTH = "tls-cert-auth"
+ TLS_CERT_AUTH = "tls-cert-auth",
+ SPIFFE_AUTH = "spiffe-auth"
}
export enum IdentityJwtConfigurationType {
JWKS = "jwks",
STATIC = "static"
}
+
+export enum IdentitySpiffeConfigurationType {
+ STATIC = "static",
+ REMOTE = "remote"
+}
+
+export enum SpiffeBundleEndpointProfile {
+ HTTPS_WEB = "https_web",
+ HTTPS_SPIFFE = "https_spiffe"
+}
diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx
index 14903eaef22..7559bae3063 100644
--- a/frontend/src/hooks/api/identities/mutations.tsx
+++ b/frontend/src/hooks/api/identities/mutations.tsx
@@ -15,6 +15,7 @@ import {
AddIdentityLdapAuthDTO,
AddIdentityOciAuthDTO,
AddIdentityOidcAuthDTO,
+ AddIdentitySpiffeAuthDTO,
AddIdentityTlsCertAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
@@ -34,6 +35,7 @@ import {
DeleteIdentityLdapAuthDTO,
DeleteIdentityOciAuthDTO,
DeleteIdentityOidcAuthDTO,
+ DeleteIdentitySpiffeAuthDTO,
DeleteIdentityTlsCertAuthDTO,
DeleteIdentityTokenAuthDTO,
DeleteIdentityUniversalAuthClientSecretDTO,
@@ -48,6 +50,7 @@ import {
IdentityLdapAuth,
IdentityOciAuth,
IdentityOidcAuth,
+ IdentitySpiffeAuth,
IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
@@ -62,6 +65,7 @@ import {
UpdateIdentityLdapAuthDTO,
UpdateIdentityOciAuthDTO,
UpdateIdentityOidcAuthDTO,
+ UpdateIdentitySpiffeAuthDTO,
UpdateIdentityTlsCertAuthDTO,
UpdateIdentityTokenAuthDTO,
UpdateIdentityUniversalAuthDTO,
@@ -1209,6 +1213,159 @@ export const useDeleteIdentityJwtAuth = () => {
});
};
+export const useAddIdentitySpiffeAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identitySpiffeAuth }
+ } = await apiRequest.post<{ identitySpiffeAuth: IdentitySpiffeAuth }>(
+ `/api/v1/auth/spiffe-auth/identities/${identityId}`,
+ {
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identitySpiffeAuth;
+ },
+ onSuccess: (_, { identityId, organizationId, projectId }) => {
+ if (organizationId) {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ }
+ if (projectId) {
+ queryClient.invalidateQueries({
+ queryKey: projectKeys.getProjectIdentityMemberships(projectId)
+ });
+ queryClient.invalidateQueries({
+ queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId })
+ });
+ }
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentitySpiffeAuth(identityId) });
+ }
+ });
+};
+
+export const useUpdateIdentitySpiffeAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identitySpiffeAuth }
+ } = await apiRequest.patch<{ identitySpiffeAuth: IdentitySpiffeAuth }>(
+ `/api/v1/auth/spiffe-auth/identities/${identityId}`,
+ {
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ configurationType,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identitySpiffeAuth;
+ },
+ onSuccess: (_, { identityId, organizationId, projectId }) => {
+ if (organizationId) {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ }
+ if (projectId) {
+ queryClient.invalidateQueries({
+ queryKey: projectKeys.getProjectIdentityMemberships(projectId)
+ });
+ queryClient.invalidateQueries({
+ queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId })
+ });
+ }
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentitySpiffeAuth(identityId) });
+ }
+ });
+};
+
+export const useDeleteIdentitySpiffeAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identitySpiffeAuth }
+ } = await apiRequest.delete(`/api/v1/auth/spiffe-auth/identities/${identityId}`);
+ return identitySpiffeAuth;
+ },
+ onSuccess: (_, { organizationId, identityId, projectId }) => {
+ if (organizationId) {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ }
+ if (projectId) {
+ queryClient.invalidateQueries({
+ queryKey: projectKeys.getProjectIdentityMemberships(projectId)
+ });
+ queryClient.invalidateQueries({
+ queryKey: projectIdentityQuery.getByIdKey({ identityId, projectId })
+ });
+ }
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentitySpiffeAuth(identityId) });
+ }
+ });
+};
+
export const useAddIdentityAzureAuth = () => {
const queryClient = useQueryClient();
return useMutation({
diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx
index 866bcac8029..5614802ceb6 100644
--- a/frontend/src/hooks/api/identities/queries.tsx
+++ b/frontend/src/hooks/api/identities/queries.tsx
@@ -17,6 +17,7 @@ import {
IdentityOciAuth,
IdentityOidcAuth,
IdentityProjectMembershipV1,
+ IdentitySpiffeAuth,
IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
@@ -45,6 +46,7 @@ export const identitiesKeys = {
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const,
+ getIdentitySpiffeAuth: (identityId: string) => [{ identityId }, "identity-spiffe-auth"] as const,
getIdentityLdapAuth: (identityId: string) => [{ identityId }, "identity-ldap-auth"] as const,
getIdentityTokensTokenAuth: (identityId: string) =>
[{ identityId }, "identity-tokens-token-auth"] as const,
@@ -385,3 +387,25 @@ export const useGetIdentityJwtAuth = (
enabled: Boolean(identityId) && (options?.enabled ?? true)
});
};
+
+export const useGetIdentitySpiffeAuth = (
+ identityId: string,
+ options?: TReactQueryOptions["options"]
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentitySpiffeAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identitySpiffeAuth }
+ } = await apiRequest.get<{ identitySpiffeAuth: IdentitySpiffeAuth }>(
+ `/api/v1/auth/spiffe-auth/identities/${identityId}`
+ );
+
+ return identitySpiffeAuth;
+ },
+ staleTime: 0,
+ gcTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts
index b28e9385bda..66b6e032008 100644
--- a/frontend/src/hooks/api/identities/types.ts
+++ b/frontend/src/hooks/api/identities/types.ts
@@ -833,6 +833,72 @@ export type DeleteIdentityJwtAuthDTO = {
identityId: string;
} & ({ organizationId: string } | { projectId: string });
+export type IdentitySpiffeAuth = {
+ identityId: string;
+ trustDomain: string;
+ allowedSpiffeIds: string;
+ allowedAudiences: string;
+ configurationType: string;
+ caBundleJwks: string;
+ bundleEndpointUrl: string | null;
+ bundleEndpointProfile: string | null;
+ bundleEndpointCaCert: string;
+ cachedBundleLastRefreshedAt: string | null;
+ bundleRefreshHintSeconds: number;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentitySpiffeAuthDTO = {
+ organizationId?: string;
+ projectId?: string;
+ identityId: string;
+ trustDomain: string;
+ allowedSpiffeIds: string;
+ allowedAudiences: string;
+ configurationType: string;
+ caBundleJwks?: string;
+ bundleEndpointUrl?: string;
+ bundleEndpointProfile?: string;
+ bundleEndpointCaCert?: string;
+ bundleRefreshHintSeconds?: number;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+} & ({ organizationId: string } | { projectId: string });
+
+export type UpdateIdentitySpiffeAuthDTO = {
+ organizationId?: string;
+ projectId?: string;
+ identityId: string;
+ trustDomain?: string;
+ allowedSpiffeIds?: string;
+ allowedAudiences?: string;
+ configurationType?: string;
+ caBundleJwks?: string;
+ bundleEndpointUrl?: string;
+ bundleEndpointProfile?: string;
+ bundleEndpointCaCert?: string;
+ bundleRefreshHintSeconds?: number;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+} & ({ organizationId: string } | { projectId: string });
+
+export type DeleteIdentitySpiffeAuthDTO = {
+ organizationId?: string;
+ projectId?: string;
+ identityId: string;
+} & ({ organizationId: string } | { projectId: string });
+
export type CreateTokenIdentityTokenAuthDTO = {
identityId: string;
name: 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 1bfb904a57a..ab667c7582a 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
@@ -18,6 +18,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
import { IdentityOciAuthForm } from "./IdentityOciAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
+import { IdentitySpiffeAuthForm } from "./IdentitySpiffeAuthForm";
import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
@@ -58,7 +59,8 @@ const identityAuthMethods = [
{
label: "JWT Auth",
value: IdentityAuthMethod.JWT_AUTH
- }
+ },
+ { label: "SPIFFE Auth", value: IdentityAuthMethod.SPIFFE_AUTH }
];
const schema = z
@@ -234,6 +236,16 @@ export const IdentityAuthMethodModalContent = ({
handlePopUpToggle={handlePopUpToggle}
/>
)
+ },
+
+ [IdentityAuthMethod.SPIFFE_AUTH]: {
+ render: () => (
+
+ )
}
};
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySpiffeAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySpiffeAuthForm.tsx
new file mode 100644
index 00000000000..1f8e3961095
--- /dev/null
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySpiffeAuthForm.tsx
@@ -0,0 +1,569 @@
+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 { useParams } from "@tanstack/react-router";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ FormControl,
+ IconButton,
+ Input,
+ Select,
+ SelectItem,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+ TextArea
+} from "@app/components/v2";
+import { useOrganization, useSubscription } from "@app/context";
+import { useAddIdentitySpiffeAuth, useUpdateIdentitySpiffeAuth } from "@app/hooks/api";
+import {
+ IdentitySpiffeConfigurationType,
+ SpiffeBundleEndpointProfile
+} from "@app/hooks/api/identities/enums";
+import { useGetIdentitySpiffeAuth } from "@app/hooks/api/identities/queries";
+import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+import { IdentityFormTab } from "./types";
+
+const commonSchema = z.object({
+ trustDomain: z.string().trim().min(1, "Trust domain is required"),
+ allowedSpiffeIds: z.string().trim().min(1, "Allowed SPIFFE IDs are required"),
+ allowedAudiences: z.string().trim().min(1, "Allowed audiences are required"),
+ accessTokenTrustedIps: z
+ .array(
+ z.object({
+ ipAddress: z.string().max(50)
+ })
+ )
+ .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()
+});
+
+const schema = z.discriminatedUnion("configurationType", [
+ z
+ .object({
+ configurationType: z.literal(IdentitySpiffeConfigurationType.STATIC),
+ caBundleJwks: z.string().trim().min(1, "CA Bundle JWKS is required for static configuration"),
+ bundleEndpointUrl: z.string().trim().optional().default(""),
+ bundleEndpointProfile: z
+ .nativeEnum(SpiffeBundleEndpointProfile)
+ .optional()
+ .default(SpiffeBundleEndpointProfile.HTTPS_WEB),
+ bundleEndpointCaCert: z.string().trim().optional().default(""),
+ bundleRefreshHintSeconds: z.string().optional().default("300")
+ })
+ .merge(commonSchema),
+ z
+ .object({
+ configurationType: z.literal(IdentitySpiffeConfigurationType.REMOTE),
+ caBundleJwks: z.string().trim().optional().default(""),
+ bundleEndpointUrl: z.string().trim().url("Must be a valid URL"),
+ bundleEndpointProfile: z
+ .nativeEnum(SpiffeBundleEndpointProfile)
+ .default(SpiffeBundleEndpointProfile.HTTPS_WEB),
+ bundleEndpointCaCert: z.string().trim().optional().default(""),
+ bundleRefreshHintSeconds: z.string().default("300")
+ })
+ .merge(commonSchema)
+]);
+
+export type FormData = z.infer;
+
+type Props = {
+ handlePopUpOpen: (
+ popUpName: keyof UsePopUpState<["upgradePlan"]>,
+ data?: { featureName?: string }
+ ) => void;
+ handlePopUpToggle: (
+ popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
+ state?: boolean
+ ) => void;
+ identityId?: string;
+ isUpdate?: boolean;
+};
+
+export const IdentitySpiffeAuthForm = ({
+ handlePopUpOpen,
+ handlePopUpToggle,
+ identityId,
+ isUpdate
+}: Props) => {
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+ const { subscription } = useSubscription();
+ const { projectId } = useParams({
+ strict: false
+ });
+ const { mutateAsync: addMutateAsync } = useAddIdentitySpiffeAuth();
+ const { mutateAsync: updateMutateAsync } = useUpdateIdentitySpiffeAuth();
+ const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration);
+
+ const { data } = useGetIdentitySpiffeAuth(identityId ?? "", {
+ enabled: isUpdate
+ });
+
+ const {
+ watch,
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ configurationType: IdentitySpiffeConfigurationType.STATIC,
+ trustDomain: "",
+ allowedSpiffeIds: "",
+ allowedAudiences: "",
+ caBundleJwks: "",
+ bundleEndpointUrl: "",
+ bundleEndpointProfile: SpiffeBundleEndpointProfile.HTTPS_WEB,
+ bundleEndpointCaCert: "",
+ bundleRefreshHintSeconds: "300",
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ }
+ });
+
+ const selectedConfigurationType = watch("configurationType") as IdentitySpiffeConfigurationType;
+ const selectedBundleEndpointProfile = watch("bundleEndpointProfile");
+
+ const {
+ fields: accessTokenTrustedIpsFields,
+ append: appendAccessTokenTrustedIp,
+ remove: removeAccessTokenTrustedIp
+ } = useFieldArray({ control, name: "accessTokenTrustedIps" });
+
+ useEffect(() => {
+ if (data) {
+ reset({
+ configurationType:
+ (data.configurationType as IdentitySpiffeConfigurationType) ||
+ IdentitySpiffeConfigurationType.STATIC,
+ trustDomain: data.trustDomain,
+ allowedSpiffeIds: data.allowedSpiffeIds,
+ allowedAudiences: data.allowedAudiences,
+ caBundleJwks: data.caBundleJwks || "",
+ bundleEndpointUrl: data.bundleEndpointUrl || "",
+ bundleEndpointProfile:
+ (data.bundleEndpointProfile as SpiffeBundleEndpointProfile) ||
+ SpiffeBundleEndpointProfile.HTTPS_WEB,
+ bundleEndpointCaCert: data.bundleEndpointCaCert || "",
+ bundleRefreshHintSeconds: String(data.bundleRefreshHintSeconds ?? 300),
+ 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}` : ""}`
+ };
+ }
+ )
+ });
+ }
+ }, [data]);
+
+ const onFormSubmit = async ({
+ accessTokenTrustedIps,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ configurationType,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds
+ }: FormData) => {
+ if (!identityId) {
+ return;
+ }
+
+ if (data) {
+ await updateMutateAsync({
+ identityId,
+ ...(projectId ? { projectId } : { organizationId: orgId }),
+ configurationType,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds: Number(bundleRefreshHintSeconds),
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ } else {
+ await addMutateAsync({
+ identityId,
+ configurationType,
+ trustDomain,
+ allowedSpiffeIds,
+ allowedAudiences,
+ caBundleJwks,
+ bundleEndpointUrl,
+ bundleEndpointProfile,
+ bundleEndpointCaCert,
+ bundleRefreshHintSeconds: Number(bundleRefreshHintSeconds),
+ ...(projectId ? { projectId } : { organizationId: orgId }),
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ }
+
+ handlePopUpToggle("identityAuthMethod", false);
+
+ createNotification({
+ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
+ type: "success"
+ });
+ reset();
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx
index d6e322487ff..8b4a8cb82af 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx
@@ -38,6 +38,7 @@ import {
useDeleteIdentityLdapAuth,
useDeleteIdentityOciAuth,
useDeleteIdentityOidcAuth,
+ useDeleteIdentitySpiffeAuth,
useDeleteIdentityTlsCertAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
@@ -51,6 +52,7 @@ import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManage
import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
import { IdentityOciAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm";
import { IdentityOidcAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm";
+import { IdentitySpiffeAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySpiffeAuthForm";
import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm";
import { IdentityTokenAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
@@ -64,6 +66,7 @@ import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthC
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
+import { ViewIdentitySpiffeAuthContent } from "./ViewIdentitySpiffeAuthContent";
import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
@@ -87,7 +90,8 @@ const AuthMethodComponentMap = {
[IdentityAuthMethod.AWS_AUTH]: ViewIdentityAwsAuthContent,
[IdentityAuthMethod.ALICLOUD_AUTH]: ViewIdentityAliCloudAuthContent,
[IdentityAuthMethod.AZURE_AUTH]: ViewIdentityAzureAuthContent,
- [IdentityAuthMethod.JWT_AUTH]: ViewIdentityJwtAuthContent
+ [IdentityAuthMethod.JWT_AUTH]: ViewIdentityJwtAuthContent,
+ [IdentityAuthMethod.SPIFFE_AUTH]: ViewIdentitySpiffeAuthContent
};
const EditAuthMethodMap = {
@@ -102,7 +106,8 @@ const EditAuthMethodMap = {
[IdentityAuthMethod.OCI_AUTH]: IdentityOciAuthForm,
[IdentityAuthMethod.OIDC_AUTH]: IdentityOidcAuthForm,
[IdentityAuthMethod.JWT_AUTH]: IdentityJwtAuthForm,
- [IdentityAuthMethod.LDAP_AUTH]: IdentityLdapAuthForm
+ [IdentityAuthMethod.LDAP_AUTH]: IdentityLdapAuthForm,
+ [IdentityAuthMethod.SPIFFE_AUTH]: IdentitySpiffeAuthForm
};
export const Content = ({
@@ -136,6 +141,7 @@ export const Content = ({
const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
+ const { mutateAsync: revokeSpiffeAuth } = useDeleteIdentitySpiffeAuth();
const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth();
const RemoveAuthMethodMap = {
@@ -150,6 +156,7 @@ export const Content = ({
[IdentityAuthMethod.OCI_AUTH]: revokeOciAuth,
[IdentityAuthMethod.OIDC_AUTH]: revokeOidcAuth,
[IdentityAuthMethod.JWT_AUTH]: revokeJwtAuth,
+ [IdentityAuthMethod.SPIFFE_AUTH]: revokeSpiffeAuth,
[IdentityAuthMethod.LDAP_AUTH]: revokeLdapAuth
};
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentitySpiffeAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentitySpiffeAuthContent.tsx
new file mode 100644
index 00000000000..0c3f942739b
--- /dev/null
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentitySpiffeAuthContent.tsx
@@ -0,0 +1,125 @@
+import { faBan } from "@fortawesome/free-solid-svg-icons";
+import { EyeIcon } from "lucide-react";
+
+import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
+import { Badge } from "@app/components/v3";
+import { useGetIdentitySpiffeAuth } from "@app/hooks/api";
+import { IdentitySpiffeConfigurationType } from "@app/hooks/api/identities/enums";
+import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityContentWrapper";
+
+import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
+import { ViewAuthMethodProps } from "./types";
+
+export const ViewIdentitySpiffeAuthContent = ({
+ identityId,
+ onEdit,
+ onDelete
+}: ViewAuthMethodProps) => {
+ const { data, isPending } = useGetIdentitySpiffeAuth(identityId);
+
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+ );
+ }
+
+ const isRemote = data.configurationType === IdentitySpiffeConfigurationType.REMOTE;
+
+ return (
+
+
+ {data.trustDomain}
+
+
+ {data.allowedSpiffeIds
+ ?.split(",")
+ .map((id) => id.trim())
+ .join(", ")}
+
+
+ {data.allowedAudiences
+ ?.split(",")
+ .map((aud) => aud.trim())
+ .join(", ")}
+
+
+ {isRemote ? "Remote" : "Static"}
+
+ {isRemote ? (
+ <>
+
+ {data.bundleEndpointUrl}
+
+
+ {data.bundleEndpointProfile}
+
+
+ {data.bundleEndpointCaCert && (
+
+ {data.bundleEndpointCaCert}
+
+ }
+ >
+
+
+ Reveal
+
+
+ )}
+
+
+ {data.cachedBundleLastRefreshedAt
+ ? new Date(data.cachedBundleLastRefreshedAt).toLocaleString()
+ : null}
+
+
+ {data.bundleRefreshHintSeconds}
+
+ >
+ ) : (
+
+ {data.caBundleJwks && (
+
+ {data.caBundleJwks}
+
+ }
+ >
+
+
+ Reveal
+
+
+ )}
+
+ )}
+
+ {data.accessTokenTTL}
+
+
+ {data.accessTokenMaxTTL}
+
+
+ {data.accessTokenNumUsesLimit}
+
+
+ {data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
+
+
+ );
+};