diff --git a/backend/src/db/migrations/20240505154140_kubernetes-auth.ts b/backend/src/db/migrations/20240512231707_kubernetes-auth.ts similarity index 93% rename from backend/src/db/migrations/20240505154140_kubernetes-auth.ts rename to backend/src/db/migrations/20240512231707_kubernetes-auth.ts index f5f79c3a3..dd281a3ad 100644 --- a/backend/src/db/migrations/20240505154140_kubernetes-auth.ts +++ b/backend/src/db/migrations/20240512231707_kubernetes-auth.ts @@ -15,10 +15,10 @@ export async function up(knex: Knex): Promise { t.uuid("identityId").notNullable().unique(); t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); t.string("kubernetesHost").notNullable(); - t.string("encryptedCaCert").notNullable(); + t.text("encryptedCaCert").notNullable(); t.string("caCertIV").notNullable(); t.string("caCertTag").notNullable(); - t.string("encryptedTokenReviewerJwt").notNullable(); + t.text("encryptedTokenReviewerJwt").notNullable(); t.string("tokenReviewerJwtIV").notNullable(); t.string("tokenReviewerJwtTag").notNullable(); t.string("allowedNamespaces").notNullable(); diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 32249aac5..d20ea0edc 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -22,7 +22,7 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit( export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/kubernetes/login", + url: "/kubernetes-auth/login", config: { rateLimit: writeLimit }, @@ -88,7 +88,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }), body: z.object({ kubernetesHost: z.string().trim().min(1), - caCert: z.string().trim().min(1), + caCert: z.string().trim().default(""), tokenReviewerJwt: z.string().trim().min(1), allowedNamespaces: z.string(), // TODO: validation allowedNames: z.string(), @@ -174,7 +174,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }), body: z.object({ kubernetesHost: z.string().trim().min(1).optional(), - kubernetesCaCert: z.string().trim().min(1).optional(), + caCert: z.string().trim().optional(), tokenReviewerJwt: z.string().trim().min(1).optional(), allowedNamespaces: z.string().optional(), // TODO: validation allowedNames: z.string().optional(), diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index e6056ad72..cba7e87ae 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -115,11 +115,9 @@ export const identityKubernetesAuthServiceFactory = ({ "Content-Type": "application/json", Authorization: `Bearer ${tokenReviewerJwt}` }, - ...(caCert && { - httpsAgent: new https.Agent({ - ca: caCert, - rejectUnauthorized: true - }) + httpsAgent: new https.Agent({ + ca: caCert, + rejectUnauthorized: false // TODO: change to [true] }) } ); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 764761098..422fe43f3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -91,6 +91,8 @@ services: - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src + extra_hosts: + - "host.docker.internal:host-gateway" frontend: container_name: infisical-dev-frontend @@ -128,7 +130,7 @@ services: ports: - 1025:1025 # SMTP server - 8025:8025 # Web UI - + openldap: # note: more advanced configuration is available image: osixia/openldap:1.5.0 restart: always diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 64f9469c6..d06fbc776 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -2,5 +2,6 @@ import { IdentityAuthMethod } from "./enums"; export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", - [IdentityAuthMethod.AWS_AUTH]: "AWS Auth" + [IdentityAuthMethod.AWS_AUTH]: "AWS Auth", + [IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index dc619f722..716504ae8 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -1,4 +1,5 @@ export enum IdentityAuthMethod { UNIVERSAL_AUTH = "universal-auth", - AWS_AUTH = "aws-auth" + AWS_AUTH = "aws-auth", + KUBERNETES_AUTH = "kubernetes-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index cef827d9f..6b2175543 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -2,6 +2,7 @@ export { identityAuthToNameMap } from "./constants"; export { IdentityAuthMethod } from "./enums"; export { useAddIdentityAwsAuth, + useAddIdentityKubernetesAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, @@ -9,10 +10,11 @@ export { useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, useUpdateIdentityAwsAuth, - useUpdateIdentityUniversalAuth -} from "./mutations"; + useUpdateIdentityKubernetesAuth, + useUpdateIdentityUniversalAuth} from "./mutations"; export { useGetIdentityAwsAuth, + useGetIdentityKubernetesAuth, useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 8780f178d..6abf03589 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -6,6 +6,7 @@ import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { AddIdentityAwsAuthDTO, + AddIdentityKubernetesAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, @@ -15,11 +16,12 @@ import { DeleteIdentityUniversalAuthClientSecretDTO, Identity, IdentityAwsAuth, + IdentityKubernetesAuth, IdentityUniversalAuth, UpdateIdentityAwsAuthDTO, UpdateIdentityDTO, - UpdateIdentityUniversalAuthDTO -} from "./types"; + UpdateIdentityKubernetesAuthDTO, + UpdateIdentityUniversalAuthDTO} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -243,3 +245,88 @@ export const useUpdateIdentityAwsAuth = () => { } }); }; + +// --- K8s auth (TODO: add cert and token reviewer JWT fields) + +export const useAddIdentityKubernetesAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + kubernetesHost, + tokenReviewerJwt, + allowedNames, + allowedNamespaces, + allowedAudience, + caCert, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityKubernetesAuth } + } = await apiRequest.post<{ identityKubernetesAuth: IdentityKubernetesAuth }>( + `/api/v1/auth/kubernetes-auth/identities/${identityId}`, + { + kubernetesHost, + tokenReviewerJwt, + allowedNames, + allowedNamespaces, + allowedAudience, + caCert, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityKubernetesAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityKubernetesAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + kubernetesHost, + tokenReviewerJwt, + allowedNamespaces, + allowedNames, + allowedAudience, + caCert, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityKubernetesAuth } + } = await apiRequest.patch<{ identityKubernetesAuth: IdentityKubernetesAuth }>( + `/api/v1/auth/kubernetes-auth/identities/${identityId}`, + { + kubernetesHost, + tokenReviewerJwt, + allowedNames, + allowedNamespaces, + allowedAudience, + caCert, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + return identityKubernetesAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 26880d5ea..e7497b4de 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -2,14 +2,20 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { ClientSecretData, IdentityAwsAuth, IdentityUniversalAuth } from "./types"; +import { + ClientSecretData, + IdentityAwsAuth, + IdentityKubernetesAuth, + IdentityUniversalAuth} from "./types"; export const identitiesKeys = { getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => [{ identityId }, "identity-universal-auth-client-secrets"] as const, - getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const + getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, + getIdentityKubernetesAuth: (identityId: string) => + [{ identityId }, "identity-kubernetes-auth"] as const }; export const useGetIdentityUniversalAuth = (identityId: string) => { @@ -56,3 +62,18 @@ export const useGetIdentityAwsAuth = (identityId: string) => { } }); }; + +export const useGetIdentityKubernetesAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId), + queryFn: async () => { + const { + data: { identityKubernetesAuth } + } = await apiRequest.get<{ identityKubernetesAuth: IdentityKubernetesAuth }>( + `/api/v1/auth/kubernetes-auth/identities/${identityId}` + ); + return identityKubernetesAuth; + } + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 1a7ba263f..583e6add1 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -153,6 +153,54 @@ export type UpdateIdentityAwsAuthDTO = { }[]; }; +export type IdentityKubernetesAuth = { + identityId: string; + kubernetesHost: string; + tokenReviewerJwt: string; + allowedNamespaces: string; + allowedNames: string; + allowedAudience: string; + caCert: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityKubernetesAuthDTO = { + organizationId: string; + identityId: string; + kubernetesHost: string; + tokenReviewerJwt: string; + allowedNamespaces: string; + allowedNames: string; + allowedAudience: string; + caCert: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityKubernetesAuthDTO = { + organizationId: string; + identityId: string; + kubernetesHost?: string; + tokenReviewerJwt?: string; + allowedNamespaces?: string; + allowedNames?: string; + allowedAudience?: string; + caCert?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; description?: string; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index 22fbfee5e..e3ecd9414 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -15,6 +15,7 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; +import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; type Props = { @@ -28,7 +29,8 @@ type Props = { const identityAuthMethods = [ { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, - { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH } + { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, + { label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH } ]; const schema = yup @@ -75,6 +77,15 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog /> ); } + case IdentityAuthMethod.KUBERNETES_AUTH: { + return ( + + ); + } case IdentityAuthMethod.UNIVERSAL_AUTH: { return ( ; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityKubernetesAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); + + const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + kubernetesHost: "", // TODO + tokenReviewerJwt: "", + allowedNames: "", // TODO + allowedNamespaces: "", // TODO + allowedAudience: "", // TODO + caCert: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + kubernetesHost: data.kubernetesHost, + tokenReviewerJwt: data.tokenReviewerJwt, + allowedNames: data.allowedNames, + allowedNamespaces: data.allowedNamespaces, + allowedAudience: data.allowedAudience, + caCert: data.caCert, + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + kubernetesHost: "", // TODO + tokenReviewerJwt: "", + allowedNames: "", + allowedNamespaces: "", + allowedAudience: "", + caCert: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + kubernetesHost, + tokenReviewerJwt, + allowedNames, + allowedNamespaces, + allowedAudience, + caCert, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + kubernetesHost, + tokenReviewerJwt, + allowedNames, + allowedNamespaces, + allowedAudience, + caCert, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + kubernetesHost: kubernetesHost || "", + tokenReviewerJwt, + allowedNames: allowedNames || "", + allowedNamespaces: allowedNamespaces || "", + allowedAudience: allowedAudience || "", + caCert: caCert || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${ + identityAuthMethodData?.authMethod ? "updated" : "configured" + } auth method`, + type: "success" + }); + + reset(); + } catch (err) { + createNotification({ + text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + +