feat: load project kms backup

This commit is contained in:
Sheen Capadngan
2024-07-30 23:03:54 +05:30
committed by =
parent 32fc254ae1
commit 0c9e979fb8
9 changed files with 207 additions and 19 deletions
+48 -2
View File
@@ -4,7 +4,7 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -205,7 +205,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "PATCH", method: "PATCH",
url: "/:workspaceId/kms", url: "/:workspaceId/kms",
config: { config: {
rateLimit: readLimit rateLimit: writeLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
@@ -292,4 +292,50 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return backup; return backup;
} }
}); });
server.route({
method: "POST",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
backup: z.string().min(1)
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.loadProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
backup: req.body.backup
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.LOAD_PROJECT_KMS_BACKUP
}
});
return backup;
}
});
}; };
@@ -145,7 +145,8 @@ export enum EventType {
DELETE_KMS = "delete-kms", DELETE_KMS = "delete-kms",
GET_KMS = "get-kms", GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms", UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup" GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -1228,6 +1229,10 @@ interface GetProjectKmsBackupEvent {
type: EventType.GET_PROJECT_KMS_BACKUP; type: EventType.GET_PROJECT_KMS_BACKUP;
} }
interface LoadProjectKmsBackupEvent {
type: EventType.LOAD_PROJECT_KMS_BACKUP;
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1335,4 +1340,5 @@ export type Event =
| DeleteKmsEvent | DeleteKmsEvent
| GetKmsEvent | GetKmsEvent
| UpdateProjectKmsEvent | UpdateProjectKmsEvent
| GetProjectKmsBackupEvent; | GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent;
+2
View File
@@ -116,6 +116,8 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64"); export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
export const generateAsymmetricKeyPair = () => { export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair(); const pair = nacl.box.keyPair();
+62 -3
View File
@@ -11,6 +11,7 @@ import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto"; import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -598,16 +599,73 @@ export const kmsServiceFactory = ({
const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt }); const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt });
const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey }); const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey });
// format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey) // backup format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey).verificationHash
const secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString( let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString(
"base64" "base64"
)}`; )}`;
const verificationHash = generateHash(secretManagerBackup);
secretManagerBackup = `${secretManagerBackup}.${verificationHash}`;
return { return {
secretManager: secretManagerBackup secretManager: secretManagerBackup
}; };
}; };
const loadProjectKeyBackup = async (projectId: string, backup: string) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const [, backupProjectId, , backupKmsKeyId, backupBase64EncryptedDataKey, backupHash] = backup.split(".");
const computedHash = generateHash(backup.substring(0, backup.lastIndexOf(".")));
if (computedHash !== backupHash) {
throw new BadRequestError({
message: "Invalid backup"
});
}
if (backupProjectId !== projectId) {
throw new BadRequestError({
message: "Invalid backup for project"
});
}
const kmsDecryptor = await decryptWithKmsKey({ kmsId: backupKmsKeyId });
const dataKey = await kmsDecryptor({
cipherTextBlob: Buffer.from(backupBase64EncryptedDataKey, "base64")
});
const newKms = await kmsDAL.transaction(async (tx) => {
const key = await generateKmsKey({
isReserved: true,
orgId: project.orgId,
tx
});
const kmsEncryptor = await encryptWithKmsKey({ kmsId: key.id }, tx);
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
await projectDAL.updateById(
projectId,
{
kmsSecretManagerKeyId: key.id,
kmsSecretManagerEncryptedDataKey: cipherTextBlob
},
tx
);
return kmsDAL.findByIdWithAssociatedKms(key.id, tx);
});
return {
secretManagerKmsKey: newKms
};
};
const startService = async () => { const startService = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future // This will switch to a seal process and HMS flow in future
@@ -666,6 +724,7 @@ export const kmsServiceFactory = ({
getProjectSecretManagerKmsDataKey, getProjectSecretManagerKmsDataKey,
getProjectSecretManagerKmsKey, getProjectSecretManagerKmsKey,
updateProjectSecretManagerKmsKey, updateProjectSecretManagerKmsKey,
getProjectKeyBackup getProjectKeyBackup,
loadProjectKeyBackup
}; };
}; };
@@ -41,6 +41,7 @@ import {
TGetProjectDTO, TGetProjectDTO,
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertsDTO, TListProjectCertsDTO,
TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO, TUpdateProjectDTO,
@@ -78,7 +79,10 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
kmsService: Pick<TKmsServiceFactory, "updateProjectSecretManagerKmsKey" | "getProjectKeyBackup">; kmsService: Pick<
TKmsServiceFactory,
"updateProjectSecretManagerKmsKey" | "getProjectKeyBackup" | "loadProjectKeyBackup"
>;
}; };
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>; export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -721,6 +725,35 @@ export const projectServiceFactory = ({
return kmsBackup; return kmsBackup;
}; };
const loadProjectKmsBackup = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
backup
}: TLoadProjectKmsBackupDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to load KMS backup due to plan restriction. Upgrade to the enterprise plan."
});
}
const kmsBackup = await kmsService.loadProjectKeyBackup(projectId, backup);
return kmsBackup;
};
return { return {
createProject, createProject,
deleteProject, deleteProject,
@@ -736,6 +769,7 @@ export const projectServiceFactory = ({
updateVersionLimit, updateVersionLimit,
updateAuditLogsRetention, updateAuditLogsRetention,
updateProjectKmsKey, updateProjectKmsKey,
getProjectKmsBackup getProjectKmsBackup,
loadProjectKmsBackup
}; };
}; };
@@ -107,3 +107,7 @@ export type TListProjectCertsDTO = {
export type TUpdateProjectKmsDTO = { export type TUpdateProjectKmsDTO = {
secretManagerKmsKeyId: string; secretManagerKmsKeyId: string;
} & TProjectPermission; } & TProjectPermission;
export type TLoadProjectKmsBackupDTO = {
backup: string;
} & TProjectPermission;
+1
View File
@@ -1,5 +1,6 @@
export { export {
useAddAwsExternalKms, useAddAwsExternalKms,
useLoadProjectKmsBackup,
useRemoveExternalKms, useRemoveExternalKms,
useUpdateAwsExternalKms, useUpdateAwsExternalKms,
useUpdateProjectKms useUpdateProjectKms
+16
View File
@@ -140,3 +140,19 @@ export const useUpdateProjectKms = (projectId: string) => {
} }
}); });
}; };
export const useLoadProjectKmsBackup = (projectId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (backup: string) => {
const { data } = await apiRequest.post(`/api/v1/workspace/${projectId}/kms/backup`, {
backup
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getActiveProjectKms(projectId));
}
});
};
@@ -24,7 +24,12 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useGetActiveProjectKms, useGetExternalKmsList, useUpdateProjectKms } from "@app/hooks/api"; import {
useGetActiveProjectKms,
useGetExternalKmsList,
useLoadProjectKmsBackup,
useUpdateProjectKms
} from "@app/hooks/api";
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries"; import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
import { Organization, Workspace } from "@app/hooks/api/types"; import { Organization, Workspace } from "@app/hooks/api/types";
@@ -64,14 +69,10 @@ const BackupConfirmationModal = ({
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Create KMS backup"> <ModalContent title="Create KMS backup">
<p className="mb-8 text-bunker-300"> <p className="mb-10 text-bunker-300">
In case of interruptions with your configured external KMS, use this generated backup to In case of interruptions with your configured external KMS, load the generated backup to
set the project&apos;s KMS back to the default Infisical KMS. set the project&apos;s KMS back to the default Infisical KMS.
</p> </p>
<p className="mb-8 text-bunker-300">
Note: The project data key will be encrypted the organization&apos;s default Infisical
KMS.
</p>
<Button onClick={downloadKmsBackup}>Continue</Button> <Button onClick={downloadKmsBackup}>Continue</Button>
<Button <Button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
@@ -98,14 +99,26 @@ const LoadBackupModal = ({
workspace?: Workspace; workspace?: Workspace;
}) => { }) => {
const fileUploadRef = useRef<HTMLInputElement>(null); const fileUploadRef = useRef<HTMLInputElement>(null);
const { mutateAsync: loadKmsBackup, isLoading } = useLoadProjectKmsBackup(workspace?.id!);
const [backupContent, setBackupContent] = useState(""); const [backupContent, setBackupContent] = useState("");
const [backupFileName, setBackupFileName] = useState(""); const [backupFileName, setBackupFileName] = useState("");
const uploadKmsBackup = async () => { const uploadKmsBackup = async () => {
if (!workspace || !org) { if (!workspace || !org) {
// eslint-disable-next-line no-useless-return
return; return;
} }
try {
await loadKmsBackup(backupContent);
createNotification({
text: "Successfully loaded KMS backup",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
}
}; };
const parseFile = (file?: File) => { const parseFile = (file?: File) => {
@@ -174,9 +187,16 @@ const LoadBackupModal = ({
<FontAwesomeIcon icon={faUpload} size="3x" /> <FontAwesomeIcon icon={faUpload} size="3x" />
</IconButton> </IconButton>
</div> </div>
{backupFileName && <div className="mt-2 flex justify-center">{backupFileName}</div>} {backupFileName && (
<div className="mt-2 flex justify-center px-4 text-center">{backupFileName}</div>
)}
{backupContent && ( {backupContent && (
<Button onClick={uploadKmsBackup} className="mt-10 w-fit"> <Button
onClick={uploadKmsBackup}
className="mt-10 w-fit"
disabled={isLoading}
isLoading={isLoading}
>
Continue Continue
</Button> </Button>
)} )}