mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 01:27:41 +00:00
feat: load project kms backup
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,5 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
useAddAwsExternalKms,
|
useAddAwsExternalKms,
|
||||||
|
useLoadProjectKmsBackup,
|
||||||
useRemoveExternalKms,
|
useRemoveExternalKms,
|
||||||
useUpdateAwsExternalKms,
|
useUpdateAwsExternalKms,
|
||||||
useUpdateProjectKms
|
useUpdateProjectKms
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
+30
-10
@@ -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's KMS back to the default Infisical KMS.
|
set the project'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'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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user