Compare commits

...

44 Commits

Author SHA1 Message Date
089d6812fd Update ldap-fns.ts 2024-11-25 16:00:20 +04:00
1cd459fda7 Merge branch 'heads/main' into daniel/request-ids 2024-11-23 00:14:50 +04:00
38917327d9 feat: request lifecycle request ID 2024-11-22 23:19:07 +04:00
d7b494c6f8 Merge pull request #2775 from akhilmhdh/fix/patches-3
fix: db error on token auth and permission issue
2024-11-22 12:43:20 -05:00
=
93208afb36 fix: db error on token auth and permission issue 2024-11-22 22:41:53 +05:30
1a084d8fcf add direct link td provider 2024-11-21 21:26:46 -05:00
dd4f133c6c Merge pull request #2769 from Infisical/misc/made-identity-metadata-value-not-nullable-again
misc: made identity metadata value not nullable
2024-11-22 01:59:01 +08:00
c41d27e1ae misc: made identity metadata value not nullable 2024-11-21 21:27:56 +08:00
1866ed8d23 Merge pull request #2742 from Infisical/feat/totp-dynamic-secret
feat: TOTP dynamic secret provider
2024-11-21 12:00:12 +08:00
7b3b232dde replace loader with spinner 2024-11-20 14:09:26 -08:00
9d618b4ae9 minor text revisions/additions and add colors/icons to totp token expiry countdown 2024-11-20 14:01:40 -08:00
5330ab2171 Merge pull request #2768 from BnjmnZmmrmn/k8s_integration_docs_typo
fixing small typo in docs/integrations/platforms/kubernetes
2024-11-20 15:49:35 -05:00
662e588c22 misc: add handling for lease regen 2024-11-21 04:43:21 +08:00
90057d80ff Merge pull request #2767 from akhilmhdh/feat/permission-error
Detail error when permission validation error occurs
2024-11-21 02:00:51 +05:30
1eda7aaaac reverse license 2024-11-20 12:14:14 -08:00
00dcadbc08 misc: added timer 2024-11-21 04:09:19 +08:00
7a7289ebd0 fixing typo in docs/integrations/platforms/kubernetes 2024-11-20 11:50:13 -08:00
e5d4677fd6 improvements: minor UI/labeling adjustments, only show tags loading if can read, and remove rounded bottom on overview table 2024-11-20 11:50:10 -08:00
bce3f3d676 misc: addressed review comments 2024-11-21 02:37:56 +08:00
=
300372fa98 feat: resolve dependency cycle error 2024-11-20 23:59:49 +05:30
47a4f8bae9 Merge pull request #2766 from Infisical/omar/eng-1886-make-terraform-integration-secrets-marked-as-sensitive
Improvement(Terraform Cloud Integration): Synced secrets are hidden from Terraform UI
2024-11-20 13:16:45 -05:00
=
863719f296 feat: added action button for notification toast and one action each for forbidden error and validation error details 2024-11-20 22:55:14 +05:30
=
7317dc1cf5 feat: modified error handler to return possible rules for a validation failed rules 2024-11-20 22:50:21 +05:30
75df898e78 Merge pull request #2762 from Infisical/daniel/cli-installer-readme
chore(cli-installer): readme improvements
2024-11-20 20:29:23 +04:00
0de6add3f7 set all new and existing secrets to be sensitive: true 2024-11-20 17:28:09 +01:00
0c008b6393 Update README.md 2024-11-20 20:26:00 +04:00
0c3894496c feat: added support for configuring totp with secret key 2024-11-20 23:40:36 +08:00
35fbd5d49d Merge pull request #2764 from Infisical/daniel/pre-commit-cli-check
chore: check for CLI installation before pre-commit
2024-11-20 19:01:54 +04:00
d03b453e3d Merge pull request #2765 from Infisical/daniel/actor-id-mismatch
fix(audit-logs): actor / actor ID mismatch
2024-11-20 18:58:33 +04:00
96e331b678 fix(audit-logs): actor / actor ID mismatch 2024-11-20 18:50:29 +04:00
d4d468660d chore: check for CLI installation before pre-commit 2024-11-20 17:29:36 +04:00
75a4965928 requested changes 2024-11-20 16:23:59 +04:00
660c09ded4 Merge branch 'feat/totp-dynamic-secret' of https://github.com/Infisical/infisical into feat/totp-dynamic-secret 2024-11-20 18:56:56 +08:00
b5287d91c0 misc: addressed comments 2024-11-20 18:56:16 +08:00
6a17763237 docs: dynamic secret doc typos addressed 2024-11-19 19:58:01 -08:00
f2bd3daea2 Update README.md 2024-11-20 03:24:05 +04:00
73e0a54518 feat: request ID support 2024-11-20 00:01:25 +04:00
0d295a2824 fix: application crash on zod api error 2024-11-20 00:00:30 +04:00
9a62efea4f Merge pull request #2759 from Infisical/docs-update-note
update docs note
2024-11-19 23:51:44 +04:00
3be3d807d2 misc: added URL string validation 2024-11-18 19:32:57 +08:00
9f7ea3c4e5 doc: added docs for totp dynamic secret 2024-11-18 19:27:45 +08:00
e67218f170 misc: finalized option setting logic 2024-11-18 18:34:27 +08:00
269c40c67c Merge remote-tracking branch 'origin/main' into feat/totp-dynamic-secret 2024-11-18 17:31:19 +08:00
ba1fd8a3f7 feat: totp dynamic secret 2024-11-16 02:48:28 +08:00
72 changed files with 1556 additions and 209 deletions

View File

@ -1,6 +1,12 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check if infisical is installed
if ! command -v infisical >/dev/null 2>&1; then
echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n"
exit 1
fi
npx lint-staged
infisical scan git-changes --staged -v

View File

@ -24,6 +24,7 @@
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
@ -5528,6 +5529,15 @@
"toad-cache": "^3.3.0"
}
},
"node_modules/@fastify/request-context": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz",
"integrity": "sha512-PM7wrLJOEylVDpxabOFLaYsdAiaa0lpDUcP2HMFJ1JzgiWuC6k4r3duf6Pm9YLnzlGmT+Yp4tkQjqsu7V/pSOA==",
"license": "MIT",
"dependencies": {
"fastify-plugin": "^4.0.0"
}
},
"node_modules/@fastify/send": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",

View File

@ -132,6 +132,7 @@
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",

View File

@ -0,0 +1,7 @@
import "@fastify/request-context";
declare module "@fastify/request-context" {
interface RequestContextData {
requestId: string;
}
}

View File

@ -1,6 +1,6 @@
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { Logger } from "pino";
import { CustomLogger } from "@app/lib/logger/logger";
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
declare global {
@ -8,7 +8,7 @@ declare global {
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
Readonly<Logger>,
Readonly<CustomLogger>,
ZodTypeProvider
>;

View File

@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex(TableName.IdentityMetadata).whereNull("value").delete();
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).alter();
});
}
}

View File

@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
[DynamicSecretProviders.Totp]: TotpProvider()
});

View File

@ -17,6 +17,17 @@ export enum LdapCredentialType {
Static = "static"
}
export enum TotpConfigType {
URL = "url",
MANUAL = "manual"
}
export enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@ -221,6 +232,34 @@ export const LdapSchema = z.union([
})
]);
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
z.object({
configType: z.literal(TotpConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(TotpConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@ -234,7 +273,8 @@ export enum DynamicSecretProviders {
AzureEntraID = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -250,7 +290,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
]);
export type TDynamicProviderFns = {

View File

@ -0,0 +1,92 @@
import { authenticator } from "otplib";
import { HashAlgorithms } from "otplib/core";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
export const TotpProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs);
return providerInputs;
};
const validateConnection = async () => {
return true;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
if (providerInputs.configType === TotpConfigType.URL) {
const urlObj = new URL(providerInputs.url);
secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
if (periodFromUrl) {
period = +periodFromUrl;
}
if (digitsFromUrl) {
digits = +digitsFromUrl;
}
if (algorithmFromUrl) {
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
}
} else {
secret = providerInputs.secret;
period = providerInputs.period;
digits = providerInputs.digits;
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
}
if (digits) {
authenticatorInstance.options = { digits };
}
if (algorithm) {
authenticatorInstance.options = { algorithm };
}
if (period) {
authenticatorInstance.options = { step: period };
}
return {
entityId,
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
};
};
const revoke = async (_inputs: unknown, entityId: string) => {
return { entityId };
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renew = async (_inputs: unknown, _entityId: string) => {
throw new BadRequestError({
message: "Lease renewal is not supported for TOTPs"
});
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -27,7 +27,7 @@ export const initializeHsmModule = () => {
logger.info("PKCS#11 module initialized");
} catch (err) {
logger.error("Failed to initialize PKCS#11 module:", err);
logger.error(err, "Failed to initialize PKCS#11 module");
throw err;
}
};
@ -39,7 +39,7 @@ export const initializeHsmModule = () => {
isInitialized = false;
logger.info("PKCS#11 module finalized");
} catch (err) {
logger.error("Failed to finalize PKCS#11 module:", err);
logger.error(err, "Failed to finalize PKCS#11 module");
throw err;
}
}

View File

@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean>
});
ldapClient.on("error", (err) => {
logger.error("LDAP client error:", err);
logger.error(err);
logger.error(err, "LDAP client error");
resolve(false);
});

View File

@ -161,8 +161,8 @@ export const licenseServiceFactory = ({
}
} catch (error) {
logger.error(
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`,
error
error,
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`
);
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(orgId),

View File

@ -127,14 +127,15 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
const docs = await db
.replicaNode()(TableName.Users)
.where(`${TableName.Users}.id`, userId)
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
void queryBuilder
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
// @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it
.andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups);
})
.leftJoin(
TableName.GroupProjectMembershipRole,

View File

@ -1,14 +1,7 @@
import picomatch from "picomatch";
import { z } from "zod";
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}
import { PermissionConditionOperators } from "@app/lib/casl";
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),

View File

@ -1,10 +1,10 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import { conditionsMatcher } from "@app/lib/casl";
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
import { PermissionConditionSchema } from "./permission-types";
export enum ProjectPermissionActions {
Read = "read",

View File

@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
}
return rateLimit;
} catch (err) {
logger.error("Error fetching rate limits %o", err);
logger.error(err, "Error fetching rate limits");
return undefined;
}
};
@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
mfaRateLimit: rateLimit.mfaRateLimit
};
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration");
Object.freeze(newRateLimitMaxConfiguration);
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
}
} catch (error) {
logger.error(`Error syncing rate limit configurations: %o`, error);
logger.error(error, "Error syncing rate limit configurations");
}
};

View File

@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({
});
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
logger.error("Failed to secret scan on push", job?.data, err);
logger.error(err, "Failed to secret scan on push", job?.data);
});
queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => {
logger.error("Failed to do full repo secret scan", job?.data, err);
logger.error(err, "Failed to do full repo secret scan", job?.data);
});
return { startFullRepoScan, startPushEventScan };

View File

@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2:
return set1.size >= set2.size;
};
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}

View File

@ -1,7 +1,7 @@
import { Logger } from "pino";
import { z } from "zod";
import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
@ -212,7 +212,7 @@ let envCfg: Readonly<z.infer<typeof envSchema>>;
export const getConfig = () => envCfg;
// cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger?: Logger) => {
export const initEnvConfig = (logger?: CustomLogger) => {
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
(logger ?? console).error("Invalid environment variables. Check the error below");

View File

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// logger follows a singleton pattern
// easier to use it that's all.
import { requestContext } from "@fastify/request-context";
import pino, { Logger } from "pino";
import { z } from "zod";
@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record<string, string> = {
"60": "CRITICAL"
};
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<Logger>;
// akhilmhdh:
// The logger is not placed in the main app config to avoid a circular dependency.
// The config requires the logger to display errors when an invalid environment is supplied.
// On the other hand, the logger needs the config to obtain credentials for AWS or other transports.
// By keeping the logger separate, it becomes an independent package.
// We define our own custom logger interface to enforce structure to the logging methods.
export interface CustomLogger extends Omit<Logger, "info" | "error" | "warn" | "debug"> {
info: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
error: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
warn: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
debug: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
}
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<CustomLogger>;
const loggerConfig = z.object({
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
@ -62,6 +87,17 @@ const redactedKeys = [
"config"
];
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
const extractRequestId = () => {
try {
return requestContext.get("requestId") || UNKNOWN_REQUEST_ID;
} catch (err) {
console.log("failed to get request context", err);
return UNKNOWN_REQUEST_ID;
}
};
export const initLogger = async () => {
const cfg = loggerConfig.parse(process.env);
const targets: pino.TransportMultiOptions["targets"][number][] = [
@ -94,6 +130,30 @@ export const initLogger = async () => {
targets
});
const wrapLogger = (originalLogger: Logger): CustomLogger => {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).info(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).error(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).warn(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).debug(obj, msg, ...args);
};
return originalLogger;
};
logger = pino(
{
mixin(_context, level) {
@ -113,5 +173,6 @@ export const initLogger = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
transport
);
return logger;
return wrapLogger(logger);
};

View File

@ -10,13 +10,15 @@ import fastifyFormBody from "@fastify/formbody";
import helmet from "@fastify/helmet";
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit";
import { fastifyRequestContext } from "@fastify/request-context";
import fastify from "fastify";
import { Knex } from "knex";
import { Logger } from "pino";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { CustomLogger } from "@app/lib/logger/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TQueueServiceFactory } from "@app/queue";
import { TSmtpService } from "@app/services/smtp/smtp-service";
@ -35,7 +37,7 @@ type TMain = {
auditLogDb?: Knex;
db: Knex;
smtp: TSmtpService;
logger?: Logger;
logger?: CustomLogger;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
@ -47,7 +49,9 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger,
genReqId: () => `req-${alphaNumericNanoId(14)}`,
trustProxy: true,
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
ignoreTrailingSlash: true,
pluginTimeout: 40_000
@ -104,6 +108,13 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register(maintenanceMode);
await server.register(fastifyRequestContext, {
defaultStoreValues: (request) => ({
requestId: request.id,
log: request.log.child({ requestId: request.id })
})
});
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) {

View File

@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, PureAbility } from "@casl/ability";
import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
import { ZodError } from "zod";
@ -39,77 +39,102 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
if (error instanceof BadRequestError) {
void res
.status(HttpStatusCodes.BadRequest)
.send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
.send({ requestId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
} else if (error instanceof NotFoundError) {
void res
.status(HttpStatusCodes.NotFound)
.send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
.send({ requestId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) {
void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name });
void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
message: error.message,
error: error.name
});
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: "Something went wrong",
error: error.name
});
} else if (error instanceof GatewayTimeoutError) {
void res
.status(HttpStatusCodes.GatewayTimeout)
.send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name });
void res.status(HttpStatusCodes.GatewayTimeout).send({
requestId: req.id,
statusCode: HttpStatusCodes.GatewayTimeout,
message: error.message,
error: error.name
});
} else if (error instanceof ZodError) {
void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues });
void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
error: "ValidationFailure",
message: error.issues
});
} else if (error instanceof ForbiddenError) {
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}`
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({
action: el.action,
inverted: el.inverted,
subject: el.subject,
conditions: el.conditions
}))
});
} else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
message: error.message,
error: error.name
});
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({
requestId: req.id,
statusCode: HttpStatusCodes.TooManyRequests,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) {
void res.status(error.status).send({
requestId: req.id,
schemas: error.schemas,
status: error.status,
detail: error.detail
});
} else if (error instanceof OidcAuthError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: error.message,
error: error.name
});
} else if (error instanceof jwt.JsonWebTokenError) {
const message = (() => {
if (error.message === JWTErrors.JwtExpired) {
return "Your token has expired. Please re-authenticate.";
}
if (error.message === JWTErrors.JwtMalformed) {
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
}
if (error.message === JWTErrors.InvalidAlgorithm) {
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
let errorMessage = error.message;
return error.message;
})();
if (error.message === JWTErrors.JwtExpired) {
errorMessage = "Your token has expired. Please re-authenticate.";
} else if (error.message === JWTErrors.JwtMalformed) {
errorMessage =
"The provided access token is malformed. Please use a valid token or generate a new one and try again.";
} else if (error.message === JWTErrors.InvalidAlgorithm) {
errorMessage =
"The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
error: "TokenError",
message
message: errorMessage
});
} else {
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError",
message: "Something went wrong"

View File

@ -19,7 +19,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
app.on("installation", async (context) => {
const { payload } = context;
logger.info("Installed secret scanner to:", { repositories: payload.repositories });
logger.info({ repositories: payload.repositories }, "Installed secret scanner to");
});
app.on("push", async (context) => {

View File

@ -30,26 +30,32 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
export const DefaultResponseErrorsSchema = {
400: z.object({
requestId: z.string(),
statusCode: z.literal(400),
message: z.string(),
error: z.string()
}),
404: z.object({
requestId: z.string(),
statusCode: z.literal(404),
message: z.string(),
error: z.string()
}),
401: z.object({
requestId: z.string(),
statusCode: z.literal(401),
message: z.any(),
error: z.string()
}),
403: z.object({
requestId: z.string(),
statusCode: z.literal(403),
message: z.string(),
details: z.any().optional(),
error: z.string()
}),
500: z.object({
requestId: z.string(),
statusCode: z.literal(500),
message: z.string(),
error: z.string()

View File

@ -385,8 +385,8 @@ export const identityTokenAuthServiceFactory = ({
actorOrgId
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findOne({
id: tokenId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
@ -444,8 +444,8 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false,
authMethod: IdentityAuthMethod.TOKEN_AUTH
[`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!identityAccessToken)
throw new NotFoundError({

View File

@ -3075,7 +3075,7 @@ const syncSecretsTerraformCloud = async ({
}) => {
// get secrets from Terraform Cloud
const terraformSecrets = (
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
await request.get<{ data: { attributes: { key: string; value: string; sensitive: boolean }; id: string }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
{
headers: {
@ -3089,7 +3089,7 @@ const syncSecretsTerraformCloud = async ({
...obj,
[secret.attributes.key]: secret
}),
{} as Record<string, { attributes: { key: string; value: string }; id: string }>
{} as Record<string, { attributes: { key: string; value: string; sensitive: boolean }; id: string }>
);
const secretsToAdd: { [key: string]: string } = {};
@ -3170,7 +3170,8 @@ const syncSecretsTerraformCloud = async ({
attributes: {
key,
value: secrets[key]?.value,
category: integration.targetService
category: integration.targetService,
sensitive: true
}
}
},
@ -3183,7 +3184,11 @@ const syncSecretsTerraformCloud = async ({
}
);
// case: secret exists in Terraform Cloud
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
} else if (
// we now set secrets to sensitive in Terraform Cloud, this checks if existing secrets are not sensitive and updates them accordingly
!terraformSecrets[key].attributes.sensitive ||
secrets[key]?.value !== terraformSecrets[key].attributes.value
) {
// -> update secret
await request.patch(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
@ -3193,7 +3198,8 @@ const syncSecretsTerraformCloud = async ({
id: terraformSecrets[key].id,
attributes: {
...terraformSecrets[key],
value: secrets[key]?.value
value: secrets[key]?.value,
sensitive: true
}
}
},

View File

@ -285,11 +285,14 @@ export const projectQueueFactory = ({
if (!orgMembership) {
// This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case.
logger.info("User is not in organization", {
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
});
logger.info(
{
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
},
"User is not in organization"
);
// eslint-disable-next-line no-continue
continue;
}
@ -551,10 +554,10 @@ export const projectQueueFactory = ({
.catch(() => [null]);
if (!project) {
logger.error("Failed to upgrade project, because no project was found", data);
logger.error(data, "Failed to upgrade project, because no project was found");
} else {
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
logger.error("Failed to upgrade project", err, {
logger.error(err, "Failed to upgrade project", {
extra: {
project,
jobData: data

View File

@ -142,7 +142,7 @@ export const fnTriggerWebhook = async ({
!isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
);
if (!toBeTriggeredHooks.length) return;
logger.info("Secret webhook job started", { environment, secretPath, projectId });
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
const project = await projectDAL.findById(projectId);
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
@ -195,5 +195,5 @@ export const fnTriggerWebhook = async ({
);
}
});
logger.info("Secret webhook job ended", { environment, secretPath, projectId });
logger.info({ environment, secretPath, projectId }, "Secret webhook job ended");
};

View File

@ -3,7 +3,7 @@ title: 'Install'
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
---
The Infisical CLI is powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
You can use it across various environments, whether it's local development, CI/CD, staging, or production.
## Installation

View File

@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -66,7 +66,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -138,12 +138,12 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions.
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions.
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>

View File

@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -93,12 +93,12 @@ Create a user with the required permission in your Redis instance. This user wil
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -106,13 +106,13 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)

View File

@ -0,0 +1,70 @@
---
title: "TOTP"
description: "Learn how to dynamically generate time-based one-time passwords."
---
The Infisical TOTP dynamic secret allows you to generate time-based one-time passwords on demand.
## Prerequisite
- Infisical requires either an OTP url or a secret key from a TOTP provider.
## Set up Dynamic Secrets with TOTP
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](/images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select TOTP">
![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Configuration Type" type="string" required>
There are two supported configuration types - `url` and `manual`.
When `url` is selected, you can configure the TOTP generator using the OTP URL.
When `manual` is selected, you can configure the TOTP generator using the secret key along with other configurations like period, number of digits, and algorithm.
</ParamField>
<ParamField path="URL" type="string">
OTP URL in `otpauth://` format used to generate TOTP codes.
</ParamField>
<ParamField path="Secret Key" type="string">
Base32 encoded secret used to generate TOTP codes.
</ParamField>
<ParamField path="Period" type="number">
Time interval in seconds between generating new TOTP codes.
</ParamField>
<ParamField path="Digits" type="number">
Number of digits to generate in each TOTP code.
</ParamField>
<ParamField path="Algorithm" type="string">
Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512.
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png)
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png)
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand TOTPs.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/totp-lease-value.png)
</Step>
</Steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

View File

@ -1,8 +1,9 @@
---
title: "Terraform"
title: "Terraform Provider"
description: "Learn how to fetch Secrets From Infisical With Terraform."
url: "https://registry.terraform.io/providers/Infisical/infisical/latest/docs"
---
{/*
This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform.
## Prerequisites
@ -98,4 +99,4 @@ Terraform will now fetch your secrets from Infisical and display them as output
## Conclusion
You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). */}

View File

@ -94,7 +94,7 @@ spec:
projectSlug: new-ob-em
envSlug: dev # "dev", "staging", "prod", etc..
secretsPath: "/" # Root is "/"
recursive: true # Wether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
credentialsRef:
secretName: universal-auth-credentials
secretNamespace: default

View File

@ -189,7 +189,8 @@
"documentation/platform/dynamic-secrets/azure-entra-id",
"documentation/platform/dynamic-secrets/ldap",
"documentation/platform/dynamic-secrets/sap-hana",
"documentation/platform/dynamic-secrets/snowflake"
"documentation/platform/dynamic-secrets/snowflake",
"documentation/platform/dynamic-secrets/totp"
]
},
"documentation/platform/project-templates",

View File

@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas
export type TNotification = {
title?: string;
text: ReactNode;
children?: ReactNode;
};
export const NotificationContent = ({ title, text }: TNotification) => {
export const NotificationContent = ({ title, text, children }: TNotification) => {
return (
<div className="msg-container">
{title && <div className="text-md mb-1 font-medium">{title}</div>}
<div className={title ? "text-sm" : "text-md"}>{text}</div>
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
{children && <div className="mt-2">{children}</div>}
</div>
);
};
@ -23,7 +25,13 @@ export const createNotification = (
position: "bottom-right",
...toastProps,
theme: "dark",
type: myProps?.type || "info",
type: myProps?.type || "info"
});
export const NotificationContainer = () => <ToastContainer pauseOnHover toastClassName="border border-mineshaft-500" style={{ width: "400px" }} />;
export const NotificationContainer = () => (
<ToastContainer
pauseOnHover
toastClassName="border border-mineshaft-500"
style={{ width: "400px" }}
/>
);

View File

@ -33,6 +33,15 @@ export enum PermissionConditionOperators {
$GLOB = "$glob"
}
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "contains",
[PermissionConditionOperators.$ALL]: "contains all",
[PermissionConditionOperators.$NEQ]: "not equal to",
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
[PermissionConditionOperators.$REGEX]: "matches regex pattern"
};
export type TPermissionConditionOperators = {
[PermissionConditionOperators.$IN]: string[];
[PermissionConditionOperators.$ALL]: string[];

View File

@ -9,7 +9,7 @@ export type TGetAuditLogsFilter = {
eventMetadata?: Record<string, string>;
actorType?: ActorType;
projectId?: string;
actorId?: string; // user ID format
actor?: string; // user ID format
startDate?: Date;
endDate?: Date;
limit: number;

View File

@ -28,7 +28,8 @@ export enum DynamicSecretProviders {
AzureEntraId = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export enum SqlProviders {
@ -230,6 +231,21 @@ export type TDynamicSecretProvider =
revocationStatement: string;
renewStatement?: string;
};
}
| {
type: DynamicSecretProviders.Totp;
inputs:
| {
configType: "url";
url: string;
}
| {
configType: "manual";
secret: string;
period?: number;
algorithm?: string;
digits?: number;
};
};
export type TCreateDynamicSecretDTO = {
projectSlug: string;

View File

@ -1,3 +1,4 @@
import { PureAbility } from "@casl/ability";
import { ZodIssue } from "zod";
export type { TAccessApprovalPolicy } from "./accessApproval/types";
@ -50,12 +51,26 @@ export enum ApiErrorTypes {
export type TApiErrors =
| {
requestId: string;
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 401;
}
| {
requestId: string;
error: ApiErrorTypes.UnauthorizedError;
message: string;
statusCode: 401;
}
| {
requestId: string;
error: ApiErrorTypes.ForbiddenError;
message: string;
details: PureAbility["rules"];
statusCode: 403;
}
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
| {
requestId: string;
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;

View File

@ -3,6 +3,14 @@ import axios from "axios";
import { createNotification } from "@app/components/notifications";
// akhilmhdh: doing individual imports to avoid cyclic import error
import { Button } from "./components/v2/Button";
import { Modal, ModalContent, ModalTrigger } from "./components/v2/Modal";
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "./components/v2/Table";
import {
formatedConditionsOperatorNames,
PermissionConditionOperators
} from "./context/ProjectPermissionContext/types";
import { ApiErrorTypes, TApiErrors } from "./hooks/api/types";
// this is saved in react-query cache
@ -10,38 +18,164 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
export const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as TApiErrors;
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
createNotification({
title: "Validation Error",
type: "error",
text: (
<div>
{serverResponse.message?.map(({ message, path }) => (
<div className="flex space-y-2" key={path.join(".")}>
<div>
Field <i>{path.join(".")}</i> {message.toLowerCase()}
createNotification(
{
title: "Validation Error",
type: "error",
text: (
<div>
<p>Please check the input and try again.</p>
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
</div>
),
children: (
<Modal>
<ModalTrigger>
<Button variant="outline_bg" size="xs">
Show more
</Button>
</ModalTrigger>
<ModalContent title="Validation Error Details">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Field</Th>
<Th>Issue</Th>
</Tr>
</THead>
<TBody>
{serverResponse.message?.map(({ message, path }) => (
<Tr key={path.join(".")}>
<Td>{path.join(".")}</Td>
<Td>{message.toLowerCase()}</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
)
},
{ closeOnClick: false }
);
return;
}
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
createNotification(
{
title: "Forbidden Access",
type: "error",
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
children: serverResponse?.details?.length ? (
<Modal>
<ModalTrigger>
<Button variant="outline_bg" size="xs">
Show more
</Button>
</ModalTrigger>
<ModalContent
title="Validation Rules"
subTitle="Please review the allowed rules below."
>
<div className="flex flex-col gap-2">
{serverResponse.details?.map((el, index) => {
const hasConditions = Object.keys(el.conditions || {}).length;
return (
<div
key={`Forbidden-error-details-${index + 1}`}
className="rounded-md border border-gray-600 p-4"
>
<div>
{el.inverted ? "Cannot" : "Can"}{" "}
<span className="text-yellow-600">
{el.action.toString().replaceAll(",", ", ")}
</span>{" "}
{el.subject.toString()} {hasConditions && "with conditions:"}
</div>
{hasConditions && (
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
const operators = (
el.conditions as Record<
string,
| string
| { [K in PermissionConditionOperators]: string | string[] }
>
)[field];
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
if (typeof operators === "string") {
return (
<li
key={`Forbidden-error-details-${index + 1}-${
fieldIndex + 1
}`}
>
<span className="font-bold capitalize">
{formattedFieldName}
</span>{" "}
<span className="text-mineshaft-200">equal to</span>{" "}
<span className="text-yellow-600">{operators}</span>
</li>
);
}
return Object.keys(operators).map((operator, operatorIndex) => (
<li
key={`Forbidden-error-details-${index + 1}-${
fieldIndex + 1
}-${operatorIndex + 1}`}
>
<span className="font-bold capitalize">
{formattedFieldName}
</span>{" "}
<span className="text-mineshaft-200">
{
formatedConditionsOperatorNames[
operator as PermissionConditionOperators
]
}
</span>{" "}
<span className="text-yellow-600">
{operators[
operator as PermissionConditionOperators
].toString()}
</span>
</li>
));
})}
</ul>
)}
</div>
);
})}
</div>
</div>
))}
</div>
)
});
</ModalContent>
</Modal>
) : undefined
},
{ closeOnClick: false }
);
return;
}
if (serverResponse.statusCode === 401) {
createNotification({
title: "Forbidden Access",
type: "error",
text: serverResponse.message
});
return;
}
createNotification({ title: "Bad Request", type: "error", text: serverResponse.message });
createNotification({
title: "Bad Request",
type: "error",
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
});
}
}
}),

View File

@ -13,6 +13,14 @@ html {
@apply rounded-md;
}
.Toastify__toast-body {
@apply items-start;
}
.Toastify__toast-icon {
@apply w-4 pt-1;
}
.rdp-day,
.rdp-nav_button {
@apply rounded-md hover:text-mineshaft-500;

View File

@ -98,7 +98,7 @@ export const LogsSection = ({
userAgentType,
startDate,
endDate,
actorId: actor
actor
}}
/>
<UpgradePlanModal

View File

@ -63,32 +63,21 @@ export const IdentityClientSecretModal = ({ popUp, handlePopUpToggle }: Props) =
};
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
try {
const { clientSecret } = await createClientSecret({
identityId: popUpData.identityId,
description,
ttl: Number(ttl),
numUsesLimit: Number(numUsesLimit)
});
const { clientSecret } = await createClientSecret({
identityId: popUpData.identityId,
description,
ttl: Number(ttl),
numUsesLimit: Number(numUsesLimit)
});
setToken(clientSecret);
setToken(clientSecret);
createNotification({
text: "Successfully created client secret",
type: "success"
});
createNotification({
text: "Successfully created client secret",
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to create client secret";
createNotification({
text,
type: "error"
});
}
reset();
};
return (

View File

@ -11,7 +11,7 @@ import {
SiSnowflake
} from "react-icons/si";
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
@ -31,6 +31,7 @@ import { RabbitMqInputForm } from "./RabbitMqInputForm";
import { RedisInputForm } from "./RedisInputForm";
import { SapHanaInputForm } from "./SapHanaInputForm";
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
import { TotpInputForm } from "./TotpInputForm";
type Props = {
isOpen?: boolean;
@ -110,6 +111,11 @@ const DYNAMIC_SECRET_LIST = [
icon: <SiSnowflake size="1.5rem" />,
provider: DynamicSecretProviders.Snowflake,
title: "Snowflake"
},
{
icon: <FontAwesomeIcon icon={faClock} size="lg" />,
provider: DynamicSecretProviders.Totp,
title: "TOTP"
}
];
@ -405,6 +411,24 @@ export const CreateDynamicSecretForm = ({
/>
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === DynamicSecretProviders.Totp && (
<motion.div
key="dynamic-totp-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<TotpInputForm
onCompleted={handleFormReset}
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>

View File

@ -0,0 +1,314 @@
import { Controller, useForm } from "react-hook-form";
import Link from "next/link";
import { faArrowUpRightFromSquare, faBookOpen } 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, Input, Select, SelectItem } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
enum ConfigType {
URL = "url",
MANUAL = "manual"
}
enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
const formSchema = z.object({
provider: z.discriminatedUnion("configType", [
z.object({
configType: z.literal(ConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(ConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]),
name: z
.string()
.trim()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
});
type TForm = z.infer<typeof formSchema>;
type Props = {
onCompleted: () => void;
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
};
export const TotpInputForm = ({
onCompleted,
onCancel,
environment,
secretPath,
projectSlug
}: Props) => {
const {
control,
watch,
formState: { isSubmitting },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues: {
provider: {
configType: ConfigType.URL
}
}
});
const selectedConfigType = watch("provider.configType");
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isLoading) return;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.Totp, inputs: provider },
maxTTL: "24h",
name,
path: secretPath,
defaultTTL: "1m",
projectSlug,
environmentSlug: environment
});
onCompleted();
} catch (err) {
createNotification({
type: "error",
text: err instanceof Error ? err.message : "Failed to create dynamic secret"
});
}
};
return (
<div>
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<Link
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
passHref
>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
<div className="flex flex-col">
<Controller
control={control}
name="provider.configType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
onValueChange={(val) => {
field.onChange(val);
}}
dropdownContainerClassName="max-w-full"
>
<SelectItem value={ConfigType.URL} key="config-type-url">
URL
</SelectItem>
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
Manual
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigType === ConfigType.URL && (
<Controller
control={control}
name="provider.url"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="OTP URL"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} placeholder="otpauth://" />
</FormControl>
)}
/>
)}
{selectedConfigType === ConfigType.MANUAL && (
<>
<Controller
control={control}
name="provider.secret"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex flex-row gap-2">
<Controller
control={control}
name="provider.period"
defaultValue={30}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Period"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.digits"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digits"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.algorithm"
defaultValue={TotpAlgorithm.SHA1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Algorithm"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
dropdownContainerClassName="max-w-full"
onValueChange={(val) => {
field.onChange(val);
}}
>
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
SHA1
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
SHA256
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
SHA512
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<p className="mb-8 text-sm font-normal text-gray-400">
The period, digits, and algorithm values can remain at their defaults unless
your TOTP provider specifies otherwise.
</p>
</>
)}
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</div>
);
};

View File

@ -154,7 +154,7 @@ export const CreateSecretForm = ({
isMulti
name="tagIds"
isDisabled={!canReadTags}
isLoading={isTagsLoading}
isLoading={isTagsLoading && canReadTags}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
value={field.value}
onChange={field.onChange}

View File

@ -1,6 +1,6 @@
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faClock, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
@ -9,8 +9,16 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, SecretInput, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import {
Button,
FormControl,
IconButton,
Input,
SecretInput,
Spinner,
Tooltip
} from "@app/components/v2";
import { useTimedReset, useToggle } from "@app/hooks";
import { useCreateDynamicSecretLease } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
@ -54,7 +62,76 @@ const OutputDisplay = ({
);
};
const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
const TotpOutputDisplay = ({
totp,
remainingSeconds,
triggerLeaseRegeneration
}: {
totp: string;
remainingSeconds: number;
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>;
}) => {
const [remainingTime, setRemainingTime] = useState(remainingSeconds);
const [shouldShowRegenerate, setShouldShowRegenerate] = useToggle(false);
useEffect(() => {
setRemainingTime(remainingSeconds);
setShouldShowRegenerate.off();
// Set up countdown interval
const intervalId = setInterval(() => {
setRemainingTime((prevTime) => {
if (prevTime <= 1) {
clearInterval(intervalId);
setShouldShowRegenerate.on();
return 0;
}
return prevTime - 1;
});
}, 1000);
// Cleanup interval on unmount or when totp changes
return () => clearInterval(intervalId);
}, [totp, remainingSeconds]);
return (
<div className="h-36">
<OutputDisplay label="Time-based one-time password" value={totp} />
{remainingTime > 0 ? (
<div
className={`ml-2 flex items-center text-sm ${
remainingTime < 10 ? "text-red-500" : "text-yellow-500"
} transition-colors duration-500`}
>
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
<span>
Expires in {remainingTime} {remainingTime > 1 ? "seconds" : "second"}
</span>
</div>
) : (
<div className="ml-2 flex items-center text-sm text-red-500">
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
Expired
</div>
)}
{shouldShowRegenerate && (
<Button
colorSchema="secondary"
className="mt-2"
onClick={() => triggerLeaseRegeneration({})}
>
Regenerate
</Button>
)}
</div>
);
};
const renderOutputForm = (
provider: DynamicSecretProviders,
data: unknown,
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>
) => {
if (
provider === DynamicSecretProviders.SqlDatabase ||
provider === DynamicSecretProviders.Cassandra ||
@ -242,11 +319,29 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
);
}
if (provider === DynamicSecretProviders.Totp) {
const { TOTP, TIME_REMAINING } = data as {
TOTP: string;
TIME_REMAINING: number;
};
return (
<TotpOutputDisplay
totp={TOTP}
remainingSeconds={TIME_REMAINING}
triggerLeaseRegeneration={triggerLeaseRegeneration}
/>
);
}
return null;
};
const formSchema = z.object({
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
});
type TForm = z.infer<typeof formSchema>;
@ -259,6 +354,8 @@ type Props = {
secretPath: string;
};
const PROVIDERS_WITH_AUTOGENERATE_SUPPORT = [DynamicSecretProviders.Totp];
export const CreateDynamicSecretLease = ({
onClose,
projectSlug,
@ -277,6 +374,9 @@ export const CreateDynamicSecretLease = ({
ttl: "1h"
}
});
const [isPreloading, setIsPreloading] = useToggle(
PROVIDERS_WITH_AUTOGENERATE_SUPPORT.includes(provider)
);
const createDynamicSecretLease = useCreateDynamicSecretLease();
@ -290,10 +390,13 @@ export const CreateDynamicSecretLease = ({
ttl,
dynamicSecretName
});
createNotification({
type: "success",
text: "Successfully leased dynamic secret"
});
setIsPreloading.off();
} catch (error) {
console.log(error);
createNotification({
@ -303,8 +406,23 @@ export const CreateDynamicSecretLease = ({
}
};
const handleLeaseRegeneration = async (data: { ttl?: string }) => {
setIsPreloading.on();
handleDynamicSecretLeaseCreate(data);
};
useEffect(() => {
if (provider === DynamicSecretProviders.Totp) {
handleDynamicSecretLeaseCreate({});
}
}, [provider]);
const isOutputMode = Boolean(createDynamicSecretLease?.data);
if (isPreloading) {
return <Spinner className="mx-auto h-40 text-mineshaft-700" />;
}
return (
<div>
<AnimatePresence>
@ -350,7 +468,11 @@ export const CreateDynamicSecretLease = ({
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
{renderOutputForm(provider, createDynamicSecretLease.data?.data)}
{renderOutputForm(
provider,
createDynamicSecretLease.data?.data,
handleLeaseRegeneration
)}
</motion.div>
)}
</AnimatePresence>

View File

@ -101,10 +101,20 @@ export const DynamicSecretListView = ({
role="button"
tabIndex={0}
onKeyDown={(evt) => {
// no lease view for TOTP because it's irrelevant
if (secret.type === DynamicSecretProviders.Totp) {
return;
}
if (evt.key === "Enter" && !isRevoking)
handlePopUpOpen("dynamicSecretLeases", secret.id);
}}
onClick={() => {
// no lease view for TOTP because it's irrelevant
if (secret.type === DynamicSecretProviders.Totp) {
return;
}
if (!isRevoking) {
handlePopUpOpen("dynamicSecretLeases", secret.id);
}

View File

@ -17,6 +17,7 @@ import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProv
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm";
type Props = {
onClose: () => void;
@ -276,6 +277,23 @@ export const EditDynamicSecretForm = ({
/>
</motion.div>
)}
{dynamicSecretDetails?.type === DynamicSecretProviders.Totp && (
<motion.div
key="totp-edit"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EditDynamicSecretTotpForm
onClose={onClose}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecret={dynamicSecretDetails}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@ -0,0 +1,318 @@
import { Controller, useForm } from "react-hook-form";
import Link from "next/link";
import { faArrowUpRightFromSquare, faBookOpen } 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, Input, Select, SelectItem } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
enum ConfigType {
URL = "url",
MANUAL = "manual"
}
enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
const formSchema = z.object({
inputs: z
.discriminatedUnion("configType", [
z.object({
configType: z.literal(ConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(ConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
])
.optional(),
newName: z
.string()
.trim()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
});
type TForm = z.infer<typeof formSchema>;
type Props = {
onClose: () => void;
dynamicSecret: TDynamicSecret & { inputs: unknown };
secretPath: string;
projectSlug: string;
environment: string;
};
export const EditDynamicSecretTotpForm = ({
onClose,
dynamicSecret,
environment,
secretPath,
projectSlug
}: Props) => {
const {
control,
formState: { isSubmitting },
watch,
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
newName: dynamicSecret.name,
inputs: dynamicSecret.inputs as TForm["inputs"]
}
});
const selectedConfigType = watch("inputs.configType");
const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => {
// wait till previous request is finished
if (updateDynamicSecret.isLoading) return;
try {
await updateDynamicSecret.mutateAsync({
name: dynamicSecret.name,
path: secretPath,
projectSlug,
environmentSlug: environment,
data: {
inputs,
newName: newName === dynamicSecret.name ? undefined : newName
}
});
onClose();
createNotification({
type: "success",
text: "Successfully updated dynamic secret"
});
} catch (err) {
createNotification({
type: "error",
text: err instanceof Error ? err.message : "Failed to update dynamic secret"
});
}
};
return (
<div>
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="newName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<Link
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
passHref
>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
<div className="flex flex-col">
<Controller
control={control}
name="inputs.configType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
onValueChange={(val) => {
field.onChange(val);
}}
dropdownContainerClassName="max-w-full"
>
<SelectItem value={ConfigType.URL} key="config-type-url">
URL
</SelectItem>
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
Manual
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigType === ConfigType.URL && (
<Controller
control={control}
name="inputs.url"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="OTP URL"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
)}
{selectedConfigType === ConfigType.MANUAL && (
<>
<Controller
control={control}
name="inputs.secret"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex flex-row gap-2">
<Controller
control={control}
name="inputs.period"
defaultValue={30}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Period"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.digits"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digits"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.algorithm"
defaultValue={TotpAlgorithm.SHA1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Algorithm"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
dropdownContainerClassName="max-w-full"
onValueChange={(val) => {
field.onChange(val);
}}
>
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
SHA1
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
SHA256
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
SHA512
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<p className="mb-8 text-sm font-normal text-gray-400">
The period, digits, and algorithm values can remain at their defaults unless
your TOTP provider specifies otherwise.
</p>
</>
)}
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</div>
);
};

View File

@ -867,7 +867,7 @@ export const SecretOverviewPage = () => {
<div className="thin-scrollbar mt-4">
<TableContainer
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
className="thin-scrollbar"
className="thin-scrollbar rounded-b-none"
>
<Table>
<THead>

View File

@ -255,7 +255,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
isMulti
name="tagIds"
isDisabled={!canReadTags}
isLoading={isTagsLoading}
isLoading={isTagsLoading && canReadTags}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
value={field.value}
onChange={field.onChange}

View File

@ -1,10 +1,11 @@
<h1 align="center">Infisical</h1>
<h1 align="center">Infisical CLI</h1>
<p align="center">
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
<p align="center"><b>Embrace shift-left security with the Infisical CLI and strengthen your DevSecOps practices by seamlessly managing secrets across your workflows, pipelines, and applications.</b></p>
</p>
<h4 align="center">
<a href="https://infisical.com/slack">Slack</a> |
<a href="https://www.npmjs.com/package/@infisical/sdk">Node.js SDK</a> |
<a href="https://infisical.com/">Infisical Cloud</a> |
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
@ -12,7 +13,6 @@
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
</h4>
<h4 align="center">
<a href="https://github.com/Infisical/infisical/blob/main/LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
@ -36,10 +36,7 @@
### Introduction
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI.
We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. You can use it across various environments, whether its local development, CI/CD, staging, or production.
### Installation