mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 14:27:30 +00:00
Merge pull request #4286 from Infisical/ENG-3376
feat(app-connections, PKI): Cloudflare as DNS provider
This commit is contained in:
+29
-3
@@ -46,7 +46,6 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
|
|||||||
const { connectionId } = req.params;
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
const projects = await server.services.appConnection.cloudflare.listPagesProjects(connectionId, req.permission);
|
const projects = await server.services.appConnection.cloudflare.listPagesProjects(connectionId, req.permission);
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -73,9 +72,36 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { connectionId } = req.params;
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
const projects = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission);
|
const scripts = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission);
|
||||||
|
return scripts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return projects;
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/cloudflare-zones`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const zones = await server.services.appConnection.cloudflare.listZones(connectionId, req.permission);
|
||||||
|
return zones;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
TCloudflareConnection,
|
TCloudflareConnection,
|
||||||
TCloudflareConnectionConfig,
|
TCloudflareConnectionConfig,
|
||||||
TCloudflarePagesProject,
|
TCloudflarePagesProject,
|
||||||
TCloudflareWorkersScript
|
TCloudflareWorkersScript,
|
||||||
|
TCloudflareZone
|
||||||
} from "./cloudflare-connection-types";
|
} from "./cloudflare-connection-types";
|
||||||
|
|
||||||
export const getCloudflareConnectionListItem = () => {
|
export const getCloudflareConnectionListItem = () => {
|
||||||
@@ -66,6 +67,27 @@ export const listCloudflareWorkersScripts = async (
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listCloudflareZones = async (appConnection: TCloudflareConnection): Promise<TCloudflareZone[]> => {
|
||||||
|
const {
|
||||||
|
credentials: { apiToken }
|
||||||
|
} = appConnection;
|
||||||
|
|
||||||
|
const { data } = await request.get<{ result: { name: string; id: string }[] }>(
|
||||||
|
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.result.map((a) => ({
|
||||||
|
name: a.name,
|
||||||
|
id: a.id
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
|
export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
|
||||||
const { apiToken, accountId } = config.credentials;
|
const { apiToken, accountId } = config.credentials;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import { listCloudflarePagesProjects, listCloudflareWorkersScripts } from "./cloudflare-connection-fns";
|
import {
|
||||||
|
listCloudflarePagesProjects,
|
||||||
|
listCloudflareWorkersScripts,
|
||||||
|
listCloudflareZones
|
||||||
|
} from "./cloudflare-connection-fns";
|
||||||
import { TCloudflareConnection } from "./cloudflare-connection-types";
|
import { TCloudflareConnection } from "./cloudflare-connection-types";
|
||||||
|
|
||||||
type TGetAppConnectionFunc = (
|
type TGetAppConnectionFunc = (
|
||||||
@@ -16,7 +20,6 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
|
|||||||
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||||
try {
|
try {
|
||||||
const projects = await listCloudflarePagesProjects(appConnection);
|
const projects = await listCloudflarePagesProjects(appConnection);
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -30,9 +33,8 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
|
|||||||
const listWorkersScripts = async (connectionId: string, actor: OrgServiceActor) => {
|
const listWorkersScripts = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||||
try {
|
try {
|
||||||
const projects = await listCloudflareWorkersScripts(appConnection);
|
const scripts = await listCloudflareWorkersScripts(appConnection);
|
||||||
|
return scripts;
|
||||||
return projects;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
error,
|
error,
|
||||||
@@ -42,8 +44,20 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||||
|
try {
|
||||||
|
const zones = await listCloudflareZones(appConnection);
|
||||||
|
return zones;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to list Cloudflare Zones for Cloudflare connection [connectionId=${connectionId}]`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listPagesProjects,
|
listPagesProjects,
|
||||||
listWorkersScripts
|
listWorkersScripts,
|
||||||
|
listZones
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,3 +32,8 @@ export type TCloudflarePagesProject = {
|
|||||||
export type TCloudflareWorkersScript = {
|
export type TCloudflareWorkersScript = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCloudflareZone = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export enum AcmeDnsProvider {
|
export enum AcmeDnsProvider {
|
||||||
Route53 = "route53"
|
Route53 = "route53",
|
||||||
|
Cloudflare = "cloudflare"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import acme from "acme-client";
|
import acme from "acme-client";
|
||||||
|
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
import { TAwsConnection, TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
@@ -39,6 +37,8 @@ import {
|
|||||||
TCreateAcmeCertificateAuthorityDTO,
|
TCreateAcmeCertificateAuthorityDTO,
|
||||||
TUpdateAcmeCertificateAuthorityDTO
|
TUpdateAcmeCertificateAuthorityDTO
|
||||||
} from "./acme-certificate-authority-types";
|
} from "./acme-certificate-authority-types";
|
||||||
|
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||||
|
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||||
|
|
||||||
type TAcmeCertificateAuthorityFnsDeps = {
|
type TAcmeCertificateAuthorityFnsDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||||
@@ -95,74 +95,6 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const route53InsertTxtRecord = async (
|
|
||||||
connection: TAwsConnectionConfig,
|
|
||||||
hostedZoneId: string,
|
|
||||||
domain: string,
|
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
|
||||||
const route53Client = new Route53Client({
|
|
||||||
sha256: CustomAWSHasher,
|
|
||||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
|
||||||
credentials: config.credentials!,
|
|
||||||
region: config.region
|
|
||||||
});
|
|
||||||
|
|
||||||
const command = new ChangeResourceRecordSetsCommand({
|
|
||||||
HostedZoneId: hostedZoneId,
|
|
||||||
ChangeBatch: {
|
|
||||||
Comment: "Set ACME challenge TXT record",
|
|
||||||
Changes: [
|
|
||||||
{
|
|
||||||
Action: "UPSERT",
|
|
||||||
ResourceRecordSet: {
|
|
||||||
Name: domain,
|
|
||||||
Type: "TXT",
|
|
||||||
TTL: 30,
|
|
||||||
ResourceRecords: [{ Value: value }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await route53Client.send(command);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const route53DeleteTxtRecord = async (
|
|
||||||
connection: TAwsConnectionConfig,
|
|
||||||
hostedZoneId: string,
|
|
||||||
domain: string,
|
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
|
||||||
const route53Client = new Route53Client({
|
|
||||||
credentials: config.credentials!,
|
|
||||||
region: config.region
|
|
||||||
});
|
|
||||||
|
|
||||||
const command = new ChangeResourceRecordSetsCommand({
|
|
||||||
HostedZoneId: hostedZoneId,
|
|
||||||
ChangeBatch: {
|
|
||||||
Comment: "Delete ACME challenge TXT record",
|
|
||||||
Changes: [
|
|
||||||
{
|
|
||||||
Action: "DELETE",
|
|
||||||
ResourceRecordSet: {
|
|
||||||
Name: domain,
|
|
||||||
Type: "TXT",
|
|
||||||
TTL: 30,
|
|
||||||
ResourceRecords: [{ Value: value }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await route53Client.send(command);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AcmeCertificateAuthorityFns = ({
|
export const AcmeCertificateAuthorityFns = ({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
@@ -209,6 +141,12 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dnsProviderConfig.provider === AcmeDnsProvider.Cloudflare && appConnection.app !== AppConnection.Cloudflare) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `App connection with ID '${dnsAppConnectionId}' is not a Cloudflare connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// validates permission to connect
|
// validates permission to connect
|
||||||
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
|
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
|
||||||
|
|
||||||
@@ -289,6 +227,15 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dnsProviderConfig.provider === AcmeDnsProvider.Cloudflare &&
|
||||||
|
appConnection.app !== AppConnection.Cloudflare
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `App connection with ID '${dnsAppConnectionId}' is not a Cloudflare connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// validates permission to connect
|
// validates permission to connect
|
||||||
await appConnectionService.connectAppConnectionById(
|
await appConnectionService.connectAppConnectionById(
|
||||||
appConnection.app as AppConnection,
|
appConnection.app as AppConnection,
|
||||||
@@ -443,26 +390,56 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||||
|
|
||||||
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
|
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||||
await route53InsertTxtRecord(
|
case AcmeDnsProvider.Route53: {
|
||||||
connection as TAwsConnection,
|
await route53InsertTxtRecord(
|
||||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
connection as TAwsConnection,
|
||||||
recordName,
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
recordValue
|
recordName,
|
||||||
);
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AcmeDnsProvider.Cloudflare: {
|
||||||
|
await cloudflareInsertTxtRecord(
|
||||||
|
connection as TCloudflareConnection,
|
||||||
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
|
recordName,
|
||||||
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||||
|
|
||||||
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
|
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||||
await route53DeleteTxtRecord(
|
case AcmeDnsProvider.Route53: {
|
||||||
connection as TAwsConnection,
|
await route53DeleteTxtRecord(
|
||||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
connection as TAwsConnection,
|
||||||
recordName,
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
recordValue
|
recordName,
|
||||||
);
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AcmeDnsProvider.Cloudflare: {
|
||||||
|
await cloudflareDeleteTxtRecord(
|
||||||
|
connection as TCloudflareConnection,
|
||||||
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
|
recordName,
|
||||||
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { TCloudflareConnectionConfig } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
export const cloudflareInsertTxtRecord = async (
|
||||||
|
connection: TCloudflareConnectionConfig,
|
||||||
|
hostedZoneId: string,
|
||||||
|
domain: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
credentials: { apiToken }
|
||||||
|
} = connection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request.post(
|
||||||
|
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones/${encodeURIComponent(hostedZoneId)}/dns_records`,
|
||||||
|
{
|
||||||
|
type: "TXT",
|
||||||
|
name: domain,
|
||||||
|
content: value,
|
||||||
|
ttl: 60,
|
||||||
|
proxied: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const firstErrorMessage = (
|
||||||
|
error.response?.data as {
|
||||||
|
errors?: { message: string }[];
|
||||||
|
}
|
||||||
|
)?.errors?.[0]?.message;
|
||||||
|
if (firstErrorMessage) {
|
||||||
|
throw new Error(firstErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cloudflareDeleteTxtRecord = async (
|
||||||
|
connection: TCloudflareConnectionConfig,
|
||||||
|
hostedZoneId: string,
|
||||||
|
domain: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
credentials: { apiToken }
|
||||||
|
} = connection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listRecordsResponse = await request.get<{
|
||||||
|
result: { id: string; type: string; name: string; content: string }[];
|
||||||
|
}>(`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones/${encodeURIComponent(hostedZoneId)}/dns_records`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: "TXT",
|
||||||
|
name: domain,
|
||||||
|
content: value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dnsRecords = listRecordsResponse.data?.result;
|
||||||
|
|
||||||
|
if (Array.isArray(dnsRecords) && dnsRecords.length > 0) {
|
||||||
|
const recordToDelete = dnsRecords.find(
|
||||||
|
(record) => record.type === "TXT" && record.name === domain && record.content === value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recordToDelete) {
|
||||||
|
await request.delete(
|
||||||
|
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones/${encodeURIComponent(hostedZoneId)}/dns_records/${recordToDelete.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const firstErrorMessage = (
|
||||||
|
error.response?.data as {
|
||||||
|
errors?: { message: string }[];
|
||||||
|
}
|
||||||
|
)?.errors?.[0]?.message;
|
||||||
|
if (firstErrorMessage) {
|
||||||
|
throw new Error(firstErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
||||||
|
|
||||||
|
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||||
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
|
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
|
|
||||||
|
export const route53InsertTxtRecord = async (
|
||||||
|
connection: TAwsConnectionConfig,
|
||||||
|
hostedZoneId: string,
|
||||||
|
domain: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||||
|
const route53Client = new Route53Client({
|
||||||
|
sha256: CustomAWSHasher,
|
||||||
|
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||||
|
credentials: config.credentials!,
|
||||||
|
region: config.region
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new ChangeResourceRecordSetsCommand({
|
||||||
|
HostedZoneId: hostedZoneId,
|
||||||
|
ChangeBatch: {
|
||||||
|
Comment: "Set ACME challenge TXT record",
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: "UPSERT",
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: domain,
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: 30,
|
||||||
|
ResourceRecords: [{ Value: value }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await route53Client.send(command);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const route53DeleteTxtRecord = async (
|
||||||
|
connection: TAwsConnectionConfig,
|
||||||
|
hostedZoneId: string,
|
||||||
|
domain: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||||
|
const route53Client = new Route53Client({
|
||||||
|
credentials: config.credentials!,
|
||||||
|
region: config.region
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new ChangeResourceRecordSetsCommand({
|
||||||
|
HostedZoneId: hostedZoneId,
|
||||||
|
ChangeBatch: {
|
||||||
|
Comment: "Delete ACME challenge TXT record",
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: "DELETE",
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: domain,
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: 30,
|
||||||
|
ResourceRecords: [{ Value: value }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await route53Client.send(command);
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ ACME is a protocol that automates the process of certificate issuance and renewa
|
|||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[ACME CA Provider<br>e.g., Let's Encrypt] <-->|ACME v2 Protocol| B[Infisical]
|
A[ACME CA Provider<br>e.g., Let's Encrypt] <-->|ACME v2 Protocol| B[Infisical]
|
||||||
B -->|Creates TXT Records<br>via Route53| C[DNS Validation]
|
B -->|Creates TXT Records<br>via Route53/Cloudflare| C[DNS Validation]
|
||||||
B -->|Manages Certificates| D[Subscribers]
|
B -->|Manages Certificates| D[Subscribers]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ We recommend reading about [ACME protocol](https://tools.ietf.org/html/rfc8555)
|
|||||||
|
|
||||||
A typical workflow for using Infisical with ACME Certificate Authorities consists of the following steps:
|
A typical workflow for using Infisical with ACME Certificate Authorities consists of the following steps:
|
||||||
|
|
||||||
1. Setting up AWS Route53 credentials with appropriate DNS permissions.
|
1. Setting up AWS Route53 or Cloudflare credentials with appropriate DNS permissions.
|
||||||
2. Creating an AWS connection in Infisical to store the Route53 credentials.
|
2. Creating an AWS/Cloudflare connection in Infisical to store the credentials.
|
||||||
3. Registering an ACME Certificate Authority (like Let's Encrypt) with Infisical.
|
3. Registering an ACME Certificate Authority (like Let's Encrypt) with Infisical.
|
||||||
4. Creating subscribers that use the ACME CA as their issuing authority.
|
4. Creating subscribers that use the ACME CA as their issuing authority.
|
||||||
5. Managing certificate lifecycle events such as issuance, renewal, and revocation through Infisical.
|
5. Managing certificate lifecycle events such as issuance, renewal, and revocation through Infisical.
|
||||||
@@ -55,59 +55,75 @@ This automated process eliminates the need for manual intervention in domain val
|
|||||||
In the following steps, we explore how to set up ACME Certificate Authority integration with Infisical using Let's Encrypt as an example.
|
In the following steps, we explore how to set up ACME Certificate Authority integration with Infisical using Let's Encrypt as an example.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Set Up AWS Connection with Required Permissions">
|
<Step title="Create App Connection with Required Permissions">
|
||||||
Before proceeding with the ACME CA registration, you need to set up an AWS connection with the appropriate permissions for DNS validation:
|
Before proceeding with the ACME CA registration, you need to set up an App Connection with the appropriate permissions for DNS validation:
|
||||||
|
|
||||||
1. Navigate to your Organization Settings > App Connections and create a new AWS connection.
|
<Tabs>
|
||||||
|
<Tab title="Route53">
|
||||||
|
1. Navigate to your Organization Settings > App Connections and create a new AWS connection.
|
||||||
|
|
||||||
2. Ensure your AWS connection has the following minimum permissions for Route53 DNS validation:
|
2. Ensure your AWS connection has the following minimum permissions for Route53 DNS validation:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
"Statement": [
|
"Statement": [
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": "route53:GetChange",
|
"Action": "route53:GetChange",
|
||||||
"Resource": "arn:aws:route53:::change/*"
|
"Resource": "arn:aws:route53:::change/*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": "route53:ListHostedZonesByName",
|
"Action": "route53:ListHostedZonesByName",
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"route53:ListResourceRecordSets"
|
"route53:ListResourceRecordSets"
|
||||||
],
|
],
|
||||||
"Resource": [
|
"Resource": [
|
||||||
"arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID"
|
"arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"route53:ChangeResourceRecordSets"
|
"route53:ChangeResourceRecordSets"
|
||||||
],
|
],
|
||||||
"Resource": [
|
"Resource": [
|
||||||
"arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID"
|
"arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID"
|
||||||
],
|
],
|
||||||
"Condition": {
|
"Condition": {
|
||||||
"ForAllValues:StringEquals": {
|
"ForAllValues:StringEquals": {
|
||||||
"route53:ChangeResourceRecordSetsRecordTypes": [
|
"route53:ChangeResourceRecordSetsRecordTypes": [
|
||||||
"TXT"
|
"TXT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
```
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace `YOUR_HOSTED_ZONE_ID` with your actual Route53 hosted zone ID.
|
Replace `YOUR_HOSTED_ZONE_ID` with your actual Route53 hosted zone ID.
|
||||||
|
|
||||||
For detailed instructions on setting up an AWS connection, see the [AWS Connection](/integrations/app-connections/aws) documentation.
|
For detailed instructions on setting up an AWS connection, see the [AWS Connection](/integrations/app-connections/aws) documentation.
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Cloudflare">
|
||||||
|
1. Navigate to your Organization Settings > App Connections and create a new Cloudflare connection.
|
||||||
|
|
||||||
|
2. Ensure your Cloudflare token has the following minimum permissions for DNS validation:
|
||||||
|
|
||||||
|
```
|
||||||
|
Account:Account Settings:Read
|
||||||
|
Zone:DNS:Edit
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation.
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Register ACME Certificate Authority">
|
<Step title="Register ACME Certificate Authority">
|
||||||
<Tabs>
|
<Tabs>
|
||||||
@@ -127,7 +143,7 @@ In the following steps, we explore how to set up ACME Certificate Authority inte
|
|||||||
- **Type**: Select "ACME" as the External CA type.
|
- **Type**: Select "ACME" as the External CA type.
|
||||||
- **Name**: Enter a name for the ACME CA (e.g., "lets-encrypt-production").
|
- **Name**: Enter a name for the ACME CA (e.g., "lets-encrypt-production").
|
||||||
- **DNS App Connection**: Select from available DNS app connections or configure a new one. This connection provides Infisical with the credentials needed to create and remove DNS records for ACME validation.
|
- **DNS App Connection**: Select from available DNS app connections or configure a new one. This connection provides Infisical with the credentials needed to create and remove DNS records for ACME validation.
|
||||||
- **Hosted Zone ID**: Enter your Route53 hosted zone ID (e.g., Z04044I124N1GOOMCOYX1) for the domain(s) you'll be requesting certificates for.
|
- **Zone ID**: Enter the Zone ID for the domain(s) you'll be requesting certificates for.
|
||||||
- **Directory URL**: Enter the ACME v2 directory URL for your chosen CA provider (e.g., `https://acme-v02.api.letsencrypt.org/directory` for Let's Encrypt).
|
- **Directory URL**: Enter the ACME v2 directory URL for your chosen CA provider (e.g., `https://acme-v02.api.letsencrypt.org/directory` for Let's Encrypt).
|
||||||
- **Account Email**: Email address to associate with your ACME account. This email will receive important notifications about your certificates.
|
- **Account Email**: Email address to associate with your ACME account. This email will receive important notifications about your certificates.
|
||||||
- **Enable Direct Issuance**: Toggle on to allow direct certificate issuance without requiring subscribers.
|
- **Enable Direct Issuance**: Toggle on to allow direct certificate issuance without requiring subscribers.
|
||||||
@@ -226,7 +242,7 @@ In the following steps, we explore how to set up ACME Certificate Authority inte
|
|||||||
1. Infisical generates a key pair for the certificate
|
1. Infisical generates a key pair for the certificate
|
||||||
2. Sends a Certificate Signing Request (CSR) to the ACME CA
|
2. Sends a Certificate Signing Request (CSR) to the ACME CA
|
||||||
3. Receives a DNS-01 challenge from the ACME provider
|
3. Receives a DNS-01 challenge from the ACME provider
|
||||||
4. Creates a TXT record in Route53 to satisfy the challenge
|
4. Creates a TXT record in Route53/Cloudflare to satisfy the challenge
|
||||||
5. Notifies the ACME provider that the challenge is ready for validation
|
5. Notifies the ACME provider that the challenge is ready for validation
|
||||||
6. Once validated, the ACME provider issues the certificate
|
6. Once validated, the ACME provider issues the certificate
|
||||||
7. Infisical stores and manages the certificate for your subscriber
|
7. Infisical stores and manages the certificate for your subscriber
|
||||||
@@ -265,7 +281,7 @@ Let's Encrypt is a free, automated, and open Certificate Authority that provides
|
|||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="What DNS validation methods are supported?">
|
<Accordion title="What DNS validation methods are supported?">
|
||||||
Currently, Infisical supports DNS-01 validation through AWS Route53. The DNS-01 challenge method is preferred for ACME integrations because it:
|
Currently, Infisical supports DNS-01 validation through AWS Route53 or Cloudflare. The DNS-01 challenge method is preferred for ACME integrations because it:
|
||||||
|
|
||||||
- Works with wildcard certificates
|
- Works with wildcard certificates
|
||||||
- Doesn't require your servers to be publicly accessible
|
- Doesn't require your servers to be publicly accessible
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ Get started with External CA integration:
|
|||||||
- **Enterprise CAs**: HashiCorp Vault PKI, Step CA
|
- **Enterprise CAs**: HashiCorp Vault PKI, Step CA
|
||||||
- **Cloud CAs**: ACME-compatible managed services
|
- **Cloud CAs**: ACME-compatible managed services
|
||||||
|
|
||||||
Integration uses DNS-01 validation through Route53. Learn more about [supported DNS validation methods](/documentation/platform/pki/acme-ca#what-dns-validation-methods-are-supported).
|
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.
|
Support for additional integration protocols (EST, SCEP, direct APIs) is planned for future releases.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
@@ -50,6 +50,17 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab title="PKI">
|
||||||
|
Use the following permissions to grant Infisical access to verify certificates using DNS TXT records with ACME:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Required Permissions:**
|
||||||
|
- **Account** - **Account Settings** - **Read**
|
||||||
|
- **Zone** - **DNS** - **Edit**
|
||||||
|
|
||||||
|
Add these permissions to your API token and click **Continue to summary**, then **Create Token** to generate your API token.
|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { appConnectionKeys } from "../queries";
|
import { appConnectionKeys } from "../queries";
|
||||||
import { TCloudflarePagesProject, TCloudflareWorkersScript } from "./types";
|
import { TCloudflarePagesProject, TCloudflareWorkersScript, TCloudflareZone } from "./types";
|
||||||
|
|
||||||
const cloudflareConnectionKeys = {
|
const cloudflareConnectionKeys = {
|
||||||
all: [...appConnectionKeys.all, "cloudflare"] as const,
|
all: [...appConnectionKeys.all, "cloudflare"] as const,
|
||||||
listPagesProjects: (connectionId: string) =>
|
listPagesProjects: (connectionId: string) =>
|
||||||
[...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const,
|
[...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const,
|
||||||
listWorkersScripts: (connectionId: string) =>
|
listWorkersScripts: (connectionId: string) =>
|
||||||
[...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const
|
[...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const,
|
||||||
|
listZones: (connectionId: string) =>
|
||||||
|
[...cloudflareConnectionKeys.all, "zones", connectionId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCloudflareConnectionListPagesProjects = (
|
export const useCloudflareConnectionListPagesProjects = (
|
||||||
@@ -62,3 +64,28 @@ export const useCloudflareConnectionListWorkersScripts = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCloudflareConnectionListZones = (
|
||||||
|
connectionId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TCloudflareZone[],
|
||||||
|
unknown,
|
||||||
|
TCloudflareZone[],
|
||||||
|
ReturnType<typeof cloudflareConnectionKeys.listZones>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: cloudflareConnectionKeys.listZones(connectionId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TCloudflareZone[]>(
|
||||||
|
`/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-zones`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,3 +6,8 @@ export type TCloudflarePagesProject = {
|
|||||||
export type TCloudflareWorkersScript = {
|
export type TCloudflareWorkersScript = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCloudflareZone = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { AppConnection } from "../appConnections/enums";
|
||||||
import { SshCaStatus } from "../sshCa";
|
import { SshCaStatus } from "../sshCa";
|
||||||
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
|
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
|
||||||
import { CaStatus, InternalCaType } from "./enums";
|
import { AcmeDnsProvider, CaStatus, InternalCaType } from "./enums";
|
||||||
|
|
||||||
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
|
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
|
||||||
[InternalCaType.ROOT]: "Root",
|
[InternalCaType.ROOT]: "Root",
|
||||||
@@ -13,6 +14,16 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
|||||||
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
||||||
|
[AcmeDnsProvider.ROUTE53]: "Route53",
|
||||||
|
[AcmeDnsProvider.Cloudflare]: "Cloudflare"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
||||||
|
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
||||||
|
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
|
||||||
|
};
|
||||||
|
|
||||||
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => {
|
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case CaStatus.ACTIVE:
|
case CaStatus.ACTIVE:
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ export enum CaRenewalType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum AcmeDnsProvider {
|
export enum AcmeDnsProvider {
|
||||||
ROUTE53 = "route53"
|
ROUTE53 = "route53",
|
||||||
|
Cloudflare = "cloudflare"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type TAcmeCertificateAuthority = {
|
|||||||
configuration: {
|
configuration: {
|
||||||
dnsAppConnectionId: string;
|
dnsAppConnectionId: string;
|
||||||
dnsProviderConfig: {
|
dnsProviderConfig: {
|
||||||
provider: AcmeDnsProvider.ROUTE53;
|
provider: AcmeDnsProvider;
|
||||||
hostedZoneId: string;
|
hostedZoneId: string;
|
||||||
};
|
};
|
||||||
directoryUrl: string;
|
directoryUrl: string;
|
||||||
|
|||||||
+87
-49
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { SingleValue } from "react-select";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -16,7 +17,15 @@ import {
|
|||||||
Switch
|
Switch
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import {
|
||||||
|
TAvailableAppConnection,
|
||||||
|
useListAvailableAppConnections
|
||||||
|
} from "@app/hooks/api/appConnections";
|
||||||
|
import {
|
||||||
|
TCloudflareZone,
|
||||||
|
useCloudflareConnectionListZones
|
||||||
|
} from "@app/hooks/api/appConnections/cloudflare";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import {
|
import {
|
||||||
AcmeDnsProvider,
|
AcmeDnsProvider,
|
||||||
@@ -26,6 +35,10 @@ import {
|
|||||||
useGetCa,
|
useGetCa,
|
||||||
useUpdateCa
|
useUpdateCa
|
||||||
} from "@app/hooks/api/ca";
|
} from "@app/hooks/api/ca";
|
||||||
|
import {
|
||||||
|
ACME_DNS_PROVIDER_APP_CONNECTION_MAP,
|
||||||
|
ACME_DNS_PROVIDER_NAME_MAP
|
||||||
|
} from "@app/hooks/api/ca/constants";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
@@ -42,7 +55,7 @@ const schema = z
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string()
|
name: z.string()
|
||||||
}),
|
}),
|
||||||
// currently specific to Route53 but can be extended to others by differentiating via the provider property
|
// currently specific to Route53 & Cloudflare but can be extended to others by differentiating via the provider property
|
||||||
dnsProviderConfig: z.object({
|
dnsProviderConfig: z.object({
|
||||||
provider: z.nativeEnum(AcmeDnsProvider),
|
provider: z.nativeEnum(AcmeDnsProvider),
|
||||||
hostedZoneId: z.string()
|
hostedZoneId: z.string()
|
||||||
@@ -105,12 +118,29 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const caType = watch("type");
|
const caType = watch("type");
|
||||||
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
|
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
|
||||||
|
|
||||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
|
||||||
AppConnection.AWS,
|
useListAvailableAppConnections(AppConnection.AWS, {
|
||||||
{
|
enabled: caType === CaType.ACME
|
||||||
enabled: dnsProvider === AcmeDnsProvider.ROUTE53
|
});
|
||||||
}
|
|
||||||
);
|
const { data: availableCloudflareConnections, isPending: isCloudflarePending } =
|
||||||
|
useListAvailableAppConnections(AppConnection.Cloudflare, {
|
||||||
|
enabled: caType === CaType.ACME
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableConnections: TAvailableAppConnection[] = [
|
||||||
|
...(availableRoute53Connections || []),
|
||||||
|
...(availableCloudflareConnections || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
const isPending = isRoute53Pending || isCloudflarePending;
|
||||||
|
|
||||||
|
const dnsAppConnection = watch("configuration.dnsAppConnection");
|
||||||
|
|
||||||
|
const { data: cloudflareZones = [], isPending: isZonesPending } =
|
||||||
|
useCloudflareConnectionListZones(dnsAppConnection.id, {
|
||||||
|
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ca) {
|
if (ca) {
|
||||||
@@ -138,25 +168,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
reset({
|
|
||||||
type: CaType.ACME,
|
|
||||||
name: "",
|
|
||||||
status: CaStatus.ACTIVE,
|
|
||||||
enableDirectIssuance: true,
|
|
||||||
configuration: {
|
|
||||||
dnsAppConnection: {
|
|
||||||
id: "",
|
|
||||||
name: ""
|
|
||||||
},
|
|
||||||
dnsProviderConfig: {
|
|
||||||
provider: AcmeDnsProvider.ROUTE53,
|
|
||||||
hostedZoneId: ""
|
|
||||||
},
|
|
||||||
directoryUrl: "",
|
|
||||||
accountEmail: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [ca, availableConnections]);
|
}, [ca, availableConnections]);
|
||||||
|
|
||||||
@@ -284,12 +295,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
isDisabled={Boolean(ca)}
|
isDisabled={Boolean(ca)}
|
||||||
>
|
>
|
||||||
<SelectItem
|
{Object.values(AcmeDnsProvider).map((provider) => (
|
||||||
value={String(AcmeDnsProvider.ROUTE53)}
|
<SelectItem value={String(provider)} key={provider}>
|
||||||
key={AcmeDnsProvider.ROUTE53}
|
{ACME_DNS_PROVIDER_NAME_MAP[provider]}
|
||||||
>
|
</SelectItem>
|
||||||
Route53
|
))}
|
||||||
</SelectItem>
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -297,7 +307,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
tooltipText={`${dnsProvider === AcmeDnsProvider.ROUTE53 ? "Route53" : ""} requires an AWS App Connection. This can be created from the Organization Settings page.`}
|
tooltipText={`${ACME_DNS_PROVIDER_NAME_MAP[dnsProvider]} uses the ${APP_CONNECTION_MAP[ACME_DNS_PROVIDER_APP_CONNECTION_MAP[dnsProvider]].name} App Connection. You can create one in the Organization Settings page.`}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
label="DNS App Connection"
|
label="DNS App Connection"
|
||||||
@@ -318,21 +328,49 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="configuration.dnsAppConnection"
|
name="configuration.dnsAppConnection"
|
||||||
/>
|
/>
|
||||||
<Controller
|
{dnsProvider === AcmeDnsProvider.ROUTE53 && (
|
||||||
control={control}
|
<Controller
|
||||||
defaultValue=""
|
control={control}
|
||||||
name="configuration.dnsProviderConfig.hostedZoneId"
|
defaultValue=""
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="configuration.dnsProviderConfig.hostedZoneId"
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Hosted Zone ID"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
label="Hosted Zone ID"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
isRequired
|
errorText={error?.message}
|
||||||
>
|
isRequired
|
||||||
<Input {...field} placeholder="Z040441124N1GOOMCQYX1" />
|
>
|
||||||
</FormControl>
|
<Input {...field} placeholder="Z040441124N1GOOMCQYX1" />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{dnsProvider === AcmeDnsProvider.Cloudflare && (
|
||||||
|
<Controller
|
||||||
|
name="configuration.dnsProviderConfig.hostedZoneId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Zone"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
isLoading={isZonesPending && !!dnsAppConnection.id}
|
||||||
|
isDisabled={!dnsAppConnection.id}
|
||||||
|
value={cloudflareZones.find((zone) => zone.id === value)}
|
||||||
|
onChange={(option) => {
|
||||||
|
onChange((option as SingleValue<TCloudflareZone>)?.id ?? null);
|
||||||
|
}}
|
||||||
|
options={cloudflareZones}
|
||||||
|
placeholder="Select a zone..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
|
|||||||
+5
-1
@@ -82,7 +82,11 @@ export const SecretDetectionIgnoreValuesSection = () => {
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<p className="text-xl font-semibold">Secret Detection</p>
|
<p className="text-xl font-semibold">Secret Detection</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">Define secret values to ignore when scanning designated parameter folders. Add values here to prevent false positives or allow approved sensitive data. These ignored values will not trigger policy violation alerts.</p>
|
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
|
||||||
|
Define secret values to ignore when scanning designated parameter folders. Add values here
|
||||||
|
to prevent false positives or allow approved sensitive data. These ignored values will not
|
||||||
|
trigger policy violation alerts.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(handleIgnoreValuesSubmit)} autoComplete="off">
|
<form onSubmit={handleSubmit(handleIgnoreValuesSubmit)} autoComplete="off">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user