Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/src/ee/services/audit-log/audit-log-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ export enum EventType {
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
CMEK_GET_PRIVATE_KEY = "cmek-get-private-key",
CMEK_BULK_EXPORT_PRIVATE_KEYS = "cmek-bulk-export-private-keys",
CMEK_BULK_IMPORT_KEYS = "cmek-bulk-import-keys",

UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
Expand Down Expand Up @@ -3675,6 +3676,15 @@ interface CmekBulkGetPrivateKeysEvent {
};
}

interface CmekBulkImportKeysEvent {
type: EventType.CMEK_BULK_IMPORT_KEYS;
metadata: {
keyNames: string[];
failedKeyNames: string[];
projectId: string;
};
}

interface GetExternalGroupOrgRoleMappingsEvent {
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
metadata?: Record<string, never>; // not needed, based off orgId
Expand Down Expand Up @@ -6453,6 +6463,7 @@ export type Event =
| CmekGetPublicKeyEvent
| CmekGetPrivateKeyEvent
| CmekBulkGetPrivateKeysEvent
| CmekBulkImportKeysEvent
| GetExternalGroupOrgRoleMappingsEvent
| UpdateExternalGroupOrgRoleMappingsEvent
| GetProjectTemplatesEvent
Expand Down
95 changes: 95 additions & 0 deletions backend/src/server/routes/v1/cmek-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,101 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
}
});

server.route({
method: "POST",
url: "/keys/bulk-import",
config: { rateLimit: writeLimit },
schema: {
hide: false,
operationId: "bulkImportKmsKeys",
tags: [ApiDocsTags.KmsKeys],
description: "Bulk import KMS keys with provided key material into a project.",
body: z.object({
projectId: z.string().uuid(),
keys: z
.array(
z
.object({
name: keyNameSchema,
keyUsage: z.nativeEnum(KmsKeyUsage),
encryptionAlgorithm: z.enum(AllowedEncryptionKeyAlgorithms),
keyMaterial: z.string().min(1)
Comment thread
victorvhs017 marked this conversation as resolved.
Comment thread
victorvhs017 marked this conversation as resolved.
})
.superRefine((data, ctx) => {
if (
data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT &&
!Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `encryptionAlgorithm must be a symmetric algorithm for encrypt-decrypt keys`
});
}
if (
data.keyUsage === KmsKeyUsage.SIGN_VERIFY &&
!Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `encryptionAlgorithm must be an asymmetric algorithm for sign-verify keys`
});
}
if (!isBase64(data.keyMaterial)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["keyMaterial"],
message: "keyMaterial must be base64 encoded"
});
}
})
)
.min(1)
.max(100)
}),
response: {
200: z.object({
keys: z.array(z.object({ id: z.string(), name: z.string() })),
errors: z.array(z.object({ name: z.string(), message: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
body: { projectId, keys },
permission
} = req;

const { keys: importedKeys, errors } = await server.services.cmek.bulkImportKeys(
{
projectId,
keys: keys.map((k) => ({
name: k.name,
algorithm: k.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
keyUsage: k.keyUsage,
keyMaterial: k.keyMaterial
}))
},
permission
);

await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CMEK_BULK_IMPORT_KEYS,
metadata: {
keyNames: importedKeys.map((k) => k.name),
failedKeyNames: errors.map((e) => e.name),
projectId
}
}
});

return { keys: importedKeys, errors };
}
});

server.route({
method: "POST",
url: "/keys/bulk-export-private-keys",
Expand Down
61 changes: 60 additions & 1 deletion backend/src/services/cmek/cmek-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import {
TCmekBulkGetPrivateKeysDTO,
TCmekBulkImportKeysDTO,
TCmekBulkImportKeysResult,
TCmekDecryptDTO,
TCmekEncryptDTO,
TCmekGetPrivateKeyDTO,
Expand Down Expand Up @@ -493,6 +495,62 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
};
};

const bulkImportKeys = async (
{ projectId, keys }: TCmekBulkImportKeysDTO,
actor: OrgServiceActor
): Promise<TCmekBulkImportKeysResult> => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.KMS
});

ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);

const results = await Promise.allSettled(
keys.map(async (entry): Promise<{ id: string; name: string }> => {
const imported = await kmsService.importKeyMaterial({
key: Buffer.from(entry.keyMaterial, "base64"),
algorithm: entry.algorithm,
name: entry.name,
isReserved: false,
projectId,
orgId: actor.orgId,
keyUsage: entry.keyUsage
});
return { id: imported.id, name: imported.name };
})
);

const importedKeys: { id: string; name: string }[] = [];
const errors: { name: string; message: string }[] = [];

results.forEach((result, i) => {
const entry = keys[i];
if (!entry) return;
if (result.status === "fulfilled") {
importedKeys.push(result.value);
} else {
const reason = result.reason as Error;
let message = "Failed to import key";
if (
reason instanceof DatabaseError &&
(reason.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
) {
message = `A key with the name "${entry.name}" already exists in this project`;
} else if (reason instanceof BadRequestError) {
message = reason.message;
}
errors.push({ name: entry.name, message });
}
});

return { keys: importedKeys, errors, projectId };
};

return {
createCmek,
updateCmekById,
Expand All @@ -507,6 +565,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
listSigningAlgorithms,
getPublicKey,
getPrivateKey,
bulkGetPrivateKeys
bulkGetPrivateKeys,
bulkImportKeys
};
};
18 changes: 18 additions & 0 deletions backend/src/services/cmek/cmek-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ export type TCmekBulkGetPrivateKeysDTO = {
keyIds: string[];
};

export type TCmekBulkImportKeyEntry = {
name: string;
algorithm: TCmekKeyEncryptionAlgorithm;
keyUsage: KmsKeyUsage;
keyMaterial: string;
};

export type TCmekBulkImportKeysDTO = {
projectId: string;
keys: TCmekBulkImportKeyEntry[];
};

export type TCmekBulkImportKeysResult = {
keys: { id: string; name: string }[];
errors: { name: string; message: string }[];
projectId: string;
};

export type TCmekSignDTO = {
keyId: string;
data: string;
Expand Down
25 changes: 22 additions & 3 deletions backend/src/services/kms/kms-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,27 @@ export const kmsServiceFactory = ({
{ key, algorithm, name, isReserved, projectId, orgId, keyUsage, kmipMetadata }: TImportKeyMaterialDTO,
tx?: Knex
) => {
// daniel: currently we only support imports for encrypt/decrypt keys
verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT });
verifyKeyTypeAndAlgorithm(keyUsage, algorithm);

if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
const expectedLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm);
if (key.length !== expectedLength) {
throw new BadRequestError({
message: `Invalid key material length for ${algorithm}. Expected ${expectedLength} bytes, got ${key.length}.`
});
}
}

if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
const { getPublicKeyFromPrivateKey } = signingService(algorithm as AsymmetricKeyAlgorithm);
try {
getPublicKeyFromPrivateKey(key);
} catch {
throw new BadRequestError({
message: "Invalid private key material. Expected a PKCS8 PEM-encoded private key."
});
}
Comment thread
victorvhs017 marked this conversation as resolved.
}

const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);

Expand All @@ -415,7 +434,7 @@ export const kmsServiceFactory = ({
const kmsDoc = await kmsDAL.create(
{
name: sanitizedName,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
keyUsage,
orgId,
isReserved,
projectId,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/services/kms/kms-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export type TGetBulkKeyMaterialDTO = {

export type TImportKeyMaterialDTO = {
key: Buffer;
algorithm: SymmetricKeyAlgorithm;
algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
name?: string;
isReserved: boolean;
projectId: string;
Expand Down
51 changes: 51 additions & 0 deletions docs/documentation/platform/kms/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,57 @@ In the following steps, we explore how to verify data using an existing key in I
</Tab>
</Tabs>

## Bulk Export & Import

Infisical KMS supports bulk export and bulk import of key material, allowing you to export multiple keys at once or import keys in bulk from an external file.

### Bulk Export

1. Navigate to **Project > Key Management**.
2. Use the checkboxes to select the keys you want to export (up to 100 at a time).
3. Click the **Export** button that appears in the action bar above the table.
![kms bulk export](/images/platform/kms/infisical-kms/kms-bulk-export.png)
4. A `.json` file is downloaded containing the key metadata and key material.

The exported file is a JSON array where each entry is one of the following shapes:

```json
// Encrypt/Decrypt key
{
"name": "my-aes-key",
"keyType": "encrypt-decrypt",
"algorithm": "aes-256-gcm",
"keyMaterial": "<base64-encoded key material>"
}

// Sign/Verify key
{
"name": "my-rsa-key",
"keyType": "sign-verify",
"algorithm": "rsa-4096",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is incorrect format - should be RSA_4096

Image

"privateKey": "<base64-encoded private key>",
"publicKey": "<base64-encoded public key>"
}
```

<Warning>
The exported file contains sensitive cryptographic key material. Store it securely and do not share it.
</Warning>

### Bulk Import

1. Navigate to **Project > Key Management**.
2. In the top-right of the keys table, click the dropdown arrow next to **Add Key** and select **Import Keys**.
3. Upload a `.json` file matching the export format described above (drag and drop, or click to browse).
![kms import modal](/images/platform/kms/infisical-kms/kms-import-modal.png)
4. A preview of the keys to be imported is shown, along with any validation errors.
![kms import preview](/images/platform/kms/infisical-kms/kms-import-preview.png)
5. Click **Import** to create the keys in the project.

<Note>
Importing creates new key entries. It does not overwrite existing keys with the same name.
</Note>

## FAQ

<AccordionGroup>
Expand Down
Binary file modified docs/images/platform/kms/infisical-kms/kms-add-key-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/kms-add-key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/kms-decrypt-data.png
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: your nord pass is showing 😏

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/kms-decrypted-data.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/kms-encrypt-data.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/kms-encrypted-data.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/kms-key-options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/signing/add-new-rsa-key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/platform/kms/infisical-kms/signing/sign-options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions frontend/src/hooks/api/cmeks/mutations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { cmekKeys } from "@app/hooks/api/cmeks/queries";
import {
TCmekBulkExportPrivateKeysDTO,
TCmekBulkExportPrivateKeysResponse,
TCmekBulkImportKeysDTO,
TCmekBulkImportKeysResponse,
TCmekDecrypt,
TCmekDecryptResponse,
TCmekEncrypt,
Expand Down Expand Up @@ -145,3 +147,20 @@ export const useBulkExportCmekPrivateKeys = () => {
}
});
};

export const useBulkImportCmekKeys = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ projectId, keys }: TCmekBulkImportKeysDTO) => {
const { data } = await apiRequest.post<TCmekBulkImportKeysResponse>(
"/api/v1/kms/keys/bulk-import",
{ projectId, keys }
);

return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: cmekKeys.getCmeksByProjectId({ projectId }) });
}
});
};
17 changes: 17 additions & 0 deletions frontend/src/hooks/api/cmeks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,23 @@ export type TCmekBulkExportPrivateKeysResponse = {
keys: TCmekBulkExportedKey[];
};

export type TCmekBulkImportKeyEntry = {
name: string;
keyUsage: KmsKeyUsage;
encryptionAlgorithm: AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm;
keyMaterial: string;
};

export type TCmekBulkImportKeysDTO = {
projectId: string;
keys: TCmekBulkImportKeyEntry[];
};

export type TCmekBulkImportKeysResponse = {
keys: { id: string; name: string }[];
errors: { name: string; message: string }[];
};

export enum CmekOrderBy {
Name = "name"
}
Expand Down
Loading
Loading