From b7d38ab693fff94bf9dfa55dd3b3948e87e6c85b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 24 Oct 2025 00:22:17 -0700 Subject: [PATCH 001/271] Add pki concepts docs --- docs/docs.json | 23 ++++++-- .../pki/concepts/certificate-lifecycle.mdx | 53 +++++++++++++++++++ .../pki/concepts/certificate-mgmt.mdx | 20 +++++++ docs/documentation/platform/pki/overview.mdx | 15 ++++-- .../secrets-mgmt/concepts/secrets-mgmt.mdx | 2 +- 5 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 docs/documentation/platform/pki/concepts/certificate-lifecycle.mdx create mode 100644 docs/documentation/platform/pki/concepts/certificate-mgmt.mdx diff --git a/docs/docs.json b/docs/docs.json index 7f25805d8..07f6cd773 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -704,11 +704,28 @@ "item": "Infisical PKI", "groups": [ { - "group": "Infisical PKI", + "group": "Certificate Management", "pages": [ "documentation/platform/pki/overview", - "documentation/platform/pki/private-ca", - "documentation/platform/pki/external-ca", + { + "group": "Concepts", + "pages": [ + "documentation/platform/pki/concepts/certificate-mgmt", + "documentation/platform/pki/concepts/certificate-lifecycle" + ] + } + ] + }, + { + "group": "Product Reference", + "pages": [ + { + "group": "Certificate Authorities", + "pages": [ + "documentation/platform/pki/private-ca", + "documentation/platform/pki/external-ca" + ] + }, "documentation/platform/pki/subscribers", "documentation/platform/pki/certificates", "documentation/platform/pki/acme-ca", diff --git a/docs/documentation/platform/pki/concepts/certificate-lifecycle.mdx b/docs/documentation/platform/pki/concepts/certificate-lifecycle.mdx new file mode 100644 index 000000000..6d8ffc587 --- /dev/null +++ b/docs/documentation/platform/pki/concepts/certificate-lifecycle.mdx @@ -0,0 +1,53 @@ +--- +title: "Certificate Lifecycle" +description: "Learn what is the certificate lifecycle and how it works." +--- + +## Certificate Lifecycle + +Typically, a certificate goes through a series of stages during its lifetime from creation to retirement. This is called the certificate lifecycle. The exact names of these stages may vary from vendor to vendor, but they typically include [discovery](/documentation/platform/pki/concepts/certificate-lifecycle#discovery), [enrollment](/documentation/platform/pki/concepts/certificate-lifecycle#enrollment), [deployment](/documentation/platform/pki/concepts/certificate-lifecycle#deployment), [renewal](/documentation/platform/pki/concepts/certificate-lifecycle#renewal), [revocation](/documentation/platform/pki/concepts/certificate-lifecycle#revocation), and [retirement](/documentation/platform/pki/concepts/certificate-lifecycle#retirement). + +Note that not every stage is needed. For instance: + +- You are not required to discover certificates in order to start issuing and managing them. +- You may not need to revoke a certificate explicitly if it expires naturally and is replaced during routine renewal. + +## Discovery + +Certificate discovery is the process of identifying all active and inactive certificates across an environment, including those found on web servers, load balancers, services, and devices. A complete inventory prevents outages from forgotten certificates and creates the foundation for automation and monitoring. + +## Enrollment (Request / Issuance) + +Certificate enrollment is the process of requesting a certificate from a CA and can follow different approaches depending on the system or protocol in use. + +Common approaches to certificate enrollment include: + +- CSR-based enrollment: The client generates a key pair locally and submits a Certificate Signing Request (CSR) to a CA for certificate issuance. +- CSR-less enrollment: The client requests a certificate directly from a CA which may handle key generation internally and return the key pair in the response. + +Enrollment can be manually completed via API or fully automated using protocols like EST or ACME. The choice of enrollment method depends on security requirements, operational constraints, and integration context. + +## Deployment + +Certificate deployment involves installing the issued certificate on the appropriate systems and services, such as web servers, load balancers, or internal endpoints. It can also include distributing or synchronizing certificates to external systems like cloud key stores (e.g., AWS Secrets Manager, Google Secret Manager, Azure Key Vault) so they can be securely consumed by workloads running in the cloud. + +Deployment can happen manually or through automated mechanisms such as configuration pipelines, agents, or webhook integrations. + +## Renewal + +Certificate renewal is the process of requesting a new certificate from a CA before it expires to maintain trust and availability; this process can involve reusing the same key pair or rotating to a new one. + +The renewal process can be server-driven or client-driven: + +- Server-driven: Infisical automatically renews the certificate on your behalf. The renewed certificate is stored in the platform and can be synchronized to external systems such as cloud key stores. +- Client-driven: An external client, such as an agent or workload, initiates the renewal against Infisical. This is useful when key material needs to remain under client control or when rotation is tied to application-specific logic. + +This flexibility allows certificates to be renewed in a way that aligns with different security, automation, and infrastructure models. + +## Revocation + +Certificate revocation is the process of invalidating a certificate to prevent it from being used. This is required when a certificate is compromised, misconfigured, or no longer needed. The CA signals this status to clients through CRLs or OCSP. A new certificate can be issued and deployed if needed. + +## Retirement + +Certificate retirement is the process of removing a certificate from the system. This is typically done when a certificate is no longer needed or has expired. diff --git a/docs/documentation/platform/pki/concepts/certificate-mgmt.mdx b/docs/documentation/platform/pki/concepts/certificate-mgmt.mdx new file mode 100644 index 000000000..efdb8a072 --- /dev/null +++ b/docs/documentation/platform/pki/concepts/certificate-mgmt.mdx @@ -0,0 +1,20 @@ +--- +title: "Certificate Management" +description: "Learn what is certificate management and why it matters for building secure systems." +--- + +## What is a Certificate? + +A (digital) _certificate_ is a file that is tied to a cryptographic key pair and is used to verify the identity of a website, user, device, or service. It helps establish trust and secure, encrypted communication between systems. + +For example, when you visit a website over HTTPS, your browser checks the TLS certificate deployed on the web server or load balancer to make sure it’s really the site it claims to be. If the certificate is valid, your browser establishes an encrypted connection with the server. + +Certificates contain information about the subject (who it identifies), the public key, and a digital signature from the CA that issued the certificate. They also include additional fields such as key usages, validity periods, and extensions that define how and where the certificate can be used. When a certificate expires, the service presenting it is no longer trusted, and clients won't be able to establish a secure connection to the service. + +## What is Certificate Management? + +As infrastructure scales and systems become more distributed, certificates sprawl. Without proper visibility and automation in place, certificates scatter across IT infrastructure, creating blind spots that can lead to service outages when certificates aren't renewed in time. + +To solve certificate sprawl and avoid outages, organizations rely on certificate management: the practice of centralizing and automating the certificate lifecycle from issuance through renewal and revocation. + +A consistent approach makes it easier to keep certificates valid and trusted, reduce operational risk, and maintain secure communication across environments. diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index b2351813c..bc5ea0591 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -4,10 +4,15 @@ sidebarTitle: "Overview" description: "Learn how to create a Private CA hierarchy and issue X.509 certificates." --- -Infisical can be used to create and manage Certificate Authorities (CAs) and issue X.509 certificates. This allows you to manage PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. +Infisical can be used to create and manage Certificate Authorities (CAs) and issue digital X.509 certificates. This allows you to manage PKI infrastructure and issue certificates for end-entities such as load balancers, web servers, devices, and more. -Infisical's PKI offering is split into three components: +It helps teams automate certificate management including enrollment and renewal, and adopt secure workflows to ensure certificates remain valid, trusted, and synchronized across infrastructure. -- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage CAs, including root and intermediate CAs. -- [Subscribers](/documentation/platform/pki/subscribers): Define and manage entities that will request X.509 certificates from CAs. This module provides a centralized view of all subscribers, enabling you to issue certificates and monitor their status. -- [Certificates](/documentation/platform/pki/certificates): Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. +Core capabilities include: + +- Private CA: Create and manage your own private CA hierarchy including root and intermediate CAs. +- External CA integration: Integrate with external public and private CAs including Azure ADCS and ACME-compatible CAs like Let's Encrypt and DigiCert. +- Certificate Enrollment: Support enrollment methods including API, ACME, EST, and more to automate certificate issuance for services, devices, and workloads. +- Certificate Inventory: Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. +- Certificate Lifecycle Automation: Automate issuance, renewal, and revocation with policy-based workflows, ensuring certificates remain valid, compliant, and up to date across your infrastructure. +- Certificate Syncs: Push certificates to cloud certificate managers like AWS Certificate Manager and Azure Key Vault. diff --git a/docs/documentation/platform/secrets-mgmt/concepts/secrets-mgmt.mdx b/docs/documentation/platform/secrets-mgmt/concepts/secrets-mgmt.mdx index 9fe68fb07..8e19b0937 100644 --- a/docs/documentation/platform/secrets-mgmt/concepts/secrets-mgmt.mdx +++ b/docs/documentation/platform/secrets-mgmt/concepts/secrets-mgmt.mdx @@ -3,7 +3,7 @@ title: "Secrets Management" description: "Learn what is secrets management and why it matters for building secure systems." --- -## What is Secret? +## What is a Secret? A _secret_ is a confidential value used by an application such as database credential, API key, or other configuration. From ecca51b1d60f6294985218b30920fd086009582c Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 24 Oct 2025 18:04:51 -0300 Subject: [PATCH 002/271] feat(slack-integration): add support for secret sync error notifications and enhance notification handling --- ...re-slack-secret-sync-error-notification.ts | 29 +++++ .../src/db/schemas/project-slack-configs.ts | 4 +- .../notification-handlers/microsoft-teams.ts | 92 ++++++++++++++++ .../notification-handlers/slack.ts | 80 ++++++++++++++ .../trigger-notification.ts | 104 ++++-------------- .../src/lib/workflow-integrations/types.ts | 13 ++- .../project-microsoft-teams-config-dal.ts | 8 +- .../services/secret-sync/secret-sync-queue.ts | 88 ++++++++++----- .../slack/project-slack-config-dal.ts | 5 +- backend/src/services/slack/slack-fns.ts | 65 ++++++++++- 10 files changed, 367 insertions(+), 121 deletions(-) create mode 100644 backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts create mode 100644 backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts create mode 100644 backend/src/lib/workflow-integrations/notification-handlers/slack.ts diff --git a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts new file mode 100644 index 000000000..4b5bf54d3 --- /dev/null +++ b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled"))) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.boolean("isSecretSyncErrorNotificationEnabled").notNullable().defaultTo(false); + }); + } + + if (!(await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels"))) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.text("secretSyncErrorChannels").notNullable().defaultTo(""); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled")) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.dropColumn("isSecretSyncErrorNotificationEnabled"); + }); + } + + if (await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels")) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.dropColumn("secretSyncErrorChannels"); + }); + } +} diff --git a/backend/src/db/schemas/project-slack-configs.ts b/backend/src/db/schemas/project-slack-configs.ts index 0a46e5aae..48674ec8f 100644 --- a/backend/src/db/schemas/project-slack-configs.ts +++ b/backend/src/db/schemas/project-slack-configs.ts @@ -16,7 +16,9 @@ export const ProjectSlackConfigsSchema = z.object({ isSecretRequestNotificationEnabled: z.boolean().default(false), secretRequestChannels: z.string().default(""), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + isSecretSyncErrorNotificationEnabled: z.boolean().default(false), + secretSyncErrorChannels: z.string().default("") }); export type TProjectSlackConfigs = z.infer; diff --git a/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts b/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts new file mode 100644 index 000000000..0cfe28491 --- /dev/null +++ b/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts @@ -0,0 +1,92 @@ +import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { + TProjectMicrosoftTeamsConfigDALFactory, + TProjectMicrosoftTeamsConfigWithIntegrations +} from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; + +import { logger } from "../../logger"; +import { TNotification, TriggerFeature } from "../types"; + +const handleMicrosoftTeamsNotification = async ({ + microsoftTeamsConfig, + notification, + orgId, + microsoftTeamsService +}: { + microsoftTeamsConfig: TProjectMicrosoftTeamsConfigWithIntegrations; + notification: TNotification; + orgId: string; + microsoftTeamsService: Pick; +}): Promise => { + let targetChannels: unknown; + let isEnabled = false; + + switch (notification.type) { + case TriggerFeature.ACCESS_REQUEST: + case TriggerFeature.ACCESS_REQUEST_UPDATED: + targetChannels = microsoftTeamsConfig.accessRequestChannels; + isEnabled = microsoftTeamsConfig.isAccessRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_APPROVAL: + targetChannels = microsoftTeamsConfig.secretRequestChannels; + isEnabled = microsoftTeamsConfig.isSecretRequestNotificationEnabled; + break; + default: + return; + } + + if (isEnabled && targetChannels) { + const { success, data, error: validationError } = validateMicrosoftTeamsChannelsSchema.safeParse(targetChannels); + + if (!success) { + logger.error(validationError, "Invalid Microsoft Teams channel configuration"); + return; + } + + if (data) { + await microsoftTeamsService + .sendNotification({ + notification, + target: data, + tenantId: microsoftTeamsConfig.tenantId, + microsoftTeamsIntegrationId: microsoftTeamsConfig.id, + orgId + }) + .catch((error) => { + logger.error( + error, + `Error sending Microsoft Teams notification. Notification type: ${notification.type}, Tenant ID: ${microsoftTeamsConfig.tenantId}, Project ID: ${microsoftTeamsConfig.projectId}` + ); + }); + } + } +}; + +export const triggerMicrosoftTeamsNotification = async ({ + projectId, + notification, + orgId, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService +}: { + projectId: string; + notification: TNotification; + orgId: string; + projectMicrosoftTeamsConfigDAL: Pick; + microsoftTeamsService: Pick; +}): Promise => { + try { + const config = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId); + if (config) { + await handleMicrosoftTeamsNotification({ + microsoftTeamsConfig: config, + notification, + orgId, + microsoftTeamsService + }); + } + } catch (error) { + logger.error(error, `Error handling Microsoft Teams notification. Project ID: ${projectId}`); + } +}; diff --git a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts new file mode 100644 index 000000000..66fd58b3a --- /dev/null +++ b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts @@ -0,0 +1,80 @@ +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TProjectSlackConfigDALFactory, + TProjectSlackConfigWithIntegrations +} from "@app/services/slack/project-slack-config-dal"; +import { sendSlackNotification } from "@app/services/slack/slack-fns"; + +import { logger } from "../../logger"; +import { TNotification, TriggerFeature } from "../types"; + +const handleSlackNotification = async ({ + slackConfig, + notification, + orgId, + kmsService +}: { + slackConfig: TProjectSlackConfigWithIntegrations; + notification: TNotification; + orgId: string; + kmsService: Pick; +}): Promise => { + let targetChannelIds: string[] = []; + let isEnabled = false; + + switch (notification.type) { + case TriggerFeature.ACCESS_REQUEST: + case TriggerFeature.ACCESS_REQUEST_UPDATED: + targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; + isEnabled = slackConfig.isAccessRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_APPROVAL: + targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; + isEnabled = slackConfig.isSecretRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_SYNC_ERROR: + targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ") || []; + isEnabled = slackConfig.isSecretSyncErrorNotificationEnabled; + break; + default: + return; + } + + if (targetChannelIds.length && isEnabled) { + await sendSlackNotification({ + orgId, + notification, + kmsService, + targetChannelIds, + slackIntegration: slackConfig + }).catch((error) => { + logger.error( + error, + `Error sending Slack notification. Notification type: ${notification.type}, Target channel IDs: ${targetChannelIds.join(", ")}, Project ID: ${slackConfig.projectId}` + ); + }); + } +}; + +export const triggerSlackNotification = async ({ + projectId, + notification, + orgId, + projectSlackConfigDAL, + kmsService +}: { + projectId: string; + notification: TNotification; + orgId: string; + projectSlackConfigDAL: Pick; + kmsService: Pick; +}): Promise => { + try { + const config = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId); + if (config) { + await handleSlackNotification({ slackConfig: config, notification, orgId, kmsService }); + } + } catch (error) { + logger.error(error, `Error handling Slack notification. Project ID: ${projectId}`); + } +}; diff --git a/backend/src/lib/workflow-integrations/trigger-notification.ts b/backend/src/lib/workflow-integrations/trigger-notification.ts index 355761f73..2dc6f61fe 100644 --- a/backend/src/lib/workflow-integrations/trigger-notification.ts +++ b/backend/src/lib/workflow-integrations/trigger-notification.ts @@ -1,8 +1,7 @@ -import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; -import { sendSlackNotification } from "@app/services/slack/slack-fns"; - import { logger } from "../logger"; -import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types"; +import { triggerMicrosoftTeamsNotification } from "./notification-handlers/microsoft-teams"; +import { triggerSlackNotification } from "./notification-handlers/slack"; +import { TTriggerWorkflowNotificationDTO } from "./types"; export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => { try { @@ -16,88 +15,25 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl return; } - const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId); - const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId); + const handlerPromises = [ + triggerSlackNotification({ + projectId, + notification, + orgId: project.orgId, + projectSlackConfigDAL, + kmsService + }), - if (slackConfig) { - if ( - notification.type === TriggerFeature.ACCESS_REQUEST || - notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED - ) { - const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; - if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) { - await sendSlackNotification({ - orgId: project.orgId, - notification, - kmsService, - targetChannelIds, - slackIntegration: slackConfig - }).catch((error) => { - logger.error(error, "Error sending Slack notification"); - }); - } - } else if (notification.type === TriggerFeature.SECRET_APPROVAL) { - const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; - if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) { - await sendSlackNotification({ - orgId: project.orgId, - notification, - kmsService, - targetChannelIds, - slackIntegration: slackConfig - }).catch((error) => { - logger.error(error, "Error sending Slack notification"); - }); - } - } - } + triggerMicrosoftTeamsNotification({ + projectId, + notification, + orgId: project.orgId, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService + }) + ]; - if (microsoftTeamsConfig) { - if ( - notification.type === TriggerFeature.ACCESS_REQUEST || - notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED - ) { - if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) { - const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse( - microsoftTeamsConfig.accessRequestChannels - ); - - if (success && data) { - await microsoftTeamsService - .sendNotification({ - notification, - target: data, - tenantId: microsoftTeamsConfig.tenantId, - microsoftTeamsIntegrationId: microsoftTeamsConfig.id, - orgId: project.orgId - }) - .catch((error) => { - logger.error(error, "Error sending Microsoft Teams notification"); - }); - } - } - } else if (notification.type === TriggerFeature.SECRET_APPROVAL) { - if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) { - const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse( - microsoftTeamsConfig.secretRequestChannels - ); - - if (success && data) { - await microsoftTeamsService - .sendNotification({ - notification, - target: data, - tenantId: microsoftTeamsConfig.tenantId, - microsoftTeamsIntegrationId: microsoftTeamsConfig.id, - orgId: project.orgId - }) - .catch((error) => { - logger.error(error, "Error sending Microsoft Teams notification"); - }); - } - } - } - } + await Promise.allSettled(handlerPromises); } catch (error) { logger.error(error, "Error triggering workflow integration notification"); } diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts index f8f55eadd..08f75d89c 100644 --- a/backend/src/lib/workflow-integrations/types.ts +++ b/backend/src/lib/workflow-integrations/types.ts @@ -7,7 +7,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack export enum TriggerFeature { SECRET_APPROVAL = "secret-approval", ACCESS_REQUEST = "access-request", - ACCESS_REQUEST_UPDATED = "access-request-updated" + ACCESS_REQUEST_UPDATED = "access-request-updated", + SECRET_SYNC_ERROR = "secret-sync-error" } export type TNotification = @@ -51,6 +52,16 @@ export type TNotification = editorFullName?: string; editorEmail?: string; }; + } + | { + type: TriggerFeature.SECRET_SYNC_ERROR; + payload: { + syncName: string; + syncActionLabel: string; + syncDestination: string; + failureMessage: string; + syncUrl: string; + }; }; export type TTriggerWorkflowNotificationDTO = { diff --git a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts index 918b96e89..b1f9fec02 100644 --- a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts +++ b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts @@ -1,16 +1,20 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TMicrosoftTeamsIntegrations } from "@app/db/schemas"; +import { TProjectMicrosoftTeamsConfigs } from "@app/db/schemas/project-microsoft-teams-configs"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType; +export type TProjectMicrosoftTeamsConfigWithIntegrations = TProjectMicrosoftTeamsConfigs & TMicrosoftTeamsIntegrations; export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => { const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs); const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { - return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs) + return (tx || db.replicaNode())( + TableName.ProjectMicrosoftTeamsConfigs + ) .join( TableName.MicrosoftTeamsIntegrations, `${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`, diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index e83a0033f..acb2ea4d1 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -10,6 +10,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; +import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { SecretNameSchema } from "@app/server/lib/schemas"; import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; @@ -62,8 +64,11 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service"; +import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; +import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; export type TSecretSyncQueueFactory = ReturnType; @@ -104,6 +109,9 @@ type TSecretSyncQueueFactoryDep = { gatewayService: Pick; gatewayV2Service: Pick; notificationService: Pick; + projectSlackConfigDAL: Pick; + projectMicrosoftTeamsConfigDAL: Pick; + microsoftTeamsService: Pick; }; type SecretSyncActionJob = Job< @@ -147,7 +155,10 @@ export const secretSyncQueueFactory = ({ licenseService, gatewayService, gatewayV2Service, - notificationService + notificationService, + projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -923,32 +934,57 @@ export const secretSyncQueueFactory = ({ const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`; - await notificationService.createUserNotifications( - projectAdmins.map((admin) => ({ - userId: admin.userId, - orgId: project.orgId, - type: NotificationType.SECRET_SYNC_FAILED, - title: `Secret Sync Failed to ${actionLabel} Secrets`, - body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`, - link: syncPath - })) - ); + const notifications = [ + triggerWorkflowIntegrationNotification({ + input: { + notification: { + type: TriggerFeature.SECRET_SYNC_ERROR, + payload: { + syncName: name, + syncDestination, + failureMessage: failureMessage || "An unknown error occurred", + syncUrl: `${appCfg.SITE_URL}${syncPath}`, + syncActionLabel: actionLabel + } + }, + projectId: project.id + }, + dependencies: { + projectDAL, + projectSlackConfigDAL, + kmsService, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL + } + }), + notificationService.createUserNotifications( + projectAdmins.map((admin) => ({ + userId: admin.userId, + orgId: project.orgId, + type: NotificationType.SECRET_SYNC_FAILED, + title: `Secret Sync Failed to ${actionLabel} Secrets`, + body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`, + link: syncPath + })) + ), + smtpService.sendMail({ + recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + template: SmtpTemplates.SecretSyncFailed, + subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`, + substitutions: { + syncName: name, + syncDestination, + content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`, + failureMessage, + secretPath: folder?.path, + environment: environment?.name, + projectName: project.name, + syncUrl: `${appCfg.SITE_URL}${syncPath}` + } + }) + ]; - await smtpService.sendMail({ - recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), - template: SmtpTemplates.SecretSyncFailed, - subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`, - substitutions: { - syncName: name, - syncDestination, - content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`, - failureMessage, - secretPath: folder?.path, - environment: environment?.name, - projectName: project.name, - syncUrl: `${appCfg.SITE_URL}${syncPath}` - } - }); + await Promise.allSettled(notifications); }; const queueSecretSyncsSyncSecretsByPath = async ({ diff --git a/backend/src/services/slack/project-slack-config-dal.ts b/backend/src/services/slack/project-slack-config-dal.ts index 276442b1b..4b5b2146e 100644 --- a/backend/src/services/slack/project-slack-config-dal.ts +++ b/backend/src/services/slack/project-slack-config-dal.ts @@ -1,16 +1,17 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TProjectSlackConfigs, TSlackIntegrations } from "@app/db/schemas"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TProjectSlackConfigDALFactory = ReturnType; +export type TProjectSlackConfigWithIntegrations = TProjectSlackConfigs & TSlackIntegrations; export const projectSlackConfigDALFactory = (db: TDbClient) => { const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs); const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { - return (tx || db.replicaNode())(TableName.ProjectSlackConfigs) + return (tx || db.replicaNode())(TableName.ProjectSlackConfigs) .join( TableName.SlackIntegrations, `${TableName.ProjectSlackConfigs}.slackIntegrationId`, diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 88db1a84d..92684a707 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -8,6 +8,9 @@ import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/ty import { KmsDataKey } from "../kms/kms-types"; import { TSendSlackNotificationDTO } from "./slack-types"; +const COMPANY_BRAND_COLOR = "#e0ed34"; +const ERROR_COLOR = "#e74c3c"; + export const fetchSlackChannels = async (botKey: string) => { const slackChannels: { name: string; @@ -74,7 +77,8 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR }; } case TriggerFeature.ACCESS_REQUEST: { @@ -112,7 +116,8 @@ User Note: ${payload.note}` return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR }; } case TriggerFeature.ACCESS_REQUEST_UPDATED: { @@ -150,7 +155,52 @@ Editor Note: ${payload.editNote}` return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR + }; + } + case TriggerFeature.SECRET_SYNC_ERROR: { + const { payload } = notification; + const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel} + +Sync Error: ${payload.failureMessage}`; + + const payloadBlocks = [ + { + type: "header", + text: { + type: "plain_text", + text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, + emoji: true + } + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Sync Error:* ${payload.failureMessage}` + } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: `Open ${payload.syncName}`, + emoji: true + }, + url: payload.syncUrl + } + ] + } + ]; + + return { + payloadMessage: messageBody, + payloadBlocks, + color: ERROR_COLOR }; } default: { @@ -177,7 +227,7 @@ export const sendSlackNotification = async ({ }).toString("utf8"); const slackWebClient = new WebClient(botKey); - const { payloadMessage, payloadBlocks } = buildSlackPayload(notification); + const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification); for await (const conversationId of targetChannelIds) { // we send both text and blocks for compatibility with barebone clients @@ -185,7 +235,12 @@ export const sendSlackNotification = async ({ .postMessage({ channel: conversationId, text: payloadMessage, - blocks: payloadBlocks + attachments: [ + { + color, + blocks: payloadBlocks + } + ] }) .catch((err) => logger.error(err)); } From a76d013ce089e7f2ce3047e403500f7f856bc033 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 26 Oct 2025 16:48:01 -0700 Subject: [PATCH 003/271] continue pki docs restructuring --- docs/docs.json | 46 +++--- docs/documentation/platform/pki/acme-ca.mdx | 17 ++- .../documentation/platform/pki/azure-adcs.mdx | 136 +++++++++--------- .../pki/integration-guides/gloo-mesh.mdx | 14 +- .../documentation/platform/pki/pki-issuer.mdx | 2 +- 5 files changed, 116 insertions(+), 99 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 07f6cd773..6844850b0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -728,30 +728,30 @@ }, "documentation/platform/pki/subscribers", "documentation/platform/pki/certificates", - "documentation/platform/pki/acme-ca", - "documentation/platform/pki/azure-adcs", "documentation/platform/pki/est", - "documentation/platform/pki/alerting", - { - "group": "Integrations", - "pages": [ - "documentation/platform/pki/pki-issuer", - "documentation/platform/pki/integration-guides/gloo-mesh" - ] - }, - { - "group": "Certificate Syncs", - "pages": [ - "documentation/platform/pki/certificate-syncs/overview", - { - "group": "Syncs", - "pages": [ - "documentation/platform/pki/certificate-syncs/aws-certificate-manager", - "documentation/platform/pki/certificate-syncs/azure-key-vault" - ] - } - ] - } + "documentation/platform/pki/alerting" + ] + }, + { + "group": "Infrastructure Integrations", + "pages": [ + "documentation/platform/pki/pki-issuer", + "documentation/platform/pki/integration-guides/gloo-mesh" + ] + }, + { + "group": "Certificate Syncs", + "pages": [ + "documentation/platform/pki/certificate-syncs/overview", + "documentation/platform/pki/certificate-syncs/aws-certificate-manager", + "documentation/platform/pki/certificate-syncs/azure-key-vault" + ] + }, + { + "group": "CA Integrations", + "pages": [ + "documentation/platform/pki/acme-ca", + "documentation/platform/pki/azure-adcs" ] } ] diff --git a/docs/documentation/platform/pki/acme-ca.mdx b/docs/documentation/platform/pki/acme-ca.mdx index bf130da9e..475288ee0 100644 --- a/docs/documentation/platform/pki/acme-ca.mdx +++ b/docs/documentation/platform/pki/acme-ca.mdx @@ -1,5 +1,5 @@ --- -title: "Certificates with ACME CA" +title: "ACME CA" description: "Learn how to automatically provision and manage TLS certificates using ACME Certificate Authorities like Let's Encrypt with Infisical PKI" --- @@ -257,6 +257,7 @@ In the following steps, we explore how to set up ACME Certificate Authority inte - Downloaded directly from the Infisical UI - Retrieved via the Infisical API for programmatic access using the [latest certificate bundle endpoint](/api-reference/endpoints/pki/subscribers/get-latest-cert-bundle) + ## Example: Let's Encrypt Integration @@ -264,19 +265,23 @@ In the following steps, we explore how to set up ACME Certificate Authority inte Let's Encrypt is a free, automated, and open Certificate Authority that provides domain-validated SSL/TLS certificates. Here's how the integration works with Infisical: ### Production Environment + - **Directory URL**: `https://acme-v02.api.letsencrypt.org/directory` - **Rate Limits**: 50 certificates per registered domain per week - **Certificate Validity**: 90 days with automatic renewal - **Trusted By**: All major browsers and operating systems ### Staging Environment (for testing) + - **Directory URL**: `https://acme-staging-v02.api.letsencrypt.org/directory` - **Rate Limits**: Much higher limits for testing - **Certificate Validity**: 90 days (not trusted by browsers) - **Use Case**: Testing your ACME integration without hitting production rate limits - Always test your ACME integration using Let's Encrypt's staging environment first. This allows you to verify your DNS configuration and certificate issuance process without consuming your production rate limits. + Always test your ACME integration using Let's Encrypt's staging environment + first. This allows you to verify your DNS configuration and certificate + issuance process without consuming your production rate limits. ## Example: DigiCert Integration @@ -289,7 +294,9 @@ DigiCert is a leading commercial Certificate Authority providing a wide range of - **Trusted By**: All major browsers and operating systems. - When integrating with DigiCert ACME, ensure you have obtained the necessary External Account Binding (EAB) Key Identifier (KID) and HMAC Key from your DigiCert account. + When integrating with DigiCert ACME, ensure you have obtained the necessary + External Account Binding (EAB) Key Identifier (KID) and HMAC Key from your + DigiCert account. ## FAQ @@ -303,11 +310,13 @@ DigiCert is a leading commercial Certificate Authority providing a wide range of - Can be fully automated without manual intervention Support for additional DNS providers is planned for future releases. + Yes! ACME CAs like Let's Encrypt support wildcard certificates (e.g., `*.example.com`) when using DNS-01 validation. Simply specify the wildcard domain in your subscriber configuration. Note that wildcard certificates still require DNS-01 validation - HTTP-01 validation cannot be used for wildcard certificates. + Most ACME providers issue certificates with 90-day validity periods. This shorter validity period is designed to: @@ -317,6 +326,7 @@ DigiCert is a leading commercial Certificate Authority providing a wide range of - Ensure systems stay up-to-date with certificate management practices When configured, Infisical automatically handles certificate renewal for subscribers. + Yes! You can register multiple ACME CAs in the same project: @@ -326,5 +336,6 @@ DigiCert is a leading commercial Certificate Authority providing a wide range of - Backup providers for redundancy Each subscriber can be configured to use a specific ACME CA based on your requirements. + diff --git a/docs/documentation/platform/pki/azure-adcs.mdx b/docs/documentation/platform/pki/azure-adcs.mdx index b0cea565e..1a0e2d505 100644 --- a/docs/documentation/platform/pki/azure-adcs.mdx +++ b/docs/documentation/platform/pki/azure-adcs.mdx @@ -1,5 +1,5 @@ --- -title: "Certificates with Azure ADCS" +title: "Azure ADCS" description: "Learn how to issue and manage certificates using Microsoft Active Directory Certificate Services (ADCS) with Infisical." --- @@ -10,7 +10,7 @@ Issue and manage certificates using Microsoft Active Directory Certificate Servi Before setting up ADCS integration, ensure you have: - Microsoft Active Directory Certificate Services (ADCS) server running and accessible -- Domain administrator account with certificate management permissions +- Domain administrator account with certificate management permissions - ADCS web enrollment enabled on your server - Network connectivity from Infisical to the ADCS server - **IP whitelisting**: Your ADCS server must allow connections from Infisical's IP addresses @@ -24,67 +24,56 @@ This section walks you through the complete end-to-end process of setting up Azu - In your Infisical project, go to your **Certificate Project** → **Certificate Authority** to access the external CAs page. - - ![External CA Page](/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png) + In your Infisical project, go to your **Certificate Project** → + **Certificate Authority** to access the external CAs page. ![External CA + Page](/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png) - - - Click **Create CA** and configure: - - **Type**: Choose **Azure AD Certificate Service** - - **Name**: Friendly name for this CA (e.g., "Production ADCS CA") - - **App Connection**: Choose your ADCS connection from the dropdown - - ![External CA Form](/images/platform/pki/azure-adcs/azure-adcs-external-ca-form.png) - - - - Once created, your Azure ADCS Certificate Authority will appear in the list and be ready for use. - - ![External CA Created](/images/platform/pki/azure-adcs/azure-adcs-external-ca-created.png) - - - - Go to **Subscribers** to access the subscribers page. - - ![Subscribers Page](/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png) - - - - Click **Add Subscriber** and configure: - - **Name**: Unique subscriber name (e.g., "web-server-certs") - - **Certificate Authority**: Select your ADCS CA - - **Common Name**: Certificate CN (e.g., "api.example.com") - - **Certificate Template**: Select from dynamically loaded ADCS templates - - **Subject Alternative Names**: DNS names, IP addresses, or email addresses - - **TTL**: Certificate validity period (e.g., "1y" for 1 year) - - **Additional Subject Fields**: Organization, OU, locality, state, country, email (if required by template) - - ![Subscribers Form](/images/platform/pki/azure-adcs/azure-adcs-subscribers-form.png) - - - - Your subscriber is now created and ready to issue certificates. - - ![Subscriber Created](/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png) - - - - Click into your subscriber and click **Order Certificate** to generate a new certificate using your ADCS template. - - ![Issue New Certificate](/images/platform/pki/azure-adcs/azure-adcs-subscriber-issue-new-certificate.png) - - - - Your certificate has been successfully issued by the ADCS server and is ready for use. - - ![Certificate Created](/images/platform/pki/azure-adcs/azure-adcs-certificate-created.png) - - + + Click **Create CA** and configure: - **Type**: Choose **Azure AD Certificate + Service** - **Name**: Friendly name for this CA (e.g., "Production ADCS CA") - + **App Connection**: Choose your ADCS connection from the dropdown ![External + CA Form](/images/platform/pki/azure-adcs/azure-adcs-external-ca-form.png) + + + Once created, your Azure ADCS Certificate Authority will appear in the list + and be ready for use. ![External CA + Created](/images/platform/pki/azure-adcs/azure-adcs-external-ca-created.png) + + + Go to **Subscribers** to access the subscribers page. ![Subscribers + Page](/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png) + + + Click **Add Subscriber** and configure: - **Name**: Unique subscriber name + (e.g., "web-server-certs") - **Certificate Authority**: Select your ADCS CA - + **Common Name**: Certificate CN (e.g., "api.example.com") - **Certificate + Template**: Select from dynamically loaded ADCS templates - **Subject + Alternative Names**: DNS names, IP addresses, or email addresses - **TTL**: + Certificate validity period (e.g., "1y" for 1 year) - **Additional Subject + Fields**: Organization, OU, locality, state, country, email (if required by + template) ![Subscribers + Form](/images/platform/pki/azure-adcs/azure-adcs-subscribers-form.png) + + + Your subscriber is now created and ready to issue certificates. ![Subscriber + Created](/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png) + + + Click into your subscriber and click **Order Certificate** to generate a new + certificate using your ADCS template. ![Issue New + Certificate](/images/platform/pki/azure-adcs/azure-adcs-subscriber-issue-new-certificate.png) + + + Your certificate has been successfully issued by the ADCS server and is ready + for use. ![Certificate + Created](/images/platform/pki/azure-adcs/azure-adcs-certificate-created.png) + + - Navigate to **Certificates** to view detailed information about all issued certificates, including expiration dates, serial numbers, and certificate chains. - - ![Certificates Page](/images/platform/pki/azure-adcs/azure-adcs-certificates-page.png) + Navigate to **Certificates** to view detailed information about all issued + certificates, including expiration dates, serial numbers, and certificate + chains. ![Certificates + Page](/images/platform/pki/azure-adcs/azure-adcs-certificates-page.png) @@ -95,6 +84,7 @@ Infisical automatically retrieves available certificate templates from your ADCS ### Common Template Types ADCS templates you might see include: + - **Web Server**: For SSL/TLS certificates with server authentication - **Computer**: For machine authentication certificates - **User**: For client authentication certificates @@ -106,13 +96,16 @@ ADCS templates you might see include: ### Template Requirements Ensure your ADCS templates are configured with: + - **Enroll permissions** for your connection account - **Auto-enroll permissions** if using automated workflows - **Subject name requirements** matching your certificate requests - **Key usage extensions** appropriate for your use case -**Dynamic Template Discovery**: Infisical queries your ADCS server in real-time to populate available templates. Only templates you have permission to use will be displayed during certificate issuance. + **Dynamic Template Discovery**: Infisical queries your ADCS server in + real-time to populate available templates. Only templates you have permission + to use will be displayed during certificate issuance. ## Certificate Issuance Limitations @@ -120,10 +113,13 @@ Ensure your ADCS templates are configured with: ### Immediate Issuance Only -**Manual Approval Not Supported**: Infisical currently supports only **immediate certificate issuance**. Certificates that require manual approval or are held by ADCS policies cannot be issued through Infisical yet. + **Manual Approval Not Supported**: Infisical currently supports only + **immediate certificate issuance**. Certificates that require manual approval + or are held by ADCS policies cannot be issued through Infisical yet. For successful certificate issuance, ensure your ADCS templates and policies are configured to: + - **Auto-approve** certificate requests without manual intervention - **Not require** administrator approval for the templates you plan to use - **Allow** the connection account to request and receive certificates immediately @@ -131,19 +127,22 @@ For successful certificate issuance, ensure your ADCS templates and policies are ### What Happens with Manual Approval If a certificate request requires manual approval: + 1. The request will be submitted to ADCS successfully 2. Infisical will attempt to retrieve the certificate with exponential backoff (up to 5 retries over ~1 minute) 3. If the certificate is not approved within this timeframe, the request will **fail** 4. **No background polling**: Currently, Infisical does not check for certificates that might be approved hours or days later -**Future Enhancement**: Background polling for delayed certificate approvals is planned for future releases. + **Future Enhancement**: Background polling for delayed certificate approvals + is planned for future releases. ### Certificate Revocation -Certificate revocation is **not supported** by the Azure ADCS connector due to security and complexity considerations. + Certificate revocation is **not supported** by the Azure ADCS connector due to + security and complexity considerations. ## Advanced Configuration @@ -166,28 +165,33 @@ This allows Infisical to control certificate expiration dates directly. ### Common Issues **Certificate Request Denied** + - Verify ADCS template permissions for your connection account - Check template subject name requirements - Ensure template allows the requested key algorithm and size **Revocation Service Unavailable** + - Verify IIS is running and the revocation endpoint is accessible - Check IIS application pool permissions - Test endpoint connectivity from Infisical **Template Not Found** + - Verify template exists on ADCS server and is published - Check that your connection account has enrollment permissions for the template - Ensure the template is properly configured and available in the ADCS web enrollment interface - Templates are dynamically loaded - refresh the PKI Subscriber form if templates don't appear **Certificate Request Pending/Timeout** + - Check if your ADCS template requires manual approval - Infisical only supports immediate issuance - Verify the certificate template is configured for auto-approval - Ensure your connection account has sufficient permissions to request certificates without approval - Review ADCS server policies that might be holding the certificate request **Network Connectivity Issues** + - Verify your ADCS server's firewall allows connections from Infisical - For Infisical Cloud: Ensure Infisical's IP addresses are whitelisted (see [Networking Configuration](/documentation/setup/networking)) - For self-hosted: Whitelist your Infisical server's IP address on the ADCS server @@ -195,11 +199,13 @@ This allows Infisical to control certificate expiration dates directly. - Check for any network security appliances blocking the connection **Authentication Failures** + - Verify ADCS connection credentials - Check domain account permissions - Ensure network connectivity to ADCS server **SSL/TLS Certificate Errors** + - For ADCS servers with self-signed or private certificates: disable "Reject Unauthorized" in the SSL tab of your Azure ADCS app connection, or provide the certificate in PEM format - Common SSL errors: `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED` - The SSL configuration applies to all HTTPS communications between Infisical and your ADCS server diff --git a/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx b/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx index 2a04f5e3a..d1f1273fd 100644 --- a/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx +++ b/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx @@ -1,5 +1,5 @@ --- -title: "Gloo Mesh Integration" +title: "Gloo Mesh" description: "Learn how to automatically provision and manage Istio intermediate CA certificates for Gloo Mesh using Infisical PKI" --- @@ -7,8 +7,8 @@ This guide will provide a high level overview on how you can use Infisical PKI a ## Overview -In this setup, we will use Infisical PKI to generate and store your root CA and subordinate CAs that are used to generate Istio intermediate CAs for your Gloo Mesh workload clusters. -To manage the lifecycle of Istio intermediate CA certificates, you'll also install [cert-manager](https://cert-manager.io/). +In this setup, we will use Infisical PKI to generate and store your root CA and subordinate CAs that are used to generate Istio intermediate CAs for your Gloo Mesh workload clusters. +To manage the lifecycle of Istio intermediate CA certificates, you'll also install [cert-manager](https://cert-manager.io/). Cert-manager is a Kubernetes controller that helps you automate the process of obtaining and renewing certificates from various PKI providers. With this approach, you get the following benefits: @@ -18,10 +18,10 @@ With this approach, you get the following benefits: - Use cert-manager to automatically issue and renew Istio intermediate CA certificates from the same root, ensuring cross-cluster workload communication. - Increased auditability of private key infrastructure. - ## General Setup + The certificate provisioning workflow begins with setting up your PKI hierarchy in Infisical, where you create root and subordinate certificate authorities. -When you deploy a `Certificate` CRD in your workload cluster, `cert-manager` uses the Infisical PKI Issuer controller to authenticate with Infisical using machine identity credentials and request an intermediate CA certificate. +When you deploy a `Certificate` CRD in your workload cluster, `cert-manager` uses the Infisical PKI Issuer controller to authenticate with Infisical using machine identity credentials and request an intermediate CA certificate. Infisical verifies the request against your certificate templates and returns the signed certificate. From there, Istio's control plane will automatically use this intermediate CA to sign leaf certificates for workloads in the service mesh, enabling secure mTLS communication across your entire Gloo Mesh infrastructure. @@ -35,5 +35,5 @@ For Gloo Mesh-specific configuration, ensure that: ## Using the certificates -Once the `cacerts` Kubernetes secret is created in the `istio-system` namespace, Istio automatically uses the custom CA certificate instead of the default self-signed certificate. -When you deploy applications to your Gloo Mesh service mesh, the workloads will receive leaf certificates signed by your Infisical PKI intermediate CA, enabling secure mTLS communication across your entire mesh infrastructure. \ No newline at end of file +Once the `cacerts` Kubernetes secret is created in the `istio-system` namespace, Istio automatically uses the custom CA certificate instead of the default self-signed certificate. +When you deploy applications to your Gloo Mesh service mesh, the workloads will receive leaf certificates signed by your Infisical PKI intermediate CA, enabling secure mTLS communication across your entire mesh infrastructure. diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx index 13214bfb7..a1d07c98b 100644 --- a/docs/documentation/platform/pki/pki-issuer.mdx +++ b/docs/documentation/platform/pki/pki-issuer.mdx @@ -1,5 +1,5 @@ --- -title: "Cert Manager Issuer" +title: "Kubernetes Issuer" description: "Learn how to automatically provision and manage TLS certificates in Kubernetes using Infisical PKI" --- From c1dc3c2b7689a2e54174ded0a0263ede6aae2030 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 27 Oct 2025 09:29:16 -0300 Subject: [PATCH 004/271] feat(workflow-integration): enhance project workflow integration with secret sync error notifications and improve Slack notification formatting --- .../access-approval-request-service.ts | 4 +- .../src/lib/workflow-integrations/types.ts | 5 + backend/src/server/routes/index.ts | 5 +- .../src/server/routes/v1/project-router.ts | 12 +- .../src/services/project/project-service.ts | 20 +- backend/src/services/project/project-types.ts | 2 + .../services/secret-sync/secret-sync-queue.ts | 12 +- backend/src/services/slack/slack-fns.ts | 70 +++-- .../hooks/api/workflowIntegrations/types.ts | 4 + .../WorkflowIntegrationTab.tsx | 2 +- .../components/MicrosoftTeamsConfigRow.tsx | 4 +- .../components/SlackConfigRow.tsx | 17 +- .../components/SlackIntegrationForm.tsx | 255 ++++++++++-------- 13 files changed, 260 insertions(+), 152 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 1027995a7..83d3a23a7 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -243,7 +243,8 @@ export const accessApprovalRequestServiceFactory = ({ ); const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; - const approvalPath = `/projects/secret-management/${project.id}/approval`; + const projectPath = `/projects/secret-management/${project.id}`; + const approvalPath = `${projectPath}/approval`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; await triggerWorkflowIntegrationNotification({ @@ -252,6 +253,7 @@ export const accessApprovalRequestServiceFactory = ({ type: TriggerFeature.ACCESS_REQUEST, payload: { projectName: project.name, + projectPath, requesterFullName, isTemporary, requesterEmail: requestedByUser.email as string, diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts index 08f75d89c..db0725475 100644 --- a/backend/src/lib/workflow-integrations/types.ts +++ b/backend/src/lib/workflow-integrations/types.ts @@ -32,6 +32,7 @@ export type TNotification = secretPath: string; environment: string; projectName: string; + projectPath: string; permissions: string[]; approvalUrl: string; note?: string; @@ -61,6 +62,10 @@ export type TNotification = syncDestination: string; failureMessage: string; syncUrl: string; + environment: string; + secretPath: string; + projectName: string; + projectPath: string; }; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b42d01850..0125df798 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1244,7 +1244,10 @@ export const registerRoutes = async ( licenseService, gatewayService, gatewayV2Service, - notificationService + notificationService, + projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }); const secretQueueService = secretQueueFactory({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index c1f4140e5..3bd050ac8 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -769,7 +769,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { isAccessRequestNotificationEnabled: true, accessRequestChannels: true, isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + secretRequestChannels: true, + isSecretSyncErrorNotificationEnabled: true, + secretSyncErrorChannels: true }).merge( z.object({ integration: z.literal(WorkflowIntegration.SLACK), @@ -873,7 +875,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { accessRequestChannels: validateSlackChannelsField, secretRequestChannels: validateSlackChannelsField, isAccessRequestNotificationEnabled: z.boolean(), - isSecretRequestNotificationEnabled: z.boolean() + isSecretRequestNotificationEnabled: z.boolean(), + secretSyncErrorChannels: validateSlackChannelsField, + isSecretSyncErrorNotificationEnabled: z.boolean() }), z.object({ integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), @@ -891,7 +895,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { isAccessRequestNotificationEnabled: true, accessRequestChannels: true, isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + secretRequestChannels: true, + isSecretSyncErrorNotificationEnabled: true, + secretSyncErrorChannels: true }).merge( z.object({ integration: z.literal(WorkflowIntegration.SLACK), diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index e29f18404..3c6d541ce 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1568,8 +1568,14 @@ export const projectServiceFactory = ({ isAccessRequestNotificationEnabled, accessRequestChannels, isSecretRequestNotificationEnabled, - secretRequestChannels - }: TUpdateProjectWorkflowIntegration) => { + secretRequestChannels, + secretSyncErrorChannels, + isSecretSyncErrorNotificationEnabled + }: TUpdateProjectWorkflowIntegration & { + // workaround intersection type while we don't have the microsoft teams integration for failed secret syncs + isSecretSyncErrorNotificationEnabled: boolean; + secretSyncErrorChannels: string; + }) => { const project = await projectDAL.findById(projectId); if (!project) { throw new NotFoundError({ @@ -1591,6 +1597,7 @@ export const projectServiceFactory = ({ const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels); const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels); + const sanitizedSecretSyncErrorChannels = validateSlackChannelsField.parse(secretSyncErrorChannels); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId); @@ -1628,7 +1635,9 @@ export const projectServiceFactory = ({ isAccessRequestNotificationEnabled, accessRequestChannels: sanitizedAccessRequestChannels, isSecretRequestNotificationEnabled, - secretRequestChannels: sanitizedSecretRequestChannels + secretRequestChannels: sanitizedSecretRequestChannels, + isSecretSyncErrorNotificationEnabled, + secretSyncErrorChannels: sanitizedSecretSyncErrorChannels }, tx ); @@ -1641,7 +1650,9 @@ export const projectServiceFactory = ({ isAccessRequestNotificationEnabled, accessRequestChannels: sanitizedAccessRequestChannels, isSecretRequestNotificationEnabled, - secretRequestChannels: sanitizedSecretRequestChannels + secretRequestChannels: sanitizedSecretRequestChannels, + isSecretSyncErrorNotificationEnabled, + secretSyncErrorChannels: sanitizedSecretSyncErrorChannels }, tx ); @@ -1651,6 +1662,7 @@ export const projectServiceFactory = ({ ...updatedWorkflowIntegration, accessRequestChannels: sanitizedAccessRequestChannels, secretRequestChannels: sanitizedSecretRequestChannels, + secretSyncErrorChannels: sanitizedSecretSyncErrorChannels, integrationId: slackIntegration.id, integration: WorkflowIntegration.SLACK } as const; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 18ae74350..318b08a8d 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -184,8 +184,10 @@ export type TUpdateProjectWorkflowIntegration = ( integration: WorkflowIntegration.SLACK; isAccessRequestNotificationEnabled: boolean; isSecretRequestNotificationEnabled: boolean; + isSecretSyncErrorNotificationEnabled: boolean; accessRequestChannels?: string; secretRequestChannels?: string; + secretSyncErrorChannels?: string; } | { integrationId: string; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index acb2ea4d1..f6e23dded 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -932,7 +932,9 @@ export const secretSyncQueueFactory = ({ break; } - const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`; + const baseProjectPath = `/projects/secret-management/${projectId}`; + const overviewPath = `${baseProjectPath}/overview`; + const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`; const notifications = [ triggerWorkflowIntegrationNotification({ @@ -944,10 +946,14 @@ export const secretSyncQueueFactory = ({ syncDestination, failureMessage: failureMessage || "An unknown error occurred", syncUrl: `${appCfg.SITE_URL}${syncPath}`, - syncActionLabel: actionLabel + syncActionLabel: actionLabel, + environment: environment?.name || "-", + secretPath: folder?.path || "-", + projectName: project.name, + projectPath: overviewPath } }, - projectId: project.id + projectId }, dependencies: { projectDAL, diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 92684a707..d813cb9ae 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -76,6 +76,7 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo ]; return { + headerBlocks: [], payloadMessage: messageBody, payloadBlocks, color: COMPANY_BRAND_COLOR @@ -83,20 +84,15 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo } case TriggerFeature.ACCESS_REQUEST: { const { payload } = notification; - const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${ - payload.isTemporary ? "temporary" : "permanent" - } access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}. - -The following permissions are requested: ${payload.permissions.join(", ")} + const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`; + const accessType = payload.isTemporary ? "temporary" : "permanent"; + const permissionsFormatted = payload.permissions.map((p) => `*${p}*`).join(", "); -View the request and approve or deny it <${payload.approvalUrl}|here>.${ - payload.note - ? ` -User Note: ${payload.note}` - : "" + const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${accessType} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.\n\nThe following permissions are requested: ${payload.permissions.join(", ")}${ + payload.note ? `\n\nUser note\n${payload.note}` : "" }`; - const payloadBlocks = [ + const headerBlocks = [ { type: "header", text: { @@ -104,17 +100,38 @@ User Note: ${payload.note}` text: "New access approval request pending for review", emoji: true } - }, + } + ]; + + const payloadBlocks = [ { type: "section", text: { type: "mrkdwn", - text: messageBody + text: `*${payload.requesterFullName}* (${payload.requesterEmail}) has requested *${accessType}* access to *${payload.secretPath}* in the *${payload.environment}* environment of *<${projectUrl}|${payload.projectName}>*.\n\nThe following permissions are requested: ${permissionsFormatted}${ + payload.note ? `\n\n*User note*\n${payload.note}` : "" + }` } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View request", + emoji: true + }, + style: "primary", + url: payload.approvalUrl + } + ] } ]; return { + headerBlocks, payloadMessage: messageBody, payloadBlocks, color: COMPANY_BRAND_COLOR @@ -125,7 +142,7 @@ User Note: ${payload.note}` const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${ payload.isTemporary ? "temporary" : "permanent" } access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}. - + The following permissions are requested: ${payload.permissions.join(", ")} View the request and approve or deny it <${payload.approvalUrl}|here>.${ @@ -154,6 +171,7 @@ Editor Note: ${payload.editNote}` ]; return { + headerBlocks: [], payloadMessage: messageBody, payloadBlocks, color: COMPANY_BRAND_COLOR @@ -161,24 +179,26 @@ Editor Note: ${payload.editNote}` } case TriggerFeature.SECRET_SYNC_ERROR: { const { payload } = notification; - const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel} + const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`; + const messageBody = `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}\n\n\nEnvironment: ${payload.environment}\n\n\nSecret Path: ${payload.secretPath}\n\n\nProject: ${payload.projectName} (${projectUrl})\n\n\nReason:\n${payload.failureMessage}`; -Sync Error: ${payload.failureMessage}`; - - const payloadBlocks = [ + const headerBlocks = [ { type: "header", text: { type: "plain_text", - text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, + text: `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, emoji: true } - }, + } + ]; + + const payloadBlocks = [ { type: "section", text: { type: "mrkdwn", - text: `*Sync Error:* ${payload.failureMessage}` + text: `*Environment*\n${payload.environment}\n\n\n*Secret Path*\n${payload.secretPath}\n\n\n*Project*\n<${projectUrl}|${payload.projectName}>\n\n\n*Reason*\n${payload.failureMessage}` } }, { @@ -188,9 +208,10 @@ Sync Error: ${payload.failureMessage}`; type: "button", text: { type: "plain_text", - text: `Open ${payload.syncName}`, + text: "Open secret sync", emoji: true }, + style: "primary", url: payload.syncUrl } ] @@ -199,6 +220,7 @@ Sync Error: ${payload.failureMessage}`; return { payloadMessage: messageBody, + headerBlocks, payloadBlocks, color: ERROR_COLOR }; @@ -227,14 +249,16 @@ export const sendSlackNotification = async ({ }).toString("utf8"); const slackWebClient = new WebClient(botKey); - const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification); + const { payloadMessage, payloadBlocks, color, headerBlocks } = buildSlackPayload(notification); for await (const conversationId of targetChannelIds) { // we send both text and blocks for compatibility with barebone clients + await slackWebClient.chat .postMessage({ channel: conversationId, text: payloadMessage, + blocks: headerBlocks, attachments: [ { color, diff --git a/frontend/src/hooks/api/workflowIntegrations/types.ts b/frontend/src/hooks/api/workflowIntegrations/types.ts index 668850124..7c51ae259 100644 --- a/frontend/src/hooks/api/workflowIntegrations/types.ts +++ b/frontend/src/hooks/api/workflowIntegrations/types.ts @@ -86,6 +86,8 @@ export type ProjectWorkflowIntegrationConfig = accessRequestChannels: string; isSecretRequestNotificationEnabled: boolean; secretRequestChannels: string; + isSecretSyncErrorNotificationEnabled: boolean; + secretSyncErrorChannels: string; } | { id: string; @@ -112,6 +114,8 @@ export type TUpdateProjectWorkflowIntegrationConfigDTO = accessRequestChannels: string; isSecretRequestNotificationEnabled: boolean; secretRequestChannels: string; + isSecretSyncErrorNotificationEnabled: boolean; + secretSyncErrorChannels: string; } | { integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx index fb48717cc..2dbb43fe6 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx @@ -111,7 +111,7 @@ export const WorkflowIntegrationTab = () => { Provider Access Request Notifications Destination Secret Request Notifications Destination - + Secret Sync Error Notifications Destination diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx index ef7617d52..ff1f2622b 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx @@ -92,7 +92,9 @@ export const MicrosoftTeamsConfigRow = ({ Disabled )} - + + Coming Soon + diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx index df7211f99..418d7982d 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx @@ -89,7 +89,22 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon Disabled )} - + + {slackConfig.isSecretSyncErrorNotificationEnabled && + !isLoadingConfig && + slackConfig.secretSyncErrorChannels.length > 0 ? ( + + {slackConfig.secretSyncErrorChannels + .split(", ") + .map((channel) => slackChannelIdToName[channel]) + .join(", ")} + + ) : isLoadingConfig ? ( + + ) : ( + Disabled + )} + diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index bcc2d81b5..d73206969 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -37,7 +37,9 @@ const formSchema = z.object({ isSecretRequestNotificationEnabled: z.boolean(), secretRequestChannels: z.string().array(), isAccessRequestNotificationEnabled: z.boolean(), - accessRequestChannels: z.string().array() + accessRequestChannels: z.string().array(), + isSecretSyncErrorNotificationEnabled: z.boolean(), + secretSyncErrorChannels: z.string().array() }); type TSlackConfigForm = z.infer; @@ -71,7 +73,9 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { isAccessRequestNotificationEnabled: false, accessRequestChannels: [], isSecretRequestNotificationEnabled: false, - secretRequestChannels: [] + secretRequestChannels: [], + isSecretSyncErrorNotificationEnabled: false, + secretSyncErrorChannels: [] } }); @@ -87,7 +91,8 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { integration: WorkflowIntegrationPlatform.SLACK, integrationId: data.slackIntegrationId, accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), - secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ") + secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", "), + secretSyncErrorChannels: data.secretSyncErrorChannels.filter(Boolean).join(", ") }); createNotification({ @@ -107,6 +112,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); const selectedSlackIntegrationId = watch("slackIntegrationId"); const accessRequestNotifState = watch("isAccessRequestNotificationEnabled"); + const secretSyncErrorNotifState = watch("isSecretSyncErrorNotificationEnabled"); const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId); const slackChannelIdToName = Object.fromEntries( @@ -117,7 +123,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { ); useEffect(() => { - if (slackConfig) { + if (slackConfig && slackConfig.integration === WorkflowIntegrationPlatform.SLACK) { setValue("slackIntegrationId", slackConfig.integrationId); setValue( "isSecretRequestNotificationEnabled", @@ -127,22 +133,30 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { "isAccessRequestNotificationEnabled", slackConfig.isAccessRequestNotificationEnabled ); + setValue( + "isSecretSyncErrorNotificationEnabled", + slackConfig.isSecretSyncErrorNotificationEnabled + ); - if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) { - if (slackChannels) { - setValue( - "secretRequestChannels", - slackConfig.secretRequestChannels - .split(", ") - .filter((channel) => channel in slackChannelIdToName) - ); - setValue( - "accessRequestChannels", - slackConfig.accessRequestChannels - .split(", ") - .filter((channel) => channel in slackChannelIdToName) - ); - } + if (slackChannels) { + setValue( + "secretRequestChannels", + slackConfig.secretRequestChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); + setValue( + "accessRequestChannels", + slackConfig.accessRequestChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); + setValue( + "secretSyncErrorChannels", + slackConfig.secretSyncErrorChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); } } }, [slackConfig, slackChannels]); @@ -215,54 +229,14 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { control={control} name="secretRequestChannels" render={({ field: { value, onChange }, fieldState: { error } }) => ( - - - - slackChannelIdToName[entry]) - .join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`secret-requests-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - - + )} /> )} @@ -288,54 +262,47 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { control={control} name="accessRequestChannels" render={({ field: { value, onChange }, fieldState: { error } }) => ( - - - - slackChannelIdToName[entry]) - .join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`access-requests-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - + + )} + /> + )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Secret Sync Errors

+
+ ); + }} + /> + {secretSyncErrorNotifState && ( + ( + )} /> )} @@ -353,3 +320,63 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { ); }; + +type TChannelSelectorProps = { + value: string[]; + onChange: (value: string[]) => void; + error?: { message?: string }; + slackChannelIdToName: Record; + sortedSlackChannels?: { id: string; name: string }[]; + keyPrefix: string; +}; + +const ChannelSelector = ({ + value, + onChange, + error, + slackChannelIdToName, + sortedSlackChannels, + keyPrefix +}: TChannelSelectorProps) => ( + + + + slackChannelIdToName[entry]).join(", ")} + className="text-left" + /> + + + {sortedSlackChannels?.map((slackChannel) => { + const isChecked = value?.includes(slackChannel.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== slackChannel.id) + : [...(value || []), slackChannel.id] + ); + }} + key={`${keyPrefix}-slack-channel-${slackChannel.id}`} + iconPos="right" + icon={isChecked && } + > + {slackChannel.name} + + ); + })} + + + +); \ No newline at end of file From 16cf4a91c00df3fbbf8212b9940bdece752648b2 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 27 Oct 2025 09:44:09 -0300 Subject: [PATCH 005/271] fix(slack-integration): improve channel handling by filtering empty values and update Microsoft Teams badge status --- .../workflow-integrations/notification-handlers/slack.ts | 6 +++--- .../components/MicrosoftTeamsConfigRow.tsx | 2 +- .../components/SlackIntegrationForm.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts index 66fd58b3a..b55f172cb 100644 --- a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts +++ b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts @@ -25,15 +25,15 @@ const handleSlackNotification = async ({ switch (notification.type) { case TriggerFeature.ACCESS_REQUEST: case TriggerFeature.ACCESS_REQUEST_UPDATED: - targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; + targetChannelIds = slackConfig.accessRequestChannels?.split(", ").filter(Boolean) || []; isEnabled = slackConfig.isAccessRequestNotificationEnabled; break; case TriggerFeature.SECRET_APPROVAL: - targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; + targetChannelIds = slackConfig.secretRequestChannels?.split(", ").filter(Boolean) || []; isEnabled = slackConfig.isSecretRequestNotificationEnabled; break; case TriggerFeature.SECRET_SYNC_ERROR: - targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ") || []; + targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ").filter(Boolean) || []; isEnabled = slackConfig.isSecretSyncErrorNotificationEnabled; break; default: diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx index ff1f2622b..66a228ebe 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx @@ -93,7 +93,7 @@ export const MicrosoftTeamsConfigRow = ({ )} - Coming Soon + Disabled diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index d73206969..6092a465e 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -141,19 +141,19 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { if (slackChannels) { setValue( "secretRequestChannels", - slackConfig.secretRequestChannels + (slackConfig.secretRequestChannels || "") .split(", ") .filter((channel) => channel in slackChannelIdToName) ); setValue( "accessRequestChannels", - slackConfig.accessRequestChannels + (slackConfig.accessRequestChannels || "") .split(", ") .filter((channel) => channel in slackChannelIdToName) ); setValue( "secretSyncErrorChannels", - slackConfig.secretSyncErrorChannels + (slackConfig.secretSyncErrorChannels || "") .split(", ") .filter((channel) => channel in slackChannelIdToName) ); From 66e7b44fef0eef0baf16580809a171d2b472d70c Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 27 Oct 2025 10:20:07 -0300 Subject: [PATCH 006/271] feat(secret-sync): add support for secret sync error notifications in project router and Slack integration, including channel selection enhancements --- .../routes/v1/deprecated-project-router.ts | 8 +- .../src/services/project/project-service.ts | 4 +- .../components/SlackIntegrationForm.tsx | 123 +++++++++--------- 3 files changed, 71 insertions(+), 64 deletions(-) diff --git a/backend/src/server/routes/v1/deprecated-project-router.ts b/backend/src/server/routes/v1/deprecated-project-router.ts index 687d95b9b..f6efdb78a 100644 --- a/backend/src/server/routes/v1/deprecated-project-router.ts +++ b/backend/src/server/routes/v1/deprecated-project-router.ts @@ -614,8 +614,10 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider integrationId: z.string(), accessRequestChannels: validateSlackChannelsField, secretRequestChannels: validateSlackChannelsField, + secretSyncErrorChannels: validateSlackChannelsField, isAccessRequestNotificationEnabled: z.boolean(), - isSecretRequestNotificationEnabled: z.boolean() + isSecretRequestNotificationEnabled: z.boolean(), + isSecretSyncErrorNotificationEnabled: z.boolean() }), z.object({ integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), @@ -633,7 +635,9 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider isAccessRequestNotificationEnabled: true, accessRequestChannels: true, isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + secretRequestChannels: true, + isSecretSyncErrorNotificationEnabled: true, + secretSyncErrorChannels: true }).merge( z.object({ integration: z.literal(WorkflowIntegration.SLACK), diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 3c6d541ce..19a13f45b 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1573,8 +1573,8 @@ export const projectServiceFactory = ({ isSecretSyncErrorNotificationEnabled }: TUpdateProjectWorkflowIntegration & { // workaround intersection type while we don't have the microsoft teams integration for failed secret syncs - isSecretSyncErrorNotificationEnabled: boolean; - secretSyncErrorChannels: string; + isSecretSyncErrorNotificationEnabled?: boolean; + secretSyncErrorChannels?: string; }) => { const project = await projectDAL.findById(projectId); if (!project) { diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx index 6092a465e..cf5113fad 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -44,6 +44,69 @@ const formSchema = z.object({ type TSlackConfigForm = z.infer; +type TChannelSelectorProps = { + value: string[]; + onChange: (value: string[]) => void; + error?: { message?: string }; + slackChannelIdToName: Record; + sortedSlackChannels?: { id: string; name: string }[]; + keyPrefix: string; +}; + +const ChannelSelector = ({ + value, + onChange, + error, + slackChannelIdToName, + sortedSlackChannels, + keyPrefix +}: TChannelSelectorProps) => ( + + + + slackChannelIdToName[entry]) + .join(", ")} + className="text-left" + /> + + + {sortedSlackChannels?.map((slackChannel) => { + const isChecked = value?.includes(slackChannel.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== slackChannel.id) + : [...(value || []), slackChannel.id] + ); + }} + key={`${keyPrefix}-slack-channel-${slackChannel.id}`} + iconPos="right" + icon={isChecked && } + > + {slackChannel.name} + + ); + })} + + + +); + type Props = { onClose: () => void; }; @@ -320,63 +383,3 @@ export const SlackIntegrationForm = ({ onClose }: Props) => { ); }; - -type TChannelSelectorProps = { - value: string[]; - onChange: (value: string[]) => void; - error?: { message?: string }; - slackChannelIdToName: Record; - sortedSlackChannels?: { id: string; name: string }[]; - keyPrefix: string; -}; - -const ChannelSelector = ({ - value, - onChange, - error, - slackChannelIdToName, - sortedSlackChannels, - keyPrefix -}: TChannelSelectorProps) => ( - - - - slackChannelIdToName[entry]).join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`${keyPrefix}-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - - -); \ No newline at end of file From c4c4ff4086b98c31c8746f64a667fd565b4f8819 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 29 Oct 2025 12:43:28 -0700 Subject: [PATCH 007/271] finish updating ca docs --- docs/docs.json | 34 +++- .../platform/pki/{ => ca}/acme-ca.mdx | 0 .../platform/pki/{ => ca}/azure-adcs.mdx | 0 .../platform/pki/ca/external-ca.mdx | 50 +++++ .../platform/pki/ca/overview.mdx | 13 ++ .../platform/pki/{ => ca}/private-ca.mdx | 17 +- .../pki/certificates/certificates.mdx | 4 + .../platform/pki/certificates/overview.mdx | 14 ++ .../platform/pki/certificates/profiles.mdx | 4 + .../platform/pki/certificates/templates.mdx | 4 + .../pki/enrollment-methods/overview.mdx | 0 .../platform/pki/external-ca.mdx | 192 ------------------ docs/documentation/platform/pki/overview.mdx | 4 +- 13 files changed, 128 insertions(+), 208 deletions(-) rename docs/documentation/platform/pki/{ => ca}/acme-ca.mdx (100%) rename docs/documentation/platform/pki/{ => ca}/azure-adcs.mdx (100%) create mode 100644 docs/documentation/platform/pki/ca/external-ca.mdx create mode 100644 docs/documentation/platform/pki/ca/overview.mdx rename docs/documentation/platform/pki/{ => ca}/private-ca.mdx (93%) create mode 100644 docs/documentation/platform/pki/certificates/certificates.mdx create mode 100644 docs/documentation/platform/pki/certificates/overview.mdx create mode 100644 docs/documentation/platform/pki/certificates/profiles.mdx create mode 100644 docs/documentation/platform/pki/certificates/templates.mdx create mode 100644 docs/documentation/platform/pki/enrollment-methods/overview.mdx delete mode 100644 docs/documentation/platform/pki/external-ca.mdx diff --git a/docs/docs.json b/docs/docs.json index 6844850b0..4b75fe35e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -722,8 +722,18 @@ { "group": "Certificate Authorities", "pages": [ - "documentation/platform/pki/private-ca", - "documentation/platform/pki/external-ca" + "documentation/platform/pki/ca/overview", + "documentation/platform/pki/ca/private-ca", + "documentation/platform/pki/ca/external-ca" + ] + }, + { + "group": "Certificates", + "pages": [ + "documentation/platform/pki/certificates/overview", + "documentation/platform/pki/certificates/profiles", + "documentation/platform/pki/certificates/templates", + "documentation/platform/pki/certificates/certificates" ] }, "documentation/platform/pki/subscribers", @@ -750,8 +760,8 @@ { "group": "CA Integrations", "pages": [ - "documentation/platform/pki/acme-ca", - "documentation/platform/pki/azure-adcs" + "documentation/platform/pki/ca/acme-ca", + "documentation/platform/pki/ca/azure-adcs" ] } ] @@ -2939,6 +2949,22 @@ { "source": "/sdks/languages/csharp", "destination": "/sdks/languages/dotnet" + }, + { + "source": "/documentation/platform/pki/private-ca", + "destination": "/documentation/platform/pki/ca/private-ca" + }, + { + "source": "/documentation/platform/pki/external-ca", + "destination": "/documentation/platform/pki/ca/external-ca" + }, + { + "source": "/documentation/platform/pki/acme-ca", + "destination": "/documentation/platform/pki/ca/acme-ca" + }, + { + "source": "/documentation/platform/pki/azure-adcs", + "destination": "/documentation/platform/pki/ca/azure-adcs" } ] } diff --git a/docs/documentation/platform/pki/acme-ca.mdx b/docs/documentation/platform/pki/ca/acme-ca.mdx similarity index 100% rename from docs/documentation/platform/pki/acme-ca.mdx rename to docs/documentation/platform/pki/ca/acme-ca.mdx diff --git a/docs/documentation/platform/pki/azure-adcs.mdx b/docs/documentation/platform/pki/ca/azure-adcs.mdx similarity index 100% rename from docs/documentation/platform/pki/azure-adcs.mdx rename to docs/documentation/platform/pki/ca/azure-adcs.mdx diff --git a/docs/documentation/platform/pki/ca/external-ca.mdx b/docs/documentation/platform/pki/ca/external-ca.mdx new file mode 100644 index 000000000..1dc89ec96 --- /dev/null +++ b/docs/documentation/platform/pki/ca/external-ca.mdx @@ -0,0 +1,50 @@ +--- +title: "External CA" +sidebarTitle: "External CA" +description: "Learn how to connect External Certificate Authorities with Infisical." +--- + +## Concept + +Infisical lets you integrate with External Certificate Authorities (CAs), allowing you to use existing PKI infrastructure or connect to public CAs to issue digital certificates for your end-entities. + +
+ +```mermaid +graph TD + A1[External Public CA
e.g. Let's Encrypt, ZeroSSL, ...] --> Infisical + A2[External Private CA
e.g. AWS Private CA, HashiCorp Vault PKI, ...] --> Infisical +``` + +
+ +As shown above, these CAs commonly fall under two categories: + +- External Private CAs: CAs like AWS Private CA, HashiCorp Vault PKI, Azure ADCS, etc. that are privately owned and are used to issue certificates for internal services; these are often either cloud-hosted private CAs or on-prem / enterprise CAs. +- External Public CAs: CAs like Let's Encrypt, DigiCert, GlobalSign, etc. that are publicly trusted and are used to issue certificates for public-facing services. + +Note that Infisical can also act as an _ACME client_, allowing you to integrate upstream with any ACME-compatible CA to automate certificate issuance and renewal. + +## Workflow + +A typical workflow for integrating an External CA with Infisical consists of choosing the desired External CA type +and specifying the configuration or connection details necessary to connect to the CA. + +The specific steps and requirements vary depending on the External CA type you choose to integrate. + +## Supported External CA Types + +Infisical currently supports the following External CA types out of the box: + +- [ACME CA](/documentation/platform/pki/ca/acme-ca): An ACME-compatible CA that supports the ACME protocol, such as Let's Encrypt, ZeroSSL, Buypass, Digicert, etc. +- [Azure ADCS](/documentation/platform/pki/ca/azure-adcs): A Microsoft Active Directory Certificate Services (ADCS) that supports the ADCS protocol, such as AWS Private CA, Azure ADCS, etc. + +If you don’t see a specific external CA listed here or need a dedicated integration guide, please reach out to sales@infisical.com and we’ll help you set up the integration for your external CA. + +## FAQ + + + + Yes. You can have both Private and External CAs in the same project. + + diff --git a/docs/documentation/platform/pki/ca/overview.mdx b/docs/documentation/platform/pki/ca/overview.mdx new file mode 100644 index 000000000..9a815992a --- /dev/null +++ b/docs/documentation/platform/pki/ca/overview.mdx @@ -0,0 +1,13 @@ +--- +title: "Overview" +sidebarTitle: "Overview" +--- + +Before issuing and managing certificates with Infisical, you'll need to configure a Certificate Authority (CA). + +This is the trusted entity that signs and validates the X.509 certificates used to secure your end-entities. + +Infisical supports two categories of CAs: + +- [Internal CA](/documentation/platform/pki/ca/private-ca): Internally operated root and intermediate CAs managed within Infisical. This is useful if you need complete control over your PKI and are issuing certificates for private networks, internal services, or managed devices. +- [External CA](/documentation/platform/pki/ca/external-ca): Third-party public (e.g. Let's Encrypt, DigiCert) or private (e.g. AWS Private CA, HashiCorp Vault PKI, etc.) CAs that can be integrated with Infisical. This is useful if you want to leverage existing PKI infrastructure or issue publicly trusted certificates. diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/ca/private-ca.mdx similarity index 93% rename from docs/documentation/platform/pki/private-ca.mdx rename to docs/documentation/platform/pki/ca/private-ca.mdx index 7d7ee1220..e6065e49e 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/ca/private-ca.mdx @@ -1,13 +1,12 @@ --- -title: "Private CA" -sidebarTitle: "Private CA" +title: "Internal CA" +sidebarTitle: "Internal CA" description: "Learn how to create a Private CA hierarchy with Infisical." --- ## Concept -The first step to creating your Internal PKI is to create a Private Certificate Authority (CA) hierarchy that is a structure of entities -used to issue digital certificates for your [subscribers](/documentation/platform/pki/subscribers). +Infisical lets you build your Internal PKI through a Private Certificate Authority (CA) hierarchy, enabling you to issue and manage digital certificates for your end-entities.
@@ -47,7 +46,7 @@ consisting of an (optional) root CA and an intermediate CA. If you wish to use an external root CA, you can skip this step and head to step 2 to create an intermediate CA. - To create a root CA, head to your Project > Internal PKI > Certificate Authorities and press **Create CA**. + To create a root CA, head to your Certificate Management Project > Certificate Authorities > Internal Certificate Authorities and press **Create CA**. ![pki create ca](/images/platform/pki/ca/ca-create.png) @@ -55,18 +54,17 @@ consisting of an (optional) root CA and an intermediate CA. ![pki create root ca](/images/platform/pki/ca/ca-create-root.png) - Here's some guidance on each field: + Here's some guidance for each field: - Valid Until: The date until which the CA is valid in the date time string format specified [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). For example, the following formats would be valid: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss.sssZ`. - Path Length: The maximum number of intermediate CAs that can be chained to this CA. A path of `-1` implies no limit; a path of `0` implies no intermediate CAs can be chained. - Key Algorithm: The type of public key algorithm and size, in bits, of the key pair that the CA creates when it issues a certificate. Supported key algorithms are `RSA 2048`, `RSA 4096`, `ECDSA P-256`, and `ECDSA P-384` with the default being `RSA 2048`. - - Friendly Name: A friendly name for the CA; this is only for display and defaults to the subject of the CA if left empty. + - Name: A slug-friendly name for the CA. - Organization (O): The organization name. - Country (C): The country code. - State or Province Name: The state or province. - Locality Name: The city or locality. - Common Name: The name of the CA. - - Require Template for Certificate Issuance: Whether or not certificates for this CA can only be issued through certificate templates (recommended). The Organization, Country, State or Province Name, Locality Name, and Common Name make up the **Distinguished Name (DN)** or **subject** of the CA. @@ -98,8 +96,7 @@ consisting of an (optional) root CA and an intermediate CA. ![pki cas](/images/platform/pki/ca/cas.png) - Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. - Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. + Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the Certificates section to learn more about how to issue X.509 certificates using the intermediate CA. 2.3b. If you have an external root CA, select **External CA** for the **Parent CA Type** field. diff --git a/docs/documentation/platform/pki/certificates/certificates.mdx b/docs/documentation/platform/pki/certificates/certificates.mdx new file mode 100644 index 000000000..bc7d83428 --- /dev/null +++ b/docs/documentation/platform/pki/certificates/certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Certificates" +sidebarTitle: "Certificates" +--- diff --git a/docs/documentation/platform/pki/certificates/overview.mdx b/docs/documentation/platform/pki/certificates/overview.mdx new file mode 100644 index 000000000..9fa167d76 --- /dev/null +++ b/docs/documentation/platform/pki/certificates/overview.mdx @@ -0,0 +1,14 @@ +--- +title: "Overview" +sidebarTitle: "Overview" +--- + +To issue a certificate with Infisical, you'll need to create a certificate profile and a certificate template to go along with it. + +There are three components to understand: + +- [Certificate Profile](/documentation/platform/pki/certificates/profiles): A configuration set specifying how certificates should be issued under that profile including the [issuing CA](/documentation/platform/pki/ca/overview), a certificate template, and the enrollment method (such as ACME, EST, API, etc.) used to enroll certificates. When requesting a certificate, you issue it against a specific profile. + +- [Certificate Template](/documentation/platform/pki/certificates/templates): A policy specifying the structure and permitted attributes of a certificate, such as subject naming conventions, SAN fields, key usages, and extended key usages. + +- [Certificate](/documentation/platform/pki/certificates/certificate): The actual X.509 certificate issued for a profile. Once issued, a certificate kept track of in the certificate inventory. diff --git a/docs/documentation/platform/pki/certificates/profiles.mdx b/docs/documentation/platform/pki/certificates/profiles.mdx new file mode 100644 index 000000000..12daba67d --- /dev/null +++ b/docs/documentation/platform/pki/certificates/profiles.mdx @@ -0,0 +1,4 @@ +--- +title: "Certificate Profiles" +sidebarTitle: "Profiles" +--- diff --git a/docs/documentation/platform/pki/certificates/templates.mdx b/docs/documentation/platform/pki/certificates/templates.mdx new file mode 100644 index 000000000..d613816c5 --- /dev/null +++ b/docs/documentation/platform/pki/certificates/templates.mdx @@ -0,0 +1,4 @@ +--- +title: "Certificate Templates" +sidebarTitle: "Templates" +--- diff --git a/docs/documentation/platform/pki/enrollment-methods/overview.mdx b/docs/documentation/platform/pki/enrollment-methods/overview.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/docs/documentation/platform/pki/external-ca.mdx b/docs/documentation/platform/pki/external-ca.mdx deleted file mode 100644 index efe029149..000000000 --- a/docs/documentation/platform/pki/external-ca.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: "External CA" -sidebarTitle: "External CA" -description: "Learn how to connect External Certificate Authorities with Infisical." ---- - -## Concept - -In addition to creating a Private CA hierarchy, Infisical allows you to integrate with External Certificate Authorities (CAs) to issue digital certificates for your [subscribers](/documentation/platform/pki/subscribers). This integration enables you to leverage established certificate authority infrastructure while centralizing your certificate management within Infisical. - -
- -```mermaid -graph TD - B[Infisical] -->|Manages Certificates| D[Subscribers] - - A1[Public CAs
Let's Encrypt, ZeroSSL] -->|ACME Protocol| B - A2[Enterprise CAs
Vault PKI, Step CA] -->|ACME Protocol| B - A3[Cloud CAs
ACME-compatible services] -->|ACME Protocol| B - - A4[Future: Enterprise CAs] -.->|EST/SCEP Protocols| B - A5[Future: Cloud CAs] -.->|REST APIs| B -``` - -
- -When you integrate an External CA with Infisical, you benefit from: - -1. **Trust by Default**: Certificates issued by public CAs are trusted by default in browsers and operating systems. -2. **Unified Management**: Manage all certificates—both internally and externally issued—from a single platform. -3. **Automation**: Leverage Infisical's automation capabilities for certificate lifecycle management. -4. **Compliance**: Meet requirements for publicly trusted certificates, especially for public-facing services. -5. **Flexibility**: Choose the most appropriate CA for different use cases while maintaining consistent management. - -## General Workflow - -A typical workflow for integrating an External CA with Infisical consists of the following steps: - -1. **Select External CA Type**: Choose the appropriate external CA based on your requirements and supported protocols. -2. **Configure Prerequisites**: Set up any required credentials, connections, or configurations specific to your chosen CA type. -3. **Register External CA**: Add the External CA configuration to your Infisical project. -4. **Create Subscribers**: Set up subscribers that use the External CA as their issuing authority. -5. **Manage Certificate Lifecycle**: Handle certificate issuance, renewal, and revocation through Infisical's unified interface. - -The specific steps and requirements vary depending on the External CA type you choose to integrate. - -## Supported Integration Methods - -Infisical currently supports integration with External Certificate Authorities through the following protocol: - -### ACME Protocol Integration - -ACME (Automatic Certificate Management Environment) is a widely adopted protocol for automated certificate issuance and management. Infisical can integrate with any CA that supports the ACME protocol, including: - -**Public Certificate Authorities:** -- Let's Encrypt - Free, automated SSL/TLS certificates -- ZeroSSL - Free and premium SSL certificates -- Buypass - Norwegian CA with free ACME certificates - -**Enterprise Certificate Authorities:** -- HashiCorp Vault PKI - Enterprise secret management with ACME support -- Step CA - Open-source certificate authority with ACME - -**Cloud Certificate Authorities:** -- Some managed certificate services that support ACME protocol - -[Learn more about ACME integration →](/documentation/platform/pki/acme-ca) - -## Use Cases - -External CA integration is ideal for various scenarios: - -### Public-Facing Services -Use publicly trusted CAs for websites and services that need browser compatibility: -- Web applications and APIs -- Load balancers and CDNs -- Public-facing microservices - -### Compliance Requirements -Meet specific compliance standards that require certificates from accredited CAs: -- PCI DSS compliance -- SOC 2 requirements -- Industry-specific regulations - -### Hybrid Infrastructure -Combine internal and external CAs for different use cases: -- Internal services with Private CAs -- Public services with External CAs -- Development vs. production environments - -### Legacy System Integration -Integrate with existing enterprise PKI infrastructure: -- Windows Active Directory Certificate Services -- Network device management -- IoT device provisioning - -## Benefits of Centralized Management - -Managing External CAs through Infisical provides several advantages over direct CA management: - -### Unified Certificate Inventory -- Single dashboard for all certificates -- Centralized expiration tracking -- Cross-CA certificate analytics - -### Automated Lifecycle Management -- Automatic certificate reissuance before expiration -- Proactive expiration alerts -- Standardized certificate management processes - -### Enhanced Security -- Centralized access controls -- Audit trails for all certificate operations -- Policy enforcement across CAs - -### Operational Efficiency -- Reduced manual certificate management -- Consistent deployment workflows -- API-driven automation -- Integration with existing tools - -## Available Integration Guides - -Get started with External CA integration: - - - - Set up automated certificate issuance with any ACME-compatible CA - - - Custom CA integrations via REST APIs (Coming Soon) - - - -## FAQ - - - - Currently, Infisical supports any Certificate Authority that implements the ACME protocol, including: - - - **Public CAs**: Let's Encrypt, ZeroSSL, Buypass - - **Enterprise CAs**: HashiCorp Vault PKI, Step CA - - **Cloud CAs**: ACME-compatible managed services - - Integration uses DNS-01 validation through Route53 or Cloudflare. Learn more about [supported DNS validation methods](/documentation/platform/pki/acme-ca#what-dns-validation-methods-are-supported). - - Support for additional integration protocols (EST, SCEP, direct APIs) is planned for future releases. - - - Yes. You can have both Private CAs (root and intermediate) and External CAs in the same project, allowing you flexibility in how you issue certificates for different use cases. This hybrid approach enables you to: - - - Use Private CAs for internal services and applications - - Use External CAs for public-facing services - - Apply consistent management practices across all certificate types - - Implement appropriate security controls based on certificate usage - - - The types of certificates you can issue depend on the External CA provider and type: - - - **Public CAs**: Typically support Domain Validation (DV) certificates, with some offering Organization Validation (OV) - - **Enterprise CAs**: Support internal certificates, device certificates, and custom certificate types - - **Cloud CAs**: Support various certificate types depending on the service - - Certificate capabilities vary by provider and integration method. - - - Certificate reissuance is handled automatically by Infisical based on the CA type: - - - **Public CAs**: Automatic reissuance using ACME protocol with the same certificate extensions before expiration - - **Other CA types**: Certificate management methods depend on the specific integration (when available) - - All certificate lifecycle events are tracked and managed through Infisical's unified interface, ensuring continuous certificate validity. - - - Authentication methods vary by CA type: - - - **Public CAs**: ACME account registration with email and account keys - - **Enterprise CAs**: Client certificates, username/password, or domain authentication (when available) - - **Cloud CAs**: API keys, OAuth tokens, or service account authentication (when available) - - Infisical securely stores and manages all authentication credentials. - - - Yes, Infisical provides policy enforcement capabilities: - - - Certificate template constraints - - Monitoring and alerting policies - - Access controls for certificate operations - - These policies ensure consistent governance across both internal and external certificate sources. - - diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index bc5ea0591..7faaad4d6 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -10,8 +10,8 @@ It helps teams automate certificate management including enrollment and renewal, Core capabilities include: -- Private CA: Create and manage your own private CA hierarchy including root and intermediate CAs. -- External CA integration: Integrate with external public and private CAs including Azure ADCS and ACME-compatible CAs like Let's Encrypt and DigiCert. +- [Private CA](/documentation/platform/pki/ca/private-ca): Create and manage your own private CA hierarchy including root and intermediate CAs. +- [External CA integration](/documentation/platform/pki/ca/external-ca): Integrate with external public and private CAs including [Azure ADCS](/documentation/platform/pki/ca/azure-adcs) and [ACME-compatible CAs](/documentation/platform/pki/ca/acme-ca) like Let's Encrypt and DigiCert. - Certificate Enrollment: Support enrollment methods including API, ACME, EST, and more to automate certificate issuance for services, devices, and workloads. - Certificate Inventory: Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. - Certificate Lifecycle Automation: Automate issuance, renewal, and revocation with policy-based workflows, ensuring certificates remain valid, compliant, and up to date across your infrastructure. From 910705fcd75405b18a9019f0b0107fec80994a0c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 31 Oct 2025 16:46:07 -0700 Subject: [PATCH 008/271] further progress on docs --- docs/docs.json | 10 +++++++ .../pki/certificates/certificates.mdx | 7 +++++ .../platform/pki/certificates/overview.mdx | 6 ++--- .../platform/pki/certificates/profiles.mdx | 22 ++++++++++++++++ .../platform/pki/certificates/templates.mdx | 24 +++++++++++++++++ .../platform/pki/enrollment-methods/acme.mdx | 4 +++ .../platform/pki/enrollment-methods/api.mdx | 26 +++++++++++++++++++ .../platform/pki/enrollment-methods/est.mdx | 4 +++ .../platform/pki/enrollment-methods/scep.mdx | 4 +++ 9 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 docs/documentation/platform/pki/enrollment-methods/acme.mdx create mode 100644 docs/documentation/platform/pki/enrollment-methods/api.mdx create mode 100644 docs/documentation/platform/pki/enrollment-methods/est.mdx create mode 100644 docs/documentation/platform/pki/enrollment-methods/scep.mdx diff --git a/docs/docs.json b/docs/docs.json index 4b75fe35e..616affd65 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -736,6 +736,16 @@ "documentation/platform/pki/certificates/certificates" ] }, + { + "group": "Enrollment Methods", + "pages": [ + "documentation/platform/pki/enrollment-methods/overview", + "documentation/platform/pki/enrollment-methods/api", + "documentation/platform/pki/enrollment-methods/est", + "documentation/platform/pki/enrollment-methods/acme", + "documentation/platform/pki/enrollment-methods/scep" + ] + }, "documentation/platform/pki/subscribers", "documentation/platform/pki/certificates", "documentation/platform/pki/est", diff --git a/docs/documentation/platform/pki/certificates/certificates.mdx b/docs/documentation/platform/pki/certificates/certificates.mdx index bc7d83428..e4888db86 100644 --- a/docs/documentation/platform/pki/certificates/certificates.mdx +++ b/docs/documentation/platform/pki/certificates/certificates.mdx @@ -2,3 +2,10 @@ title: "Certificates" sidebarTitle: "Certificates" --- + +## Concept + +A certificate is the actual X.509 certificate issued for a certificate profile. + +Once issued, a certificate is kept track of in the certificate inventory +where you can manage various aspects of its lifecycle including deployment to cloud key stores, server-side auto-renewal behavior, revocation, and more. diff --git a/docs/documentation/platform/pki/certificates/overview.mdx b/docs/documentation/platform/pki/certificates/overview.mdx index 9fa167d76..e60acf1a0 100644 --- a/docs/documentation/platform/pki/certificates/overview.mdx +++ b/docs/documentation/platform/pki/certificates/overview.mdx @@ -3,12 +3,12 @@ title: "Overview" sidebarTitle: "Overview" --- -To issue a certificate with Infisical, you'll need to create a certificate profile and a certificate template to go along with it. +To issue a certificate with Infisical, you create a certificate profile and a certificate template to go along with it. You then issue a certificate by making a request against that specific profile. There are three components to understand: -- [Certificate Profile](/documentation/platform/pki/certificates/profiles): A configuration set specifying how certificates should be issued under that profile including the [issuing CA](/documentation/platform/pki/ca/overview), a certificate template, and the enrollment method (such as ACME, EST, API, etc.) used to enroll certificates. When requesting a certificate, you issue it against a specific profile. +- [Certificate Profile](/documentation/platform/pki/certificates/profiles): A configuration set specifying how certificates should be issued under that profile including the [issuing CA](/documentation/platform/pki/ca/overview), a certificate template, and the enrollment method (such as ACME, EST, API, etc.) used to enroll certificates. -- [Certificate Template](/documentation/platform/pki/certificates/templates): A policy specifying the structure and permitted attributes of a certificate, such as subject naming conventions, SAN fields, key usages, and extended key usages. +- [Certificate Template](/documentation/platform/pki/certificates/templates): A policy structure specifying the permitted attributes for requested certificates including subject naming conventions, SAN fields, key usages, and extended key usages. - [Certificate](/documentation/platform/pki/certificates/certificate): The actual X.509 certificate issued for a profile. Once issued, a certificate kept track of in the certificate inventory. diff --git a/docs/documentation/platform/pki/certificates/profiles.mdx b/docs/documentation/platform/pki/certificates/profiles.mdx index 12daba67d..30289138e 100644 --- a/docs/documentation/platform/pki/certificates/profiles.mdx +++ b/docs/documentation/platform/pki/certificates/profiles.mdx @@ -2,3 +2,25 @@ title: "Certificate Profiles" sidebarTitle: "Profiles" --- + +## Concept + +A certificate profile is a configuration set specifying how leaf certificates should be issued for a group of end-entities including the [issuing CA](/documentation/platform/pki/ca/overview), a [certificate template](/documentation/platform/pki/certificates/templates), and the enrollment method (e.g. ACME, EST, API, etc.) used to enroll certificates. + +You typically request certificates against a certificate profile through its associated enrollment method. Each method defines its own interaction flow which you can read more about in its respective documentation. + +## Guide to Creating a Certificate Profile + +To create a certificate profile, head to your Certificate Management Project > Certificates > Certificate Profiles and press **Create Profile**. + +TODO: image + +Here's some guidance on each field: + +- Name: A slug-friendly name for the profile such as `web-servers`. +- Description: An optional description for the profile. +- Issuing CA: The [issuing CA](/documentation/platform/pki/ca/overview) that should be used to issue certificates for the profile. +- Certificate Template: The [certificate template](/documentation/platform/pki/certificates/templates) that should be used to validate certificate requests for the profile. +- Enrollment Method: The enrollment method that should be used to enroll certificates for the profile such as ACME, EST, API, etc. + +Depending on which enrollment method you choose, you may be presented with additional enrollment-specific configuration fields. diff --git a/docs/documentation/platform/pki/certificates/templates.mdx b/docs/documentation/platform/pki/certificates/templates.mdx index d613816c5..633123a16 100644 --- a/docs/documentation/platform/pki/certificates/templates.mdx +++ b/docs/documentation/platform/pki/certificates/templates.mdx @@ -2,3 +2,27 @@ title: "Certificate Templates" sidebarTitle: "Templates" --- + +## Concept + +A certificate template is a policy structure specifying permitted attributes for requested certificates. This includes constraints around subject naming conventions, SAN fields, key usages, and extended key usages. + +Each certificate requested against a profile is validated against the template bound to that profile. If the request fails any criteria included in the template, the certificate is not issued. This helps administrators enforce uniformity and security standards across all issued certificates. + +## Guide to Creating a Certificate Template + +To create a certificate template, head to your Certificate Management Project > Certificates > Certificate Templates and press **Create Template**. + +TODO: image + +Here's some guidance on each field: + +- Template Name: The name of the template such as `tls-server`. +- Description: An optional description for the template. +- Subject Attributes: A list of common names that can be included in the certificate subject. Each row accepts a fixed value or pattern such as `example.com` or `*.example.com` and whether it is allowed or denied. +- Subject Alternative Names (SANs): A list of SANs that can appear in the certificate. Each row accepts a SAN type (e.g. DNS, IP, Email, URI), a fixed value or pattern such as `example.com` or `*.example.com`, and an allow or deny flag. +- Allowed Signature Algorithms: The set of signature algorithms permitted to sign certificates under this template such as `SHA256-RSA`, `SHA512-RSA`, etc. +- Allowed Key Algorithms: The set of public key algorithms permitted for certificate requests such as `RSA-2048`, `RSA-4096`, etc. +- Key Usages: The cryptographic purposes of the certificate such as Digital Signature, Key Encipherment, etc. +- Extended Key Usages: The higher-level intended uses of the certificate such as Server Authentication, Client Authentication, etc. +- Certificate Validity: The maximum lifetime of certificates that can be requested for certificates validated against this template. You can specify both a duration and unit (days, months, or years). diff --git a/docs/documentation/platform/pki/enrollment-methods/acme.mdx b/docs/documentation/platform/pki/enrollment-methods/acme.mdx new file mode 100644 index 000000000..a0700f366 --- /dev/null +++ b/docs/documentation/platform/pki/enrollment-methods/acme.mdx @@ -0,0 +1,4 @@ +--- +title: "Certificate Enrollment via ACME" +sidebarTitle: "ACME" +--- diff --git a/docs/documentation/platform/pki/enrollment-methods/api.mdx b/docs/documentation/platform/pki/enrollment-methods/api.mdx new file mode 100644 index 000000000..22bf5c833 --- /dev/null +++ b/docs/documentation/platform/pki/enrollment-methods/api.mdx @@ -0,0 +1,26 @@ +--- +title: "Certificate Enrollment via API" +sidebarTitle: "API" +--- + + + +
    +
  • + Enable Auto-Renewal: Whether or not to opt-in issued certificates for + (server-side) auto-renewal. +
  • +
  • + Auto-Renewal Days: The number of days before the certificate expires to + trigger certificate renewal. +
  • +
+ + Note that auto-renewal only applies to certificates issued through + CSR-less enrollment where key generation is done internally by + Infisical; conversely certificates issued via CSR submission are not eligible for auto-renewal. + + +
+ Test +
diff --git a/docs/documentation/platform/pki/enrollment-methods/est.mdx b/docs/documentation/platform/pki/enrollment-methods/est.mdx new file mode 100644 index 000000000..56ec5bb7d --- /dev/null +++ b/docs/documentation/platform/pki/enrollment-methods/est.mdx @@ -0,0 +1,4 @@ +--- +title: "Certificate Enrollment via EST" +sidebarTitle: "EST" +--- diff --git a/docs/documentation/platform/pki/enrollment-methods/scep.mdx b/docs/documentation/platform/pki/enrollment-methods/scep.mdx new file mode 100644 index 000000000..3c9902b2e --- /dev/null +++ b/docs/documentation/platform/pki/enrollment-methods/scep.mdx @@ -0,0 +1,4 @@ +--- +title: "Certificate Enrollment via SCEP" +sidebarTitle: "SCEP" +--- From c3248a0c4c466242d679c97ff8b485601c98aa56 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 16:30:53 -0300 Subject: [PATCH 009/271] Refactor migration to use TableName constants for project_slack_configs schema changes --- ...ure-slack-secret-sync-error-notification.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts index 4b5bf54d3..203333fed 100644 --- a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts +++ b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts @@ -1,28 +1,30 @@ import { Knex } from "knex"; +import { TableName } from "@app/db/schemas"; + export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled"))) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (!(await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "isSecretSyncErrorNotificationEnabled"))) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.boolean("isSecretSyncErrorNotificationEnabled").notNullable().defaultTo(false); }); } - if (!(await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels"))) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (!(await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "secretSyncErrorChannels"))) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.text("secretSyncErrorChannels").notNullable().defaultTo(""); }); } } export async function down(knex: Knex): Promise { - if (await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled")) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "isSecretSyncErrorNotificationEnabled")) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.dropColumn("isSecretSyncErrorNotificationEnabled"); }); } - if (await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels")) { - await knex.schema.alterTable("project_slack_configs", (table) => { + if (await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "secretSyncErrorChannels")) { + await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => { table.dropColumn("secretSyncErrorChannels"); }); } From 88e6c5badfd88ab9c6eb84c50825738a147d3d7f Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 3 Nov 2025 16:40:50 -0300 Subject: [PATCH 010/271] Update SlackConfigRow component to improve error notification display and styling --- .../components/SlackConfigRow.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx index f4ba81823..afabe826c 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx @@ -100,16 +100,19 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon {slackConfig.isSecretSyncErrorNotificationEnabled && !isLoadingConfig && slackConfig.secretSyncErrorChannels.length > 0 ? ( - +

{slackConfig.secretSyncErrorChannels .split(", ") .map((channel) => slackChannelIdToName[channel]) .join(", ")} - +

) : isLoadingConfig ? ( ) : ( - Disabled + + + Disabled + )} From 80329e69ddbb43792cd1108b89dff1023869b575 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 5 Nov 2025 16:16:39 -0800 Subject: [PATCH 011/271] continue pki v3 docs --- docs/docs.json | 15 +- .../pki/certificates/certificates.mdx | 151 +++++++++++++++- .../platform/pki/certificates/overview.mdx | 5 +- .../platform/pki/certificates/profiles.mdx | 2 +- .../platform/pki/enrollment-methods/api.mdx | 162 ++++++++++++++++-- .../platform/pki/enrollment-methods/est.mdx | 64 +++++++ .../pki/enrollment-methods/overview.mdx | 11 ++ docs/documentation/platform/pki/overview.mdx | 7 +- .../PkiManagerLayout/PkiManagerLayout.tsx | 14 +- 9 files changed, 396 insertions(+), 35 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 616affd65..66976630b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -741,14 +741,9 @@ "pages": [ "documentation/platform/pki/enrollment-methods/overview", "documentation/platform/pki/enrollment-methods/api", - "documentation/platform/pki/enrollment-methods/est", - "documentation/platform/pki/enrollment-methods/acme", - "documentation/platform/pki/enrollment-methods/scep" + "documentation/platform/pki/enrollment-methods/est" ] }, - "documentation/platform/pki/subscribers", - "documentation/platform/pki/certificates", - "documentation/platform/pki/est", "documentation/platform/pki/alerting" ] }, @@ -2975,6 +2970,14 @@ { "source": "/documentation/platform/pki/azure-adcs", "destination": "/documentation/platform/pki/ca/azure-adcs" + }, + { + "source": "/documentation/platform/pki/certificates", + "destination": "/documentation/platform/pki/certificates/certificates" + }, + { + "source": "/documentation/platform/pki/est", + "destination": "/documentation/platform/pki/enrollment-methods/est" } ] } diff --git a/docs/documentation/platform/pki/certificates/certificates.mdx b/docs/documentation/platform/pki/certificates/certificates.mdx index e4888db86..b27e50145 100644 --- a/docs/documentation/platform/pki/certificates/certificates.mdx +++ b/docs/documentation/platform/pki/certificates/certificates.mdx @@ -3,9 +3,158 @@ title: "Certificates" sidebarTitle: "Certificates" --- + + PKI architecture is a complex topic and there are many ways to orchestrate + certificate management including renewal operations. For specific guidance and + access to enterprise features, we recommend reaching out to + sales@infisical.com to schedule a demo. + + ## Concept -A certificate is the actual X.509 certificate issued for a certificate profile. +A certificate is the (X.509) leaf certificate issued for a certificate profile. Once issued, a certificate is kept track of in the certificate inventory where you can manage various aspects of its lifecycle including deployment to cloud key stores, server-side auto-renewal behavior, revocation, and more. + +## Guide to Issuing Certificates + +To issue a certificate, you must first create a [certificate profile](/documentation/platform/pki/certificates/profiles) and a [certificate template](/documentation/platform/pki/certificates/templates) to go along with it. + +The [enrollment method](/documentation/platform/pki/enrollment-methods/overview) configured on the certificate profile determines how a certificate is issued for it. +Refer to the documentation for each enrollment method below to learn more about how to issue certificates using it. + +- [API](/documentation/platform/pki/certificates/api): Issue a certificate over UI or by making an API request to Infisical. +- [EST](/documentation/platform/pki/certificates/est): Issue a certificate over the EST protocol. +- [ACME](/documentation/platform/pki/certificates/acme): Issue a certificate over the ACME protocol. +- [SCEP](/documentation/platform/pki/certificates/scep): Issue a certificate over the SCEP protocol. + +## Guide to Renewing Certificates + +To [renew a certificate](/documentation/platform/pki/concepts/certificate-lifecycle#renewal), you can either request a new certificate from a certificate profile or have the platform +automatically request a new one for you. Whether you pursue a client-driven or server-driven approach is totally dependent on the enrollment method configured on your certificate +profile as well as your infrastructure use-case. + +### Client-Driven Certificate Renewal + +Client-driven certificate renewal is when renewal is initiated client-side by the end-entity consuming the certificate. +This is the most common approach to certificate renewal and is suitable for most use-cases. + +### Server-Driven Certificate Renewal + +Server-driven certificate renewal is when renewal is initiated server-side by Infisical rather than by the end-entity consuming the certificate. +When a certificate considered for auto-renewal meets a specified _renewal days before expiration_ threshold, Infisical reaches out to the issuing CA bound to the [certificate profile](/documentation/platform/pki/certificates/profiles) of the expiring certificate +to request for a new one. +The resulting renewed certificate is stored in the platform and made available to be fetched back or pushed downstream to end-entities or external systems such as cloud key stores. + +Note that server-driven certificate renewal is only available for certificates issued via the [API enrollment method](/documentation/platform/pki/enrollment-methods/api) where key pairs are generated server-side. +A certificate can be considered for auto-renewal at time of issuance if the **Enable Auto-Renewal By Default** option is selected on its [certificate profile](/documentation/platform/pki/certificates/profiles) or after issuance by toggling this option manually. + +The following examples demonstrate different approaches to certificate renewal: + +- Using the ACME enrollment method, you may connect an ACME client like [certbot](https://certbot.eff.org/) to fetch back and renew certificates for Apache, Nginx, or other server. The ACME client will pursue a client-driven approach and submit certificate requests upon certificate expiration for you, saving renewed certificates back to the server's configuration. +- Using the ACME enrollment method, you may use [cert-manager](https://cert-manager.io/) with Infisical to issue and renew certificates for Kubernetes workloads; cert-manager will pursue a client-driven approach and submit certificate requests upon certificate expiration for you, saving renewed certificates back to Kubernetes secrets. +- Using the API enrollment method, you may push and auto-renew certificates to AWS and Azure using [certificate syncs](/documentation/platform/pki/certificate-syncs/overview). Certificates issued over the API enrollment method, where key pairs are generated server-side, are also eligible for server-side auto-renewal; once renewed, certificates are automatically pushed back to their sync destination. + +## Guide to Revoking Certificates + +In the following steps, we explore how to revoke a X.509 certificate and obtain a Certificate Revocation List (CRL) for a CA. + + + + + + Assuming that you've issued a certificate under a CA, you can revoke it by + selecting the **Revoke Certificate** option for it and specifying the reason + for revocation. + + ![pki revoke certificate](/images/platform/pki/cert-revoke.png) + + ![pki revoke certificate modal](/images/platform/pki/cert-revoke-modal.png) + + + + In order to check the revocation status of a certificate, you can check it + against the CRL of a CA by heading to its Issuing CA and downloading the CRL. + + ![pki view crl](/images/platform/pki/ca-crl.png) + + To verify a certificate against the + downloaded CRL with OpenSSL, you can use the following command: + +```bash +openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem +``` + +Note that you can also obtain the CRL from the certificate itself by +referencing the CRL distribution point extension on the certificate. + +To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command: + +```bash +openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem +``` + + + + + + + + Assuming that you've issued a certificate under a CA, you can revoke it by making an API request to the [Revoke Certificate](/api-reference/endpoints/certificate-authorities/revoke) API endpoint, + specifying the serial number of the certificate and the reason for revocation. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates//revoke' \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "revocationReason": "UNSPECIFIED" + }' + ``` + + ### Sample response + + ```bash Response + { + message: "Successfully revoked certificate", + serialNumber: "...", + revokedAt: "..." + } + ``` + + + In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA. + To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint. + + ### Sample request + + ```bash Request + curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca//crls' \ + --header 'Authorization: Bearer ' + ``` + + ### Sample response + + ```bash Response + [ + { + id: "...", + crl: "..." + }, + ... + ] + ``` + + To verify a certificate against the CRL with OpenSSL, you can use the following command: + + ```bash + openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem + ``` + + + + + diff --git a/docs/documentation/platform/pki/certificates/overview.mdx b/docs/documentation/platform/pki/certificates/overview.mdx index e60acf1a0..a4688388a 100644 --- a/docs/documentation/platform/pki/certificates/overview.mdx +++ b/docs/documentation/platform/pki/certificates/overview.mdx @@ -3,11 +3,12 @@ title: "Overview" sidebarTitle: "Overview" --- -To issue a certificate with Infisical, you create a certificate profile and a certificate template to go along with it. You then issue a certificate by making a request against that specific profile. +To issue a certificate with Infisical, you create a certificate profile and a certificate template to go along with it. You then issue a certificate against +a specific profile depending on the enrollment method associated with it. There are three components to understand: -- [Certificate Profile](/documentation/platform/pki/certificates/profiles): A configuration set specifying how certificates should be issued under that profile including the [issuing CA](/documentation/platform/pki/ca/overview), a certificate template, and the enrollment method (such as ACME, EST, API, etc.) used to enroll certificates. +- [Certificate Profile](/documentation/platform/pki/certificates/profiles): A configuration set specifying how certificates should be issued under that profile including the [issuing CA](/documentation/platform/pki/ca/overview), a certificate template, and the [enrollment method](/documentation/platform/pki/enrollment-methods/overview) (such as ACME, EST, API, etc.) used to enroll certificates. - [Certificate Template](/documentation/platform/pki/certificates/templates): A policy structure specifying the permitted attributes for requested certificates including subject naming conventions, SAN fields, key usages, and extended key usages. diff --git a/docs/documentation/platform/pki/certificates/profiles.mdx b/docs/documentation/platform/pki/certificates/profiles.mdx index 30289138e..3d4fb9380 100644 --- a/docs/documentation/platform/pki/certificates/profiles.mdx +++ b/docs/documentation/platform/pki/certificates/profiles.mdx @@ -5,7 +5,7 @@ sidebarTitle: "Profiles" ## Concept -A certificate profile is a configuration set specifying how leaf certificates should be issued for a group of end-entities including the [issuing CA](/documentation/platform/pki/ca/overview), a [certificate template](/documentation/platform/pki/certificates/templates), and the enrollment method (e.g. ACME, EST, API, etc.) used to enroll certificates. +A certificate profile is a configuration set specifying how leaf certificates should be issued for a group of end-entities including the [issuing CA](/documentation/platform/pki/ca/overview), a [certificate template](/documentation/platform/pki/certificates/templates), and the [enrollment method](/documentation/platform/pki/enrollment-methods/overview) (e.g. ACME, EST, API, etc.) used to enroll certificates. You typically request certificates against a certificate profile through its associated enrollment method. Each method defines its own interaction flow which you can read more about in its respective documentation. diff --git a/docs/documentation/platform/pki/enrollment-methods/api.mdx b/docs/documentation/platform/pki/enrollment-methods/api.mdx index 22bf5c833..e628f736c 100644 --- a/docs/documentation/platform/pki/enrollment-methods/api.mdx +++ b/docs/documentation/platform/pki/enrollment-methods/api.mdx @@ -3,24 +3,156 @@ title: "Certificate Enrollment via API" sidebarTitle: "API" --- +## Concept + +The API enrollment method allows you to issue certificates against a specific certificate profile over Web UI or by making an API request to Infisical. + +## Guide to Certificate Enrollment via API + +In the following steps, we explore how to issue a X.509 certificate using the API enrollment method. + - -
    -
  • - Enable Auto-Renewal: Whether or not to opt-in issued certificates for - (server-side) auto-renewal. -
  • -
  • - Auto-Renewal Days: The number of days before the certificate expires to - trigger certificate renewal. -
  • -
+ + + + + Create a [certificate + profile](/documentation/platform/pki/certificates/profiles) with **API** + selected as the enrollment method. + + Notice that the API enrollment method supports an option called **Enable Auto-Renewal By Default**. + If selected, _eligible_ certificates are automatically considered for server-side auto-renewal based + on a specified renewal days before expiration threshold at the time of issuance; for more information + about server-side auto-renewal, refer to the documentation [here](/documentation/platform/pki/certificates/certificates#guide-to-renewing-certificates). + + + + To create a certificate, head to your Project > Certificates > Certificates and press **Issue**. + + TODO: Image + +Here, select the certificate profile from step 1 that will be used to issue the certificate and fill out the rest of the details for the certificate to be issued. + + + + Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**. + + TODO: Image + - Note that auto-renewal only applies to certificates issued through - CSR-less enrollment where key generation is done internally by - Infisical; conversely certificates issued via CSR submission are not eligible for auto-renewal. + Make sure to download and store the **Private Key** in a secure location as it + will only be displayed once at the time of certificate issuance. The + **Certificate Body** and **Certificate Chain** will remain accessible and can + be copied at any time. + + + + + + + + A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection. + + With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year. + + To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificate-templates' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "caId": "", + "name": "My Certificate Template", + "commonName": ".*.acme.com", + "subjectAlternativeName": ".*.acme.com", + "ttl": "1y", + }' + ``` + + ### Sample response + + ```bash Response + { + id: "...", + caId: "...", + name: "...", + commonName: "...", + subjectAlternativeName: "...", + ttl: "...", + } + ``` + + + + To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, + specifying the issuing CA. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/issue-certificate' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "certificateTemplateId": "", + "commonName": "service.acme.com", + "ttl": "1y", + }' + ``` + + ### Sample response + + ```bash Response + { + certificate: "...", + certificateChain: "...", + issuingCaCertificate: "...", + privateKey: "...", + serialNumber: "..." + } + ``` + + + Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** + and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. + + That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. + + + + Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time. + + + If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-certificate) API endpoint, specifying the issuing CA. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/sign-certificate' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "certificateTemplateId": "", + "csr": "...", + "ttl": "1y", + }' + ``` + + ### Sample response + + ```bash Response + { + certificate: "...", + certificateChain: "...", + issuingCaCertificate: "...", + privateKey: "...", + serialNumber: "..." + } + ``` + + + - Test
diff --git a/docs/documentation/platform/pki/enrollment-methods/est.mdx b/docs/documentation/platform/pki/enrollment-methods/est.mdx index 56ec5bb7d..c0c6fc943 100644 --- a/docs/documentation/platform/pki/enrollment-methods/est.mdx +++ b/docs/documentation/platform/pki/enrollment-methods/est.mdx @@ -2,3 +2,67 @@ title: "Certificate Enrollment via EST" sidebarTitle: "EST" --- + +## Concept + +The API enrollment method allows you to issue and manage certificates against a specific certificate profile using the [EST protocol](https://en.wikipedia.org/wiki/Enrollment_over_Secure_Transport). +This method is suitable for environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services. + +Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints: + +- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA. +- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server +- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate. + +These endpoints are exposed on port 8443 under the .well-known/est path (e.g. +`https://app.infisical.com:8443/.well-known/est/:estLabel/cacerts`). + +## Prerequisites + +- Your client devices need to have a bootstrap/pre-installed certificate. +- Your client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. + + + For Infisical Cloud users, the devices must be configured to trust the [Amazon + root CA certificates](https://www.amazontrust.com/repository). + + +## Guide to Certificate Enrollment via EST + +In the following steps, we explore how to issue a X.509 certificate using the EST enrollment method. + + + + Create a [certificate + profile](/documentation/platform/pki/certificates/profiles) with **EST** + selected as the enrollment method and fill in EST-specific configuration. + + Here's some guidance on each EST-specific configuration field: + + - Disable Bootstrap CA Validation: Enable this if your devices are not configured with a bootstrap certificate. + - EST Passphrase: This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password. + - CA Chain Certificate: This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server. + + Note that forsecurity reasons, Infisical authenticates EST clients using both client certificate and passphrase. + + + + Once the configuration of enrollment options is completed, a new EST Label field will appear in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical. + + The complete URL of the supported EST endpoints may look like the following: + + - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts + - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll + - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simplereenroll + + + + To use the EST passphrase in your clients, configure it as the EST password. The EST username can be set to any arbitrary value. + Use the appropriate client certificates for invoking the EST endpoints. + - For `simpleenroll`, use the bootstrapped/manufacturer client certificate. + - For `simplereenroll`, use a valid EST-issued client certificate. + When configuring the PKCS#12 objects for the client certificates, only include the leaf certificate and the private key. + + + + diff --git a/docs/documentation/platform/pki/enrollment-methods/overview.mdx b/docs/documentation/platform/pki/enrollment-methods/overview.mdx index e69de29bb..f1af9375d 100644 --- a/docs/documentation/platform/pki/enrollment-methods/overview.mdx +++ b/docs/documentation/platform/pki/enrollment-methods/overview.mdx @@ -0,0 +1,11 @@ +--- +title: "Overview" +sidebarTitle: "Overview" +--- + +Enrollment methods determine how certificates are issued and managed for a [certificate profile](/documentation/platform/pki/certificates/profiles). + +Refer to the documentation for each enrollment method to learn more about how to enroll certificates using it. + +- [API](/documentation/platform/pki/enrollment-methods/api): Enroll certificates via API. +- [EST](/documentation/platform/pki/enrollment-methods/est): Enroll certificates via EST protocol. diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index 7faaad4d6..38a3e7a8b 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -12,7 +12,8 @@ Core capabilities include: - [Private CA](/documentation/platform/pki/ca/private-ca): Create and manage your own private CA hierarchy including root and intermediate CAs. - [External CA integration](/documentation/platform/pki/ca/external-ca): Integrate with external public and private CAs including [Azure ADCS](/documentation/platform/pki/ca/azure-adcs) and [ACME-compatible CAs](/documentation/platform/pki/ca/acme-ca) like Let's Encrypt and DigiCert. -- Certificate Enrollment: Support enrollment methods including API, ACME, EST, and more to automate certificate issuance for services, devices, and workloads. +- [Certificate Enrollment](/documentation/platform/pki/enrollment-methods/overview): Support enrollment methods including [API](/documentation/platform/pki/enrollment-methods/api), ACME, [EST](/documentation/platform/pki/enrollment-methods/est), and more to automate certificate issuance for services, devices, and workloads. - Certificate Inventory: Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. -- Certificate Lifecycle Automation: Automate issuance, renewal, and revocation with policy-based workflows, ensuring certificates remain valid, compliant, and up to date across your infrastructure. -- Certificate Syncs: Push certificates to cloud certificate managers like AWS Certificate Manager and Azure Key Vault. +- Certificate Lifecycle Automation: Automate issuance, [renewal](/documentation/platform/pki/certificates/certificates#guide-to-renewing-certificates), and [revocation](/documentation/platform/pki/certificates/certificates#guide-to-revoking-certificates) with policy-based workflows, ensuring certificates remain valid, compliant, and up to date across your infrastructure. +- [Certificate Syncs](/documentation/platform/pki/certificate-syncs/overview): Push certificates to cloud certificate managers like [AWS Certificate Manager](/documentation/platform/pki/certificate-syncs/aws-certificate-manager) and [Azure Key Vault](/documentation/platform/pki/certificate-syncs/azure-key-vault). +- [Certificate Alerts](/documentation/platform/pki/alerting): Receive real-time alerts and webhook events for certificate lifecycle changes such as certificate expiration. diff --git a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx index 2c088c293..31c7aecf2 100644 --- a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx +++ b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx @@ -34,7 +34,7 @@ export const PkiManagerLayout = () => { return ( <>
-
+
{ App Connections )} - {showLegacySection && ( + { <> - {(subscription.pkiLegacyTemplates || hasExistingSubscribers) && ( + { { Subscribers (Legacy) )} - )} + } {(subscription.pkiLegacyTemplates || hasExistingTemplates) && ( { )} - )} + } {
{assumedPrivilegeDetails && } -
+
-
+

{` ${t("common.no-mobile")} `} From 0f9140a410116c55ac0d875d93a2b2b48aafb9d7 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 5 Nov 2025 16:44:33 -0800 Subject: [PATCH 012/271] update ref to cert docs in private ca page --- docs/documentation/platform/pki/ca/private-ca.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/documentation/platform/pki/ca/private-ca.mdx b/docs/documentation/platform/pki/ca/private-ca.mdx index e6065e49e..74913d4cc 100644 --- a/docs/documentation/platform/pki/ca/private-ca.mdx +++ b/docs/documentation/platform/pki/ca/private-ca.mdx @@ -96,7 +96,7 @@ consisting of an (optional) root CA and an intermediate CA. ![pki cas](/images/platform/pki/ca/cas.png) - Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the Certificates section to learn more about how to issue X.509 certificates using the intermediate CA. + Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the [Certificates section](/documentation/platform/pki/certificates/overview) to learn more about how to issue X.509 certificates using the intermediate CA. 2.3b. If you have an external root CA, select **External CA** for the **Parent CA Type** field. @@ -107,7 +107,7 @@ consisting of an (optional) root CA and an intermediate CA. Finally, press **Install** to import the certificate and certificate chain as part of the installation step for the intermediate CA Great! You've successfully created a Private CA hierarchy with an intermediate CA chained to an external root CA. - Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. + Now check out the [Certificates section](/documentation/platform/pki/certificates/overview) to learn more about how to issue X.509 certificates using the intermediate CA. From 32587a3c999cb0a8731dd0f58da15d0f42b14f0b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 6 Nov 2025 05:59:57 +0400 Subject: [PATCH 013/271] feat(app-connections/azure-client-secrets): certificate auth --- backend/src/lib/api-docs/constants.ts | 5 +- .../azure-client-secrets-connection-enums.ts | 3 +- .../azure-client-secrets-connection-fns.ts | 183 +++++++++++++++++- ...azure-client-secrets-connection-schemas.ts | 56 +++++- .../azure-client-secrets-connection-types.ts | 5 + .../components/v2/SecretInput/SecretInput.tsx | 16 +- frontend/src/helpers/appConnections.ts | 3 + .../types/azure-client-secrets-connection.ts | 12 +- .../AzureClientSecretsConnectionForm.tsx | 101 +++++++++- 9 files changed, 369 insertions(+), 15 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 0cb606cbf..b0829bd3b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2308,7 +2308,10 @@ export const AppConnections = { code: "The OAuth code to use to connect with Azure Client Secrets.", tenantId: "The Tenant ID to use to connect with Azure Client Secrets.", clientId: "The Client ID to use to connect with Azure Client Secrets.", - clientSecret: "The Client Secret to use to connect with Azure Client Secrets." + clientSecret: "The Client Secret to use to connect with Azure Client Secrets.", + certificate: "The certificate to use to connect with Azure Client Secrets.", + privateKey: + "The private key to use to connect with Azure Client Secrets. This is never transmitted to Azure and is only used to sign the Azure client assertion with." }, AZURE_DEVOPS: { code: "The OAuth code to use to connect with Azure DevOps.", diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts index eb0521c64..1f7fc808d 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts @@ -1,4 +1,5 @@ export enum AzureClientSecretsConnectionMethod { OAuth = "oauth", - ClientSecret = "client-secret" + ClientSecret = "client-secret", + Certificate = "certificate" } diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index 22cec0ae7..d1599cd3e 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -1,9 +1,13 @@ /* eslint-disable no-case-declarations */ import { AxiosError, AxiosResponse } from "axios"; +import type { KeyObject } from "crypto"; +import { v4 as uuidv4 } from "uuid"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { decryptAppConnectionCredentials, encryptAppConnectionCredentials, @@ -17,11 +21,82 @@ import { AppConnection } from "../app-connection-enums"; import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums"; import { ExchangeCodeAzureResponse, + TAzureClientSecretsConnectionCertificateCredentials, TAzureClientSecretsConnectionClientSecretCredentials, TAzureClientSecretsConnectionConfig, TAzureClientSecretsConnectionCredentials } from "./azure-client-secrets-connection-types"; +const generateClientAssertion = ( + clientId: string, + tenantId: string, + privateKey: string, + certificate: string +): string => { + const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const certBuffer = Buffer.from( + certificate + .replace(/-----BEGIN CERTIFICATE-----/, "") + .replace(/-----END CERTIFICATE-----/, "") + .replace(/\s/g, ""), + "base64" + ); + + // thumbprint of the certificate is used for the jwt header + const thumbprint = crypto.nativeCrypto.createHash("sha1").update(certBuffer).digest("hex"); + const x5t = Buffer.from(thumbprint, "hex").toString("base64url"); + + // JWT Header + const header = { + alg: "RS256", + typ: "JWT", + x5t + }; + + const now = Math.floor(Date.now() / 1000); + const payload = { + aud: tokenEndpoint, + exp: now + 600, // expire the assertion in 10 minutes (not the access access token TTL, but rather the assertion TTL itself) + iss: clientId, + jti: uuidv4(), // random ID for the JWT + nbf: now, // not before the jwt is valid + sub: clientId + }; + + // encode header and payload + const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url"); + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"); + const signatureInput = `${encodedHeader}.${encodedPayload}`; + + let keyObject: KeyObject; + + try { + if (privateKey.includes("BEGIN PRIVATE KEY")) { + keyObject = crypto.nativeCrypto.createPrivateKey(privateKey); + } else { + // if user forgot to wrap in begin/end private key, decode and use as der format + keyObject = crypto.nativeCrypto.createPrivateKey({ + key: Buffer.from(privateKey, "base64"), + format: "der", + type: "pkcs8" + }); + } + } catch (error) { + throw new BadRequestError({ + message: "Invalid private key format provided. Expected PEM format private key." + }); + } + + // sign with private key + const signer = crypto.nativeCrypto.createSign("RSA-SHA256"); + signer.update(signatureInput); + signer.end(); + const signature = signer.sign(keyObject, "base64url"); + + return `${signatureInput}.${signature}`; +}; + export const getAzureClientSecretsConnectionListItem = () => { const { INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig(); @@ -30,7 +105,8 @@ export const getAzureClientSecretsConnectionListItem = () => { app: AppConnection.AzureClientSecrets as const, methods: Object.values(AzureClientSecretsConnectionMethod) as [ AzureClientSecretsConnectionMethod.OAuth, - AzureClientSecretsConnectionMethod.ClientSecret + AzureClientSecretsConnectionMethod.ClientSecret, + AzureClientSecretsConnectionMethod.Certificate ], oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID }; @@ -64,7 +140,7 @@ export const getAzureConnectionAccessToken = async ( const { refreshToken } = credentials; const currentTime = Date.now(); switch (appConnection.method) { - case AzureClientSecretsConnectionMethod.OAuth: + case AzureClientSecretsConnectionMethod.OAuth: { if ( !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET @@ -101,7 +177,8 @@ export const getAzureConnectionAccessToken = async ( await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials }); return data.access_token; - case AzureClientSecretsConnectionMethod.ClientSecret: + } + case AzureClientSecretsConnectionMethod.ClientSecret: { const accessTokenCredentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, projectId: appConnection.projectId, @@ -139,6 +216,50 @@ export const getAzureConnectionAccessToken = async ( await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); return clientData.access_token; + } + + case AzureClientSecretsConnectionMethod.Certificate: { + const accessTokenCredentials = (await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + projectId: appConnection.projectId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials + })) as TAzureClientSecretsConnectionCertificateCredentials; + const { accessToken, expiresAt, clientId, tenantId, certificate, privateKey } = accessTokenCredentials; + if (accessToken && expiresAt && expiresAt > currentTime + 300000) { + return accessToken; + } + + const clientAssertion = generateClientAssertion(clientId, tenantId, privateKey, certificate); + const { data: clientData } = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"), + new URLSearchParams({ + grant_type: "client_credentials", + scope: `https://graph.microsoft.com/.default`, + client_id: clientId, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: clientAssertion + }) + ); + + const updatedClientCredentials = { + ...accessTokenCredentials, + accessToken: clientData.access_token, + expiresAt: currentTime + clientData.expires_in * 1000 + }; + + const encryptedClientCredentials = await encryptAppConnectionCredentials({ + credentials: updatedClientCredentials, + orgId: appConnection.orgId, + projectId: appConnection.projectId, + kmsService + }); + + await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); + + return clientData.access_token; + } + default: throw new InternalServerError({ message: `Unhandled Azure connection method: ${appConnection.method as AzureClientSecretsConnectionMethod}` @@ -156,7 +277,7 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA } = getConfig(); switch (method) { - case AzureClientSecretsConnectionMethod.OAuth: + case AzureClientSecretsConnectionMethod.OAuth: { if (!SITE_URL) { throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); } @@ -221,8 +342,9 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA refreshToken: tokenResp.data.refresh_token, expiresAt: Date.now() + tokenResp.data.expires_in * 1000 }; + } - case AzureClientSecretsConnectionMethod.ClientSecret: + case AzureClientSecretsConnectionMethod.ClientSecret: { const { tenantId, clientId, clientSecret } = inputCredentials; try { const { data: clientData } = await request.post( @@ -255,6 +377,57 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA }); } } + } + case AzureClientSecretsConnectionMethod.Certificate: { + const { tenantId, certificate, privateKey, clientId } = inputCredentials; + try { + const clientAssertion = generateClientAssertion(clientId, tenantId, privateKey, certificate); + + const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const params = new URLSearchParams({ + client_id: clientId, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: clientAssertion, + scope: "https://graph.microsoft.com/.default", + grant_type: "client_credentials" + }); + + const response = await request.post(tokenEndpoint, params.toString(), { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }); + + return { + tenantId, + clientId, + certificate, + privateKey, + accessToken: response.data.access_token, + expiresAt: Date.now() + response.data.expires_in * 1000 + }; + } catch (e: unknown) { + if (e instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to get access token: ${ + (e?.response?.data as { error_description?: string })?.error_description || "Unknown error" + }` + }); + } else if (e instanceof BadRequestError) { + throw e; + } else { + logger.error( + e, + "validateAzureClientSecretsConnectionCredentials: Failed to get access token using certificate authentication" + ); + throw new InternalServerError({ + message: "Failed to get access token" + }); + } + } + } + default: throw new InternalServerError({ message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}` diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts index d9f178a06..3f4130e28 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts @@ -48,6 +48,31 @@ export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z. .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId) }); +export const AzureClientSecretsConnectionCertificateInputCredentialsSchema = z.object({ + tenantId: z + .string() + .uuid() + .trim() + .min(1, "Tenant ID required") + .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId), + clientId: z + .string() + .uuid() + .trim() + .min(1, "Client ID required") + .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId), + certificate: z + .string() + .trim() + .min(1, "Certificate required") + .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.certificate), + privateKey: z + .string() + .trim() + .min(1, "Private Key required") + .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.privateKey) +}); + export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z.object({ clientId: z.string(), clientSecret: z.string(), @@ -56,6 +81,15 @@ export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z expiresAt: z.number() }); +export const AzureClientSecretsConnectionCertificateOutputCredentialsSchema = z.object({ + clientId: z.string(), + tenantId: z.string(), + certificate: z.string(), + privateKey: z.string(), + accessToken: z.string(), + expiresAt: z.number() +}); + export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [ z.object({ method: z @@ -72,6 +106,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin credentials: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe( AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials ) + }), + z.object({ + method: z + .literal(AzureClientSecretsConnectionMethod.Certificate) + .describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method), + credentials: AzureClientSecretsConnectionCertificateInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials + ) }) ]); @@ -84,7 +126,8 @@ export const UpdateAzureClientSecretsConnectionSchema = z credentials: z .union([ AzureClientSecretsConnectionOAuthInputCredentialsSchema, - AzureClientSecretsConnectionClientSecretInputCredentialsSchema + AzureClientSecretsConnectionClientSecretInputCredentialsSchema, + AzureClientSecretsConnectionCertificateInputCredentialsSchema ]) .optional() .describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials) @@ -105,6 +148,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection( z.object({ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret), credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema + }), + z.object({ + method: z.literal(AzureClientSecretsConnectionMethod.Certificate), + credentials: AzureClientSecretsConnectionCertificateOutputCredentialsSchema }) ]) ); @@ -122,6 +169,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion( clientId: true, tenantId: true }) + }), + BaseAzureClientSecretsConnectionSchema.extend({ + method: z.literal(AzureClientSecretsConnectionMethod.Certificate), + credentials: AzureClientSecretsConnectionCertificateOutputCredentialsSchema.pick({ + tenantId: true, + clientId: true + }) }) ]); diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts index 1ad5a3411..e8a66cbd9 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts @@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; import { + AzureClientSecretsConnectionCertificateOutputCredentialsSchema, AzureClientSecretsConnectionClientSecretOutputCredentialsSchema, AzureClientSecretsConnectionOAuthOutputCredentialsSchema, AzureClientSecretsConnectionSchema, @@ -35,6 +36,10 @@ export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer< typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema >; +export type TAzureClientSecretsConnectionCertificateCredentials = z.infer< + typeof AzureClientSecretsConnectionCertificateOutputCredentialsSchema +>; + export interface ExchangeCodeAzureResponse { token_type: string; scope: string; diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 93077c07f..27f0617ed 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -12,12 +12,14 @@ const syntaxHighlight = ( isVisible?: boolean, isImport?: boolean, isLoadingValue?: boolean, - isErrorLoadingValue?: boolean + isErrorLoadingValue?: boolean, + placeholder?: string ) => { if (isLoadingValue) return HIDDEN_SECRET_VALUE; if (isErrorLoadingValue) return Error loading secret value.; if (isImport && !content) return "IMPORTED"; + if (placeholder && (content === "" || !content)) return placeholder; if (content === "") return "EMPTY"; if (!content) return "EMPTY"; if (!isVisible) return HIDDEN_SECRET_VALUE; @@ -79,6 +81,7 @@ export const SecretInput = forwardRef( canEditButNotView, isLoadingValue, isErrorLoadingValue, + placeholder, ...props }, ref @@ -93,18 +96,25 @@ export const SecretInput = forwardRef(

             
-              
+              
                 {syntaxHighlight(
                   value,
                   isVisible || (isSecretFocused && !valueAlwaysHidden),
                   isImport,
                   isLoadingValue,
-                  isErrorLoadingValue
+                  isErrorLoadingValue,
+                  placeholder
                 )}