Merge branch 'main' into ENG-2647

This commit is contained in:
x032205
2025-05-06 17:00:40 -04:00
55 changed files with 911 additions and 88 deletions

View File

@@ -0,0 +1,33 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateBody)) {
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
t.binary("encryptedCertificateChain").nullable();
});
}
if (!(await knex.schema.hasTable(TableName.CertificateSecret))) {
await knex.schema.createTable(TableName.CertificateSecret, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("certId").notNullable().unique();
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
t.binary("encryptedPrivateKey").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateSecret)) {
await knex.schema.dropTable(TableName.CertificateSecret);
}
if (await knex.schema.hasTable(TableName.CertificateBody)) {
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
t.dropColumn("encryptedCertificateChain");
});
}
}

View File

@@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
certId: z.string().uuid(),
encryptedCertificate: zodBuffer
encryptedCertificate: zodBuffer,
encryptedCertificateChain: zodBuffer.nullable().optional()
});
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;

View File

@@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const CertificateSecretsSchema = z.object({
@@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
certId: z.string().uuid(),
pk: z.string(),
sk: z.string()
encryptedPrivateKey: zodBuffer
});
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;

View File

@@ -1,11 +1,11 @@
import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns";
import {
SecretScanningResolvedStatus,
SecretScanningRiskStatus
} from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
body: z.object({ organizationId: z.string().trim() }),
response: {
200: z.object({
sessionId: z.string()
sessionId: z.string(),
gitAppSlug: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const appCfg = getConfig();
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
if (!canUseSecretScanning(req.auth.orgId)) {
throw new BadRequestError({
message: "Secret scanning is temporarily unavailable."
});

View File

@@ -224,6 +224,8 @@ export enum EventType {
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
@@ -1790,6 +1792,24 @@ interface GetCertBody {
};
}
interface GetCertPrivateKey {
type: EventType.GET_CERT_PRIVATE_KEY;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface GetCertBundle {
type: EventType.GET_CERT_BUNDLE;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
@@ -2824,6 +2844,8 @@ export type Event =
| DeleteCert
| RevokeCert
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert

View File

@@ -24,8 +24,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
if (net.isIPv4(el)) {
exclusiveIps.push(el);
} else {
const resolvedIps = await dns.resolve4(el);
exclusiveIps.push(...resolvedIps);
try {
const resolvedIps = await dns.resolve4(el);
exclusiveIps.push(...resolvedIps);
} catch (error) {
// only try lookup if not found
if ((error as { code: string })?.code !== "ENOTFOUND") throw error;
const resolvedIps = (await dns.lookup(el, { all: true, family: 4 })).map(({ address }) => address);
exclusiveIps.push(...resolvedIps);
}
}
}
}
@@ -38,8 +46,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") {
throw new BadRequestError({ message: "Invalid db host" });
}
const resolvedIps = await dns.resolve4(host);
inputHostIps.push(...resolvedIps);
try {
const resolvedIps = await dns.resolve4(host);
inputHostIps.push(...resolvedIps);
} catch (error) {
// only try lookup if not found
if ((error as { code: string })?.code !== "ENOTFOUND") throw error;
const resolvedIps = (await dns.lookup(host, { all: true, family: 4 })).map(({ address }) => address);
inputHostIps.push(...resolvedIps);
}
}
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {

View File

@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionCertificateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
ReadPrivateKey = "read-private-key"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
@@ -232,7 +240,7 @@ export type ProjectPermissionSet =
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
@@ -478,7 +486,7 @@ const GeneralPermissionSchema = [
}),
z.object({
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
"Describe what action an entity can take."
)
}),
@@ -688,7 +696,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
@@ -708,6 +715,17 @@ const buildAdminPermissionRules = () => {
);
});
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.ReadPrivateKey
],
ProjectPermissionSub.Certificates
);
can(
[
ProjectPermissionSshHostActions.Edit,
@@ -965,10 +983,10 @@ const buildMemberPermissionRules = () => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete
],
ProjectPermissionSub.Certificates
);
@@ -1041,7 +1059,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);

View File

@@ -0,0 +1,11 @@
import { getConfig } from "@app/lib/config/env";
export const canUseSecretScanning = (orgId: string) => {
const appCfg = getConfig();
if (!appCfg.isCloud) {
return true;
}
return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId);
};

View File

@@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors";
import { TGitAppDALFactory } from "./git-app-dal";
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
import { TSecretScanningDALFactory } from "./secret-scanning-dal";
import { canUseSecretScanning } from "./secret-scanning-fns";
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
import {
SecretScanningRiskStatus,
@@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TInstallAppSessionDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const sessionId = crypto.randomBytes(16).toString("hex");
await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId });
return { sessionId };
return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG };
};
const linkInstallationToOrg = async ({
@@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
if (canUseSecretScanning(actorOrgId)) {
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
@@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({
)
);
}
return { installatedApp };
};
@@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({
};
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
const appCfg = getConfig();
const { commits, repository, installation, pusher } = payload;
if (!commits || !repository || !installation || !pusher) {
return;
@@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({
});
if (!installationLink) return;
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
if (canUseSecretScanning(installationLink.orgId)) {
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },

View File

@@ -1619,7 +1619,8 @@ export const CERTIFICATES = {
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
certificate: "The certificate body of the certificate.",
certificateChain: "The certificate chain of the certificate.",
serialNumberRes: "The serial number of the certificate."
serialNumberRes: "The serial number of the certificate.",
privateKey: "The private key of the certificate."
}
};

View File

@@ -146,6 +146,7 @@ const envSchema = z
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
SECRET_SCANNING_GIT_APP_SLUG: zpStr(z.string().default("infisical-radar")),
// LICENSE
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),

View File

@@ -0,0 +1,8 @@
import { FastifyReply } from "fastify";
export const addNoCacheHeaders = (reply: FastifyReply) => {
void reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
void reply.header("Pragma", "no-cache");
void reply.header("Expires", "0");
void reply.header("Surrogate-Control", "no-store");
};

View File

@@ -126,6 +126,7 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
@@ -819,6 +820,7 @@ export const registerRoutes = async (
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
const certificateSecretDAL = certificateSecretDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db);
@@ -827,6 +829,7 @@ export const registerRoutes = async (
const certificateService = certificateServiceFactory({
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
@@ -898,6 +901,7 @@ export const registerRoutes = async (
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
@@ -5,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addNoCacheHeaders } from "@app/server/lib/caching";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -64,6 +66,111 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
});
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
server.route({
method: "GET",
url: "/:serialNumber/private-key",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate private key",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
}),
response: {
200: z.string().trim()
}
},
handler: async (req, reply) => {
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
serialNumber: req.params.serialNumber,
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_CERT_PRIVATE_KEY,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
return certPrivateKey;
}
});
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
server.route({
method: "GET",
url: "/:serialNumber/bundle",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate bundle including the certificate, chain, and private key.",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
}
},
handler: async (req, reply) => {
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
await server.services.certificate.getCertBundle({
serialNumber: req.params.serialNumber,
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_CERT_BUNDLE,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
return {
certificate,
certificateChain,
serialNumber,
privateKey
};
}
});
server.route({
method: "POST",
url: "/issue-certificate",
@@ -411,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
}
@@ -429,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.DELETE_CERT,
type: EventType.GET_CERT_BODY,
metadata: {
certId: cert.id,
cn: cert.commonName,

View File

@@ -6,7 +6,11 @@ import { z } from "zod";
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -21,6 +25,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
@@ -75,6 +80,7 @@ type TCertificateAuthorityServiceFactoryDep = {
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
@@ -96,6 +102,7 @@ export const certificateAuthorityServiceFactory = ({
certificateTemplateDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
@@ -1157,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
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" });
@@ -1373,6 +1383,23 @@ export const certificateAuthorityServiceFactory = ({
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
@@ -1396,7 +1423,16 @@ export const certificateAuthorityServiceFactory = ({
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
@@ -1414,17 +1450,9 @@ export const certificateAuthorityServiceFactory = ({
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
certificateChain: certificateChainPem,
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
@@ -1487,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
}

View File

@@ -1,6 +1,11 @@
import crypto from "node:crypto";
import * as x509 from "@peculiar/x509";
import { CrlReason } from "./certificate-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
switch (crlReason) {
@@ -46,3 +51,73 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.map((cert) => cert.toString("pem"))
.join("\n")
.trim();
/**
* Return the public and private key of certificate
* Note: credentials are returned as PEM strings
*/
export const getCertificateCredentials = async ({
certId,
projectId,
certificateSecretDAL,
projectDAL,
kmsService
}: TGetCertificateCredentialsDTO) => {
const certificateSecret = await certificateSecretDAL.findOne({ certId });
if (!certificateSecret)
throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` });
const keyId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedPrivateKey = await kmsDecryptor({
cipherTextBlob: certificateSecret.encryptedPrivateKey
});
try {
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" });
const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString();
const pkObj = crypto.createPublicKey(skObj);
const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString();
return {
certificateSecret,
certPrivateKey,
certPublicKey
};
} catch (error) {
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
}
};
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
// Otherwise we'll fallback to manually building the chain
export const buildCertificateChain = async ({
caCert,
caCertChain,
encryptedCertificateChain,
kmsService,
kmsId
}: TBuildCertificateChainDTO) => {
if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
return null;
}
let certificateChain = `${caCert}\n${caCertChain}`.trim();
if (encryptedCertificateChain) {
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
}
return certificateChain;
};

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
export const certificateSecretDALFactory = (db: TDbClient) => {
const certSecretOrm = ormify(db, TableName.CertificateSecret);
return certSecretOrm;
};

View File

@@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
@@ -15,11 +18,21 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { revocationReasonToCrlCode } from "./certificate-fns";
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import {
CertStatus,
TDeleteCertDTO,
TGetCertBodyDTO,
TGetCertBundleDTO,
TGetCertDTO,
TGetCertPrivateKeyDTO,
TRevokeCertDTO
} from "./certificate-types";
type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
@@ -34,6 +47,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
export const certificateServiceFactory = ({
certificateDAL,
certificateSecretDAL,
certificateBodyDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
@@ -59,7 +73,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
return {
cert,
@@ -67,6 +84,48 @@ export const certificateServiceFactory = ({
};
};
/**
* Get certificate private key.
*/
const getCertPrivateKey = async ({
serialNumber,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetCertPrivateKeyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: ca.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
ca,
cert,
certPrivateKey
};
};
/**
* Delete certificate with serial number [serialNumber]
*/
@@ -83,7 +142,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Delete,
ProjectPermissionSub.Certificates
);
const deletedCert = await certificateDAL.deleteById(cert.id);
@@ -118,7 +180,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Delete,
ProjectPermissionSub.Certificates
);
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
@@ -165,7 +230,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
@@ -192,19 +260,107 @@ export const certificateServiceFactory = ({
kmsService
});
const certificateChain = await buildCertificateChain({
caCert,
caCertChain,
kmsId: certificateManagerKeyId,
kmsService,
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
return {
certificate: certObj.toString("pem"),
certificateChain: `${caCert}\n${caCertChain}`.trim(),
certificateChain,
serialNumber: certObj.serialNumber,
cert,
ca
};
};
/**
* Return certificate body and certificate chain for certificate with
* serial number [serialNumber]
*/
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate
});
const certObj = new x509.X509Certificate(decryptedCert);
const certificate = certObj.toString("pem");
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChain = await buildCertificateChain({
caCert,
caCertChain,
kmsId: certificateManagerKeyId,
kmsService,
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: ca.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
certificate,
certificateChain,
privateKey: certPrivateKey,
serialNumber,
cert,
ca
};
};
return {
getCert,
getCertPrivateKey,
deleteCert,
revokeCert,
getCertBody
getCertBody,
getCertBundle
};
};

View File

@@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509";
import { TProjectPermission } from "@app/lib/types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
export enum CertStatus {
ACTIVE = "active",
REVOKED = "revoked"
@@ -73,3 +77,27 @@ export type TRevokeCertDTO = {
export type TGetCertBodyDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertPrivateKeyDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertBundleDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertificateCredentialsDTO = {
certId: string;
projectId: string;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TBuildCertificateChainDTO = {
caCert?: string;
caCertChain?: string;
encryptedCertificateChain?: Buffer;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
kmsId: string;
};

View File

@@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSecretActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub
@@ -948,7 +949,10 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
const cas = await certificateAuthorityDAL.find({ projectId });

View File

@@ -0,0 +1,8 @@
---
title: "Get Certificate Bundle"
openapi: "GET /api/v2/workspace/{slug}/bundle"
---
<Note>
You must have the certificate `read-private-key` permission in order to call this endpoint.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Get Certificate Private Key"
openapi: "GET /api/v2/workspace/{slug}/private-key"
---

View File

@@ -7,6 +7,113 @@ The Infisical Secret Scanner allows you to keep an overview and stay alert of ex
To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes.
<Accordion title="Self-hosting">
To setup secret scanning on your own instance of Infisical, you can follow the steps below.
<Steps>
<Step title="Create a GitHub App">
Create a new GitHub app in your GitHub organization or personal [Developer Settings](https://github.com/settings/apps).
![Create GitHub App](/images/platform/secret-scanning/github-create-app.png)
### Configure the GitHub App
To configure the GitHub app to work with Infisical, you'll need to modify the following settings:
- **Homepage URL**: Required to be set. Set it to the URL of your Infisical instance. (e.g. `https://app.infisical.com`)
- **Setup URL**: Set this to `https://<your-infisical-instance.com>/organization/secret-scanning`
- **Webhook URL**: Set this to `https://<your-infisical-instance.com>/api/v1/secret-scanning/webhook`
- **Webhook Secret**: Set this to a random string. This is used to verify the webhook request from Infisical. Use `openssl rand -base64 32` in your terminal to generate a random secret.
<Note>
Remember to save the webhook secret as you will need it in the next step.
</Note>
![GitHub App Settings](/images/platform/secret-scanning/github-configure-app.png)
### Configure the GitHub App Permissions
The GitHub app needs the following permissions:
Repository permissions:
- `Checks`: Read and Write
- `Contents`: Read-only
- `Issues`: Read and Write
- `Pull Requests`: Read and Write
- `Metadata`: Read-only (enabled by default)
![Github App Repository Permissions](/images/platform/secret-scanning/github-repo-permissions.png)
Subscribed events:
- `Check run`
- `Pull request`
- `Push`
![Github App Subscribed Events](/images/platform/secret-scanning/github-subscribed-events.png)
### Create the GitHub App
Now you can create the GitHub app by clicking on the "Create GitHub App" button.
<Note>
If you want other Github users to be able to install the app, you need to tick the "Any account" option under "Where can this GitHub App be installed?"
</Note>
![Create GitHub App](/images/platform/secret-scanning/github-create-app-button.png)
</Step>
<Step title="Retrieve the GitHub App ID">
After clicking the "Create GitHub App" button, you will be redirected to the GitHub settings page. Here you can copy the "App ID" and save it for later when you need to configure your environment variables for your Infisical instance.
![Github App ID](/images/platform/secret-scanning/github-app-copy-app-id.png)
</Step>
<Step title="Retrieve your GitHub App slug">
The GitHub App slug is the name of the app you created in a slug friendly format. You can find the slug in the URL of the app you created.
![Github App Slug](/images/platform/secret-scanning/github-app-copy-slug.png)
</Step>
<Step title="Create a new GitHub App private key">
Create a new app private key by clicking on the "Generate a private key" button under the "Private keys" section.
Once you click the "Generate a private key" button, the private key will be downloaded to your computer. Save this file for later as you will need the private key when configuring Infisical.
![Github App Private Key](/images/platform/secret-scanning/github-app-create-private-key.png)
<Note>
Remember to save the private key as you will need it in the next step.
</Note>
</Step>
<Step title="Configure your Infisical instance">
Now you can configure your Infisical instance by setting the following environment variables:
- `SECRET_SCANNING_GIT_APP_ID`: The App ID of your GitHub App.
- `SECRET_SCANNING_GIT_APP_SLUG`: The slug of your GitHub App.
- `SECRET_SCANNING_PRIVATE_KEY`: The private key of your GitHub App that you created in a previous step.
- `SECRET_SCANNING_WEBHOOK_SECRET`: The webhook secret of your GitHub App that you created in a previous step.
</Step>
</Steps>
After restarting your Infisical instance, you should be able to use the secret scanning feature within your organization. Follow the steps below to add the GitHub App to your Infisical organization.
</Accordion>
## Install the Infisical Radar GitHub App
To install the GitHub App, press the "Integrate With GitHub" button in the top right corner of your Infisical Secret Scanning dashboard.
![Integrate With GitHub](/images/platform/secret-scanning/infisical-connect-secret-scanner.png)
Next, you'll be prompted to select which organization you'd like to install the app into. Select the organization you'd like to install the app into by clicking the organization in the menu.
![Select Organization](/images/platform/secret-scanning/github-select-org-2.png)
Select the repositories you'd like to scan for secrets and press the "Install" button.
![Select Repositories](/images/platform/secret-scanning/github-select-repos.png)
## Code Scanning
![Scanning Overview](/images/platform/secret-scanning/overview.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -41,7 +41,7 @@ All final reward amounts are determined at Infisical's discretion based on impac
### Out of Scope
- Social engineering or phishing
- Social engineering or phishing (including email hyperlink injection without code execution)
- Rate limiting issues on non-sensitive endpoints
- Denial-of-service attacks that require authentication and don't impact core service availability
- Findings based on outdated or forked code not maintained by the Infisical team
@@ -57,4 +57,24 @@ We ask that researchers:
- Use testing accounts where possible
- Give us a reasonable window to investigate and patch before going public
Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally.
Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally.
### Program Conduct and Enforcement
We value professional and collaborative interaction with security researchers. To maintain the integrity of our bug bounty program, we expect all participants to adhere to the following guidelines:
- Maintain professional communication in all interactions
- Do not threaten public disclosure of vulnerabilities before we've had reasonable time to investigate and address the issue
- Do not attempt to extort or coerce compensation through threats
- Follow the responsible disclosure process outlined in this document
- Do not use automated scanning tools without prior permission
Violations of these guidelines may result in:
1. **Warning**: For minor violations, we may issue a warning explaining the violation and requesting compliance with program guidelines.
2. **Temporary Ban**: Repeated minor violations or more serious violations may result in a temporary suspension from the program.
3. **Permanent Ban**: Severe violations such as threats, extortion attempts, or unauthorized public disclosure will result in permanent removal from the Infisical Bug Bounty Program.
We reserve the right to reject reports, withhold bounties, and remove participants from the program at our discretion for conduct that undermines the collaborative spirit of security research.
Infisical is committed to working respectfully with security researchers who follow these guidelines, and we strive to recognize and reward valuable contributions that help protect our platform and users.

View File

@@ -252,11 +252,12 @@ Supports conditions and permission inversion
#### Subject: `certificates`
| Action | Description |
| -------- | ----------------------------- |
| `read` | View certificates |
| `create` | Issue new certificates |
| `delete` | Revoke or remove certificates |
| Action | Description |
| -------------------- | ----------------------------- |
| `read` | View certificates |
| `read-private-key` | Read certificate private key |
| `create` | Issue new certificates |
| `delete` | Revoke or remove certificates |
#### Subject: `certificate-templates`

View File

@@ -1454,6 +1454,8 @@
"api-reference/endpoints/certificates/revoke",
"api-reference/endpoints/certificates/delete",
"api-reference/endpoints/certificates/cert-body",
"api-reference/endpoints/certificates/bundle",
"api-reference/endpoints/certificates/private-key",
"api-reference/endpoints/certificates/issue-certificate",
"api-reference/endpoints/certificates/sign-certificate"
]

View File

@@ -29,6 +29,19 @@ Used to configure platform-specific security and operational settings
Specifies the internal port on which the application listens.
</ParamField>
<ParamField query="HOST" type="string" default="localhost" optional>
Specifies the network interface Infisical will bind to when accepting incoming connections.
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
Example values:
- `localhost` (default, same as `127.0.0.1`)
- `0.0.0.0` (all interfaces, accessible externally)
- `192.168.1.100` (specific interface IP)
</ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
Telemetry helps us improve Infisical but if you want to disable it you may set
this to `false`.
@@ -612,6 +625,26 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
</ParamField>
</Accordion>
## Secret Scanning
<Accordion title="GitHub">
<ParamField query="SECRET_SCANNING_GIT_APP_ID" type="string" default="none" optional>
The App ID of your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_GIT_APP_SLUG" type="string" default="none" optional>
The slug of your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_PRIVATE_KEY" type="string" default="none" optional>
A private key for your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_WEBHOOK_SECRET" type="string" default="none" optional>
The webhook secret of your GitHub App.
</ParamField>
</Accordion>
## Observability
You can configure Infisical to collect and expose telemetry data for analytics and monitoring.

View File

@@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types";
export {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,

View File

@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionCertificateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
ReadPrivateKey = "read-private-key"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
@@ -268,7 +276,7 @@ export type ProjectPermissionSet =
)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]

View File

@@ -10,6 +10,7 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,

View File

@@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_CERT]: "Delete certificate",
[EventType.REVOKE_CERT]: "Revoke certificate",
[EventType.GET_CERT_BODY]: "Get certificate body",
[EventType.GET_CERT_PRIVATE_KEY]: "Get certificate private key",
[EventType.GET_CERT_BUNDLE]: "Get certificate bundle",
[EventType.CREATE_PKI_ALERT]: "Create PKI alert",
[EventType.GET_PKI_ALERT]: "Get PKI alert",
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",

View File

@@ -78,6 +78,8 @@ export enum EventType {
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",

View File

@@ -620,6 +620,24 @@ interface GetCertBody {
};
}
interface GetCertPrivateKey {
type: EventType.GET_CERT_PRIVATE_KEY;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface GetCertBundle {
type: EventType.GET_CERT_BUNDLE;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
@@ -881,6 +899,8 @@ export type Event =
| DeleteCert
| RevokeCert
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert

View File

@@ -6,7 +6,8 @@ import { TCertificate } from "./types";
export const certKeys = {
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"]
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
};
export const useGetCert = (serialNumber: string) => {
@@ -38,3 +39,19 @@ export const useGetCertBody = (serialNumber: string) => {
enabled: Boolean(serialNumber)
});
};
export const useGetCertBundle = (serialNumber: string) => {
return useQuery({
queryKey: certKeys.getCertBundle(serialNumber),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificate: string;
certificateChain: string;
serialNumber: string;
privateKey: string;
}>(`/api/v1/pki/certificates/${serialNumber}/bundle`);
return data;
},
enabled: Boolean(serialNumber)
});
};

View File

@@ -10,15 +10,17 @@ import {
} from "./types";
export const useCreateNewInstallationSession = () => {
return useMutation<{ sessionId: string }, object, { organizationId: string }>({
mutationFn: async (opt) => {
const { data } = await apiRequest.post(
"/api/v1/secret-scanning/create-installation-session/organization",
opt
);
return data;
return useMutation<{ sessionId: string; gitAppSlug: string }, object, { organizationId: string }>(
{
mutationFn: async (opt) => {
const { data } = await apiRequest.post(
"/api/v1/secret-scanning/create-installation-session/organization",
opt
);
return data;
}
}
});
);
};
export const useUpdateRiskStatus = () => {

View File

@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { PkiCollectionSection } from "../AlertingPage/components";
import { CertificatesSection } from "./components";
@@ -17,7 +22,7 @@ export const CertificatesPage = () => {
ProjectPermissionSub.PkiCollections
);
const canAccessCerts = permission.can(
ProjectPermissionActions.Read,
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
@@ -40,7 +45,7 @@ export const CertificatesPage = () => {
)}
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
<CertificatesSection />

View File

@@ -1,5 +1,11 @@
import { Modal, ModalContent } from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { useGetCertBody } from "@app/hooks/api";
import { useGetCertBundle } from "@app/hooks/api/certificates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertificateContent } from "./CertificateContent";
@@ -10,10 +16,29 @@ type Props = {
};
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const { data } = useGetCertBody(
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""
const { permission } = useProjectPermission();
const serialNumber =
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "";
const canReadPrivateKey = permission.can(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
// useGetCertBundle fails unless user has the correct permissions
const { data: bundleData } = useGetCertBundle(serialNumber);
const { data: bodyData } = useGetCertBody(serialNumber);
const data:
| {
certificate: string;
certificateChain: string;
serialNumber: string;
privateKey?: string;
}
| undefined = canReadPrivateKey ? bundleData : bodyData;
return (
<Modal
isOpen={popUp?.certificateCert?.isOpen}
@@ -27,6 +52,7 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
serialNumber={data.serialNumber}
certificate={data.certificate}
certificateChain={data.certificateChain}
privateKey={data.privateKey}
/>
) : (
<div />

View File

@@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { useDeleteCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -50,7 +54,7 @@ export const CertificatesSection = () => {
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
I={ProjectPermissionCertificateActions.Create}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (

View File

@@ -30,7 +30,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -110,7 +114,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
@@ -131,7 +135,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
@@ -152,7 +156,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
@@ -173,7 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (

View File

@@ -108,7 +108,7 @@ export const SecretScanningPage = withPermission(
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession({ organizationId });
window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`;
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
};
return (

View File

@@ -114,7 +114,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
<div
key={el.id}
className={twMerge(
"relative bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md",
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
draggedItem === rootIndex ? "opacity-50" : ""
)}
@@ -179,7 +179,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
</div>
)}
</div>
<div className="flex text-gray-300">
<div className="flex gap-4 text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }, index) => {

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
} from "@app/context";
@@ -32,6 +33,14 @@ const GeneralPolicyActionSchema = z.object({
create: z.boolean().optional()
});
const CertificatePolicyActionSchema = z.object({
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Read]: z.boolean().optional(),
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional()
});
const SecretPolicyActionSchema = z.object({
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
@@ -219,7 +228,7 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
@@ -371,7 +380,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.CertificateTemplates,
@@ -507,6 +515,25 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.Certificates) {
const canRead = action.includes(ProjectPermissionCertificateActions.Read);
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0].read = true;
if (canEdit) formVal[subject]![0].edit = true;
if (canCreate) formVal[subject]![0].create = true;
if (canDelete) formVal[subject]![0].delete = true;
if (canReadPrivateKey)
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true;
return;
}
if (subject === ProjectPermissionSub.Project) {
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
@@ -1014,10 +1041,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Certificates]: {
title: "Certificates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
{ label: "Read", value: ProjectPermissionCertificateActions.Read },
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey },
{ label: "Create", value: ProjectPermissionCertificateActions.Create },
{ label: "Modify", value: ProjectPermissionCertificateActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete }
]
},
[ProjectPermissionSub.CertificateTemplates]: {