diff --git a/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts b/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts new file mode 100644 index 000000000..fa359ab44 --- /dev/null +++ b/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + const hasRequireTemplateForIssuanceColumn = await knex.schema.hasColumn( + TableName.CertificateAuthority, + "requireTemplateForIssuance" + ); + if (!hasRequireTemplateForIssuanceColumn) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.dropColumn("requireTemplateForIssuance"); + }); + } +} diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index e59a9225c..ffe0f7c44 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -28,7 +28,8 @@ export const CertificateAuthoritiesSchema = z.object({ keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - activeCaCertId: z.string().uuid().nullable().optional() + activeCaCertId: z.string().uuid().nullable().optional(), + requireTemplateForIssuance: z.boolean().default(false) }); export type TCertificateAuthorities = z.infer; diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index be5643e2b..2d0fc5eb5 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,8 +21,8 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), - password: z.string().nullable().optional(), - lastViewedAt: z.date().nullable().optional() + lastViewedAt: z.date().nullable().optional(), + password: z.string().nullable().optional() }); export type TSecretSharing = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 981b3777e..0496cf984 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -140,6 +140,7 @@ export enum EventType { GET_CA_CRLS = "get-certificate-authority-crls", ISSUE_CERT = "issue-cert", SIGN_CERT = "sign-cert", + GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates", GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", @@ -1192,6 +1193,14 @@ interface SignCert { }; } +interface GetCaCertificateTemplates { + type: EventType.GET_CA_CERTIFICATE_TEMPLATES; + metadata: { + caId: string; + dn: string; + }; +} + interface GetCert { type: EventType.GET_CERT; metadata: { @@ -1547,6 +1556,7 @@ export type Event = | GetCaCrls | IssueCert | SignCert + | GetCaCertificateTemplates | GetCert | DeleteCert | RevokeCert diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d38620837..b08c70d93 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1037,14 +1037,18 @@ export const CERTIFICATE_AUTHORITIES = { maxPathLength: "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.", keyAlgorithm: - "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA." + "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA.", + requireTemplateForIssuance: + "Whether or not certificates for this CA can only be issued through certificate templates." }, GET: { caId: "The ID of the CA to get" }, UPDATE: { caId: "The ID of the CA to update", - status: "The status of the CA to update to. This can be one of active or disabled" + status: "The status of the CA to update to. This can be one of active or disabled", + requireTemplateForIssuance: + "Whether or not certificates for this CA can only be issued through certificate templates." }, DELETE: { caId: "The ID of the CA to delete" diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 9a866f66e..77ee70e57 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,7 +1,7 @@ import ms from "ms"; import { z } from "zod"; -import { CertificateAuthoritiesSchema } from "@app/db/schemas"; +import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -42,7 +42,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { keyAlgorithm: z .nativeEnum(CertKeyAlgorithm) .default(CertKeyAlgorithm.RSA_2048) - .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm) + .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm), + requireTemplateForIssuance: z + .boolean() + .default(false) + .describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance) }) .refine( (data) => { @@ -148,7 +152,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId) }), body: z.object({ - status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status) + status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status), + requireTemplateForIssuance: z + .boolean() + .optional() + .describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance) }), response: { 200: z.object({ @@ -700,6 +708,51 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:caId/certificate-templates", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get list of certificate templates for the CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) + }), + response: { + 200: z.object({ + certificateTemplates: CertificateTemplatesSchema.array() + }) + } + }, + handler: async (req) => { + const { certificateTemplates, ca } = await server.services.certificateAuthority.getCaCertificateTemplates({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CERTIFICATE_TEMPLATES, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + certificateTemplates + }; + } + }); + server.route({ method: "GET", url: "/:caId/crls", diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index bfd7489f7..1c2a5a689 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -41,6 +41,7 @@ import { TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, + TGetCaCertificateTemplatesDTO, TGetCaCertsDTO, TGetCaCsrDTO, TGetCaDTO, @@ -64,7 +65,7 @@ type TCertificateAuthorityServiceFactoryDep = { >; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; - certificateTemplateDAL: Pick; + certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; certificateBodyDAL: Pick; @@ -108,6 +109,7 @@ export const certificateAuthorityServiceFactory = ({ notAfter, maxPathLength, keyAlgorithm, + requireTemplateForIssuance, actorId, actorAuthMethod, actor, @@ -170,7 +172,8 @@ export const certificateAuthorityServiceFactory = ({ notBefore: notBeforeDate, notAfter: notAfterDate, serialNumber - }) + }), + requireTemplateForIssuance }, tx ); @@ -302,7 +305,15 @@ export const certificateAuthorityServiceFactory = ({ * Update CA with id [caId]. * Note: Used to enable/disable CA */ - const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => { + const updateCaById = async ({ + caId, + status, + requireTemplateForIssuance, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -319,7 +330,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const updatedCa = await certificateAuthorityDAL.updateById(caId, { status }); + const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance }); return updatedCa; }; @@ -1077,6 +1088,9 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance && !certificateTemplate) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { @@ -1347,6 +1361,9 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance && !certificateTemplate) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); @@ -1568,6 +1585,40 @@ export const certificateAuthorityServiceFactory = ({ }; }; + /** + * Return list of certificate templates for CA with id [caId]. + */ + const getCaCertificateTemplates = async ({ + caId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetCaCertificateTemplatesDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + + const certificateTemplates = await certificateTemplateDAL.find({ caId }); + + return { + certificateTemplates, + ca + }; + }; + return { createCa, getCaById, @@ -1580,6 +1631,7 @@ export const certificateAuthorityServiceFactory = ({ signIntermediate, importCertToCa, issueCertFromCa, - signCertFromCa + signCertFromCa, + getCaCertificateTemplates }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 764a5dca9..5876c0057 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -38,6 +38,7 @@ export type TCreateCaDTO = { notAfter?: string; maxPathLength: number; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; } & Omit; export type TGetCaDTO = { @@ -47,6 +48,7 @@ export type TGetCaDTO = { export type TUpdateCaDTO = { caId: string; status?: CaStatus; + requireTemplateForIssuance?: boolean; } & Omit; export type TDeleteCaDTO = { @@ -125,6 +127,10 @@ export type TSignCertFromCaDTO = notAfter?: string; } & Omit); +export type TGetCaCertificateTemplatesDTO = { + caId: string; +} & Omit; + export type TDNParts = { commonName?: string; organization?: string; diff --git a/docs/documentation/platform/pki/certificate-templates.mdx b/docs/documentation/platform/pki/certificate-templates.mdx deleted file mode 100644 index a52bf4364..000000000 --- a/docs/documentation/platform/pki/certificate-templates.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: "Certificate Templates" -sidebarTitle: "Certificate Templates" -description: "Learn how to use certificate templates to enforce policies." ---- - -## Concept - -In order to ensure your certificates follow certain policies, you can use certificate templates during the issuance and signing flows. - -A certificate template is linked to a certificate authority. It contains custom policies for certificate fields, allowing you to define rules based on your security policies. - -## Workflow - -The typical workflow for using certificate templates consists of the following steps: - -1. Creating a certificate template attached to an existing CA along with defining custom rules for certificate fields. -2. Selecting the certificate template during the creation of new certificates. - - - Note that this workflow can be executed via the Infisical UI or manually such - as via API. - - -## Guide to using Certificate Templates - -In the following steps, we explore how to issue a X.509 certificate using a certificate template. - - - - - - - To create a certificate template, head to your Project > Internal PKI > Certificate Templates and press **Create Certificate Template**. - - ![certificate-template create template dashboard](/images/platform/pki/certificate-template/create-template-dashboard.png) - - Here, set the **Issuing CA** to the CA you want to issue certificates under when the certificate template is used. - - ![certificate-template create template modal](/images/platform/pki/certificate-template/create-template-form.png) - - Here's some guidance on each field: - - Template Name: A descriptive name for the certificate template. - - Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template. - - Certificate Collection: The collection where certificates issued with this template will be added. - - Common Name (CN): The regular expression used to validate the common name in certificate requests. - - Alternative Names (SANs): The regular expression used to validate subject alternative names in certificate requests. - - TTL: The maximum Time-to-Live (TTL) for certificates issued using this template. - - - - Once you have created the certificate template from step 1, you can select it when issuing certificates. - - ![certificate-template select template](/images/platform/pki/certificate-template/select-template.png) - - - - - - - To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint. - - ### Sample request - - ```bash Request - curl --request POST \ - --url https://app.infisical.com/api/v1/pki/certificate-templates \ - --header 'Content-Type: application/json' \ - --data '{ - "caId": "", - "pkiCollectionId": "", - "name": "", - "commonName": "", - "subjectAlternativeName": "", - "ttl": "" - }' - ``` - - ### Sample response - - ```bash Response - { - "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "caId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "name": "certificate-template-1", - "commonName": "", - ... - } - ``` - - - To use the certificate template, attach the certificate template ID when invoking the API endpoint for [issuing](/api-reference/endpoints/certificates/issue-certificate) or [signing](/api-reference/endpoints/certificates/sign-certificate) new certificates. - - ### Sample request - - ```bash Request - curl --request POST \ - --url https://app.infisical.com/api/v1/pki/certificates/issue-certificate \ - --header 'Content-Type: application/json' \ - --data '{ - "certificateTemplateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "friendlyName": "my-new-certificate", - "commonName": "CERT", - ... - }' - ``` - - - - - diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index 41a5cb8c9..a4f1ba02c 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -25,7 +25,7 @@ graph TD The typical workflow for managing certificates consists of the following steps: -1. Issuing a certificate under an intermediate CA with details like name and validity period. +1. Issuing a certificate under an intermediate CA with details like name and validity period. As part of certificate issuance, you can either issue a certificate directly from a CA or do it via a certificate template. 2. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow, you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked. @@ -43,28 +43,51 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. + + 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. + + Head to your Project > Certificate Authorities > Your Issuing CA and create a certificate template. + + ![pki certificate template modal](/images/platform/pki/certificate/cert-template-modal.png) + + Here's some guidance on each field: + + - Template Name: A name for the certificate template. + - Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template. + - Certificate Collection (Optional): The certificate collection that certificates should be added to when issued under the template. + - Common Name (CN): A regular expression used to validate the common name in certificate requests. + - Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests. + - TTL: The maximum Time-to-Live (TTL) for certificates issued using this template. + - To create a certificate, head to your Project > Internal PKI > Certificates and press **Create Certificate**. + To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section. - ![pki issue certificate](/images/platform/pki/cert-issue.png) + ![pki issue certificate](/images/platform/pki/certificate/cert-issue.png) - Here, set the **CA** to the CA you want to issue the certificate under and fill out details for the certificate. + Here, set the **Certificate Template** to the template from step 1 and fill out the rest of the details for the certificate to be issued. - ![pki issue certificate modal](/images/platform/pki/cert-issue-modal.png) + ![pki issue certificate modal](/images/platform/pki/certificate/cert-issue-modal.png) Here's some guidance on each field: - - Issuing CA: The CA under which to issue the certificate. - Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty. - Common Name (CN): The (common) name for the certificate like `service.acme.com`. - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. - TTL: The lifetime of the certificate in seconds. - + + + 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. + 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**. - ![pki certificate body](/images/platform/pki/cert-body.png) + ![pki certificate body](/images/platform/pki/certificate/cert-body.png) 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. @@ -74,16 +97,54 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. - To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, + + + + 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/ca//issue-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/issue-certificate' \ --header 'Content-Type: application/json' \ --data-raw '{ - "commonName": "My Certificate", + "certificateTemplateId": "", + "commonName": "service.acme.com", "ttl": "1y", }' ``` @@ -100,18 +161,26 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. } ``` + + 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-cert) API endpoint, specifying the issuing CA. + 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/ca//sign-certificate' \ + 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", }' @@ -128,7 +197,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. serialNumber: "..." } ``` - + + diff --git a/docs/documentation/platform/pki/est.mdx b/docs/documentation/platform/pki/est.mdx index ee66c32b6..a31a5672e 100644 --- a/docs/documentation/platform/pki/est.mdx +++ b/docs/documentation/platform/pki/est.mdx @@ -26,7 +26,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. ## Guide to configuring EST -1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates). +1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificates#guide-to-issuing-certificates). 2. Proceed to the certificate template's enrollment settings ![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png) diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx index 44b2cd4fb..c02e477c6 100644 --- a/docs/documentation/platform/pki/pki-issuer.mdx +++ b/docs/documentation/platform/pki/pki-issuer.mdx @@ -214,10 +214,13 @@ In the following steps, we explore how to install the Infisical PKI Issuer using Data ==== + ca.crt: 1306 bytes + tls.crt: 2380 bytes tls.key: 227 bytes - tls.crt: 912 bytes ``` + Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate. + We can decode the certificate and print it out using `openssl`: ```bash diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/private-ca.mdx index 0baa13abb..d7f3f896c 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/private-ca.mdx @@ -66,6 +66,7 @@ consisting of an (optional) root CA and an intermediate CA. - 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. diff --git a/docs/images/platform/pki/cert-body.png b/docs/images/platform/pki/cert-body.png deleted file mode 100644 index 8ed67a7ec..000000000 Binary files a/docs/images/platform/pki/cert-body.png and /dev/null differ diff --git a/docs/images/platform/pki/cert-issue-modal.png b/docs/images/platform/pki/cert-issue-modal.png deleted file mode 100644 index 1516ab1cb..000000000 Binary files a/docs/images/platform/pki/cert-issue-modal.png and /dev/null differ diff --git a/docs/images/platform/pki/cert-issue.png b/docs/images/platform/pki/cert-issue.png deleted file mode 100644 index 6b3e5887b..000000000 Binary files a/docs/images/platform/pki/cert-issue.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate-template/create-template-dashboard.png b/docs/images/platform/pki/certificate-template/create-template-dashboard.png deleted file mode 100644 index 6f193effa..000000000 Binary files a/docs/images/platform/pki/certificate-template/create-template-dashboard.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate-template/create-template-form.png b/docs/images/platform/pki/certificate-template/create-template-form.png deleted file mode 100644 index e69791edd..000000000 Binary files a/docs/images/platform/pki/certificate-template/create-template-form.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate-template/select-template.png b/docs/images/platform/pki/certificate-template/select-template.png deleted file mode 100644 index c10031a45..000000000 Binary files a/docs/images/platform/pki/certificate-template/select-template.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate/cert-body.png b/docs/images/platform/pki/certificate/cert-body.png new file mode 100644 index 000000000..8c1433b54 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-body.png differ diff --git a/docs/images/platform/pki/certificate/cert-issue-modal.png b/docs/images/platform/pki/certificate/cert-issue-modal.png new file mode 100644 index 000000000..f73462c8f Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-issue-modal.png differ diff --git a/docs/images/platform/pki/certificate/cert-issue.png b/docs/images/platform/pki/certificate/cert-issue.png new file mode 100644 index 000000000..614271d19 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-issue.png differ diff --git a/docs/images/platform/pki/certificate/cert-template-modal.png b/docs/images/platform/pki/certificate/cert-template-modal.png new file mode 100644 index 000000000..f3995b6e4 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-template-modal.png differ diff --git a/docs/images/platform/pki/certs.png b/docs/images/platform/pki/certs.png deleted file mode 100644 index 4e1b49959..000000000 Binary files a/docs/images/platform/pki/certs.png and /dev/null differ diff --git a/docs/images/platform/pki/est/template-enroll-hover.png b/docs/images/platform/pki/est/template-enroll-hover.png index 8b13cdd60..cc0f6f658 100644 Binary files a/docs/images/platform/pki/est/template-enroll-hover.png and b/docs/images/platform/pki/est/template-enroll-hover.png differ diff --git a/docs/mint.json b/docs/mint.json index 5528a22de..3fb414e0c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -108,7 +108,6 @@ "documentation/platform/pki/overview", "documentation/platform/pki/private-ca", "documentation/platform/pki/certificates", - "documentation/platform/pki/certificate-templates", "documentation/platform/pki/pki-issuer", "documentation/platform/pki/est", "documentation/platform/pki/alerting" @@ -708,7 +707,7 @@ "api-reference/endpoints/certificate-authorities/import-cert", "api-reference/endpoints/certificate-authorities/issue-cert", "api-reference/endpoints/certificate-authorities/sign-cert", - "api-reference/endpoints/certificate-authorities/crls" + "api-reference/endpoints/certificate-authorities/crl" ] }, { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3148f5da2..95104cdf1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "frontend", "dependencies": { "@casl/ability": "^6.5.0", "@casl/react": "^3.1.0", diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 5c0ff4caa..4993a8fdc 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -8,4 +8,4 @@ export { useSignIntermediate, useUpdateCa } from "./mutations"; -export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries"; +export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates,useGetCaCrls, useGetCaCsr } from "./queries"; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 48652f3fd..5e668ecef 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -43,8 +43,9 @@ export const useUpdateCa = () => { } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body); return ca; }, - onSuccess: (_, { projectSlug }) => { + onSuccess: ({ id }, { projectSlug }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaById(id)); } }); }; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 996043f19..a1e633776 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TCertificateTemplate } from "../certificateTemplates/types"; import { TCertificateAuthority } from "./types"; export const caKeys = { @@ -11,6 +12,7 @@ export const caKeys = { getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"], + getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"], getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"] }; @@ -90,3 +92,16 @@ export const useGetCaCrls = (caId: string) => { enabled: Boolean(caId) }); }; + +export const useGetCaCertTemplates = (caId: string) => { + return useQuery({ + queryKey: caKeys.getCaCertTemplates(caId), + queryFn: async () => { + const { data } = await apiRequest.get<{ + certificateTemplates: TCertificateTemplate[]; + }>(`/api/v1/pki/ca/${caId}/certificate-templates`); + return data; + }, + enabled: Boolean(caId) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index e09ae16b8..52e363fa7 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -19,6 +19,7 @@ export type TCertificateAuthority = { notAfter?: string; notBefore?: string; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; activeCaCertId?: string; createdAt: string; updatedAt: string; @@ -37,12 +38,14 @@ export type TCreateCaDTO = { notAfter?: string; maxPathLength: number; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; }; export type TUpdateCaDTO = { projectSlug: string; caId: string; status?: CaStatus; + requireTemplateForIssuance?: boolean; }; export type TDeleteCaDTO = { diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index 101507af0..f633c0332 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { caKeys } from "../ca/queries"; import { workspaceKeys } from "../workspace/queries"; import { certTemplateKeys } from "./queries"; import { @@ -23,8 +24,9 @@ export const useCreateCertTemplate = () => { ); return certificateTemplate; }, - onSuccess: (_, { projectId }) => { + onSuccess: ({ caId }, { projectId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; @@ -40,22 +42,25 @@ export const useUpdateCertTemplate = () => { return certificateTemplate; }, - onSuccess: (_, { projectId, id }) => { + onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; export const useDeleteCertTemplate = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async (data) => { - return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); + const { data: certificateTemplate } = await apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); + return certificateTemplate; }, - onSuccess: (_, { projectId, id }) => { + onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx index 23f94570c..4afb537f0 100644 --- a/frontend/src/views/Project/CaPage/CaPage.tsx +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -22,6 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; +import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection"; import { CaCertificatesSection, CaCrlsSection, @@ -125,6 +126,7 @@ export const CaPage = withProjectPermission(
+
diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx index 94eae3e6b..9d5d20337 100644 --- a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx +++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx @@ -1,4 +1,4 @@ -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -33,6 +33,28 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {

CA Details

+ + {(isAllowed) => { + return ( + + { + e.stopPropagation(); + handlePopUpOpen("ca", { + caId: ca.id + }); + }} + > + + + + ); + }} +
@@ -115,6 +137,12 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}

+
+

Template Issuance Required

+

+ {ca.requireTemplateForIssuance ? "True" : "False"} +

+
{ca.status === CaStatus.ACTIVE && ( { // const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || ""); + const { mutateAsync: createMutateAsync } = useCreateCa(); + const { mutateAsync: updateMutateAsync } = useUpdateCa(); const { control, @@ -110,7 +114,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: ca.commonName, notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "", maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "", - keyAlgorithm: ca.keyAlgorithm + keyAlgorithm: ca.keyAlgorithm, + requireTemplateForIssuance: ca.requireTemplateForIssuance }); } else { reset({ @@ -124,7 +129,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: "", notAfter: getDateTenYearsFromToday(), maxPathLength: "-1", - keyAlgorithm: CertKeyAlgorithm.RSA_2048 + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + requireTemplateForIssuance: true }); } }, [ca]); @@ -140,31 +146,43 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { province, notAfter, maxPathLength, - keyAlgorithm + keyAlgorithm, + requireTemplateForIssuance }: FormData) => { try { if (!currentWorkspace?.slug) return; - - await createMutateAsync({ - projectSlug: currentWorkspace.slug, - type, - friendlyName, - commonName, - organization, - ou, - country, - province, - locality, - notAfter, - maxPathLength: Number(maxPathLength), - keyAlgorithm - }); + + if (ca) { + // update + await updateMutateAsync({ + projectSlug: currentWorkspace.slug, + caId: ca.id, + requireTemplateForIssuance + }); + } else { + // create + await createMutateAsync({ + projectSlug: currentWorkspace.slug, + type, + friendlyName, + commonName, + organization, + ou, + country, + province, + locality, + notAfter, + maxPathLength: Number(maxPathLength), + keyAlgorithm, + requireTemplateForIssuance + }); + } reset(); handlePopUpToggle("ca", false); createNotification({ - text: "Successfully created CA", + text: `Successfully ${ca ? "updated" : "created"} CA`, type: "success" }); } catch (err) { @@ -186,6 +204,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { >
+ {ca && ( + + + + )} { )} /> - {!ca && ( -
- - -
- )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Require Template for Certificate Issuance

+
+
+ ); + }} + /> +
+ + +
diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx index d4e7e957d..35c034f34 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx @@ -3,7 +3,6 @@ import { faBan, faCertificate, faEllipsis, - faEye, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -155,28 +154,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { )}
)} - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("ca", { - caId: ca.id - }); - }} - disabled={!isAllowed} - icon={} - > - View CA - - )} - {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( { @@ -14,7 +14,7 @@ export const CertificatesTab = () => { exit={{ opacity: 0, translateX: 30 }} > - + {/* */} ); diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx index 40645a07e..a5520c3c3 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -218,7 +218,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { } errorText={error?.message} isError={Boolean(error)} - className="mt-4" isRequired > + + )} ( {...field} onValueChange={(e) => onChange(e)} className="w-full" + isDisabled > {(cas || []).map(({ id, type, dn }) => ( diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx index 4c3ca73fa..b1cce918f 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx @@ -1,9 +1,13 @@ +/** + * TODO (dangtony98): Reevaluate if this component should be in main + * CertificateTab or under CA page in the future. + */ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; +import { DeleteActionModal, IconButton, UpgradePlanModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteCertTemplate } from "@app/hooks/api"; @@ -12,7 +16,11 @@ import { CertificateTemplateEnrollmentModal } from "./CertificateTemplateEnrollm import { CertificateTemplateModal } from "./CertificateTemplateModal"; import { CertificateTemplatesTable } from "./CertificateTemplatesTable"; -export const CertificateTemplatesSection = () => { +type Props = { + caId: string; +} + +export const CertificateTemplatesSection = ({ caId }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "certificateTemplate", "deleteCertificateTemplate", @@ -50,28 +58,30 @@ export const CertificateTemplatesSection = () => { }; return ( -
-
-

Certificate Templates

+
+
+

Certificate Templates

{(isAllowed) => ( - + + )}
- - +
+ +
+ void; }; -export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); +export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { const { subscription } = useSubscription(); - const { data, isLoading } = useListWorkspaceCertificateTemplates({ - workspaceId: currentWorkspace?.id ?? "" - }); + + const { data, isLoading } = useGetCaCertTemplates(caId); return (
@@ -54,7 +55,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { Name - Certificate Authority @@ -65,13 +65,12 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { return ( {certificateTemplate.name} - {certificateTemplate.caName}
- +
@@ -143,7 +142,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { {!isLoading && !data?.certificateTemplates?.length && ( - + )}