Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0f98c49
feat(pki): add AWS ACM Public CA support
saifsmailbox98 Apr 17, 2026
aa42642
chore(backend): add @aws-sdk/client-acm dependency
saifsmailbox98 Apr 17, 2026
2f98833
chore(pki): remove ACM development mock client
saifsmailbox98 Apr 17, 2026
742a47d
fix(pki): surface AWS errors and fix ACM renewal polling
saifsmailbox98 Apr 17, 2026
f3f1f1f
fix(pki): retry ACM export when renewal relation not yet ready
saifsmailbox98 Apr 17, 2026
ab2698a
chore(pki): clean up ACM extras and add docs
saifsmailbox98 Apr 17, 2026
090f310
fix(pki): make external CA revocation atomic and surface AWS errors
saifsmailbox98 Apr 17, 2026
3d68e5b
fix(pki): preserve original region on ACM renewal and hoist AWS calls…
saifsmailbox98 Apr 17, 2026
6f61d9d
fix(pki): derive ACM signature algorithm from issued cert
saifsmailbox98 Apr 17, 2026
8df9a92
chore(pki): remove unused AwsAcmKeyAlgorithm enum
saifsmailbox98 Apr 17, 2026
32a58bf
Merge remote-tracking branch 'origin/main' into saif/pki-75-infisical…
saifsmailbox98 Apr 17, 2026
d0452dd
refactor(pki): generate ACM export passphrase with nanoid customAlphabet
saifsmailbox98 Apr 17, 2026
e3562d0
fix(ui): mark AWS Connection field as required in ACM external CA form
saifsmailbox98 Apr 17, 2026
0b3a511
docs(pki): clarify ACM auto-renewal and refresh screenshots
saifsmailbox98 Apr 17, 2026
8075347
docs(pki): add ACM public CA API reference pages
saifsmailbox98 Apr 17, 2026
c3848e7
refactor(pki): share Route 53 helper and tidy ACM internals
saifsmailbox98 Apr 21, 2026
3c6a2d8
feat(ui): pre-fill and lock TTL for ACM Public CA profiles
saifsmailbox98 Apr 21, 2026
91bccd5
docs(pki): expand ACM Public CA guide and document permissions on AWS…
saifsmailbox98 Apr 21, 2026
ea891ec
fix(pki): skip AWS ACM revoke for superseded certificates
saifsmailbox98 Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
848 changes: 467 additions & 381 deletions backend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.68",
"@aws-sdk/client-acm": "^3.1030.0",
"@aws-sdk/client-acm-pca": "^3.992.0",
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Knex } from "knex";
Comment thread
saifsmailbox98 marked this conversation as resolved.

import { TableName } from "../schemas";

export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.Certificate)) {
const hasColumn = await knex.schema.hasColumn(TableName.Certificate, "externalMetadata");
if (!hasColumn) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.jsonb("externalMetadata").nullable();
});
}
}
}

export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "externalMetadata")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("externalMetadata");
});
}
}
}
3 changes: 2 additions & 1 deletion backend/src/db/schemas/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export const CertificatesSchema = z.object({
isCA: z.boolean().nullable().optional(),
pathLength: z.number().nullable().optional(),
source: z.string().nullable().optional(),
discoveryMetadata: z.unknown().nullable().optional()
discoveryMetadata: z.unknown().nullable().optional(),
externalMetadata: z.unknown().nullable().optional()
});

export type TCertificates = z.infer<typeof CertificatesSchema>;
Expand Down
6 changes: 6 additions & 0 deletions backend/src/lib/api-docs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,12 @@ export const CertificateAuthorities = {
certificateAuthorityArn: `The ARN of the AWS Private Certificate Authority to use for issuing certificates.`,
region: `The AWS region where the Private Certificate Authority is located.`
},
AWS_ACM_PUBLIC_CA: {
appConnectionId: `The ID of the AWS App Connection to use for authenticating with AWS Certificate Manager (ACM). This connection must have permissions to request, describe, export, renew, and delete certificates.`,
dnsAppConnectionId: `The ID of the AWS App Connection to use for creating and managing Route 53 CNAME records required for ACM domain validation.`,
hostedZoneId: `The Route 53 hosted zone ID to use for ACM DNS validation CNAME records.`,
region: `The AWS region to use for the ACM API calls.`
},
INTERNAL: {
type: "The type of CA to create.",
friendlyName: "A friendly name for the CA.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
AwsAcmPublicCaCertificateAuthoritySchema,
CreateAwsAcmPublicCaCertificateAuthoritySchema,
UpdateAwsAcmPublicCaCertificateAuthoritySchema
} from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";

import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";

export const registerAwsAcmPublicCaCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
registerCertificateAuthorityEndpoints({
caType: CaType.AWS_ACM_PUBLIC_CA,
server,
responseSchema: AwsAcmPublicCaCertificateAuthoritySchema,
createSchema: CreateAwsAcmPublicCaCertificateAuthoritySchema,
updateSchema: UpdateAwsAcmPublicCaCertificateAuthoritySchema
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
import { AwsAcmPublicCaCertificateAuthoritySchema } from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas";
import { AwsPcaCertificateAuthoritySchema } from "@app/services/certificate-authority/aws-pca/aws-pca-certificate-authority-schemas";
import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
Expand All @@ -15,7 +16,8 @@
InternalCertificateAuthoritySchema,
AcmeCertificateAuthoritySchema,
AzureAdCsCertificateAuthoritySchema,
AwsPcaCertificateAuthoritySchema
AwsPcaCertificateAuthoritySchema,
AwsAcmPublicCaCertificateAuthoritySchema
]);

export const registerGeneralCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
Expand Down Expand Up @@ -70,12 +72,20 @@
projectId: req.query.projectId,
type: CaType.AWS_PCA
},
req.permission
);

const awsAcmPublicCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
{
projectId: req.query.projectId,
type: CaType.AWS_ACM_PUBLIC_CA
},
req.permission
);

await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,

Check notice on line 88 in backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts

View check run for this annotation

Claude / Claude Code Review

ListCAs makes 5 sequential DB queries instead of parallel

The ListCAs handler in both the v1 and v2 routers awaits 5 independent DB queries sequentially instead of running them in parallel with `Promise.all()`. This PR extends the pre-existing 4-query sequential pattern by adding a 5th await for `awsAcmPublicCas`, serializing what could be a concurrent round-trip.
Comment thread
saifsmailbox98 marked this conversation as resolved.
event: {
type: EventType.GET_CAS,
metadata: {
Expand All @@ -83,7 +93,8 @@
...(internalCas ?? []).map((ca) => ca.id),
...(acmeCas ?? []).map((ca) => ca.id),
...(azureAdCsCas ?? []).map((ca) => ca.id),
...(awsPcaCas ?? []).map((ca) => ca.id)
...(awsPcaCas ?? []).map((ca) => ca.id),
...(awsAcmPublicCas ?? []).map((ca) => ca.id)
]
}
}
Expand All @@ -94,7 +105,8 @@
...(internalCas ?? []),
...(acmeCas ?? []),
...(azureAdCsCas ?? []),
...(awsPcaCas ?? [])
...(awsPcaCas ?? []),
...(awsAcmPublicCas ?? [])
]
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";

import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router";
import { registerAwsAcmPublicCaCertificateAuthorityRouter } from "./aws-acm-public-ca-certificate-authority-router";
import { registerAwsPcaCertificateAuthorityRouter } from "./aws-pca-certificate-authority-router";
import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router";
import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router";
Expand All @@ -12,5 +13,6 @@ export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record<CaType, (server:
[CaType.INTERNAL]: registerInternalCertificateAuthorityRouter,
[CaType.ACME]: registerAcmeCertificateAuthorityRouter,
[CaType.AZURE_AD_CS]: registerAzureAdCsCertificateAuthorityRouter,
[CaType.AWS_PCA]: registerAwsPcaCertificateAuthorityRouter
[CaType.AWS_PCA]: registerAwsPcaCertificateAuthorityRouter,
[CaType.AWS_ACM_PUBLIC_CA]: registerAwsAcmPublicCaCertificateAuthorityRouter
};
18 changes: 15 additions & 3 deletions backend/src/server/routes/v2/certificate-authority-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
import { AwsAcmPublicCaCertificateAuthoritySchema } from "@app/services/certificate-authority/aws-acm-public-ca/aws-acm-public-ca-certificate-authority-schemas";
import { AwsPcaCertificateAuthoritySchema } from "@app/services/certificate-authority/aws-pca/aws-pca-certificate-authority-schemas";
import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
Expand All @@ -15,7 +16,8 @@ const CertificateAuthoritySchema = z.discriminatedUnion("type", [
InternalCertificateAuthoritySchema,
AcmeCertificateAuthoritySchema,
AzureAdCsCertificateAuthoritySchema,
AwsPcaCertificateAuthoritySchema
AwsPcaCertificateAuthoritySchema,
AwsAcmPublicCaCertificateAuthoritySchema
]);

export const registerCaRouter = async (server: FastifyZodProvider) => {
Expand Down Expand Up @@ -73,6 +75,14 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
req.permission
);

const awsAcmPublicCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
{
projectId: req.query.projectId,
type: CaType.AWS_ACM_PUBLIC_CA
},
req.permission
);

await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
Expand All @@ -83,7 +93,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
...(internalCas ?? []).map((ca) => ca.id),
...(acmeCas ?? []).map((ca) => ca.id),
...(azureAdCsCas ?? []).map((ca) => ca.id),
...(awsPcaCas ?? []).map((ca) => ca.id)
...(awsPcaCas ?? []).map((ca) => ca.id),
...(awsAcmPublicCas ?? []).map((ca) => ca.id)
]
}
}
Expand All @@ -94,7 +105,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
...(internalCas ?? []),
...(acmeCas ?? []),
...(azureAdCsCas ?? []),
...(awsPcaCas ?? [])
...(awsPcaCas ?? []),
...(awsAcmPublicCas ?? [])
]
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ACMClient } from "@aws-sdk/client-acm";

import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto/cryptography";
import { NotFoundError } from "@app/lib/errors";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";

export const createAcmClient = async ({
appConnectionId,
region,
appConnectionDAL,
kmsService
}: {
appConnectionId: string;
region: AWSRegion;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
}) => {
const appConnection = await appConnectionDAL.findById(appConnectionId);
if (!appConnection) {
throw new NotFoundError({ message: `App connection with ID '${appConnectionId}' not found` });
}

const decryptedConnection = (await decryptAppConnection(appConnection, kmsService)) as TAwsConnection;
const awsConfig = await getAwsConnectionConfig(decryptedConnection, region);

return new ACMClient({
sha256: CustomAWSHasher,
useFipsEndpoint: crypto.isFipsModeEnabled(),
credentials: awsConfig.credentials,
region: awsConfig.region
});
};

export const resolveDnsAwsConnection = async ({
dnsAppConnectionId,
appConnectionDAL,
kmsService
}: {
dnsAppConnectionId: string;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
}) => {
const dnsAppConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!dnsAppConnection) {
throw new NotFoundError({ message: `DNS app connection with ID '${dnsAppConnectionId}' not found` });
}
return (await decryptAppConnection(dnsAppConnection, kmsService)) as TAwsConnection;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum AwsAcmValidationMethod {
DNS = "DNS"
}

/**
* ACM public certificates have a fixed validity period (as of 2025).
* See: https://docs.aws.amazon.com/acm/latest/userguide/managed-renewal.html
*/
export const AWS_ACM_CERTIFICATE_VALIDITY_DAYS = 198;
Comment thread
saifsmailbox98 marked this conversation as resolved.
Loading
Loading