mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 15:41:27 +00:00
feat: HSM support
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
export {
|
||||
useAdminDeleteUser,
|
||||
useCreateAdminUser,
|
||||
useExportServerDecryptionKey,
|
||||
useImportServerDecryptionKey,
|
||||
useUpdateAdminSlackConfig,
|
||||
useUpdateServerConfig
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
} from "./mutation";
|
||||
export { useAdminGetUsers, useGetAdminSlackConfig, useGetServerConfig } from "./queries";
|
||||
export {
|
||||
useAdminGetUsers,
|
||||
useGetAdminSlackConfig,
|
||||
useGetServerConfig,
|
||||
useGetServerRootKmsEncryptionDetails
|
||||
} from "./queries";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { User } from "../users/types";
|
||||
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||
import {
|
||||
AdminSlackConfig,
|
||||
RootKeyEncryptionStrategy,
|
||||
TCreateAdminUserDTO,
|
||||
TServerConfig,
|
||||
TUpdateAdminSlackConfigDTO
|
||||
@@ -85,3 +86,36 @@ export const useUpdateAdminSlackConfig = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateServerEncryptionStrategy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (strategy: RootKeyEncryptionStrategy) => {
|
||||
await apiRequest.post("/api/v1/admin/encryption-strategies", { strategy });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(adminQueryKeys.getServerEncryptionStrategies());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useExportServerDecryptionKey = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiRequest.post<{ secretParts: string[] }>("/api/v1/admin/kms-export");
|
||||
return data.secretParts;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportServerDecryptionKey = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (secretParts: string[]) => {
|
||||
await apiRequest.post("/api/v1/admin/kms-import", { secretParts });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,12 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-que
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { User } from "../types";
|
||||
import { AdminGetUsersFilters, AdminSlackConfig, TServerConfig } from "./types";
|
||||
import {
|
||||
AdminGetUsersFilters,
|
||||
AdminSlackConfig,
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
} from "./types";
|
||||
|
||||
export const adminStandaloneKeys = {
|
||||
getUsers: "get-users"
|
||||
@@ -12,7 +17,8 @@ export const adminStandaloneKeys = {
|
||||
export const adminQueryKeys = {
|
||||
serverConfig: () => ["server-config"] as const,
|
||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
||||
};
|
||||
|
||||
const fetchServerConfig = async () => {
|
||||
@@ -61,8 +67,8 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAdminSlackConfig = () =>
|
||||
useQuery({
|
||||
export const useGetAdminSlackConfig = () => {
|
||||
return useQuery({
|
||||
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<AdminSlackConfig>(
|
||||
@@ -72,3 +78,17 @@ export const useGetAdminSlackConfig = () =>
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetServerRootKmsEncryptionDetails = () => {
|
||||
return useQuery({
|
||||
queryKey: adminQueryKeys.getServerEncryptionStrategies(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetServerRootKmsEncryptionDetails>(
|
||||
"/api/v1/admin/root-kms-config"
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -54,3 +54,17 @@ export type AdminSlackConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
export type TGetServerRootKmsEncryptionDetails = {
|
||||
strategies: {
|
||||
strategy: RootKeyEncryptionStrategy;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
}[];
|
||||
keyExported: boolean;
|
||||
};
|
||||
|
||||
export enum RootKeyEncryptionStrategy {
|
||||
Basic = "BASIC",
|
||||
Hsm = "HSM"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const useFileDownload = () => {
|
||||
return useCallback((content: string, filename: string) => {
|
||||
const downloadUrl = `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`;
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.setAttribute("download", filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}, []);
|
||||
};
|
||||
@@ -22,15 +22,21 @@ import {
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useServerConfig, useUser } from "@app/context";
|
||||
import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
|
||||
import {
|
||||
useGetOrganizations,
|
||||
useGetServerRootKmsEncryptionDetails,
|
||||
useUpdateServerConfig
|
||||
} from "@app/hooks/api";
|
||||
|
||||
import { AuthPanel } from "./AuthPanel";
|
||||
import { EncryptionPanel } from "./EncryptionPanel";
|
||||
import { IntegrationPanel } from "./IntegrationPanel";
|
||||
import { RateLimitPanel } from "./RateLimitPanel";
|
||||
import { UserPanel } from "./UserPanel";
|
||||
|
||||
enum TabSections {
|
||||
Settings = "settings",
|
||||
Encryption = "encryption",
|
||||
Auth = "auth",
|
||||
RateLimit = "rate-limit",
|
||||
Integrations = "integrations",
|
||||
@@ -55,6 +61,7 @@ type TDashboardForm = z.infer<typeof formSchema>;
|
||||
export const AdminDashboardPage = () => {
|
||||
const router = useRouter();
|
||||
const data = useServerConfig();
|
||||
const { data: serverRootKmsDetails } = useGetServerRootKmsEncryptionDetails();
|
||||
const { config } = data;
|
||||
|
||||
const {
|
||||
@@ -137,6 +144,7 @@ export const AdminDashboardPage = () => {
|
||||
<TabList>
|
||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||
<Tab value={TabSections.Settings}>General</Tab>
|
||||
{!!serverRootKmsDetails && <Tab value={TabSections.Encryption}>Encryption</Tab>}
|
||||
<Tab value={TabSections.Auth}>Authentication</Tab>
|
||||
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||
@@ -321,6 +329,11 @@ export const AdminDashboardPage = () => {
|
||||
</Button>
|
||||
</form>
|
||||
</TabPanel>
|
||||
{!!serverRootKmsDetails && (
|
||||
<TabPanel value={TabSections.Encryption}>
|
||||
<EncryptionPanel rootKmsDetails={serverRootKmsDetails} />
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel value={TabSections.Auth}>
|
||||
<AuthPanel />
|
||||
</TabPanel>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useCallback } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useUpdateServerEncryptionStrategy } from "@app/hooks/api";
|
||||
import {
|
||||
RootKeyEncryptionStrategy,
|
||||
TGetServerRootKmsEncryptionDetails
|
||||
} from "@app/hooks/api/admin/types";
|
||||
|
||||
import { ExportRootKmsKeyModalContent } from "./components/ExportRootKmsKeyModalContent";
|
||||
import { RestoreRootKmsKeyModalContent } from "./components/RestoreRootKmsKeyModalContent";
|
||||
|
||||
const formSchema = z.object({
|
||||
encryptionStrategy: z.nativeEnum(RootKeyEncryptionStrategy)
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
rootKmsDetails: TGetServerRootKmsEncryptionDetails;
|
||||
};
|
||||
|
||||
export const EncryptionPanel = ({ rootKmsDetails }: Props) => {
|
||||
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
|
||||
const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp([
|
||||
"exportKey",
|
||||
"restoreKey"
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
encryptionStrategy:
|
||||
rootKmsDetails?.strategies?.find((s) => s.enabled)?.strategy ??
|
||||
RootKeyEncryptionStrategy.Basic
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(async (formData: TForm) => {
|
||||
try {
|
||||
await updateEncryptionStrategy(formData.encryptionStrategy);
|
||||
|
||||
if (
|
||||
!rootKmsDetails.keyExported &&
|
||||
formData.encryptionStrategy !== RootKeyEncryptionStrategy.Basic
|
||||
) {
|
||||
handlePopUpOpen("exportKey");
|
||||
}
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Encryption strategy updated successfully"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update encryption strategy"
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
|
||||
KMS Encryption Strategy
|
||||
</div>
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
{!rootKmsDetails.keyExported && (
|
||||
<div className="mb-2 text-sm">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="mr-1 text-red-500" />
|
||||
You have not exported the KMS root encryption key. Switch to HSM encryption or
|
||||
run the{" "}
|
||||
<code>
|
||||
<span className="mt-2 rounded-md bg-mineshaft-600 p-1 text-xs text-primary-500">
|
||||
infisical kms export
|
||||
</span>
|
||||
</code>{" "}
|
||||
CLI command to export the key parts.
|
||||
</div>
|
||||
)}
|
||||
<br />
|
||||
If you experience issues with accessing projects while not using Regular
|
||||
Encryption (default), you can restore the KMS root encryption key by using your
|
||||
exported key parts.
|
||||
<br /> <br />
|
||||
If you do not have the exported key parts, you can export them by using the CLI
|
||||
command
|
||||
<br />
|
||||
<code>
|
||||
<span className="mt-2 rounded-md bg-mineshaft-600 p-1 text-xs text-primary-500">
|
||||
infisical kms export
|
||||
</span>
|
||||
</code>
|
||||
. <br />
|
||||
<br />
|
||||
<span className="font-bold">
|
||||
Please keep in mind that you can only export the key parts once.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isDisabled={!rootKmsDetails.keyExported}
|
||||
onClick={() => handlePopUpToggle("restoreKey", true)}
|
||||
>
|
||||
Restore Root KMS Encryption Key
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
|
||||
Select which type of encryption strategy you want to use for your KMS root key. HSM is
|
||||
supported on Enterprise plans.
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="encryptionStrategy"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl className="max-w-sm" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
className="w-full bg-mineshaft-700"
|
||||
dropdownContainerClassName="bg-mineshaft-800"
|
||||
defaultValue={field.value}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
{...field}
|
||||
>
|
||||
{rootKmsDetails.strategies?.map((strategy) => (
|
||||
<SelectItem key={strategy.strategy} value={strategy.strategy}>
|
||||
{strategy.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Modal
|
||||
isOpen={popUp.exportKey.isOpen}
|
||||
onOpenChange={(state) => handlePopUpToggle("exportKey", state)}
|
||||
>
|
||||
<ExportRootKmsKeyModalContent handlePopUpToggle={handlePopUpToggle} />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={popUp.restoreKey.isOpen}
|
||||
onOpenChange={(state) => handlePopUpToggle("restoreKey", state)}
|
||||
>
|
||||
<RestoreRootKmsKeyModalContent handlePopUpToggle={handlePopUpToggle} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Button, ModalContent } from "@app/components/v2";
|
||||
import { useExportServerDecryptionKey } from "@app/hooks/api";
|
||||
import { useFileDownload } from "@app/hooks/useFileDownload";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["exportKey"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const ExportRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => {
|
||||
const { mutateAsync: exportKey, isLoading } = useExportServerDecryptionKey();
|
||||
const downloadFile = useFileDownload();
|
||||
const [downloaded, setDownloaded] = useState(false);
|
||||
|
||||
const onExport = useCallback(async () => {
|
||||
const keyParts = await exportKey();
|
||||
downloadFile(keyParts.join("\n\n"), "infisical-encryption-key-parts.txt");
|
||||
setDownloaded(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
title="Export Root KMS Encryption Key"
|
||||
subTitle="We highly recommend exporting the KMS root encryption key and storing it in a secure location. Incase of a disaster, you can use our CLI to recover your projects with zero loss."
|
||||
>
|
||||
<div className="flex w-full justify-end">
|
||||
{!downloaded ? (
|
||||
<>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
onClick={() => handlePopUpToggle("exportKey", false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button isLoading={isLoading} className="ml-2" onClick={onExport}>
|
||||
Download Key
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex max-w-fit flex-col overflow-clip break-words px-2 text-sm font-normal text-gray-400">
|
||||
The key parts have been downloaded. Please store them in a safe place. You will need
|
||||
these keys incase you need to recovery the KMS root encryption key. Please consult our
|
||||
documentation for further instructions.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, ModalContent } from "@app/components/v2";
|
||||
import { useImportServerDecryptionKey } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["restoreKey"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
keyParts: z
|
||||
.array(z.string())
|
||||
.refine((data) => data.length === 4 && data.every((part) => part.length > 0), {
|
||||
message: "Enter at least 4 key parts in order to restore the KMS root decryption key."
|
||||
})
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
export const RestoreRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => {
|
||||
const { mutateAsync: importKmsRootKey } = useImportServerDecryptionKey();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting, errors, isLoading, isValid }
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
keyParts: ["", "", "", ""]
|
||||
}
|
||||
});
|
||||
|
||||
const keyParts = useMemo(() => watch("keyParts"), []);
|
||||
|
||||
console.log("lol", watch("keyParts"));
|
||||
console.log("isVal", isValid);
|
||||
console.log("errors", errors);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
title="Export Root KMS Encryption Key"
|
||||
subTitle="Recover the KMS root encryption key by entering the key parts. You can recover the key if you have 4 out of 8 key parts."
|
||||
footerContent={
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
onClick={() => handlePopUpToggle("restoreKey", false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={!!errors.keyParts || !isValid}
|
||||
isLoading={isSubmitting || isLoading}
|
||||
className="ml-2"
|
||||
onClick={handleSubmit(async (data) => {
|
||||
await importKmsRootKey(data.keyParts);
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
title: "Successfully restored KMS root key",
|
||||
text: "The KMS root key has been successfully restored."
|
||||
});
|
||||
|
||||
handlePopUpToggle("restoreKey", false);
|
||||
})}
|
||||
>
|
||||
Restore Key
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form>
|
||||
<div className="flex w-full flex-col justify-end">
|
||||
{keyParts.map((_, index) => (
|
||||
<Controller
|
||||
key={`key-part-${index + 1}`}
|
||||
name={`keyParts.${index}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<FormControl label={`Key Part ${index + 1}`}>
|
||||
<Input {...field} placeholder={`Enter key part ${index + 1}`} />
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{errors.keyParts && (
|
||||
<div className="mt-2 text-sm font-normal text-red-500">{errors.keyParts.message}</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user