Remove service account JSON requirement from GCP Auth

This commit is contained in:
Tuan Dang
2024-05-10 09:56:35 -07:00
parent 610f474ecc
commit 0a91586904
14 changed files with 229 additions and 469 deletions
@@ -14,9 +14,6 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.text("encryptedCredentials").notNullable();
t.string("credentialsIV").notNullable();
t.string("credentialsTag").notNullable();
t.string("type").notNullable();
t.string("allowedServiceAccounts").notNullable();
t.string("allowedProjects").notNullable();
@@ -16,9 +16,6 @@ export const IdentityGcpAuthsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
encryptedCredentials: z.string(),
credentialsIV: z.string(),
credentialsTag: z.string(),
type: z.string(),
allowedServiceAccounts: z.string(),
allowedProjects: z.string(),
-1
View File
@@ -711,7 +711,6 @@ export const registerRoutes = async (
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
});
@@ -1,14 +1,12 @@
import { z } from "zod";
import { IdentityGcpAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import {
ModifiedIdentityGcpAuthsSchema,
validateGcpAuthField
} from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -76,7 +74,6 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
identityId: z.string().trim()
}),
body: z.object({
credentials: z.string().trim(),
type: z.enum(["iam", "gce"]),
allowedServiceAccounts: validateGcpAuthField,
allowedProjects: validateGcpAuthField,
@@ -107,7 +104,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
}),
response: {
200: z.object({
identityGcpAuth: ModifiedIdentityGcpAuthsSchema
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
@@ -162,7 +159,6 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
identityId: z.string().trim()
}),
body: z.object({
credentials: z.string().trim().optional(),
type: z.enum(["iam", "gce"]).optional(),
allowedServiceAccounts: validateGcpAuthField,
allowedProjects: validateGcpAuthField,
@@ -186,7 +182,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
}),
response: {
200: z.object({
identityGcpAuth: ModifiedIdentityGcpAuthsSchema
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
@@ -242,7 +238,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
}),
response: {
200: z.object({
identityGcpAuth: ModifiedIdentityGcpAuthsSchema
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
@@ -1,6 +1,5 @@
import axios from "axios";
import { JWTInput, OAuth2Client } from "google-auth-library";
import { google } from "googleapis";
import { OAuth2Client } from "google-auth-library";
import jwt from "jsonwebtoken";
import { UnauthorizedError } from "@app/lib/errors";
@@ -8,45 +7,13 @@ import { UnauthorizedError } from "@app/lib/errors";
import { TDecodedGcpIamAuthJwt, TGcpGceIdTokenPayload } from "./identity-gcp-auth-types";
/**
* Return the full details of the service account corresponding to the service account email or unique ID [serviceAccount].
* @param {string} serviceAccount - The email or unique ID of the service account.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
* @returns
*/
const getGcpServiceAccountDetails = async (serviceAccount: string, credentials: string) => {
const auth = new google.auth.GoogleAuth({
credentials: JSON.parse(credentials) as JWTInput,
scopes: ["https://www.googleapis.com/auth/iam"]
});
const iam = google.iam({
version: "v1",
auth
});
const { data } = await iam.projects.serviceAccounts.get({
name: `projects/-/serviceAccounts/${serviceAccount}`
});
return data;
};
/**
* Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP GCE authentication
* Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP ID Token authentication
* is valid.
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
* @param {string} jwt - The identity token to validate.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
*/
export const validateGceIdentity = async ({
identityId,
jwt: identityToken,
credentials
}: {
identityId: string;
jwt: string;
credentials: string;
}) => {
export const validateGceIdentity = async ({ identityId, jwt: identityToken }: { identityId: string; jwt: string }) => {
const oAuth2Client = new OAuth2Client();
const response = await oAuth2Client.getFederatedSignonCerts();
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
@@ -57,9 +24,8 @@ export const validateGceIdentity = async ({
);
const payload = ticket.getPayload() as TGcpGceIdTokenPayload;
if (!payload || !payload.email) throw new UnauthorizedError();
const serviceAccountDetails = await getGcpServiceAccountDetails(payload.email, credentials);
return { serviceAccountDetails, gceInstanceDetails: payload };
return { email: payload.email, computeEngineDetails: payload.google?.compute_engine };
};
/**
@@ -71,12 +37,10 @@ export const validateGceIdentity = async ({
*/
export const validateIamIdentity = async ({
identityId,
jwt: serviceAccountJwt,
credentials
jwt: serviceAccountJwt
}: {
identityId: string;
jwt: string;
credentials: string;
}) => {
const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt;
const { sub, aud } = decodedJwt.payload;
@@ -96,6 +60,5 @@ export const validateIamIdentity = async ({
});
if (aud !== identityId) throw new UnauthorizedError();
const serviceAccountDetails = await getGcpServiceAccountDetails(sub, credentials);
return { serviceAccountDetails };
return { email: sub };
};
@@ -1,23 +1,13 @@
import { ForbiddenError } from "@casl/ability";
import { iam_v1 } from "googleapis";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityGcpAuthsUpdate } from "@app/db/schemas";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
encryptSymmetric,
generateAsymmetricKeyPair,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -28,7 +18,7 @@ import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
import { validateGceIdentity, validateIamIdentity } from "./identity-gcp-auth-fns";
import {
TAttachGcpAuthDTO,
TGcpGceIdTokenPayload,
TGcpIdentityDetails,
TGetGcpAuthDTO,
TLoginGcpAuthDTO,
TUpdateGcpAuthDTO
@@ -39,7 +29,6 @@ type TIdentityGcpAuthServiceFactoryDep = {
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@@ -51,7 +40,6 @@ export const identityGcpAuthServiceFactory = ({
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
}: TIdentityGcpAuthServiceFactoryDep) => {
@@ -62,47 +50,20 @@ export const identityGcpAuthServiceFactory = ({
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId });
if (!identityMembershipOrg) throw new UnauthorizedError();
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCredentials, credentialsIV, credentialsTag } = identityGcpAuth;
let credentials = "";
if (encryptedCredentials && credentialsIV && credentialsTag) {
credentials = decryptSymmetric({
ciphertext: encryptedCredentials,
key,
tag: credentialsTag,
iv: credentialsIV
});
}
let serviceAccountDetails: iam_v1.Schema$ServiceAccount;
let gceInstanceDetails: TGcpGceIdTokenPayload | undefined;
let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) {
case "gce": {
const gceIdentity = await validateGceIdentity({
gcpIdentityDetails = await validateGceIdentity({
identityId,
jwt: serviceAccountJwt,
credentials
jwt: serviceAccountJwt
});
serviceAccountDetails = gceIdentity.serviceAccountDetails;
gceInstanceDetails = gceIdentity.gceInstanceDetails;
break;
}
case "iam": {
const iamIdentity = await validateIamIdentity({
gcpIdentityDetails = await validateIamIdentity({
identityId,
jwt: serviceAccountJwt,
credentials
jwt: serviceAccountJwt
});
serviceAccountDetails = iamIdentity.serviceAccountDetails;
break;
}
default: {
@@ -116,30 +77,29 @@ export const identityGcpAuthServiceFactory = ({
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
.split(",")
.map((serviceAccount) => serviceAccount.trim())
.some(
(serviceAccount) =>
serviceAccount === serviceAccountDetails.email || serviceAccount === serviceAccountDetails.uniqueId
);
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
if (!isServiceAccountAllowed) throw new UnauthorizedError();
}
if (identityGcpAuth.allowedProjects) {
// note that only possible to apply allowed projects/zones filters for GCE instances
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
// validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpAuth.allowedProjects
.split(",")
.map((project) => project.trim())
.some((project) => project === serviceAccountDetails.projectId);
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
if (!isProjectAllowed) throw new UnauthorizedError();
}
if (identityGcpAuth.type === "gce" && gceInstanceDetails && identityGcpAuth.allowedZones) {
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
const isZoneAllowed = identityGcpAuth.allowedZones
.split(",")
.map((zone) => zone.trim())
.some((zone) => zone === gceInstanceDetails!.google.compute_engine.zone);
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
if (!isZoneAllowed) throw new UnauthorizedError();
}
@@ -180,7 +140,6 @@ export const identityGcpAuthServiceFactory = ({
const attachGcpAuth = async ({
identityId,
credentials,
type,
allowedServiceAccounts,
allowedProjects,
@@ -232,60 +191,6 @@ export const identityGcpAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: identityMembershipOrg.orgId,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const {
ciphertext: encryptedCredentials,
iv: credentialsIV,
tag: credentialsTag
} = encryptSymmetric(credentials, key);
const identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const doc = await identityGcpAuthDAL.create(
{
@@ -294,9 +199,6 @@ export const identityGcpAuthServiceFactory = ({
allowedServiceAccounts,
allowedProjects,
allowedZones,
encryptedCredentials,
credentialsIV,
credentialsTag,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
@@ -313,13 +215,12 @@ export const identityGcpAuthServiceFactory = ({
);
return doc;
});
return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId };
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
};
const updateGcpAuth = async ({
identityId,
type,
credentials,
allowedServiceAccounts,
allowedProjects,
allowedZones,
@@ -375,7 +276,7 @@ export const identityGcpAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updateQuery: TIdentityGcpAuthsUpdate = {
const updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, {
type,
allowedServiceAccounts,
allowedProjects,
@@ -386,38 +287,10 @@ export const identityGcpAuthServiceFactory = ({
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
};
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
if (credentials !== undefined) {
const {
ciphertext: encryptedCredentials,
iv: credentialsIV,
tag: credentialsTag
} = encryptSymmetric(credentials, key);
updateQuery.encryptedCredentials = encryptedCredentials;
updateQuery.credentialsIV = credentialsIV;
updateQuery.credentialsTag = credentialsTag;
}
const updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, updateQuery);
return {
...updatedGcpAuth,
credentials: decryptSymmetric({
ciphertext: updatedGcpAuth.encryptedCredentials,
iv: updatedGcpAuth.credentialsIV,
tag: updatedGcpAuth.credentialsTag,
key
}),
orgId: identityMembershipOrg.orgId
};
};
@@ -441,28 +314,7 @@ export const identityGcpAuthServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCredentials, credentialsIV, credentialsTag } = identityGcpAuth;
let credentials = "";
if (encryptedCredentials && credentialsIV && credentialsTag) {
credentials = decryptSymmetric({
ciphertext: encryptedCredentials,
key,
tag: credentialsTag,
iv: credentialsIV
});
}
return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId };
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
};
return {
@@ -7,7 +7,6 @@ export type TLoginGcpAuthDTO = {
export type TAttachGcpAuthDTO = {
identityId: string;
credentials: string;
type: "iam" | "gce";
allowedServiceAccounts: string;
allowedProjects: string;
@@ -20,7 +19,6 @@ export type TAttachGcpAuthDTO = {
export type TUpdateGcpAuthDTO = {
identityId: string;
credentials?: string;
type?: "iam" | "gce";
allowedServiceAccounts?: string;
allowedProjects?: string;
@@ -35,21 +33,28 @@ export type TGetGcpAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
type TComputeEngineDetails = {
instance_creation_timestamp: number;
instance_id: string;
instance_name: string;
project_id: string;
project_number: number;
zone: string;
};
export type TGcpIdentityDetails = {
email: string;
computeEngineDetails?: TComputeEngineDetails;
};
export type TGcpGceIdTokenPayload = {
aud: string;
azp: string;
email: string;
email_verified: boolean;
exp: number;
google: {
compute_engine: {
instance_creation_timestamp: number;
instance_id: string;
instance_name: string;
project_id: string;
project_number: number;
zone: string;
};
google?: {
compute_engine: TComputeEngineDetails;
};
iat: number;
iss: string;
@@ -1,15 +1,5 @@
import { z } from "zod";
import { IdentityGcpAuthsSchema } from "@app/db/schemas";
export const ModifiedIdentityGcpAuthsSchema = IdentityGcpAuthsSchema.omit({
encryptedCredentials: true,
credentialsIV: true,
credentialsTag: true
}).extend({
credentials: z.string()
});
export const validateGcpAuthField = z
.string()
.trim()