mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
5 Commits
misc/final
...
daniel/hsm
Author | SHA1 | Date | |
---|---|---|---|
|
8ef078872e | ||
|
5f93016d22 | ||
|
f220246eb4 | ||
|
6956d14e2e | ||
|
bae7c6c3d7 |
@@ -78,5 +78,3 @@ PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=
|
||||
|
@@ -118,9 +118,9 @@ describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
|
||||
value: "stage-value"
|
||||
});
|
||||
|
||||
// wait for 10 second for replication to finish
|
||||
// wait for 5 second for replication to finish
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10000); // time to breathe for db
|
||||
setTimeout(resolve, 5000); // time to breathe for db
|
||||
});
|
||||
|
||||
const secret = await getSecretByNameV2({
|
||||
@@ -173,9 +173,9 @@ describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
|
||||
value: "prod-value"
|
||||
});
|
||||
|
||||
// wait for 10 second for replication to finish
|
||||
// wait for 5 second for replication to finish
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10000); // time to breathe for db
|
||||
setTimeout(resolve, 5000); // time to breathe for db
|
||||
});
|
||||
|
||||
const secret = await getSecretByNameV2({
|
||||
@@ -343,9 +343,9 @@ describe.each([{ path: "/" }, { path: "/deep" }])(
|
||||
value: "prod-value"
|
||||
});
|
||||
|
||||
// wait for 10 second for replication to finish
|
||||
// wait for 5 second for replication to finish
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10000); // time to breathe for db
|
||||
setTimeout(resolve, 5000); // time to breathe for db
|
||||
});
|
||||
|
||||
const secret = await getSecretByNameV2({
|
||||
|
@@ -8,80 +8,61 @@ const prompt = promptSync({
|
||||
sigint: true
|
||||
});
|
||||
|
||||
const sanitizeInputParam = (value: string) => {
|
||||
// Escape double quotes and wrap the entire value in double quotes
|
||||
if (value) {
|
||||
return `"${value.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return '""';
|
||||
};
|
||||
|
||||
const exportDb = () => {
|
||||
const exportHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate from: "));
|
||||
const exportPort = sanitizeInputParam(
|
||||
prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432"
|
||||
);
|
||||
const exportUser = sanitizeInputParam(
|
||||
prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const exportPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate from: "));
|
||||
const exportDatabase = sanitizeInputParam(
|
||||
prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const exportHost = prompt("Enter your Postgres Host to migrate from: ");
|
||||
const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432";
|
||||
const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical";
|
||||
const exportPassword = prompt("Enter your Postgres Password to migrate from: ");
|
||||
const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical";
|
||||
|
||||
// we do not include the audit_log and secret_sharing entries
|
||||
execSync(
|
||||
`PGDATABASE=${exportDatabase} PGPASSWORD=${exportPassword} PGHOST=${exportHost} PGPORT=${exportPort} PGUSER=${exportUser} pg_dump -Fc infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
|
||||
`PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
|
||||
__dirname,
|
||||
"../src/db/backup.dump"
|
||||
"../src/db/dump.sql"
|
||||
)}`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
};
|
||||
|
||||
const importDbForOrg = () => {
|
||||
const importHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate to: "));
|
||||
const importPort = sanitizeInputParam(prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432");
|
||||
const importUser = sanitizeInputParam(
|
||||
prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const importPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate to: "));
|
||||
const importDatabase = sanitizeInputParam(
|
||||
prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const orgId = sanitizeInputParam(prompt("Enter the organization ID to migrate: "));
|
||||
const importHost = prompt("Enter your Postgres Host to migrate to: ");
|
||||
const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432";
|
||||
const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical";
|
||||
const importPassword = prompt("Enter your Postgres Password to migrate to: ");
|
||||
const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical";
|
||||
const orgId = prompt("Enter the organization ID to migrate: ");
|
||||
|
||||
if (!existsSync(path.join(__dirname, "../src/db/backup.dump"))) {
|
||||
if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) {
|
||||
console.log("File not found, please export the database first.");
|
||||
return;
|
||||
}
|
||||
|
||||
execSync(
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} pg_restore -d ${importDatabase} --verbose ${path.join(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join(
|
||||
__dirname,
|
||||
"../src/db/backup.dump"
|
||||
)}`,
|
||||
{ maxBuffer: 1024 * 1024 * 4096 }
|
||||
"../src/db/dump.sql"
|
||||
)}`
|
||||
);
|
||||
|
||||
execSync(
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
|
||||
);
|
||||
|
||||
// delete global/instance-level resources not relevant to the organization to migrate
|
||||
// users
|
||||
execSync(
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
|
||||
);
|
||||
|
||||
// identities
|
||||
execSync(
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
|
||||
);
|
||||
|
||||
// reset slack configuration in superAdmin
|
||||
execSync(
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
|
||||
);
|
||||
|
||||
console.log("Organization migrated successfully.");
|
||||
|
@@ -72,5 +72,5 @@ export async function down(knex: Knex): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const config = { transaction: false };
|
||||
const config = {transaction: false};
|
||||
export { config };
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
t.dropForeign("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
t.dropForeign("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization);
|
||||
});
|
||||
}
|
||||
}
|
@@ -267,8 +267,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
: "",
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: "",
|
||||
tags: el.secretVersion.tags
|
||||
: ""
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
@@ -572,7 +571,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
reminderNote: el.reminderNote,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||
key: el.key,
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
tagIds: el?.tags.map(({ id }) => id),
|
||||
...encryptedValue
|
||||
}
|
||||
};
|
||||
|
@@ -85,8 +85,7 @@ export const secretRotationDbFn = async ({
|
||||
password,
|
||||
username,
|
||||
client,
|
||||
variables,
|
||||
options
|
||||
variables
|
||||
}: TSecretRotationDbFn) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -118,8 +117,7 @@ export const secretRotationDbFn = async ({
|
||||
password,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 },
|
||||
options
|
||||
pool: { min: 0, max: 1 }
|
||||
}
|
||||
});
|
||||
const data = await db.raw(query, variables);
|
||||
@@ -155,14 +153,6 @@ export const getDbSetQuery = (db: TDbProviderClients, variables: { username: str
|
||||
variables: [variables.username]
|
||||
};
|
||||
}
|
||||
|
||||
if (db === TDbProviderClients.MsSqlServer) {
|
||||
return {
|
||||
query: `ALTER LOGIN ?? WITH PASSWORD = '${variables.password}'`,
|
||||
variables: [variables.username]
|
||||
};
|
||||
}
|
||||
|
||||
// add more based on client
|
||||
return {
|
||||
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,
|
||||
|
@@ -24,5 +24,4 @@ export type TSecretRotationDbFn = {
|
||||
query: string;
|
||||
variables: unknown[];
|
||||
ca?: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
@@ -94,9 +94,7 @@ export const secretRotationQueueFactory = ({
|
||||
// on prod it this will be in days, in development this will be second
|
||||
every: appCfg.NODE_ENV === "development" ? secondsToMillis(interval) : daysToMillisecond(interval),
|
||||
immediately: true
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -116,7 +114,6 @@ export const secretRotationQueueFactory = ({
|
||||
|
||||
queue.start(QueueName.SecretRotation, async (job) => {
|
||||
const { rotationId } = job.data;
|
||||
const appCfg = getConfig();
|
||||
logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`);
|
||||
const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
const rotationProvider = rotationTemplates.find(({ name }) => name === secretRotation?.provider);
|
||||
@@ -175,15 +172,6 @@ export const secretRotationQueueFactory = ({
|
||||
// set a random value for new password
|
||||
newCredential.internal.rotated_password = alphaNumericNanoId(32);
|
||||
const { admin_username: username, admin_password: password, host, database, port, ca } = newCredential.inputs;
|
||||
|
||||
const options =
|
||||
provider.template.client === TDbProviderClients.MsSqlServer
|
||||
? ({
|
||||
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
|
||||
cryptoCredentialsDetails: ca ? { ca } : {}
|
||||
} as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
const dbFunctionArg = {
|
||||
username,
|
||||
password,
|
||||
@@ -191,10 +179,8 @@ export const secretRotationQueueFactory = ({
|
||||
database,
|
||||
port,
|
||||
ca: ca as string,
|
||||
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client,
|
||||
options
|
||||
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client
|
||||
} as TSecretRotationDbFn;
|
||||
|
||||
// set function
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
@@ -203,17 +189,12 @@ export const secretRotationQueueFactory = ({
|
||||
username: newCredential.internal.username as string
|
||||
})
|
||||
});
|
||||
|
||||
// test function
|
||||
const testQuery =
|
||||
provider.template.client === TDbProviderClients.MsSqlServer ? "SELECT GETDATE()" : "SELECT NOW()";
|
||||
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
query: testQuery,
|
||||
query: "SELECT NOW()",
|
||||
variables: []
|
||||
});
|
||||
|
||||
newCredential.outputs.db_username = newCredential.internal.username;
|
||||
newCredential.outputs.db_password = newCredential.internal.rotated_password;
|
||||
// clean up
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { AWS_IAM_TEMPLATE } from "./aws-iam";
|
||||
import { MSSQL_TEMPLATE } from "./mssql";
|
||||
import { MYSQL_TEMPLATE } from "./mysql";
|
||||
import { POSTGRES_TEMPLATE } from "./postgres";
|
||||
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
||||
@@ -27,13 +26,6 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
||||
description: "Rotate MySQL@7/MariaDB user credentials",
|
||||
template: MYSQL_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "mssql",
|
||||
title: "Microsoft SQL Server",
|
||||
image: "mssqlserver.png",
|
||||
description: "Rotate Microsoft SQL server user credentials",
|
||||
template: MSSQL_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "aws-iam",
|
||||
title: "AWS IAM",
|
||||
|
@@ -1,33 +0,0 @@
|
||||
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
|
||||
|
||||
export const MSSQL_TEMPLATE = {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.MsSqlServer,
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_username: { type: "string" as const },
|
||||
admin_password: { type: "string" as const },
|
||||
host: { type: "string" as const },
|
||||
database: { type: "string" as const, default: "master" },
|
||||
port: { type: "integer" as const, default: "1433" },
|
||||
username1: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user1",
|
||||
desc: "SQL Server login name that must be created at server level with a matching database user"
|
||||
},
|
||||
username2: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user2",
|
||||
desc: "SQL Server login name that must be created at server level with a matching database user"
|
||||
},
|
||||
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
|
||||
},
|
||||
required: ["admin_username", "admin_password", "host", "database", "username1", "username2", "port"],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
db_username: { type: "string" },
|
||||
db_password: { type: "string" }
|
||||
}
|
||||
};
|
@@ -8,9 +8,7 @@ export enum TDbProviderClients {
|
||||
// postgres, cockroack db, amazon red shift
|
||||
Pg = "pg",
|
||||
// mysql and maria db
|
||||
MySql = "mysql",
|
||||
|
||||
MsSqlServer = "mssql"
|
||||
MySql = "mysql"
|
||||
}
|
||||
|
||||
export enum TAwsProviderSystems {
|
||||
|
@@ -29,7 +29,7 @@ export const KeyStorePrefixes = {
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10,
|
||||
AccessTokenStatusUpdateInSeconds: 120
|
||||
};
|
||||
|
||||
|
@@ -162,8 +162,7 @@ const envSchema = z
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true")
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@@ -126,8 +126,6 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
return findRootInheritedSecret(inheritedEnv.variables[secretName], secretName, envs);
|
||||
};
|
||||
|
||||
const targetIdToFolderIdsMap = new Map<string, string>();
|
||||
|
||||
const processBranches = () => {
|
||||
for (const subEnv of parsedJson.subEnvironments) {
|
||||
const app = parsedJson.apps.find((a) => a.id === subEnv.envParentId);
|
||||
@@ -137,21 +135,12 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
// Handle regular app branches
|
||||
const branchEnvironment = infisicalImportData.environments.find((e) => e.id === subEnv.parentEnvironmentId);
|
||||
|
||||
// check if the folder already exists in the same parent environment with the same name
|
||||
|
||||
const folderExists = infisicalImportData.folders.some(
|
||||
(f) => f.name === subEnv.subName && f.parentFolderId === subEnv.parentEnvironmentId
|
||||
);
|
||||
|
||||
// No need to map to target ID's here, because we are not dealing with blocks
|
||||
if (!folderExists) {
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: subEnv.parentEnvironmentId,
|
||||
environmentId: branchEnvironment!.id,
|
||||
id: subEnv.id
|
||||
});
|
||||
}
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: subEnv.parentEnvironmentId,
|
||||
environmentId: branchEnvironment!.id,
|
||||
id: subEnv.id
|
||||
});
|
||||
}
|
||||
|
||||
if (block) {
|
||||
@@ -173,22 +162,13 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchingAppEnv) continue;
|
||||
|
||||
const folderExists = infisicalImportData.folders.some(
|
||||
(f) => f.name === subEnv.subName && f.parentFolderId === matchingAppEnv.id
|
||||
);
|
||||
|
||||
if (!folderExists) {
|
||||
// 3. Create a folder in the matching app environment
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: matchingAppEnv.id,
|
||||
environmentId: matchingAppEnv.id,
|
||||
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
|
||||
});
|
||||
} else {
|
||||
// folder already exists, so lets map the old folder id to the new folder id
|
||||
targetIdToFolderIdsMap.set(subEnv.id, `${subEnv.id}-${appId}`);
|
||||
}
|
||||
// 3. Create a folder in the matching app environment
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: matchingAppEnv.id,
|
||||
environmentId: matchingAppEnv.id,
|
||||
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
|
||||
});
|
||||
|
||||
// 4. Process secrets in the block branch for this app
|
||||
const branchSecrets = parsedJson.envs[subEnv.id]?.variables || {};
|
||||
@@ -428,18 +408,17 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
|
||||
// Process each secret in this environment or branch
|
||||
for (const [secretName, secretData] of Object.entries(envData.variables)) {
|
||||
const environmentId = subEnv ? subEnv.parentEnvironmentId : env;
|
||||
const indexOfExistingSecret = infisicalImportData.secrets.findIndex(
|
||||
(s) =>
|
||||
s.name === secretName &&
|
||||
(s.environmentId === subEnv?.parentEnvironmentId || s.environmentId === env) &&
|
||||
(s.folderId ? s.folderId === subEnv?.id : true) &&
|
||||
(secretData.val ? s.value === secretData.val : true)
|
||||
(s) => s.name === secretName && s.environmentId === environmentId
|
||||
);
|
||||
|
||||
if (secretData.inheritsEnvironmentId) {
|
||||
const resolvedSecret = findRootInheritedSecret(secretData, secretName, parsedJson.envs);
|
||||
|
||||
// Check if there's already a secret with this name in the environment, if there is, we should override it. Because if there's already one, we know its coming from a block.
|
||||
// Variables from the normal environment should take precedence over variables from the block.
|
||||
|
||||
if (indexOfExistingSecret !== -1) {
|
||||
// if a existing secret is found, we should replace it directly
|
||||
const newSecret: (typeof infisicalImportData.secrets)[number] = {
|
||||
@@ -477,14 +456,12 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
continue;
|
||||
}
|
||||
|
||||
const folderId = targetIdToFolderIdsMap.get(subEnv?.id || "") || subEnv?.id;
|
||||
|
||||
infisicalImportData.secrets.push({
|
||||
id: randomUUID(),
|
||||
name: secretName,
|
||||
environmentId: subEnv ? subEnv.parentEnvironmentId : env,
|
||||
value: secretData.val || "",
|
||||
...(folderId && { folderId })
|
||||
...(subEnv && { folderId: subEnv.id }) // Add folderId if this is a branch secret
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -614,7 +591,6 @@ export const importDataIntoInfisicalFn = async ({
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
folderId?: string;
|
||||
isFromBlock?: boolean;
|
||||
}[]
|
||||
>();
|
||||
|
||||
@@ -623,8 +599,6 @@ export const importDataIntoInfisicalFn = async ({
|
||||
|
||||
// Skip if we can't find either an environment or folder mapping for this secret
|
||||
if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
|
||||
logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
@@ -632,22 +606,10 @@ export const importDataIntoInfisicalFn = async ({
|
||||
if (!mappedToEnvironmentId.has(targetId)) {
|
||||
mappedToEnvironmentId.set(targetId, []);
|
||||
}
|
||||
|
||||
const alreadyHasSecret = mappedToEnvironmentId
|
||||
.get(targetId)!
|
||||
.find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
|
||||
|
||||
if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
|
||||
// remove the existing secret if any
|
||||
mappedToEnvironmentId
|
||||
.get(targetId)!
|
||||
.splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
|
||||
}
|
||||
mappedToEnvironmentId.get(targetId)!.push({
|
||||
secretKey: secret.name,
|
||||
secretValue: secret.value || "",
|
||||
folderId: secret.folderId,
|
||||
isFromBlock: secret.appBlockOrderIndex !== undefined
|
||||
folderId: secret.folderId
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -2540,7 +2540,7 @@ const syncSecretsAzureDevops = async ({
|
||||
|
||||
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
|
||||
|
||||
const variables: Record<string, { value: string; isSecret: boolean }> = {};
|
||||
const variables: Record<string, { value: string, isSecret: boolean }> = {};
|
||||
for (const key of Object.keys(secrets)) {
|
||||
variables[key] = { value: secrets[key].value, isSecret: true };
|
||||
}
|
||||
|
@@ -112,8 +112,6 @@ export type TGetSecrets = {
|
||||
};
|
||||
|
||||
const MAX_SYNC_SECRET_DEPTH = 5;
|
||||
const SYNC_SECRET_DEBOUNCE_INTERVAL_MS = 3000;
|
||||
|
||||
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
|
||||
`secret-queue-dedupe-${environment}-${secretPath}`;
|
||||
|
||||
@@ -171,39 +169,6 @@ export const secretQueueFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const $generateActor = async (actorId?: string, isManual?: boolean): Promise<Actor> => {
|
||||
if (isManual && actorId) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
userId: user.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
};
|
||||
};
|
||||
|
||||
const $getJobKey = (projectId: string, environmentSlug: string, secretPath: string) => {
|
||||
// the idea here is a timestamp based id which will be constant in a 3s interval
|
||||
const timestampId = Math.floor(Date.now() / SYNC_SECRET_DEBOUNCE_INTERVAL_MS);
|
||||
|
||||
return `secret-queue-sync_${projectId}_${environmentSlug}_${secretPath}_${timestampId}`
|
||||
.replace("/", "-")
|
||||
.replace(":", "-");
|
||||
};
|
||||
|
||||
const addSecretReminder = async ({ oldSecret, newSecret, projectId }: TCreateSecretReminderDTO) => {
|
||||
try {
|
||||
const appCfg = getConfig();
|
||||
@@ -501,7 +466,7 @@ export const secretQueueFactory = ({
|
||||
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
|
||||
) => {
|
||||
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
|
||||
attempts: 5,
|
||||
attempts: 3,
|
||||
delay: 1000,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
@@ -514,10 +479,10 @@ export const secretQueueFactory = ({
|
||||
|
||||
const replicateSecrets = async (dto: Omit<TSyncSecretsDTO, "deDupeQueue">) => {
|
||||
await queueService.queue(QueueName.SecretReplication, QueueJobs.SecretReplication, dto, {
|
||||
attempts: 5,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
delay: 2000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
@@ -534,7 +499,6 @@ export const secretQueueFactory = ({
|
||||
logger.info(
|
||||
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environmentSlug}] [path=${dto.secretPath}]`
|
||||
);
|
||||
|
||||
const deDuplicationKey = uniqueSecretQueueKey(dto.environmentSlug, dto.secretPath);
|
||||
if (
|
||||
!dto.excludeReplication
|
||||
@@ -559,8 +523,7 @@ export const secretQueueFactory = ({
|
||||
{
|
||||
removeOnFail: true,
|
||||
removeOnComplete: true,
|
||||
jobId: $getJobKey(dto.projectId, dto.environmentSlug, dto.secretPath),
|
||||
delay: SYNC_SECRET_DEBOUNCE_INTERVAL_MS,
|
||||
delay: 1000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
@@ -569,6 +532,7 @@ export const secretQueueFactory = ({
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.isSmtpConfigured) return;
|
||||
@@ -576,6 +540,7 @@ export const secretQueueFactory = ({
|
||||
await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, {
|
||||
jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`,
|
||||
delay: 1_000 * 60, // 1 minute
|
||||
|
||||
removeOnFail: true,
|
||||
removeOnComplete: true
|
||||
});
|
||||
@@ -768,51 +733,80 @@ export const secretQueueFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
const lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||
60000,
|
||||
{
|
||||
retryCount: 10,
|
||||
retryDelay: 3000,
|
||||
retryJitter: 500
|
||||
}
|
||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||
const toBeSyncedIntegrations = integrations.filter(
|
||||
// note: sync only the integrations sourced from secretPath
|
||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||
);
|
||||
|
||||
const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
|
||||
|
||||
if (!integrations.length) return;
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
|
||||
const lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||
10000,
|
||||
{
|
||||
retryCount: 3,
|
||||
retryDelay: 2000
|
||||
}
|
||||
);
|
||||
const lockAcquiredTime = new Date();
|
||||
|
||||
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||
);
|
||||
|
||||
// check whether the integration should wait or not
|
||||
if (lastRunSyncIntegrationTimestamp) {
|
||||
const INTEGRATION_INTERVAL = 2000;
|
||||
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
||||
if (isStaleSyncIntegration) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||
lockAcquiredTime.toISOString(),
|
||||
lastRunSyncIntegrationTimestamp
|
||||
);
|
||||
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const generateActor = async (): Promise<Actor> => {
|
||||
if (isManual && actorId) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
userId: user.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
};
|
||||
};
|
||||
|
||||
// akhilmhdh: this try catch is for lock release
|
||||
try {
|
||||
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||
);
|
||||
|
||||
// check whether the integration should wait or not
|
||||
if (lastRunSyncIntegrationTimestamp) {
|
||||
const INTEGRATION_INTERVAL = 2000;
|
||||
|
||||
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||
lockAcquiredTime.toISOString(),
|
||||
lastRunSyncIntegrationTimestamp
|
||||
);
|
||||
|
||||
// give some time for integration to breath
|
||||
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL)
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, INTEGRATION_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||
const toBeSyncedIntegrations = integrations.filter(
|
||||
// note: sync only the integrations sourced from secretPath
|
||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||
);
|
||||
|
||||
if (!integrations.length) return;
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
const secrets = shouldUseSecretV2Bridge
|
||||
? await getIntegrationSecretsV2({
|
||||
environment,
|
||||
@@ -898,7 +892,7 @@ export const secretQueueFactory = ({
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await $generateActor(actorId, isManual),
|
||||
actor: await generateActor(),
|
||||
event: {
|
||||
type: EventType.INTEGRATION_SYNCED,
|
||||
metadata: {
|
||||
@@ -937,7 +931,7 @@ export const secretQueueFactory = ({
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await $generateActor(actorId, isManual),
|
||||
actor: await generateActor(),
|
||||
event: {
|
||||
type: EventType.INTEGRATION_SYNCED,
|
||||
metadata: {
|
||||
|
@@ -4,17 +4,6 @@ title: "Changelog"
|
||||
|
||||
The changelog below reflects new product developments and updates on a monthly basis.
|
||||
|
||||
|
||||
## October 2024
|
||||
- Significantly improved performance of audit log operations in UI.
|
||||
- Released [Databricks integration](https://infisical.com/docs/integrations/cloud/databricks).
|
||||
- Added ability to enforce 2FA organization-wide.
|
||||
- Added multiple resource to the [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs), including AWS and GCP integrations.
|
||||
- Released [Infisical KMS](https://infisical.com/docs/documentation/platform/kms/overview).
|
||||
- Added support for [LDAP dynamic secrets](https://infisical.com/docs/documentation/platform/ldap/overview).
|
||||
- Enabled changing auth methods for machine identities in the UI.
|
||||
- Launched [Infisical EU Cloud](https://eu.infisical.com).
|
||||
|
||||
## September 2024
|
||||
- Improved paginations for identities and secrets.
|
||||
- Significant improvements to the [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
|
||||
|
262
docs/documentation/platform/kms/hsm-integration.mdx
Normal file
262
docs/documentation/platform/kms/hsm-integration.mdx
Normal file
@@ -0,0 +1,262 @@
|
||||
---
|
||||
title: "HSM Integration"
|
||||
description: "Learn more about integrating an HSM with Infisical KMS."
|
||||
---
|
||||
|
||||
<Note>
|
||||
Changing the encryption strategy for your instance is an Enterprise-only feature.
|
||||
This section is intended for users who have obtained an Enterprise license and are on-premise.
|
||||
|
||||
|
||||
Please reach out to sales@infisical.com if you have any questions.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical KMS currently supports two encryption strategies:
|
||||
1. **Standard Encryption**: This is the default encryption strategy used by Infisical KMS. It uses a software-protected encryption key to encrypt KMS keys within your Infisical instance. The root encryption key is defined by setting the `ENCRYPTION_KEY` environment variable.
|
||||
2. **Hardware Security Module (HSM)**: This encryption strategy uses a Hardware Security Module (HSM) to create a root encryption key that is stored on a physical device to encrypt the KMS keys within your instance.
|
||||
|
||||
## Hardware Security Module (HSM)
|
||||
|
||||

|
||||
|
||||
Using a hardware security module comes with the added benefit of having a secure and tamper-proof device to store your encryption keys. This ensures that your data is protected from unauthorized access.
|
||||
|
||||
<Warning>
|
||||
All encryption keys used for cryptographic operations are stored within the HSM. This means that if the HSM is lost or destroyed, you will no longer be able to decrypt your data stored within Infisical. Most providers offer recovery options for HSM devices, which you should consider when setting up an HSM device.
|
||||
</Warning>
|
||||
|
||||
Enabling HSM encryption has a set of key benefits:
|
||||
1. **Root Key Wrapping**: The root KMS encryption key that is used to secure your Infisical instance will be encrypted using the HSM device rather than the standard software-protected key.
|
||||
2. **FIPS 140-2/3 Compliance**: Using an HSM device ensures that your Infisical instance is FIPS 140-2 or FIPS 140-3 compliant. For FIPS 140-3, ensure that your HSM is FIPS 140-3 validated.
|
||||
|
||||
#### Caveats
|
||||
- **Performance**: Using an HSM device can have a performance impact on your Infisical instance. This is due to the additional latency introduced by the HSM device. This is however only noticeable when your instance(s) start up or when the encryption strategy is changed.
|
||||
- **Key Recovery**: If the HSM device is lost or destroyed, you will no longer be able to decrypt your data stored within Infisical. Most HSM providers offer recovery options, which you should consider when setting up an HSM device.
|
||||
|
||||
### Requirements
|
||||
- An Infisical instance with a version number that is equal to or greater than `v0.91.0`.
|
||||
- If you are using Docker, your instance must be using the `infisical/infisical-fips` image.
|
||||
- An HSM device from a provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm), [AWS CloudHSM](https://aws.amazon.com/cloudhsm/), or others.
|
||||
|
||||
|
||||
### FIPS Compliance
|
||||
FIPS, also known as the Federal Information Processing Standard, is a set of standards that are used to accredit cryptographic modules. FIPS 140-2 and FIPS 140-3 are the two most common standards used for cryptographic modules. If your HSM uses FIPS 140-3 validated hardware, Infisical will automatically be FIPS 140-3 compliant. If your HSM uses FIPS 140-2 validated hardware, Infisical will be FIPS 140-2 compliant.
|
||||
|
||||
HSM devices are especially useful for organizations that operate in regulated industries such as healthcare, finance, and government, where data security and compliance are of the utmost importance.
|
||||
|
||||
For organizations that work with US government agencies, FIPS compliance is almost always a requirement when dealing with sensitive information. FIPS compliance ensures that the cryptographic modules used by the organization meet the security requirements set by the US government.
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
|
||||
<Steps>
|
||||
<Step title="Setting up an HSM Device">
|
||||
To set up HSM encryption, you need to configure an HSM provider and HSM key. The HSM provider is used to connect to the HSM device, and the HSM key is used to encrypt Infisical's KMS keys. We recommend using a Cloud HSM provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm) or [AWS CloudHSM](https://aws.amazon.com/cloudhsm/).
|
||||
|
||||
You need to follow the instructions provided by the HSM provider to set up the HSM device. Once the HSM device is set up, the HSM device can be used within Infisical.
|
||||
|
||||
After setting up the HSM from your provider, you will have a set of files that you can use to access the HSM. These files need to be present on the machine where Infisical is running.
|
||||
If you are using containers, you will need to mount the folder where these files are stored as a volume in the container.
|
||||
|
||||
The setup process for an HSM device varies depending on the provider. We have created a guide for Thales Luna Cloud HSM, which you can find below.
|
||||
|
||||
</Step>
|
||||
<Step title="Configure HSM on Infisical">
|
||||
|
||||
<Warning>
|
||||
Are you using Docker? If you are using Docker, please follow the instructions in the [Using HSM's with Docker](#using-hsms-with-docker) section.
|
||||
</Warning>
|
||||
|
||||
Configuring the HSM on Infisical requires setting a set of environment variables:
|
||||
- `HSM_LIB_PATH`: The path to the PKCS#11 library provided by the HSM provider. This usually comes in the form of a `.so` for Linux and MacOS, or a `.dll` file for Windows. For Docker, you need to mount the library path as a volume. Further instructions can be found below. If you are using Docker, make sure to set the HSM_LIB_PATH environment variable to the path where the library is mounted in the container.
|
||||
- `HSM_PIN`: The PKCS#11 PIN to use for authentication with the HSM device.
|
||||
- `HSM_SLOT`: The slot number to use for the HSM device. This is typically between `0` and `5` for most HSM devices.
|
||||
- `HSM_KEY_LABEL`: The label of the key to use for encryption. **Please note that if no key is found with the provided label, the HSM will create a new key with the provided label.**
|
||||
|
||||
You can read more about the [default instance configurations](/self-hosting/configuration/envars) here.
|
||||
</Step>
|
||||
<Step title="Restart Instance">
|
||||
After setting up the HSM, you need to restart the Infisical instance for the changes to take effect.
|
||||
</Step>
|
||||
<Step title="Navigate to the Server Admin Console">
|
||||

|
||||
</Step>
|
||||
<Step title="Update the KMS Encryption Strategy to HSM">
|
||||

|
||||
|
||||
Once you press the 'Save' button, your Infisical instance will immediately switch to the HSM encryption strategy. This will re-encrypt your KMS key with keys from the HSM device.
|
||||
</Step>
|
||||
<Step title="Verify Encryption Strategy">
|
||||
To verify that the HSM was correctly configured, you can try creating a new secret in one of your projects. If the secret is created successfully, the HSM is now being used for encryption.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Using HSMs with Docker
|
||||
When using Docker, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Docker.
|
||||
<Tabs>
|
||||
<Tab title="Thales Luna Cloud HSM">
|
||||
<Steps>
|
||||
<Step title="Create HSM client folder">
|
||||
When using Docker, you are able to set your HSM library path to any location on your machine. In this example, we are going to be using `/etc/luna-docker`.
|
||||
|
||||
```bash
|
||||
mkdir /etc/luna-docker
|
||||
```
|
||||
|
||||
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
||||
|
||||
A folder structure of a client folder will often look like this:
|
||||
```
|
||||
partition-ca-certificate.pem
|
||||
partition-certificate.pem
|
||||
server-certificate.pem
|
||||
Chrystoki.conf
|
||||
/plugins
|
||||
libcloud.plugin
|
||||
/lock
|
||||
/libs
|
||||
/64
|
||||
libCryptoki2.so
|
||||
/jsp
|
||||
LunaProvider.jar
|
||||
/64
|
||||
libLunaAPI.so
|
||||
/etc
|
||||
openssl.cnf
|
||||
/bin
|
||||
/64
|
||||
ckdemo
|
||||
lunacm
|
||||
multitoken
|
||||
vtl
|
||||
```
|
||||
|
||||
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
||||
|
||||
```bash
|
||||
cp -r /<path-to-where-your-luna-client-is-located> /etc/luna-docker
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Update Chrystoki.conf">
|
||||
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
||||
|
||||
In this example, we will be mounting the `/etc/luna-docker` folder to the Docker container under a different path. The path we will use in this example is `/usr/safenet/lunaclient`. This means `/etc/luna-docker` will be mounted to `/usr/safenet/lunaclient` in the Docker container.
|
||||
|
||||
An example config file will look like this:
|
||||
|
||||
```Chrystoki.conf
|
||||
Chrystoki2 = {
|
||||
# This path points to the mounted path, /usr/safenet/lunaclient
|
||||
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
|
||||
}
|
||||
|
||||
Luna = {
|
||||
DefaultTimeOut = 500000;
|
||||
PEDTimeout1 = 100000;
|
||||
PEDTimeout2 = 200000;
|
||||
PEDTimeout3 = 20000;
|
||||
KeypairGenTimeOut = 2700000;
|
||||
CloningCommandTimeOut = 300000;
|
||||
CommandTimeOutPedSet = 720000;
|
||||
}
|
||||
|
||||
CardReader = {
|
||||
LunaG5Slots = 0;
|
||||
RemoteCommand = 1;
|
||||
}
|
||||
|
||||
Misc = {
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
PluginModuleDir = /usr/safenet/lunaclient/plugins;
|
||||
MutexFolder = /usr/safenet/lunaclient/lock;
|
||||
PE1746Enabled = 1;
|
||||
ToolsDir = /usr/bin;
|
||||
|
||||
}
|
||||
|
||||
Presentation = {
|
||||
ShowEmptySlots = no;
|
||||
}
|
||||
|
||||
LunaSA Client = {
|
||||
ReceiveTimeout = 20000;
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
|
||||
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
||||
ClientCertFile = ./etc/ClientNameCert.pem;
|
||||
ServerCAFile = ./etc/CAFile.pem;
|
||||
NetClient = 1;
|
||||
TCPKeepAlive = 1;
|
||||
}
|
||||
|
||||
|
||||
REST = {
|
||||
AppLogLevel = error
|
||||
ServerName = <REDACTED>;
|
||||
ServerPort = 443;
|
||||
AuthTokenConfigURI = <REDACTED>;
|
||||
AuthTokenClientId = <REDACTED>;
|
||||
AuthTokenClientSecret = <REDACTED>;
|
||||
RestClient = 1;
|
||||
ClientTimeoutSec = 120;
|
||||
ClientPoolSize = 32;
|
||||
ClientEofRetryCount = 15;
|
||||
ClientConnectRetryCount = 900;
|
||||
ClientConnectIntervalMs = 1000;
|
||||
}
|
||||
XTC = {
|
||||
Enabled = 1;
|
||||
TimeoutSec = 600;
|
||||
}
|
||||
```
|
||||
|
||||
Save the file after updating the paths.
|
||||
</Step>
|
||||
|
||||
<Step title="Run Docker">
|
||||
Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command.
|
||||
|
||||
We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables.
|
||||
|
||||
<Warning>
|
||||
If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
docker run -p 80:8080 \
|
||||
-v /etc/luna-docker:/usr/safenet/lunaclient \
|
||||
-e HSM_LIB_PATH="/usr/safenet/lunaclient/libs/64/libCryptoki2.so" \
|
||||
-e HSM_PIN="<your-hsm-device-pin>" \
|
||||
-e HSM_SLOT=<hsm-device-slot> \
|
||||
-e HSM_KEY_LABEL="<your-key-label>" \
|
||||
|
||||
# The rest are unrelated to HSM setup...
|
||||
-e ENCRYPTION_KEY="<>" \
|
||||
-e AUTH_SECRET="<>" \
|
||||
-e DB_CONNECTION_URI="<>" \
|
||||
-e REDIS_URL="<>" \
|
||||
-e SITE_URL="<>" \
|
||||
infisical/infisical-fips:<version> # Replace <version> with the version you want to use
|
||||
```
|
||||
|
||||
We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
After following these steps, your Docker setup will be ready to use HSM encryption.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Disabling HSM Encryption
|
||||
|
||||
To disable HSM encryption, navigate to Infisical's Server Admin Console and set the KMS encryption strategy to `Software-based Encryption`. This will revert the encryption strategy back to the default software-based encryption.
|
||||
|
||||
<Note>
|
||||
In order to disable HSM encryption, the Infisical instance must be able to access the HSM device. If the HSM device is no longer accessible, you will not be able to disable HSM encryption.
|
||||
</Note>
|
@@ -8,6 +8,10 @@ description: "Learn how to manage and use cryptographic keys with Infisical."
|
||||
|
||||
Infisical can be used as a Key Management System (KMS), referred to as Infisical KMS, to centralize management of keys to be used for cryptographic operations like encryption/decryption.
|
||||
|
||||
By default your Infisical data such as projects and the data within them are encrypted at rest using Infisical's own KMS. This ensures that your data is secure and protected from unauthorized access.
|
||||
|
||||
If you are on-premise, your KMS root key will be created at random with the `ROOT_ENCRYPTION_KEY` environment variable. You can also use a Hardware Security Module (HSM), to create the root key. Read more about [HSM](/docs/documentation/platform/kms/encryption-strategies).
|
||||
|
||||
<Note>
|
||||
Keys managed in KMS are not extractable from the platform. Additionally, data
|
||||
is never stored when performing cryptographic operations.
|
||||
|
@@ -1,139 +0,0 @@
|
||||
---
|
||||
title: "Microsoft SQL Server"
|
||||
description: "Learn how to automatically rotate Microsoft SQL Server user passwords."
|
||||
---
|
||||
|
||||
The Infisical SQL Server secret rotation allows you to automatically rotate your database users' passwords at a predefined interval.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Create two SQL Server logins and database users with the required permissions. We'll refer to them as `user-a` and `user-b`.
|
||||
2. Create another SQL Server login with permissions to alter logins for `user-a` and `user-b`. We'll refer to this as the `admin` login.
|
||||
|
||||
Here's how to set up the prerequisites:
|
||||
|
||||
```sql
|
||||
-- Create the logins (at server level)
|
||||
CREATE LOGIN [user-a] WITH PASSWORD = 'ComplexPassword1';
|
||||
CREATE LOGIN [user-b] WITH PASSWORD = 'ComplexPassword2';
|
||||
|
||||
-- Create database users for the logins (in your specific database)
|
||||
USE [YourDatabase];
|
||||
CREATE USER [user-a] FOR LOGIN [user-a];
|
||||
CREATE USER [user-b] FOR LOGIN [user-b];
|
||||
|
||||
-- Grant necessary permissions to the users
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-a];
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-b];
|
||||
|
||||
-- Create admin login with permission to alter other logins
|
||||
CREATE LOGIN [admin] WITH PASSWORD = 'AdminComplexPassword';
|
||||
CREATE USER [admin] FOR LOGIN [admin];
|
||||
|
||||
-- Grant permission to alter any login
|
||||
GRANT ALTER ANY LOGIN TO [admin];
|
||||
```
|
||||
|
||||
To learn more about SQL Server's permission system, please visit this [documentation](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/getting-started-with-database-engine-permissions).
|
||||
|
||||
## How it works
|
||||
|
||||
1. Infisical connects to your database using the provided `admin` login credentials.
|
||||
2. A random value is generated and the password for `user-a` is updated with the new value.
|
||||
3. The new password is then tested by logging into the database.
|
||||
4. If test is successful, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
|
||||
5. The process is then repeated for `user-b` on the next rotation.
|
||||
6. The cycle repeats until secret rotation is deleted/stopped.
|
||||
|
||||
## Rotation Configuration
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Rotation Page">
|
||||
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||
</Step>
|
||||
<Step title="Click on Microsoft SQL Server card" />
|
||||
<Step title="Provide the inputs">
|
||||
<ParamField path="Admin Username" type="string" required>
|
||||
SQL Server admin username
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Admin password" type="string" required>
|
||||
SQL Server admin password
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Host" type="string" required>
|
||||
SQL Server host url (e.g., your-server.database.windows.net)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Port" type="number" required>
|
||||
Database port number (default: 1433)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Database" type="string" required>
|
||||
Database name (default: master)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username1" type="string" required>
|
||||
The first login name to rotate - `user-a`
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username2" type="string" required>
|
||||
The second login name to rotate - `user-b`
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="CA" type="string">
|
||||
Optional database certificate to connect with database
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
<Step title="Configure the output secret mapping">
|
||||
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||
|
||||
<ParamField path="Environment" type="string" required>
|
||||
The environment where the rotated credentials should be mapped to.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Secret Path" type="string" required>
|
||||
The secret path where the rotated credentials should be mapped to.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Interval" type="number" required>
|
||||
What interval should the credentials be rotated in days.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="DB USERNAME" type="string" required>
|
||||
Select an existing secret key where the rotated database username value should be saved to.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="DB PASSWORD" type="string" required>
|
||||
Select an existing select key where the rotated database password value should be saved to.
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Why can't we delete the other user when rotating?">
|
||||
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
|
||||
|
||||
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
|
||||
|
||||
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Why do you need an admin account?">
|
||||
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
|
||||
|
||||
You don't need to grant all permissions for your admin account but rather just the permission to alter logins (ALTER ANY LOGIN).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="How does this work with Azure SQL Database?">
|
||||
When using Azure SQL Database, you'll need to:
|
||||
|
||||
1. Use the full server name as your host (e.g., your-server.database.windows.net)
|
||||
2. Ensure your admin account is either the Azure SQL Server admin or an Azure AD account with appropriate permissions
|
||||
3. Configure your Azure SQL Server firewall rules to allow connections from Infisical's IP addresses
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
BIN
docs/images/platform/kms/hsm/encryption-strategy.png
Normal file
BIN
docs/images/platform/kms/hsm/encryption-strategy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
BIN
docs/images/platform/kms/hsm/hsm-illustration.png
Normal file
BIN
docs/images/platform/kms/hsm/hsm-illustration.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
docs/images/platform/kms/hsm/server-admin-console.png
Normal file
BIN
docs/images/platform/kms/hsm/server-admin-console.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
@@ -3,209 +3,81 @@ title: "Permissions"
|
||||
description: "Infisical's permissions system provides granular access control."
|
||||
---
|
||||
|
||||
## Overview
|
||||
## Summary
|
||||
|
||||
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
|
||||
|
||||
Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows.
|
||||
Permissions are built on a subject-action-object model. The subject is the resource permission is being applied to, the action is what the permission allows.
|
||||
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
|
||||
|
||||
Refer to the table below for a list of subjects and the actions they support.
|
||||
Currently Infisical supports 4 actions:
|
||||
1. `read`, allows the subject to read the object.
|
||||
2. `create`, allows the subject to create the object.
|
||||
3. `edit`, allows the subject to edit the object.
|
||||
4. `delete`, allows the subject to delete the object.
|
||||
|
||||
Most subjects support all 4 actions, but some subjects only support a subset of actions. Please view the table below for a list of subjects and the actions they support.
|
||||
|
||||
|
||||
## Subjects and Actions
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Project Permissions">
|
||||
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the
|
||||
`secrets-rollback` subject only supports `read`, and `create` as actions.
|
||||
While `secrets` support `read`, `create`, `edit`, `delete`.
|
||||
Not all actions are applicable to all subjects. As an example, the `secrets-rollback` subject only supports `read`, and `create` as actions. While `secrets` support `read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `integrations` | `read`, `create`, `edit`, `delete` |
|
||||
| `webhooks` | `read`, `create`, `edit`, `delete` |
|
||||
| `service-tokens` | `read`, `create`, `edit`, `delete` |
|
||||
| `environments` | `read`, `create`, `edit`, `delete` |
|
||||
| `tags` | `read`, `create`, `edit`, `delete` |
|
||||
| `audit-logs` | `read`, `create`, `edit`, `delete` |
|
||||
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
|
||||
| `workspace` | `edit`, `delete` |
|
||||
| `secrets` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-folders` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-imports` | `read`, `create`, `edit`, `delete` |
|
||||
| `dynamic-secrets` | `read-root-credential`, `create-root-credential`, `edit-root-credential`, `delete-root-credential`, `lease` |
|
||||
| `secret-rollback` | `read`, `create` |
|
||||
| `secret-approval` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificates` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `edit` |
|
||||
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
|
||||
| Subject | Actions |
|
||||
|-----------------------------|---------|
|
||||
| `secrets` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-approval` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rollback` | `read`, `create` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `integrations` | `read`, `create`, `edit`, `delete` |
|
||||
| `webhooks` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `service-tokens` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `environments` | `read`, `create`, `edit`, `delete` |
|
||||
| `tags` | `read`, `create`, `edit`, `delete` |
|
||||
| `audit-logs` | `read`, `create`, `edit`, `delete` |
|
||||
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificates` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||
| `workspace` | `edit`, `delete` |
|
||||
| `kms` | `edit` |
|
||||
|
||||
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
|
||||
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
|
||||
</Tab>
|
||||
|
||||
|
||||
<Tab title="Organization Permissions">
|
||||
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the `workspace`
|
||||
subject only supports `read`, and `create` as actions. While `member` support
|
||||
`read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `read` |
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the `workspace` subject only supports `read`, and `create` as actions. While `member` support `read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
|-----------------------------|------------------------------------|
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `read` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Inversion
|
||||
|
||||
Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
- cmek
|
||||
|
||||
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
|
||||
|
||||
```typescript
|
||||
// Regular permission - allows reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"]
|
||||
}
|
||||
|
||||
// Inverted permission - denies reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"],
|
||||
inverted: true
|
||||
}
|
||||
```
|
||||
|
||||
## Conditions
|
||||
|
||||
Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
|
||||
### Properties
|
||||
|
||||
Conditions can be applied to the following properties:
|
||||
|
||||
- `environment`: Control access based on environment slugs
|
||||
- `secretPath`: Control access based on secret paths
|
||||
- `secretName`: Control access based on secret names
|
||||
- `secretTags`: Control access based on tags (only supports $in operator)
|
||||
|
||||
### Operators
|
||||
|
||||
The following operators are available for conditions:
|
||||
|
||||
| Operator | Description | Example |
|
||||
| -------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| `$eq` | Equal | `{ environment: { $eq: "production" } }` |
|
||||
| `$ne` | Not equal | `{ environment: { $ne: "development" } }` |
|
||||
| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` |
|
||||
| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` |
|
||||
|
||||
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
|
||||
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
|
||||
|
||||
## Migrating from permission V1 to permission V2
|
||||
|
||||
When upgrading to V2 permissions (i.e. when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows:
|
||||
|
||||
Any permissions for `secrets` should be expanded to include equivalent permissions for:
|
||||
|
||||
- `secret-imports`
|
||||
- `secret-folders` (except for read permissions)
|
||||
- `dynamic-secrets`
|
||||
|
||||
For dynamic secrets, the actions need to be mapped differently:
|
||||
|
||||
- `read` → `read-root-credential`
|
||||
- `create` → `create-root-credential`
|
||||
- `edit` → `edit-root-credential` (also adds `lease` permission)
|
||||
- `delete` → `delete-root-credential`
|
||||
|
||||
Example:
|
||||
|
||||
```hcl
|
||||
# Old V1 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions = [
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "read"
|
||||
},
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# New V2 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions_v2 = [
|
||||
# Original secrets permission
|
||||
{
|
||||
subject = "secrets"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add equivalent secret-imports permission
|
||||
{
|
||||
subject = "secret-imports"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add secret-folders permission (without read)
|
||||
{
|
||||
subject = "secret-folders"
|
||||
action = ["edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add dynamic-secrets permission with mapped actions
|
||||
{
|
||||
subject = "dynamic-secrets"
|
||||
action = ["read-root-credential", "edit-root-credential", "lease"]
|
||||
inverted = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note: When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions.
|
||||
</Tabs>
|
@@ -118,6 +118,7 @@
|
||||
"group": "Key Management (KMS)",
|
||||
"pages": [
|
||||
"documentation/platform/kms/overview",
|
||||
"documentation/platform/kms/hsm-integration",
|
||||
"documentation/platform/kms/kubernetes-encryption"
|
||||
]
|
||||
},
|
||||
@@ -165,7 +166,6 @@
|
||||
"documentation/platform/secret-rotation/sendgrid",
|
||||
"documentation/platform/secret-rotation/postgres",
|
||||
"documentation/platform/secret-rotation/mysql",
|
||||
"documentation/platform/secret-rotation/mssql",
|
||||
"documentation/platform/secret-rotation/aws-iam"
|
||||
]
|
||||
},
|
||||
|
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@@ -65,6 +65,7 @@
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"infisical-node": "^1.0.37",
|
||||
"ip": "^2.0.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
@@ -15975,6 +15976,11 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
@@ -78,6 +78,7 @@
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"infisical-node": "^1.0.37",
|
||||
"ip": "^2.0.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@@ -1,12 +1,10 @@
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
import { DayPicker, DayPickerProps } from "react-day-picker";
|
||||
import { faCalendar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover";
|
||||
import { format, setHours, setMinutes } from "date-fns";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2";
|
||||
|
||||
export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
@@ -24,58 +22,15 @@ export const DatePicker = ({
|
||||
popUpContentProps,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||
|
||||
const handleTimeChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const time = e.target.value;
|
||||
if (time) {
|
||||
setTimeValue(time);
|
||||
if (value) {
|
||||
const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10));
|
||||
const newSelectedDate = setHours(setMinutes(value, minutes), hours);
|
||||
onChange(newSelectedDate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDaySelect = (date: Date | undefined) => {
|
||||
if (!timeValue || !date) {
|
||||
onChange(date);
|
||||
return;
|
||||
}
|
||||
|
||||
const [hours, minutes] = timeValue.split(":").map((str) => parseInt(str, 10));
|
||||
const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, minutes);
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover {...popUpProps}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||
{value ? format(value, "PPP") : "Pick a date and time"}
|
||||
{value ? format(value, "PPP") : "Pick a date"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||
<div className="mx-4 my-4">
|
||||
<Input
|
||||
type="time"
|
||||
value={timeValue}
|
||||
onChange={handleTimeChange}
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
</div>
|
||||
<DayPicker
|
||||
{...props}
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={handleDaySelect}
|
||||
modifiersStyles={{
|
||||
selected: {
|
||||
background: "#cad62d"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DayPicker {...props} mode="single" selected={value} onSelect={onChange} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
@@ -31,8 +31,7 @@ export const useCreateSecretV3 = ({
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment,
|
||||
skipMultilineEncoding,
|
||||
tagIds
|
||||
skipMultilineEncoding
|
||||
}) => {
|
||||
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
|
||||
secretPath,
|
||||
@@ -41,8 +40,7 @@ export const useCreateSecretV3 = ({
|
||||
workspaceId,
|
||||
secretValue,
|
||||
secretComment,
|
||||
skipMultilineEncoding,
|
||||
tagIds
|
||||
skipMultilineEncoding
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
@@ -132,7 +132,6 @@ export type TCreateSecretsV3DTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
type: SecretType;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TUpdateSecretsV3DTO = {
|
||||
|
@@ -12,7 +12,7 @@ export const AuditLogsPage = withPermission(
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
|
||||
<LogsSection filterClassName="static p-2" showFilters isOrgAuditLogs />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from "react";
|
||||
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faFilterCircleXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -16,7 +20,7 @@ import {
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
@@ -56,15 +60,11 @@ export const LogsFilter = ({
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
const { currentWorkspace, workspaces } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
||||
|
||||
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?.id ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (workspacesInOrg.length) {
|
||||
setValue("projectId", workspacesInOrg[0].id);
|
||||
if (workspaces.length) {
|
||||
setValue("projectId", workspaces[0].id);
|
||||
}
|
||||
}, [workspaces]);
|
||||
|
||||
@@ -130,7 +130,7 @@ export const LogsFilter = ({
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
|
||||
@@ -221,7 +221,10 @@ export const LogsFilter = ({
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
@@ -236,7 +239,7 @@ export const LogsFilter = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
{isOrgAuditLogs && workspaces.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
@@ -252,11 +255,11 @@ export const LogsFilter = ({
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 ",
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
{workspacesInOrg.map((project) => (
|
||||
{workspaces.map((project) => (
|
||||
<SelectItem value={String(project.id || "")} key={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
@@ -274,7 +277,10 @@ export const LogsFilter = ({
|
||||
<FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
@@ -293,7 +299,11 @@ export const LogsFilter = ({
|
||||
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
onChange={(pickedDate) => {
|
||||
pickedDate?.setHours(23, 59, 59, 999); // we choose the end of today not the start of it (going off of aws cloud watch)
|
||||
onChange(pickedDate);
|
||||
setIsEndDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
|
@@ -88,7 +88,7 @@ export const LogsSection = ({
|
||||
refetchInterval={refetchInterval}
|
||||
remappedHeaders={remappedHeaders}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn}
|
||||
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
|
||||
filter={{
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { Badge, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
@@ -11,10 +11,6 @@ type Props = {
|
||||
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
if (!actor) {
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
return (
|
||||
@@ -42,6 +38,491 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
}
|
||||
};
|
||||
|
||||
const renderMetadata = (event: Event) => {
|
||||
const metadataKeys = Object.keys(event.metadata);
|
||||
|
||||
switch (event.type) {
|
||||
case EventType.GET_SECRETS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.AUTHORIZE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UNAUTHORIZE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
{event.metadata.app && <p>{`Target app: ${event.metadata.app}`}</p>}
|
||||
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
|
||||
{event.metadata.targetEnvironment && (
|
||||
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
|
||||
)}
|
||||
{event.metadata.targetEnvironmentId && (
|
||||
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
{event.metadata.app && <p>{`Target App: ${event.metadata.app}`}</p>}
|
||||
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
|
||||
{event.metadata.targetEnvironment && (
|
||||
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
|
||||
)}
|
||||
{event.metadata.targetEnvironmentId && (
|
||||
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SERVICE_TOKEN:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SERVICE_TOKEN:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Slug: ${event.metadata.slug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Old name: ${event.metadata.oldName}`}</p>
|
||||
<p>{`New name: ${event.metadata.newName}`}</p>
|
||||
<p>{`Old slug: ${event.metadata.oldSlug}`}</p>
|
||||
<p>{`New slug: ${event.metadata.newSlug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Slug: ${event.metadata.slug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_WORKSPACE_MEMBER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.REMOVE_WORKSPACE_MEMBER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Folder: ${event.metadata.folderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Old folder: ${event.metadata.oldFolderName}`}</p>
|
||||
<p>{`New folder: ${event.metadata.newFolderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Folder: ${event.metadata.folderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_WEBHOOK:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_WEBHOOK_STATUS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_WEBHOOK:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_SECRET_IMPORTS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`# Imported paths: ${event.metadata.numberOfImports}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
|
||||
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
|
||||
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_USER_WORKSPACE_ROLE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
<p>{`Old role: ${event.metadata.oldRole}`}</p>
|
||||
<p>{`New role: ${event.metadata.newRole}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
{event.metadata.deniedPermissions.map((permission) => {
|
||||
return (
|
||||
<p
|
||||
key={`audit-log-denied-permission-${event.metadata.userId}-${permission.environmentSlug}-${permission.ability}`}
|
||||
>
|
||||
{`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ORG_ADMIN_ACCESS_PROJECT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_PKI_ALERT:
|
||||
case EventType.UPDATE_PKI_ALERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Alert ID: ${event.metadata.pkiAlertId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Alert Before Days: ${event.metadata.alertBeforeDays}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_ALERT:
|
||||
case EventType.DELETE_PKI_ALERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Alert ID: ${event.metadata.pkiAlertId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_COLLECTION:
|
||||
case EventType.DELETE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_COLLECTION_ITEMS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_PKI_COLLECTION_ITEM:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Collection Item ID: ${event.metadata.pkiCollectionItemId}`}</p>
|
||||
<p>{`Type: ${event.metadata.type}`}</p>
|
||||
<p>{`Item ID: ${event.metadata.itemId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_PKI_COLLECTION_ITEM:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Collection Item ID: ${event.metadata.pkiCollectionItemId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CA:
|
||||
case EventType.GET_CA:
|
||||
case EventType.UPDATE_CA:
|
||||
case EventType.DELETE_CA:
|
||||
case EventType.GET_CA_CSR:
|
||||
case EventType.GET_CA_CERT:
|
||||
case EventType.IMPORT_CA_CERT:
|
||||
case EventType.GET_CA_CRL:
|
||||
case EventType.SIGN_INTERMEDIATE:
|
||||
case EventType.ISSUE_CERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`CA DN: ${event.metadata.dn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERT:
|
||||
case EventType.DELETE_CERT:
|
||||
case EventType.REVOKE_CERT:
|
||||
case EventType.GET_CERT_BODY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Certificate Authority ID: ${event.metadata.caId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Common Name: ${event.metadata.commonName}`}</p>
|
||||
<p>{`Subject Alternative Name: ${event.metadata.subjectAlternativeName}`}</p>
|
||||
<p>{`TTL: ${event.metadata.ttl}`}</p>
|
||||
{event.metadata.pkiCollectionId && (
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE:
|
||||
case EventType.DELETE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Enabled: ${event.metadata.isEnabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PROJECT_SLACK_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Project Slack Config ID: ${event.metadata.id}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_PROJECT_SLACK_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Project Slack Config ID: ${event.metadata.id}`}</p>
|
||||
<p>{`Slack integration ID: ${event.metadata.slackIntegrationId}`}</p>
|
||||
<p>{`Access Request Notification Status: ${event.metadata.isAccessRequestNotificationEnabled}`}</p>
|
||||
<p>{`Access Request Channels: ${event.metadata.accessRequestChannels}`}</p>
|
||||
<p>{`Secret Approval Request Notification Status: ${event.metadata.isSecretRequestNotificationEnabled}`}</p>
|
||||
<p>{`Secret Request Channels: ${event.metadata.secretRequestChannels}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.INTEGRATION_SYNCED:
|
||||
return (
|
||||
<Td>
|
||||
<Tooltip
|
||||
className="max-w-xs whitespace-normal break-words"
|
||||
content={event.metadata.syncMessage!}
|
||||
isDisabled={!event.metadata.syncMessage}
|
||||
>
|
||||
<Badge variant={event.metadata.isSynced ? "success" : "danger"}>
|
||||
<p className="text-center">{event.metadata.isSynced ? "Successful" : "Failed"}</p>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.GET_WORKSPACE_KEY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Key ID: ${event.metadata.keyId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.ADD_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Identity ID: ${event.metadata.identityId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET:
|
||||
case EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Identity ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Client Secret ID: ${event.metadata.clientSecretId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
// ? If for some reason non the above events are matched, we will display the first 3 metadata items in the metadata object.
|
||||
default:
|
||||
if (metadataKeys.length) {
|
||||
const maxMetadataLength = metadataKeys.length > 3 ? 3 : metadataKeys.length;
|
||||
return (
|
||||
<Td>
|
||||
{Object.entries(event.metadata)
|
||||
.slice(0, maxMetadataLength)
|
||||
.map(([key, value]) => {
|
||||
return <p key={`audit-log-metadata-${key}`}>{`${key}: ${value}`}</p>;
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
return <Td />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
@@ -95,7 +576,7 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
{isOrgAuditLogs && <Td>{auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
{renderSource()}
|
||||
<Td className="max-w-xs break-all">{JSON.stringify(auditLog.event.metadata || {})}</Td>
|
||||
{renderMetadata(auditLog.event)}
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
@@ -96,7 +96,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<SecretInput isReadOnly value={secretVersion?.secretValue} />
|
||||
</Td>
|
||||
<Td>{secretVersion?.secretComment}</Td>
|
||||
<Td className="flex flex-wrap gap-2">
|
||||
<Td>
|
||||
{secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
@@ -118,7 +118,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<SecretInput isReadOnly value={newVersion?.secretValue} />
|
||||
</Td>
|
||||
<Td>{newVersion?.secretComment}</Td>
|
||||
<Td className="flex flex-wrap gap-2">
|
||||
<Td>
|
||||
{newVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
|
@@ -9,14 +9,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||
import {
|
||||
Checkbox,
|
||||
ContentLoader,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
@@ -48,10 +41,7 @@ import { SecretDropzone } from "./components/SecretDropzone";
|
||||
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
|
||||
import { SnapshotView } from "./components/SnapshotView";
|
||||
import {
|
||||
PopUpNames,
|
||||
StoreProvider,
|
||||
usePopUpAction,
|
||||
usePopUpState,
|
||||
useSelectedSecretActions,
|
||||
useSelectedSecrets
|
||||
} from "./SecretMainPage.store";
|
||||
@@ -133,9 +123,6 @@ const SecretMainPageContent = () => {
|
||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
||||
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
||||
|
||||
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { togglePopUp } = usePopUpAction();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isWorkspaceLoading &&
|
||||
@@ -533,24 +520,13 @@ const SecretMainPageContent = () => {
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={createSecretPopUp.isOpen}
|
||||
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create Secret"
|
||||
subTitle="Add a secret to this particular environment and folder"
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<CreateSecretForm
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
autoCapitalize={currentWorkspace?.autoCapitalization}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<CreateSecretForm
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
autoCapitalize={currentWorkspace?.autoCapitalization}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
/>
|
||||
<SecretDropzone
|
||||
secrets={secrets}
|
||||
environment={environment}
|
||||
|
@@ -1,24 +1,20 @@
|
||||
import { ClipboardEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faTriangleExclamation } 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, MultiSelect } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api";
|
||||
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||
import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store";
|
||||
|
||||
const typeSchema = z.object({
|
||||
key: z.string().trim().min(1, { message: "Secret key is required" }),
|
||||
value: z.string().optional(),
|
||||
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
||||
value: z.string().optional()
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof typeSchema>;
|
||||
@@ -47,16 +43,12 @@ export const CreateSecretForm = ({
|
||||
setValue,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||
const { closePopUp } = usePopUpAction();
|
||||
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { closePopUp, togglePopUp } = usePopUpAction();
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { permission } = useProjectPermission();
|
||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
||||
canReadTags ? workspaceId : ""
|
||||
);
|
||||
|
||||
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
|
||||
const handleFormSubmit = async ({ key, value }: TFormSchema) => {
|
||||
try {
|
||||
await createSecretV3({
|
||||
environment,
|
||||
@@ -65,8 +57,7 @@ export const CreateSecretForm = ({
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared,
|
||||
tagIds: tags?.map((el) => el.value)
|
||||
type: SecretType.Shared
|
||||
});
|
||||
closePopUp(PopUpNames.CreateSecretForm);
|
||||
reset();
|
||||
@@ -97,90 +88,67 @@ export const CreateSecretForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create secret"
|
||||
subTitle="Add a secret to the particular environment and folder"
|
||||
>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={autoCapitalize}
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={autoCapitalize}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Tags"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
helperText={
|
||||
!canReadTags ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
||||
<span>You do not have permission to read tags.</span>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
>
|
||||
<MultiSelect
|
||||
className="w-full"
|
||||
placeholder="Select tags to assign to secret..."
|
||||
isMulti
|
||||
name="tagIds"
|
||||
isDisabled={!canReadTags}
|
||||
isLoading={isTagsLoading}
|
||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@@ -1116,23 +1116,13 @@ export const SecretOverviewPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
<CreateSecretForm
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.addSecretsInAllEnvs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
className="max-h-[80vh]"
|
||||
bodyClassName="overflow-visible"
|
||||
title="Create Secrets"
|
||||
subTitle="Create a secret across multiple environments"
|
||||
>
|
||||
<CreateSecretForm
|
||||
secretPath={secretPath}
|
||||
getSecretByKey={getSecretByKey}
|
||||
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
getSecretByKey={getSecretByKey}
|
||||
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp.addFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ClipboardEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
MultiSelect,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
@@ -24,20 +25,14 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretV3,
|
||||
useGetWsTags,
|
||||
useUpdateSecretV3
|
||||
} from "@app/hooks/api";
|
||||
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
|
||||
const typeSchema = z
|
||||
.object({
|
||||
key: z.string().trim().min(1, "Key is required"),
|
||||
value: z.string().optional(),
|
||||
environments: z.record(z.boolean().optional()),
|
||||
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
||||
environments: z.record(z.boolean().optional())
|
||||
})
|
||||
.refine((data) => data.key !== undefined, {
|
||||
message: "Please enter secret name"
|
||||
@@ -49,10 +44,18 @@ type Props = {
|
||||
secretPath?: string;
|
||||
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
|
||||
// modal props
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
onTogglePopUp: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => {
|
||||
export const CreateSecretForm = ({
|
||||
secretPath = "/",
|
||||
isOpen,
|
||||
getSecretByKey,
|
||||
onClose,
|
||||
onTogglePopUp
|
||||
}: Props) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -66,18 +69,14 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { permission } = useProjectPermission();
|
||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
||||
canReadTags ? workspaceId : ""
|
||||
);
|
||||
|
||||
const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => {
|
||||
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
|
||||
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
|
||||
const isEnvironmentsSelected = environmentsSelected.length;
|
||||
|
||||
@@ -121,8 +120,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
type: SecretType.Shared,
|
||||
tagIds: tags?.map((el) => el.value)
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
environment
|
||||
};
|
||||
@@ -136,8 +134,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared,
|
||||
tagIds: tags?.map((el) => el.value)
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
environment
|
||||
};
|
||||
@@ -200,136 +197,114 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||
<ModalContent
|
||||
className="max-h-[80vh] overflow-y-auto"
|
||||
title="Bulk Create & Update"
|
||||
subTitle="Create & update a secret across many environments"
|
||||
>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Tags"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
helperText={
|
||||
!canReadTags ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
||||
<span>You do not have permission to read tags.</span>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Environments" className="mb-2" />
|
||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
||||
{environments
|
||||
.filter((environmentSlug) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: environmentSlug.slug,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<MultiSelect
|
||||
className="w-full"
|
||||
placeholder="Select tags to assign to secrets..."
|
||||
isMulti
|
||||
name="tagIds"
|
||||
isDisabled={!canReadTags}
|
||||
isLoading={isTagsLoading}
|
||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Environments" className="mb-2" />
|
||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
||||
{environments
|
||||
.filter((environmentSlug) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: environmentSlug.slug,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
)
|
||||
)
|
||||
.map((env) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`environments.${env.slug}`}
|
||||
key={`secret-input-${env.slug}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`secret-input-${env.slug}`}
|
||||
className="!justify-start"
|
||||
>
|
||||
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
||||
<span title={env.name} className="truncate">
|
||||
{env.name}
|
||||
</span>
|
||||
<span>
|
||||
{getSecretByKey(env.slug, newSecretKey) && (
|
||||
<Tooltip
|
||||
className="max-w-[150px]"
|
||||
content="Secret already exists, and it will be overwritten"
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={onClose}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
.map((env) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`environments.${env.slug}`}
|
||||
key={`secret-input-${env.slug}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`secret-input-${env.slug}`}
|
||||
className="!justify-start"
|
||||
>
|
||||
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
||||
<span title={env.name} className="truncate">
|
||||
{env.name}
|
||||
</span>
|
||||
<span>
|
||||
{getSecretByKey(env.slug, newSecretKey) && (
|
||||
<Tooltip
|
||||
className="max-w-[150px]"
|
||||
content="Secret already exists, and it will be overwritten"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faWarning}
|
||||
className="ml-1 text-yellow-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={onClose}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@@ -1,13 +1,3 @@
|
||||
## 1.4.0 (November 06, 2024)
|
||||
|
||||
Changes:
|
||||
* Chart is now fully documented
|
||||
* New fields introduced: `infisical.databaseSchemaMigrationJob.image` and `infisical.serviceAccount`
|
||||
|
||||
Features:
|
||||
|
||||
* Added support for auto creating service account with required permissions via `infisical.serviceAccount.create`
|
||||
|
||||
## 1.3.0 (October 28, 2024)
|
||||
|
||||
Changes:
|
||||
|
@@ -1,13 +1,13 @@
|
||||
apiVersion: v2
|
||||
name: infisical-standalone
|
||||
description: A helm chart to deploy Infisical
|
||||
description: A helm chart for a full Infisical application
|
||||
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.4.0
|
||||
version: 1.3.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
@@ -1,66 +0,0 @@
|
||||
# infisical-standalone
|
||||
|
||||
  
|
||||
|
||||
A helm chart to deploy Infisical
|
||||
|
||||
## Requirements
|
||||
|
||||
| Repository | Name | Version |
|
||||
|------------|------|---------|
|
||||
| https://charts.bitnami.com/bitnami | postgresql | 14.1.3 |
|
||||
| https://charts.bitnami.com/bitnami | redis | 18.14.0 |
|
||||
| https://kubernetes.github.io/ingress-nginx | ingress-nginx | 4.0.13 |
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| fullnameOverride | string | `""` | Overrides the full name of the release, affecting resource names |
|
||||
| infisical.affinity | object | `{}` | Node affinity settings for pod placement |
|
||||
| infisical.autoDatabaseSchemaMigration | bool | `true` | Automatically migrates new database schema when deploying |
|
||||
| infisical.databaseSchemaMigrationJob.image.pullPolicy | string | `"IfNotPresent"` | Pulls image only if not present on the node |
|
||||
| infisical.databaseSchemaMigrationJob.image.repository | string | `"ghcr.io/groundnuty/k8s-wait-for"` | Image repository for migration wait job |
|
||||
| infisical.databaseSchemaMigrationJob.image.tag | string | `"no-root-v2.0"` | Image tag version |
|
||||
| infisical.deploymentAnnotations | object | `{}` | Custom annotations for Infisical deployment |
|
||||
| infisical.enabled | bool | `true` | |
|
||||
| infisical.fullnameOverride | string | `""` | Override for the full name of Infisical resources in this deployment |
|
||||
| infisical.image.imagePullSecrets | list | `[]` | Secret references for pulling the image, if needed |
|
||||
| infisical.image.pullPolicy | string | `"IfNotPresent"` | Pulls image only if not already present on the node |
|
||||
| infisical.image.repository | string | `"infisical/infisical"` | Image repository for the Infisical service |
|
||||
| infisical.image.tag | string | `"v0.93.1-postgres"` | Specific version tag of the Infisical image. View the latest version here https://hub.docker.com/r/infisical/infisical |
|
||||
| infisical.kubeSecretRef | string | `"infisical-secrets"` | Kubernetes Secret reference containing Infisical root credentials |
|
||||
| infisical.name | string | `"infisical"` | |
|
||||
| infisical.podAnnotations | object | `{}` | Custom annotations for Infisical pods |
|
||||
| infisical.replicaCount | int | `2` | Number of pod replicas for high availability |
|
||||
| infisical.resources.limits.memory | string | `"600Mi"` | Memory limit for Infisical container |
|
||||
| infisical.resources.requests.cpu | string | `"350m"` | CPU request for Infisical container |
|
||||
| infisical.service.annotations | object | `{}` | Custom annotations for Infisical service |
|
||||
| infisical.service.nodePort | string | `""` | Optional node port for service when using NodePort type |
|
||||
| infisical.service.type | string | `"ClusterIP"` | Service type, can be changed based on exposure needs (e.g., LoadBalancer) |
|
||||
| infisical.serviceAccount.annotations | object | `{}` | Custom annotations for the auto-created service account |
|
||||
| infisical.serviceAccount.create | bool | `true` | Creates a new service account if true, with necessary permissions for this chart. If false and `serviceAccount.name` is not defined, the chart will attempt to use the Default service account |
|
||||
| infisical.serviceAccount.name | string | `nil` | Optional custom service account name, if existing service account is used |
|
||||
| ingress.annotations | object | `{}` | Custom annotations for ingress resource |
|
||||
| ingress.enabled | bool | `true` | Enable or disable ingress configuration |
|
||||
| ingress.hostName | string | `""` | Hostname for ingress access, e.g., app.example.com |
|
||||
| ingress.ingressClassName | string | `"nginx"` | Specifies the ingress class, useful for multi-ingress setups |
|
||||
| ingress.nginx.enabled | bool | `true` | Enable NGINX-specific settings, if using NGINX ingress controller |
|
||||
| ingress.tls | list | `[]` | TLS settings for HTTPS access |
|
||||
| nameOverride | string | `""` | Overrides the default release name |
|
||||
| postgresql.auth.database | string | `"infisicalDB"` | Database name for Infisical |
|
||||
| postgresql.auth.password | string | `"root"` | Password for PostgreSQL database access |
|
||||
| postgresql.auth.username | string | `"infisical"` | Database username for PostgreSQL |
|
||||
| postgresql.enabled | bool | `true` | Enables an in-cluster PostgreSQL deployment. To achieve HA for Postgres, we recommend deploying https://github.com/zalando/postgres-operator instead. |
|
||||
| postgresql.fullnameOverride | string | `"postgresql"` | Full name override for PostgreSQL resources |
|
||||
| postgresql.name | string | `"postgresql"` | PostgreSQL resource name |
|
||||
| postgresql.useExistingPostgresSecret.enabled | bool | `false` | Set to true if using an existing Kubernetes secret that contains PostgreSQL connection string |
|
||||
| postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key | string | `""` | Key name in the Kubernetes secret that holds the connection string |
|
||||
| postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name | string | `""` | Kubernetes secret name containing the PostgreSQL connection string |
|
||||
| redis.architecture | string | `"standalone"` | Redis deployment type (e.g., standalone or cluster) |
|
||||
| redis.auth.password | string | `"mysecretpassword"` | Redis password |
|
||||
| redis.cluster.enabled | bool | `false` | Clustered Redis deployment |
|
||||
| redis.enabled | bool | `true` | Enables an in-cluster Redis deployment |
|
||||
| redis.fullnameOverride | string | `"redis"` | Full name override for Redis resources |
|
||||
| redis.name | string | `"redis"` | Redis resource name |
|
||||
| redis.usePassword | bool | `true` | Requires a password for Redis authentication |
|
@@ -40,23 +40,6 @@ component: {{ .Values.infisical.name | quote }}
|
||||
{{ include "infisical.common.matchLabels" . }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.roleName" -}}
|
||||
{{- printf "%s-infisical" .Release.Name -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.roleBindingName" -}}
|
||||
{{- printf "%s-infisical" .Release.Name -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.serviceAccountName" -}}
|
||||
{{- if .Values.infisical.serviceAccount.create -}}
|
||||
{{- printf "%s-infisical" .Release.Name -}}
|
||||
{{- else -}}
|
||||
{{- .Values.infisical.serviceAccount.name | default "default" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{/*
|
||||
Create a fully qualified backend name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
|
@@ -34,11 +34,10 @@ spec:
|
||||
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
|
||||
serviceAccountName: {{ include "infisical.serviceAccountName" . }}
|
||||
initContainers:
|
||||
- name: "migration-init"
|
||||
image: "{{ $infisicalValues.databaseSchemaMigrationJob.image.repository }}:{{ $infisicalValues.databaseSchemaMigrationJob.image.tag }}"
|
||||
imagePullPolicy: {{ $infisicalValues.databaseSchemaMigrationJob.image.pullPolicy }}
|
||||
image: {{ $infisicalValues.databaseSchemaMigrationInitContainer.image }}
|
||||
imagePullPolicy: {{ $infisicalValues.databaseSchemaMigrationInitContainer.imagePullPolicy }}
|
||||
args:
|
||||
- "job"
|
||||
- "{{ .Release.Name }}-schema-migration-{{ .Release.Revision }}"
|
||||
|
@@ -1,25 +1,8 @@
|
||||
---
|
||||
{{- if .Values.infisical.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ printf "%s-infisical" .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "infisical.labels" . | nindent 4 }}
|
||||
{{- with .Values.infisical.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ include "infisical.roleName" . }}
|
||||
name: k8s-wait-for-infisical-schema-migration
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "infisical.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
@@ -28,15 +11,13 @@ rules:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ include "infisical.roleBindingName" . }}
|
||||
name: infisical-database-schema-migration
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "infisical.labels" . | nindent 4 }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "infisical.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
- kind: ServiceAccount
|
||||
name: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ include "infisical.roleName" . }}
|
||||
name: k8s-wait-for-infisical-schema-migration
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
@@ -16,7 +16,7 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name | quote }}
|
||||
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
spec:
|
||||
serviceAccountName: {{ include "infisical.serviceAccountName" . }}
|
||||
serviceAccountName: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
|
||||
{{- if $infisicalValues.image.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
|
||||
|
@@ -1,139 +1,81 @@
|
||||
# -- Overrides the default release name
|
||||
nameOverride: ""
|
||||
|
||||
# -- Overrides the full name of the release, affecting resource names
|
||||
fullnameOverride: ""
|
||||
|
||||
infisical:
|
||||
enabled: true # -- Enable Infisical chart deployment
|
||||
name: infisical # -- Sets the name of the deployment within this chart
|
||||
|
||||
# -- Automatically migrates new database schema when deploying
|
||||
enabled: true
|
||||
name: infisical
|
||||
autoDatabaseSchemaMigration: true
|
||||
databaseSchemaMigrationInitContainer:
|
||||
image: "ghcr.io/groundnuty/k8s-wait-for:no-root-v2.0"
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
databaseSchemaMigrationJob:
|
||||
image:
|
||||
# -- Image repository for migration wait job
|
||||
repository: ghcr.io/groundnuty/k8s-wait-for
|
||||
# -- Image tag version
|
||||
tag: no-root-v2.0
|
||||
# -- Pulls image only if not present on the node
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
serviceAccount:
|
||||
# -- Creates a new service account if true, with necessary permissions for this chart. If false and `serviceAccount.name` is not defined, the chart will attempt to use the Default service account
|
||||
create: true
|
||||
# -- Custom annotations for the auto-created service account
|
||||
annotations: {}
|
||||
# -- Optional custom service account name, if existing service account is used
|
||||
name: null
|
||||
|
||||
# -- Override for the full name of Infisical resources in this deployment
|
||||
serviceAccountName: default
|
||||
|
||||
fullnameOverride: ""
|
||||
# -- Custom annotations for Infisical pods
|
||||
podAnnotations: {}
|
||||
# -- Custom annotations for Infisical deployment
|
||||
deploymentAnnotations: {}
|
||||
# -- Number of pod replicas for high availability
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
# -- Image repository for the Infisical service
|
||||
repository: infisical/infisical
|
||||
# -- Specific version tag of the Infisical image. View the latest version here https://hub.docker.com/r/infisical/infisical
|
||||
tag: "v0.93.1-postgres"
|
||||
# -- Pulls image only if not already present on the node
|
||||
tag: "v0.46.3-postgres"
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Secret references for pulling the image, if needed
|
||||
imagePullSecrets: []
|
||||
|
||||
# -- Node affinity settings for pod placement
|
||||
affinity: {}
|
||||
# -- Kubernetes Secret reference containing Infisical root credentials
|
||||
kubeSecretRef: "infisical-secrets"
|
||||
|
||||
service:
|
||||
# -- Custom annotations for Infisical service
|
||||
annotations: {}
|
||||
# -- Service type, can be changed based on exposure needs (e.g., LoadBalancer)
|
||||
type: ClusterIP
|
||||
# -- Optional node port for service when using NodePort type
|
||||
nodePort: ""
|
||||
|
||||
resources:
|
||||
limits:
|
||||
# -- Memory limit for Infisical container
|
||||
memory: 600Mi
|
||||
requests:
|
||||
# -- CPU request for Infisical container
|
||||
cpu: 350m
|
||||
|
||||
ingress:
|
||||
# -- Enable or disable ingress configuration
|
||||
enabled: true
|
||||
# -- Hostname for ingress access, e.g., app.example.com
|
||||
hostName: ""
|
||||
# -- Specifies the ingress class, useful for multi-ingress setups
|
||||
ingressClassName: nginx
|
||||
|
||||
nginx:
|
||||
# -- Enable NGINX-specific settings, if using NGINX ingress controller
|
||||
enabled: true
|
||||
|
||||
# -- Custom annotations for ingress resource
|
||||
annotations: {}
|
||||
# -- TLS settings for HTTPS access
|
||||
tls:
|
||||
[]
|
||||
# -- TLS secret name for HTTPS
|
||||
# - secretName: letsencrypt-prod
|
||||
# -- Domain name to associate with the TLS certificate
|
||||
# hosts:
|
||||
# - some.domain.com
|
||||
|
||||
postgresql:
|
||||
# -- Enables an in-cluster PostgreSQL deployment. To achieve HA for Postgres, we recommend deploying https://github.com/zalando/postgres-operator instead.
|
||||
# -- When enabled, this will start up a in cluster Postgres
|
||||
enabled: true
|
||||
# -- PostgreSQL resource name
|
||||
name: "postgresql"
|
||||
# -- Full name override for PostgreSQL resources
|
||||
fullnameOverride: "postgresql"
|
||||
|
||||
auth:
|
||||
# -- Database username for PostgreSQL
|
||||
username: infisical
|
||||
# -- Password for PostgreSQL database access
|
||||
password: root
|
||||
# -- Database name for Infisical
|
||||
database: infisicalDB
|
||||
|
||||
useExistingPostgresSecret:
|
||||
# -- Set to true if using an existing Kubernetes secret that contains PostgreSQL connection string
|
||||
# -- When this is enabled, postgresql.enabled needs to be false
|
||||
enabled: false
|
||||
# -- The name from where to get the existing postgresql connection string
|
||||
existingConnectionStringSecret:
|
||||
# -- Kubernetes secret name containing the PostgreSQL connection string
|
||||
# -- The name of the secret that contains the postgres connection string
|
||||
name: ""
|
||||
# -- Key name in the Kubernetes secret that holds the connection string
|
||||
# -- Secret key name that contains the postgres connection string
|
||||
key: ""
|
||||
|
||||
redis:
|
||||
# -- Enables an in-cluster Redis deployment
|
||||
enabled: true
|
||||
# -- Redis resource name
|
||||
name: "redis"
|
||||
# -- Full name override for Redis resources
|
||||
fullnameOverride: "redis"
|
||||
|
||||
cluster:
|
||||
# -- Clustered Redis deployment
|
||||
enabled: false
|
||||
|
||||
# -- Requires a password for Redis authentication
|
||||
usePassword: true
|
||||
|
||||
auth:
|
||||
# -- Redis password
|
||||
password: "mysecretpassword"
|
||||
|
||||
# -- Redis deployment type (e.g., standalone or cluster)
|
||||
architecture: standalone
|
||||
|
Reference in New Issue
Block a user