Compare commits

...

80 Commits

Author SHA1 Message Date
e5947fcab9 misc: add event loop stats 2025-03-21 01:48:11 +08:00
f7cf2bb78f Merge pull request #3278 from Infisical/daniel/kubernetes-hsm-docs
docs(hsm): kubernetes deployment docs
2025-03-20 03:25:48 +04:00
ff24e76a32 docs(hsm): kubernetes deployment docs, requested changes 2025-03-20 02:59:07 +04:00
6ac802b6c9 Merge pull request #3280 from akhilmhdh/fix/patch-4
feat: added k8s abort
2025-03-19 18:01:18 -04:00
=
ff92e00503 feat: added k8s abort 2025-03-20 03:29:40 +05:30
b20474c505 Merge pull request #3279 from akhilmhdh/fix/patch-4
feat: added log and updated license ttl to 5min
2025-03-19 17:11:12 -04:00
=
e19ffc91c6 feat: added more log 2025-03-20 02:40:37 +05:30
=
61eb66efca feat: added log and updated license ttl to 5min 2025-03-20 02:36:26 +05:30
15999daa24 docs(hsm): kubernetes deployment docs 2025-03-19 23:02:39 +04:00
ec31211bca Merge pull request #3277 from Infisical/daniel/fix-helm-required-field
fix(k8s): remove required field from helm
2025-03-19 21:31:45 +04:00
0ecf6044d9 fix(k8s): remove required field from helm 2025-03-19 20:50:28 +04:00
6c512f47bf Merge pull request #3274 from Infisical/fix/bulkDeleteSecretIncorrectPermissions
Bulk Delete Secret Incorrectly says lacking permission
2025-03-19 12:30:19 -03:00
33b135f02c Merge pull request #3275 from Infisical/fix/universalAuthDocApiUrlImprovement
Universal Auth doc improvement to show US/EU API URLs
2025-03-19 11:10:23 -03:00
eed7cc6408 Add tip to universal auth doc to show US/EU API URLs 2025-03-19 10:49:33 -03:00
440ada464f Merge pull request #3259 from Infisical/feat/automated-instance-bootstrapping
feat: automated bootstrapping
2025-03-19 21:40:55 +08:00
6b7abbbeb9 Return accum if no entry is found for env.slug on secretsToDelete to avoid losing reducer process due to permission.can 2025-03-19 09:10:15 -03:00
3944e20a5b Merge pull request #3257 from Infisical/feat/addFileImportToSecretSetCLI
Add file option to secret set CLI command to retrieve secrets from .env or .yaml files
2025-03-19 08:01:11 -03:00
2079913511 misc: final doc updates 2025-03-19 08:08:11 +00:00
049f0f56a0 misc: added env support for the other cli flags 2025-03-19 15:44:56 +08:00
9ad725fd6c Merge pull request #3271 from Infisical/vmatsiiako-patch-cure53-1
Update security.mdx
2025-03-18 19:37:29 -07:00
9a954c8f15 Update security.mdx 2025-03-18 19:32:26 -07:00
81a64d081c Merge pull request #3270 from Infisical/daniel/helm-custom-volume-support
feat(helm): custom volume support
2025-03-19 06:22:59 +04:00
43804f62e6 Update CHANGELOG.md 2025-03-19 06:18:48 +04:00
67089af17a feat(helm): custom volume support 2025-03-19 05:39:00 +04:00
d83240749f Merge pull request #3265 from Infisical/minor-changes
Minor changes to oidc claims and mappings
2025-03-18 17:22:51 -04:00
36144d8c42 add proper docs 2025-03-18 17:21:48 -04:00
4478dc8659 misc: minor header label updates 2025-03-19 03:05:50 +08:00
510ddf2b1a misc: add machine identity server admin image 2025-03-18 19:05:23 +00:00
5363f8c6ff misc: doc updates 2025-03-18 18:45:43 +00:00
7d9de6acba Merge branch 'feat/automated-instance-bootstrapping' of https://github.com/Infisical/infisical into feat/automated-instance-bootstrapping 2025-03-18 18:37:42 +00:00
bac944133a doc: finalized response schema 2025-03-18 18:36:42 +00:00
f059d65b45 misc: finalized response 2025-03-19 02:26:51 +08:00
=
c487b2b34a feat: updated doc 2025-03-18 23:44:50 +05:30
015a193330 misc: added support for removing instance admin access of users 2025-03-19 02:13:24 +08:00
=
8e20531b40 feat: changed key to be required 2025-03-18 23:05:31 +05:30
d91add2e7b misc: added return statements 2025-03-18 23:49:40 +08:00
=
8ead2aa774 feat: updated documentation on identity oidc auth permission 2025-03-18 20:54:12 +05:30
=
1b2128e3cc feat: updated code to auth field in permission for identity 2025-03-18 20:53:51 +05:30
6d72524896 misc: addressed typo 2025-03-18 23:15:48 +08:00
1ec11d5963 doc: added docs 2025-03-18 14:43:19 +00:00
ad6f285b59 Throw if file and args secrets are used on secret set command 2025-03-18 09:58:10 -03:00
d4842dd273 Improve secret set --file message and make it mutually exclusive with manual secrets args 2025-03-18 08:32:31 -03:00
78f83cb478 remove default open 2025-03-17 21:51:47 -04:00
c8a871de7c fix lint 2025-03-17 19:47:08 -04:00
64c0951df3 add new line so there is no change 2025-03-17 19:38:50 -04:00
c185414a3c bring back .env example 2025-03-17 19:38:05 -04:00
f9695741f1 Minor changes to oidc claims and mappings
- Made the claims expanded by default (it looked off when they were closed)
- Moved claims from advanced to geneal tab and kept the mapping in the advanced tab
- Added better description for the tooltip

question: i feel like it would be better to access metadata like: `{{identity.auth.oidc.claim.<...>}}` instead of like how it is now: `{{identity.metadata.auth.oidc.claim.<...>}}`? What do you think
2025-03-17 19:36:40 -04:00
b7c4b11260 Merge pull request #3246 from Infisical/disable-delete-sync-option
Feature: Disable Secret Deletion Sync Option
2025-03-17 15:16:55 -07:00
81f3613393 improvements: address feedback 2025-03-17 15:10:36 -07:00
a7fe79c046 Merge pull request #3242 from akhilmhdh/feat/metadata-oidc
Feat/metadata OIDC
2025-03-17 16:55:50 -04:00
ed6306747a feat: add support for removing instance admin permission from identity 2025-03-18 03:00:54 +08:00
64569ab44b feat: added bootstrap to CLI 2025-03-18 02:27:42 +08:00
=
9eb89bb46d fix: null causing ui error 2025-03-17 23:52:11 +05:30
=
c4da1ce32d feat: resolved PR feedbacks 2025-03-17 23:38:24 +05:30
2d1d6f5ce8 misc: added privilege escalation checks 2025-03-18 01:20:01 +08:00
add97c9b38 Merge pull request #3241 from Infisical/feat/addDynamicSecretsToOverview
Add dynamic secrets modal form on secrets overview page
2025-03-17 13:14:22 -03:00
768ba4f4dc Merge pull request #3261 from Infisical/revert-3238-feat/ENG-2320-echo-environment-being-used-in-cli
Revert "feat: confirm environment exists when running `run` command"
2025-03-17 12:09:39 -04:00
18c32d872c Revert "feat: confirm environment exists when running run command" 2025-03-17 12:06:35 -04:00
1fd40ab6ab Merge pull request #3260 from akhilmhdh/fix/gateway-migration
fix: corrected table name check in migration
2025-03-17 21:31:00 +05:30
=
9d258f57ce fix: corrected table name check in migration 2025-03-17 21:28:50 +05:30
45ccbaf4c9 Merge pull request #3243 from Infisical/gcp-sync-handle-destroyed-values
Fix: Handle Disabled/Destroyed Values in GCP Sync
2025-03-17 08:41:52 -07:00
6ef358b172 feat: initialize base 2025-03-17 22:35:37 +08:00
838c1af448 Add file option to secret set CLI command to retrieve secrets from .env or .yaml files 2025-03-17 09:51:21 -03:00
8de7261c9a Update docs 2025-03-16 19:46:21 -04:00
67b1b79fe3 Merge pull request #3253 from Infisical/daniel/bump-helm
chore: bump helm
2025-03-17 00:28:19 +04:00
31477f4d2b chore: bump helm 2025-03-17 00:21:35 +04:00
f200372d74 Merge pull request #3252 from Infisical/daniel/patch-k8s-install
fix: k8s installation failing
2025-03-17 00:11:18 +04:00
e53439d586 Improvements on dynamic secrets overview 2025-03-14 23:18:57 -03:00
cc7d0d752f improvement: improve tooltip description 2025-03-14 15:30:31 -07:00
b89212a0c9 improvement: improve property description 2025-03-14 15:27:47 -07:00
d4c69d8e5d feature: disable secret deletion sync option 2025-03-14 15:24:21 -07:00
48943b4d78 improvement: refine status check 2025-03-14 11:26:59 -07:00
fd1afc2cbe fix: handle disabled/destroyed values in gcp sync 2025-03-14 11:04:49 -07:00
6905029455 Change overview dynamic secret creation modal to set only one env 2025-03-14 14:59:19 -03:00
=
2ef77c737a feat: added a simple oidc server 2025-03-14 22:11:23 +05:30
=
0f31fa3128 feat: updated form for oidc auth 2025-03-14 22:11:23 +05:30
=
1da5a5f417 feat: completed backend code for oidc permission inject 2025-03-14 22:11:22 +05:30
94d7d2b029 Fix call of onCompleted after all promises are resolved 2025-03-14 12:44:49 -03:00
e39d1a0530 Fix call of onCompleted after all promises are resolved 2025-03-14 12:26:20 -03:00
4c5f3859d6 Add dynamic secrets modal form on secrets overview page 2025-03-14 11:59:13 -03:00
139 changed files with 5133 additions and 742 deletions

8
.gitignore vendored
View File

@ -1,5 +1,3 @@
.direnv/
# backend
node_modules
.env
@ -28,6 +26,8 @@ node_modules
/.pnp
.pnp.js
.env
# testing
coverage
reports
@ -63,12 +63,10 @@ yarn-error.log*
# Editor specific
.vscode/*
**/.idea/*
.idea/*
frontend-build
# cli
.go/
*.tgz
cli/infisical-merge
cli/test/infisical-merge

View File

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

View File

@ -100,6 +100,12 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
declare module "@fastify/request-context" {
interface RequestContextData {
reqId: string;
identityAuthInfo?: {
identityId: string;
oidc?: {
claims: Record<string, string>;
};
};
}
}

View File

@ -85,7 +85,7 @@ export async function up(knex: Knex): Promise<void> {
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
// not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) {

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
if (!hasMappingField) {
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
t.jsonb("claimMetadataMapping");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
if (hasMappingField) {
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
t.dropColumn("claimMetadataMapping");
});
}
}

View File

@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas/models";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds"))) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.specificType("adminIdentityIds", "text[]");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("adminIdentityIds");
});
}
}

View File

@ -26,7 +26,8 @@ export const IdentityOidcAuthsSchema = z.object({
boundSubject: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedCaCertificate: zodBuffer.nullable().optional()
encryptedCaCertificate: zodBuffer.nullable().optional(),
claimMetadataMapping: z.unknown().nullable().optional()
});
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;

View File

@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(),
type: z.string(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),
@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
lastViewedAt: z.date().nullable().optional(),
password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(),
identifier: z.string().nullable().optional()
identifier: z.string().nullable().optional(),
type: z.string().default("share")
});
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@ -25,7 +25,8 @@ export const SuperAdminSchema = z.object({
encryptedSlackClientId: zodBuffer.nullable().optional(),
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
authConsentContent: z.string().nullable().optional(),
pageFrameContent: z.string().nullable().optional()
pageFrameContent: z.string().nullable().optional(),
adminIdentityIds: z.string().array().nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -978,6 +978,7 @@ interface AddIdentityOidcAuthEvent {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
claimMetadataMapping: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@ -1002,6 +1003,7 @@ interface UpdateIdentityOidcAuthEvent {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
claimMetadataMapping?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;

View File

@ -50,7 +50,7 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
export const licenseServiceFactory = ({
@ -142,7 +142,10 @@ export const licenseServiceFactory = ({
try {
if (instanceType === InstanceType.Cloud) {
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
if (cachedPlan) {
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
return JSON.parse(cachedPlan) as TFeatureSet;
}
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
@ -170,6 +173,8 @@ export const licenseServiceFactory = ({
JSON.stringify(onPremFeatures)
);
return onPremFeatures;
} finally {
logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`);
}
return onPremFeatures;
};

View File

@ -131,12 +131,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
}
}
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) => {
const handler = {
get(target: Record<string, string>, prop: string) {
if (!(prop in target)) {
if (!Object.hasOwn(target, prop)) {
// eslint-disable-next-line no-param-reassign
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
}
return target[prop];
}
@ -145,4 +145,4 @@ const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
return new Proxy(obj, handler);
};
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO };

View File

@ -1,5 +1,6 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
@ -22,7 +23,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
import {
TBuildOrgPermissionDTO,
TBuildProjectPermissionDTO,
@ -243,20 +244,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const templateValue = {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
};
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);
@ -317,21 +320,26 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
const unescapedMetadata = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const identityAuthInfo =
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
: {};
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
const templateValue = {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair,
auth: identityAuthInfo
};
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);
@ -424,20 +432,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const templateValue = {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
};
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);
@ -469,21 +479,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const templateValue = {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
};
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);

View File

@ -329,6 +329,7 @@ export const OIDC_AUTH = {
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
boundAudiences: "The list of intended recipients.",
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
boundSubject: "The expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.",
@ -342,6 +343,7 @@ export const OIDC_AUTH = {
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
boundAudiences: "The new list of intended recipients.",
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
boundSubject: "The new expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
@ -1725,7 +1727,8 @@ export const SecretSyncs = {
SYNC_OPTIONS: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
};
},
ADDITIONAL_SYNC_OPTIONS: {

View File

@ -0,0 +1,34 @@
/**
* Safely retrieves a value from a nested object using dot notation path
*/
export const getStringValueByDot = (
obj: Record<string, unknown> | null | undefined,
path: string,
defaultValue?: string
): string | undefined => {
// Handle null or undefined input
if (!obj) {
return defaultValue;
}
const parts = path.split(".");
let current: unknown = obj;
for (const part of parts) {
const isObject = typeof current === "object" && !Array.isArray(current) && current !== null;
if (!isObject) {
return defaultValue;
}
if (!Object.hasOwn(current as object, part)) {
// Check if the property exists as an own property
return defaultValue;
}
current = (current as Record<string, unknown>)[part];
}
if (typeof current !== "string") {
return defaultValue;
}
return current;
};

View File

@ -1,3 +1,4 @@
import { requestContext } from "@fastify/request-context";
import { FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import jwt, { JwtPayload } from "jsonwebtoken";
@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export type TAuthMode =
| {
@ -43,6 +45,7 @@ export type TAuthMode =
identityName: string;
orgId: string;
authMethod: null;
isInstanceAdmin?: boolean;
}
| {
authMode: AuthMode.SCIM_TOKEN;
@ -129,14 +132,22 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
const serverCfg = await getServerCfg();
req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor,
orgId: identity.orgId,
identityId: identity.identityId,
identityName: identity.name,
authMethod: null
authMethod: null,
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId)
};
if (token?.identityAuth?.oidc) {
requestContext.set("identityAuthInfo", {
identityId: identity.identityId,
oidc: token?.identityAuth?.oidc
});
}
break;
}
case AuthMode.SERVICE_TOKEN: {

View File

@ -1,16 +1,18 @@
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
import { ForbiddenRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const verifySuperAdmin = <T extends FastifyRequest>(
req: T,
_res: FastifyReply,
done: HookHandlerDoneFunction
) => {
if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin)
throw new ForbiddenRequestError({
message: "Requires elevated super admin privileges"
});
done();
if (isSuperAdmin(req.auth)) {
return done();
}
throw new ForbiddenRequestError({
message: "Requires elevated super admin privileges"
});
};

View File

@ -1,5 +1,6 @@
import { CronJob } from "cron";
import { Knex } from "knex";
import { monitorEventLoopDelay } from "perf_hooks";
import { z } from "zod";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
@ -96,6 +97,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, TEnvConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
@ -246,6 +248,9 @@ import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
export const registerRoutes = async (
server: FastifyZodProvider,
{
@ -637,6 +642,9 @@ export const registerRoutes = async (
userDAL,
identityDAL,
userAliasDAL,
identityTokenAuthDAL,
identityAccessTokenDAL,
identityOrgMembershipDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,
@ -1627,6 +1635,18 @@ export const registerRoutes = async (
const cfg = getConfig();
const serverCfg = await getServerCfg();
const meanLagMs = histogram.mean / 1e6;
const maxLagMs = histogram.max / 1e6;
const p99LagMs = histogram.percentile(99) / 1e6;
logger.info(
`Event loop stats - Mean: ${meanLagMs.toFixed(2)}ms, Max: ${maxLagMs.toFixed(2)}ms, p99: ${p99LagMs.toFixed(
2
)}ms`
);
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
// try {
// await db.raw("SELECT NOW()");
// } catch (err) {

View File

@ -98,7 +98,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -139,7 +139,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -171,12 +171,16 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
identities: IdentitiesSchema.pick({
name: true,
id: true
}).array()
})
.extend({
isInstanceAdmin: z.boolean()
})
.array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -206,7 +210,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -240,7 +244,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -265,7 +269,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -293,7 +297,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -316,7 +320,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
@ -394,4 +398,141 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "DELETE",
url: "/identity-management/identities/:identityId/super-admin-access",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identity: IdentitiesSchema.pick({
name: true,
id: true
})
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const identity = await server.services.superAdmin.deleteIdentitySuperAdminAccess(
req.params.identityId,
req.permission.id
);
return {
identity
};
}
});
server.route({
method: "DELETE",
url: "/user-management/users/:userId/admin-access",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
userId: z.string()
}),
response: {
200: z.object({
user: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
})
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const user = await server.services.superAdmin.deleteUserSuperAdminAccess(req.params.userId);
return {
user
};
}
});
server.route({
method: "POST",
url: "/bootstrap",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
email: z.string().email().trim().min(1),
password: z.string().trim().min(1),
organization: z.string().trim().min(1)
}),
response: {
200: z.object({
message: z.string(),
user: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true,
superAdmin: true
}),
organization: OrganizationsSchema.pick({
id: true,
name: true,
slug: true
}),
identity: IdentitiesSchema.pick({
id: true,
name: true
}).extend({
credentials: z.object({
token: z.string()
}) // would just be Token AUTH for now
})
})
}
},
handler: async (req) => {
const { user, organization, machineIdentity } = await server.services.superAdmin.bootstrapInstance({
...req.body,
organizationName: req.body.organization
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.AdminInit,
distinctId: user.user.username ?? "",
properties: {
username: user.user.username,
email: user.user.email ?? "",
lastName: user.user.lastName || "",
firstName: user.user.firstName || ""
}
});
return {
message: "Successfully bootstrapped instance",
user: user.user,
organization,
identity: machineIdentity
};
}
});
};

View File

@ -11,6 +11,7 @@ import {
validateAccountIds,
validatePrincipalArns
} from "@app/services/identity-aws-auth/identity-aws-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@ -130,7 +131,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@ -8,8 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators";
import {} from "../sanitizedSchemas";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@ -127,7 +126,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@ -8,6 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@ -121,7 +122,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@ -12,6 +12,7 @@ import {
validateJwtAuthAudiencesField,
validateJwtBoundClaimsField
} from "@app/services/identity-jwt-auth/identity-jwt-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({
encryptedJwksCaCert: true,
@ -169,7 +170,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({
id: true,
@ -147,7 +148,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@ -11,6 +11,7 @@ import {
validateOidcAuthAudiencesField,
validateOidcBoundClaimsField
} from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
id: true,
@ -23,6 +24,7 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
boundIssuer: true,
boundAudiences: true,
boundClaims: true,
claimMetadataMapping: true,
boundSubject: true,
createdAt: true,
updatedAt: true
@ -104,6 +106,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping).optional(),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
accessTokenTrustedIps: z
.object({
@ -146,7 +149,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@ -161,6 +165,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
@ -200,6 +205,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims),
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject),
accessTokenTrustedIps: z
.object({
@ -258,6 +264,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,

View File

@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
@ -118,6 +119,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.body
});
@ -166,7 +168,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId
id: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({

View File

@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => {
server.route({
@ -74,7 +75,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@ -157,7 +159,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@ -257,7 +260,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
@ -312,6 +316,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.body
});
@ -370,6 +375,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.query
});
@ -421,6 +427,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId,
isActorSuperAdmin: isSuperAdmin(req.auth),
...req.body
});
@ -470,7 +477,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId
tokenId: req.params.tokenId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
return {

View File

@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({
id: true,
@ -142,8 +143,10 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityUniversalAuth.orgId,

View File

@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = {
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string;
identityAuth: {
oidc?: {
claims: Record<string, string>;
};
};
};

View File

@ -16,6 +16,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
import { extractPrincipalArn } from "./identity-aws-auth-fns";
import {
@ -149,8 +150,11 @@ export const identityAwsAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachAwsAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@ -16,6 +16,7 @@ export type TAttachAwsAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAwsAuthDTO = {

View File

@ -14,6 +14,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal";
import { validateAzureIdentity } from "./identity-azure-auth-fns";
import {
@ -122,8 +123,11 @@ export const identityAzureAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachAzureAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@ -14,6 +14,7 @@ export type TAttachAzureAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAzureAuthDTO = {

View File

@ -14,6 +14,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns";
import {
@ -162,8 +163,11 @@ export const identityGcpAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachGcpAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@ -15,6 +15,7 @@ export type TAttachGcpAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateGcpAuthDTO = {

View File

@ -11,6 +11,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { getStringValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@ -18,6 +19,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
import {
@ -177,8 +179,9 @@ export const identityJwtAuthServiceFactory = ({
if (identityJwtAuth.boundClaims) {
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
const value = getStringValueByDot(tokenData, claimKey) || "";
if (!tokenData[claimKey]) {
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
@ -248,8 +251,11 @@ export const identityJwtAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachJwtAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@ -19,6 +19,7 @@ export type TAttachJwtAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateJwtAuthDTO = {

View File

@ -18,6 +18,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
@ -101,7 +102,8 @@ export const identityKubernetesAuthServiceFactory = ({
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
@ -227,8 +229,11 @@ export const identityKubernetesAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachKubernetesAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@ -17,6 +17,7 @@ export type TAttachKubernetesAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateKubernetesAuthDTO = {

View File

@ -12,6 +12,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { getStringValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@ -19,6 +20,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import {
@ -77,7 +79,7 @@ export const identityOidcAuthServiceFactory = ({
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
httpsAgent: requestAgent
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
}
);
const jwksUri = discoveryDoc.jwks_uri;
@ -91,7 +93,7 @@ export const identityOidcAuthServiceFactory = ({
const client = new JwksClient({
jwksUri,
requestAgent
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
});
const { kid } = decodedToken.header;
@ -108,7 +110,6 @@ export const identityOidcAuthServiceFactory = ({
message: `Access denied: ${error.message}`
});
}
throw error;
}
@ -135,10 +136,16 @@ export const identityOidcAuthServiceFactory = ({
if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
const value = getStringValueByDot(tokenData, claimKey) || "";
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
// handle both single and multi-valued claims
if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
) {
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed."
});
@ -146,6 +153,20 @@ export const identityOidcAuthServiceFactory = ({
});
}
const filteredClaims: Record<string, string> = {};
if (identityOidcAuth.claimMetadataMapping) {
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
const value = getStringValueByDot(tokenData, claimKey) || "";
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
filteredClaims[permissionKey] = value;
});
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
@ -167,7 +188,12 @@ export const identityOidcAuthServiceFactory = ({
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
oidc: {
claims: filteredClaims
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
@ -188,6 +214,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@ -196,8 +223,10 @@ export const identityOidcAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachOidcAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@ -254,6 +283,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
@ -274,6 +304,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@ -335,6 +366,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,

View File

@ -7,11 +7,13 @@ export type TAttachOidcAuthDTO = {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
claimMetadataMapping?: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOidcAuthDTO = {
@ -21,6 +23,7 @@ export type TUpdateOidcAuthDTO = {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
claimMetadataMapping?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;

View File

@ -14,6 +14,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal";
import {
TAttachTokenAuthDTO,
@ -59,8 +60,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachTokenAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@ -126,8 +130,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TUpdateTokenAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@ -218,8 +225,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TRevokeTokenAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@ -271,8 +281,11 @@ export const identityTokenAuthServiceFactory = ({
actor,
actorAuthMethod,
actorOrgId,
name
name,
isActorSuperAdmin
}: TCreateTokenAuthTokenDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@ -350,8 +363,11 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TGetTokenAuthTokensDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
@ -386,7 +402,8 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
@ -398,6 +415,8 @@ export const identityTokenAuthServiceFactory = ({
if (!identityMembershipOrg) {
throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` });
}
await validateIdentityUpdateForSuperAdminPrivileges(foundToken.identityId, isActorSuperAdmin);
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({
message: "The identity does not have Token Auth"
@ -446,18 +465,22 @@ export const identityTokenAuthServiceFactory = ({
actorId,
actor,
actorAuthMethod,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
[`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!identityAccessToken)
throw new NotFoundError({
message: `Token with ID ${tokenId} not found or already revoked`
});
await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin);
const identityOrgMembership = await identityOrgMembershipDAL.findOne({
identityId: identityAccessToken.identityId
});

View File

@ -6,6 +6,7 @@ export type TAttachTokenAuthDTO = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthDTO = {
@ -14,6 +15,7 @@ export type TUpdateTokenAuthDTO = {
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthDTO = {
@ -22,24 +24,29 @@ export type TGetTokenAuthDTO = {
export type TRevokeTokenAuthDTO = {
identityId: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TCreateTokenAuthTokenDTO = {
identityId: string;
name?: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthTokensDTO = {
identityId: string;
offset: number;
limit: number;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthTokenDTO = {
tokenId: string;
name?: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeTokenAuthTokenDTO = {
tokenId: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;

View File

@ -17,6 +17,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal";
import { TIdentityUaDALFactory } from "./identity-ua-dal";
import {
@ -150,8 +151,11 @@ export const identityUaServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
isActorSuperAdmin
}: TAttachUaDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });

View File

@ -7,6 +7,7 @@ export type TAttachUaDTO = {
accessTokenNumUsesLimit: number;
clientSecretTrustedIps: { ipAddress: string }[];
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateUaDTO = {

View File

@ -9,6 +9,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { ActorType } from "../auth/auth-type";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
@ -112,8 +113,11 @@ export const identityServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
metadata
metadata,
isActorSuperAdmin
}: TUpdateIdentityDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin);
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
@ -209,7 +213,16 @@ export const identityServiceFactory = ({
return identity;
};
const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => {
const deleteIdentity = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id,
isActorSuperAdmin
}: TDeleteIdentityDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin);
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });

View File

@ -12,10 +12,12 @@ export type TUpdateIdentityDTO = {
role?: string;
name?: string;
metadata?: { key: string; value: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TOrgPermission, "orgId">;
export type TDeleteIdentityDTO = {
id: string;
isActorSuperAdmin?: boolean;
} & Omit<TOrgPermission, "orgId">;
export type TGetIdentityByIdDTO = {

View File

@ -382,6 +382,8 @@ export const AwsParameterStoreSyncFns = {
}
}
if (syncOptions.disableSecretDeletion) return;
const parametersToDelete: AWS.SSM.Parameter[] = [];
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {

View File

@ -396,6 +396,8 @@ export const AwsSecretsManagerSyncFns = {
}
}
if (syncOptions.disableSecretDeletion) return;
for await (const secretKey of Object.keys(awsSecretsRecord)) {
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
try {

View File

@ -136,6 +136,8 @@ export const azureAppConfigurationSyncFactory = ({
}
}
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const key of Object.keys(azureAppConfigSecrets)) {
const azureSecret = azureAppConfigSecrets[key];
if (

View File

@ -189,6 +189,8 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
});
}
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const deleteSecretKey of deleteSecrets.filter(
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret)
)) {

View File

@ -112,6 +112,8 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
accessToken
});
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of databricksSecretKeys) {
if (!(secret.key in secretMap)) {
await deleteDatabricksSecrets({

View File

@ -71,8 +71,16 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} catch (error) {
// when a secret in GCP has no versions, we treat it as if it's a blank value
if (error instanceof AxiosError && error.response?.status === 404) {
// when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value
if (
error instanceof AxiosError &&
(error.response?.status === 404 ||
(error.response?.status === 400 &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.response.data.error.status === "FAILED_PRECONDITION" &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
error.response.data.error.message.match(/(?:disabled|destroyed)/i)))
) {
res[key] = "";
} else {
throw new SecretSyncError({
@ -147,6 +155,9 @@ export const GcpSyncFns = {
for await (const key of Object.keys(gcpSecrets)) {
try {
if (!(key in secretMap) || !secretMap[key].value) {
// eslint-disable-next-line no-continue
if (secretSync.syncOptions.disableSecretDeletion) continue;
// case: delete secret
await request.delete(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,

View File

@ -192,12 +192,6 @@ export const GithubSyncFns = {
const publicKey = await getPublicKey(client, secretSync);
for await (const encryptedSecret of encryptedSecrets) {
if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret);
}
}
await sodium.ready.then(async () => {
for await (const key of Object.keys(secretMap)) {
// convert secret & base64 key to Uint8Array.
@ -224,6 +218,14 @@ export const GithubSyncFns = {
}
}
});
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const encryptedSecret of encryptedSecrets) {
if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret);
}
}
},
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);

View File

@ -196,6 +196,8 @@ export const HumanitecSyncFns = {
}
}
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const humanitecSecret of humanitecSecrets) {
if (!secretMap[humanitecSecret.key]) {
await deleteSecret(secretSync, humanitecSecret);

View File

@ -23,7 +23,8 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
initialSyncBehavior: (canImportSecrets
? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
});
const schema = merge ? baseSchema.merge(merge) : baseSchema;

View File

@ -0,0 +1,30 @@
import { ForbiddenRequestError } from "@app/lib/errors";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { ActorType } from "../auth/auth-type";
import { getServerCfg } from "./super-admin-service";
export const isSuperAdmin = (auth: TAuthMode) => {
if (auth.actor === ActorType.USER && auth.user.superAdmin) {
return true;
}
if (auth.actor === ActorType.IDENTITY && auth.isInstanceAdmin) {
return true;
}
return false;
};
export const validateIdentityUpdateForSuperAdminPrivileges = async (
identityId: string,
isActorSuperAdmin?: boolean
) => {
const serverCfg = await getServerCfg();
if (serverCfg.adminIdentityIds?.includes(identityId) && !isActorSuperAdmin) {
throw new ForbiddenRequestError({
message:
"You are attempting to modify an instance admin identity. This requires elevated instance admin privileges"
});
}
};

View File

@ -1,16 +1,21 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityTokenAuthDALFactory } from "../identity-token-auth/identity-token-auth-dal";
import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
@ -20,10 +25,19 @@ import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
import {
LoginMethod,
TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO,
TAdminGetUsersDTO,
TAdminSignUpDTO
} from "./super-admin-types";
type TSuperAdminServiceFactoryDep = {
identityDAL: Pick<TIdentityDALFactory, "getIdentitiesByFilter">;
identityDAL: TIdentityDALFactory;
identityTokenAuthDAL: TIdentityTokenAuthDALFactory;
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
@ -60,7 +74,10 @@ export const superAdminServiceFactory = ({
keyStore,
kmsRootConfigDAL,
kmsService,
licenseService
licenseService,
identityAccessTokenDAL,
identityTokenAuthDAL,
identityOrgMembershipDAL
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@ -274,6 +291,137 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization };
};
const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => {
const appCfg = getConfig();
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg?.initialized) {
throw new BadRequestError({ message: "Instance has already been set up" });
}
const existingUser = await userDAL.findOne({ email });
if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" });
const userInfo = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
{
firstName: "Admin",
lastName: "User",
username: email,
email,
superAdmin: true,
isGhost: false,
isAccepted: true,
authMethods: [AuthMethod.EMAIL],
isEmailVerified: true
},
tx
);
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password);
const encKeys = await generateUserSrpKeys(email, password);
const userEnc = await userDAL.createUserEncryption(
{
userId: newUser.id,
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
return { user: newUser, enc: userEnc };
});
const initialOrganizationName = organizationName ?? "Admin Org";
const organization = await orgService.createOrganization({
userId: userInfo.user.id,
userEmail: userInfo.user.email,
orgName: initialOrganizationName
});
const { identity, credentials } = await identityDAL.transaction(async (tx) => {
const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx);
await identityOrgMembershipDAL.create(
{
identityId: newIdentity.id,
orgId: organization.id,
role: OrgMembershipRole.Admin
},
tx
);
const tokenAuth = await identityTokenAuthDAL.create(
{
identityId: newIdentity.id,
accessTokenMaxTTL: 0,
accessTokenTTL: 0,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: JSON.stringify([
{
type: "ipv4",
prefix: 0,
ipAddress: "0.0.0.0"
},
{
type: "ipv6",
prefix: 0,
ipAddress: "::"
}
])
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: newIdentity.id,
isAccessTokenRevoked: false,
accessTokenTTL: tokenAuth.accessTokenTTL,
accessTokenMaxTTL: tokenAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit,
name: "Instance Admin Token",
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
tx
);
const generatedAccessToken = jwt.sign(
{
identityId: newIdentity.id,
identityAccessTokenId: newToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET
);
return { identity: newIdentity, auth: tokenAuth, credentials: { token: generatedAccessToken } };
});
await updateServerCfg({ initialized: true, adminIdentityIds: [identity.id] }, userInfo.user.id);
return {
user: userInfo,
organization,
machineIdentity: {
...identity,
credentials
}
};
};
const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({
limit,
@ -289,13 +437,46 @@ export const superAdminServiceFactory = ({
return user;
};
const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
return identityDAL.getIdentitiesByFilter({
const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => {
const identity = await identityDAL.findById(identityId);
if (!identity) {
throw new NotFoundError({ name: "Identity", message: "Identity not found" });
}
const currentAdminIdentityIds = (await getServerCfg()).adminIdentityIds ?? [];
if (!currentAdminIdentityIds?.includes(identityId)) {
throw new BadRequestError({ name: "Identity", message: "Identity does not have super admin access" });
}
await updateServerCfg({ adminIdentityIds: currentAdminIdentityIds.filter((id) => id !== identityId) }, actorId);
return identity;
};
const deleteUserSuperAdminAccess = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) {
throw new NotFoundError({ name: "User", message: "User not found" });
}
const updatedUser = userDAL.updateById(userId, { superAdmin: false });
return updatedUser;
};
const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
const identities = await identityDAL.getIdentitiesByFilter({
limit,
offset,
searchTerm,
sortBy: "name"
});
const serverCfg = await getServerCfg();
return identities.map((identity) => ({
...identity,
isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id))
}));
};
const grantServerAdminAccessToUser = async (userId: string) => {
@ -393,12 +574,15 @@ export const superAdminServiceFactory = ({
initServerCfg,
updateServerCfg,
adminSignUp,
bootstrapInstance,
getUsers,
deleteUser,
getIdentities,
getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser
grantServerAdminAccessToUser,
deleteIdentitySuperAdminAccess,
deleteUserSuperAdminAccess
};
};

View File

@ -16,6 +16,12 @@ export type TAdminSignUpDTO = {
userAgent: string;
};
export type TAdminBootstrapInstanceDTO = {
email: string;
password: string;
organizationName: string;
};
export type TAdminGetUsersDTO = {
offset: number;
limit: number;

View File

@ -600,3 +600,23 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
return nil
}
func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) {
var resBody map[string]interface{}
response, err := httpClient.
R().
SetResult(&resBody).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain))
if err != nil {
return nil, fmt.Errorf("CallBootstrapInstance: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return nil, fmt.Errorf("CallBootstrapInstance: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return resBody, nil
}

View File

@ -2,12 +2,6 @@ package api
import "time"
type Environment struct {
Name string `json:"name"`
Slug string `json:"slug"`
ID string `json:"id"`
}
// Stores info for login one
type LoginOneRequest struct {
Email string `json:"email"`
@ -20,6 +14,7 @@ type LoginOneResponse struct {
}
// Stores info for login two
type LoginTwoRequest struct {
Email string `json:"email"`
ClientProof string `json:"clientProof"`
@ -173,10 +168,9 @@ type Secret struct {
}
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Environments []Environment `json:"environments"`
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
type RawSecret struct {
@ -654,3 +648,10 @@ type ExchangeRelayCertResponseV1 struct {
Certificate string `json:"certificate"`
CertificateChain string `json:"certificateChain"`
}
type BootstrapInstanceRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Organization string `json:"organization"`
Domain string `json:"domain"`
}

View File

@ -0,0 +1,104 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Used to bootstrap your Infisical instance",
DisableFlagsInUseLine: true,
Example: "infisical bootstrap",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
email, _ := cmd.Flags().GetString("email")
if email == "" {
if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok {
email = envEmail
}
}
if email == "" {
log.Error().Msg("email is required")
return
}
password, _ := cmd.Flags().GetString("password")
if password == "" {
if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok {
password = envPassword
}
}
if password == "" {
log.Error().Msg("password is required")
return
}
organization, _ := cmd.Flags().GetString("organization")
if organization == "" {
if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok {
organization = envOrganization
}
}
if organization == "" {
log.Error().Msg("organization is required")
return
}
domain, _ := cmd.Flags().GetString("domain")
if domain == "" {
if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok {
domain = envDomain
}
}
if domain == "" {
log.Error().Msg("domain is required")
return
}
httpClient := resty.New().
SetHeader("Accept", "application/json")
bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{
Domain: util.AppendAPIEndpoint(domain),
Email: email,
Password: password,
Organization: organization,
})
if err != nil {
log.Error().Msgf("Failed to bootstrap instance: %v", err)
return
}
responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
if err != nil {
log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
return
}
fmt.Println(string(responseJSON))
},
}
func init() {
bootstrapCmd.Flags().String("domain", "", "The domain of your self-hosted Infisical instance")
bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin")
bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin")
bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance")
rootCmd.AddCommand(bootstrapCmd)
}

View File

@ -15,9 +15,6 @@ import (
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/go-resty/resty/v2"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
@ -62,11 +59,11 @@ var runCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
environmentSlug, _ := cmd.Flags().GetString("env")
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentSlug = environmentFromWorkspace
environmentName = environmentFromWorkspace
}
}
@ -139,20 +136,8 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
log.Debug().Msgf("Confirming selected environment is valid: %s", environmentSlug)
hasEnvironment, err := confirmProjectHasEnvironment(environmentSlug, projectId, token)
if err != nil {
util.HandleError(err, "Could not confirm project has environment")
}
if !hasEnvironment {
util.HandleError(fmt.Errorf("project does not have environment '%s'", environmentSlug))
}
log.Debug().Msgf("Project '%s' has environment '%s'", projectId, environmentSlug)
request := models.GetAllSecretsParameters{
Environment: environmentSlug,
Environment: environmentName,
WorkspaceId: projectId,
TagSlugs: tagSlugs,
SecretsPath: secretsPath,
@ -323,6 +308,7 @@ func waitForExitCommand(cmd *exec.Cmd) (int, error) {
}
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) {
var cmd *exec.Cmd
var err error
var lastSecretsFetch time.Time
@ -453,53 +439,8 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt
}
}
func confirmProjectHasEnvironment(environmentSlug, projectId string, token *models.TokenDetails) (bool, error) {
var accessToken string
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
accessToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
accessToken = loggedInUserDetails.UserCredentials.JTWToken
}
if projectId == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "Unable to get local project details")
}
projectId = workspaceFile.WorkspaceId
}
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
project, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
return false, err
}
for _, env := range project.Environments {
if env.Slug == environmentSlug {
return true, nil
}
}
return false, nil
}
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
request.InfisicalToken = token.Token
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {

View File

@ -143,7 +143,15 @@ var secretsSetCmd = &cobra.Command{
Short: "Used set secrets",
Use: "set [secrets]",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
Args: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("file") {
if len(args) > 0 {
return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input")
}
return nil
}
return cobra.MinimumNArgs(1)(cmd, args)
},
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
@ -177,13 +185,18 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse secret type")
}
file, err := cmd.Flags().GetString("file")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
var secretOperations []models.SecretSetOperation
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
if projectId == "" {
util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag")
}
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token)
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file)
} else {
if projectId == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
@ -206,7 +219,7 @@ var secretsSetCmd = &cobra.Command{
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
Type: "",
Token: loggedInUserDetails.UserCredentials.JTWToken,
})
}, file)
}
if err != nil {
@ -691,6 +704,7 @@ func init() {
secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth")
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared")
secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.")
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")

View File

@ -17,6 +17,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/zalando/go-keyring"
"gopkg.in/yaml.v3"
)
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) {
@ -232,6 +233,7 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
var secretsToReturn []models.SingleEnvironmentVariable
// var serviceTokenDetails api.GetServiceTokenDetailsResponse
var errorToReturn error
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
@ -563,7 +565,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
}
func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) {
func parseSecrets(fileName string, content string) (map[string]string, error) {
secrets := make(map[string]string)
if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") {
// Handle YAML secrets
var yamlData map[string]interface{}
if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil {
return nil, fmt.Errorf("failed to parse YAML file: %v", err)
}
for key, value := range yamlData {
if strValue, ok := value.(string); ok {
secrets[key] = strValue
} else {
return nil, fmt.Errorf("YAML secret '%s' must be a string", key)
}
}
} else {
// Handle .env files
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Ignore empty lines and comments
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
// Ensure it's a valid key=value pair
splitKeyValue := strings.SplitN(line, "=", 2)
if len(splitKeyValue) != 2 {
return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line)
}
key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1])
// Handle quoted values
if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
(strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
value = value[1 : len(value)-1] // Remove surrounding quotes
}
secrets[key] = value
}
}
return secrets, nil
}
func validateSecretKey(key string) error {
if key == "" {
return errors.New("secret keys cannot be empty")
}
if unicode.IsNumber(rune(key[0])) {
return fmt.Errorf("secret key '%s' cannot start with a number", key)
}
if strings.Contains(key, " ") {
return fmt.Errorf("secret key '%s' cannot contain spaces", key)
}
return nil
}
func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) {
if file != "" {
content, err := os.ReadFile(file)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
PrintErrorMessageAndExit("File does not exist")
}
return nil, fmt.Errorf("unable to process file [err=%v]", err)
}
parsedSecrets, err := parseSecrets(file, string(content))
if err != nil {
PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err))
}
// Step 2: Validate secrets
for key, value := range parsedSecrets {
if err := validateSecretKey(key); err != nil {
PrintErrorMessageAndExit(err.Error())
}
if strings.TrimSpace(value) == "" {
PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key))
}
secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value))
}
if len(secretArgs) == 0 {
PrintErrorMessageAndExit("no valid secrets found in the file")
}
}
if tokenDetails == nil {
return nil, fmt.Errorf("unable to process set secret operations, token details are missing")

View File

@ -76,6 +76,7 @@ func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) {
if err != nil {
t.Fatalf("snapshot failed: %v", err)
}
}
func TestUserAuth_SecretsGetAll(t *testing.T) {

View File

@ -15,15 +15,3 @@ Since Infisical's team is globally distributed, it is hard for us to keep track
## Winter break
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
## Parental leave
At Infisical, we recognize that parental leave is a special and important time, significantly different from a typical vacation. Were proud to offer parental leave to everyone, regardless of gender, and whether youve become a parent through childbirth or adoption.
For team members who have been with Infisical for over a year by the time of your childs birth or adoption, you are eligible for up to 12 weeks of paid parental leave. This leave will be provided in one continuous block to allow you uninterrupted time with your family. If you have been with Infisical for less than a year, we will follow the parental leave provisions required by your local jurisdiction.
While we trust your judgment, parental leave is intended to be a distinct benefit and is not designed to be combined with our unlimited PTO policy. To ensure fairness and balance, we generally discourage combining parental leave with an extended vacation.
When youre ready, please notify Maidul about your plans for parental leave, ideally at least four months in advance. This allows us to support you fully and arrange any necessary logistics, including salary adjustments and statutory paperwork.
Were here to support you as you embark on this exciting new chapter in your life!

View File

@ -0,0 +1,132 @@
---
title: "infisical bootstrap"
description: "Automate the initial setup of a new Infisical instance for headless deployment and infrastructure-as-code workflows"
---
```bash
infisical bootstrap --domain=<domain> --email=<email> --password=<password> --organization=<organization>
```
## Description
The `infisical bootstrap` command is used when deploying Infisical in automated environments where manual UI setup is not feasible. It's ideal for:
- Containerized deployments in Kubernetes or Docker environments
- Infrastructure-as-code pipelines with Terraform or similar tools
- Continuous deployment workflows
- DevOps automation scenarios
The command initializes a fresh Infisical instance by creating an admin user, organization, and instance admin machine identity, enabling subsequent programmatic configuration without human intervention.
<Warning>
This command creates an instance admin machine identity with the highest level
of privileges. The returned token should be treated with the utmost security,
similar to a root credential. Unauthorized access to this token could
compromise your entire Infisical instance.
</Warning>
## Flags
<Accordion title="--domain" defaultOpen="true">
The URL of your Infisical instance. This can be set using the `INFISICAL_API_URL` environment variable.
```bash
# Example
infisical bootstrap --domain=https://your-infisical-instance.com
```
This flag is required.
</Accordion>
<Accordion title="--email">
Email address for the admin user account that will be created. This can be set using the `INFISICAL_ADMIN_EMAIL` environment variable.
```bash
# Example
infisical bootstrap --email=admin@example.com
```
This flag is required.
</Accordion>
<Accordion title="--password">
Password for the admin user account. This can be set using the `INFISICAL_ADMIN_PASSWORD` environment variable.
```bash
# Example
infisical bootstrap --password=your-secure-password
```
This flag is required.
</Accordion>
<Accordion title="--organization">
Name of the organization that will be created within the instance. This can be set using the `INFISICAL_ADMIN_ORGANIZATION` environment variable.
```bash
# Example
infisical bootstrap --organization=your-org-name
```
This flag is required.
</Accordion>
## Response
The command returns a JSON response with details about the created user, organization, and machine identity:
```json
{
"identity": {
"credentials": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
},
"id": "db224792-ed18-4277-9dae-57e752315854",
"name": "Instance Admin Identity"
},
"message": "Successfully bootstrapped instance",
"organization": {
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
"name": "dog",
"slug": "dog-v-e5l"
},
"user": {
"email": "admin@example.com",
"firstName": "Admin",
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
"lastName": "User",
"superAdmin": true,
"username": "admin@example.com"
}
}
```
## Usage with Automation
For automation purposes, you can extract just the machine identity token from the response:
```bash
infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq ".identity.credentials.token"
```
This extracts only the token, which can be captured in a variable or piped to other commands.
## Example: Capture Token in a Variable
```bash
TOKEN=$(infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq -r ".identity.credentials.token")
# Now use the token for further automation
echo "Token has been captured and can be used for authentication"
```
## Notes
- The bootstrap process can only be performed once on a fresh Infisical instance
- All flags are required for the bootstrap process to complete successfully
- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities
- The generated admin user account can be used to log in via the UI if needed

View File

@ -219,6 +219,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
```
</Accordion>
<Accordion title="--file">
Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory.
The file should contain secrets in the following formats:
- `key=value` for `.env` files
- `key: value` for YAML files
Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing.
```bash
# Example
infisical secrets set --file="./.env"
```
</Accordion>
</Accordion>
<Accordion title="infisical secrets delete">

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

View File

@ -0,0 +1,68 @@
---
title: "Machine identities"
description: "Learn how to set metadata and leverage authentication attributes for machine identities."
---
Machine identities can have metadata set manually, just like users. In addition, during the machine authentication process (e.g., via OIDC), extra attributes called claims—are provided, which can be used in your ABAC policies.
#### Setting Metadata on Machine Identities
<Tabs>
<Tab title="Manually Configure Metadata">
<Steps>
<Step title="Navigate to the Access Control page on the organization sidebar and select a machine identity.">
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png" />
</Step>
<Step title="On the machine identity page, click the pencil icon to edit the selected identity.">
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png" />
</Step>
<Step title="Add metadata via key-value pairs and update the machine identity.">
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png" />
</Step>
</Steps>
</Tab>
</Tabs>
#### Accessing Attributes From Machine Identity Login
When machine identities authenticate, they may receive additional payloads/attributes from the service provider.
For methods like OIDC, these come as claims in the token and can be made available in your policies.
<Tabs>
<Tab title="OIDC Login Attributes">
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
2. In the **Advanced section**, locate the Claim Mapping configuration.
3. Map the OIDC claims to permission attributes by specifying:
- **Attribute Name:** The identifier to be used in your policies (e.g., department).
- **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department).
For example, if your OIDC provider returns:
```json
{
"sub": "machine456",
"name": "Service A",
"user": {
"department": "engineering",
"role": "service"
}
}
```
You might map:
- **department:** to `user.department`
- **role:** to `user.role`
Once configured, these attributes become available in your policies using the following format:
```
{{ identity.auth.oidc.claims.<permission claim name> }}
```
<img src="/images/platform/access-controls/abac-policy-oidc-format.png" />
</Tab>
<Tab title="Other Authentication Method Attributes">
At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
</Tab>
</Tabs>

View File

@ -0,0 +1,39 @@
---
title: "Users identities"
description: "How to set and use metadata attributes on user identities for ABAC."
---
User identities can have metadata attributes assigned directly. These attributes (such as location or department) are used to define dynamic access policies.
#### Setting Metadata on Users
<Tabs>
<Tab title="Manually Configure Metadata">
<Steps>
<Step title="Navigate to the Access Control page on the organization sidebar and select a user.">
<img src="/images/platform/access-controls/add-metadata-step1.png" />
</Step>
<Step title="On the User Page, click the pencil icon to edit the selected user.">
<img src="/images/platform/access-controls/add-metadata-step2.png" />
</Step>
<Step title="Add metadata via key-value pairs and update the user identity.">
<img src="/images/platform/access-controls/add-metadata-step3.png" />
</Step>
</Steps>
</Tab>
<Tab title="Automatically Populate Metadata">
For organizations using SAML for **user logins**, Infisical automatically maps metadata attributes from SAML assertions to user identities on every login. This enables dynamic policies based on the user's SAML attributes.
</Tab>
</Tabs>
#### Applying ABAC Policies with User Metadata
Attribute-based access controls are currently only available for polices defined on Secrets Manager projects.
You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags.
<img src="/images/platform/access-controls/example-abac-1.png" />
In your policies, metadata values are accessed as follows:
- **User ID:** `{{ identity.id }}` (always available)
- **Username:** `{{ identity.username }}` (always available)
- **Metadata Attributes:** `{{ identity.metadata.<metadata-key-name> }}` (available if set)

View File

@ -0,0 +1,15 @@
---
title: "Overview"
description: "Learn the basics of ABAC for both users and machine identities."
---
Infisical's Attribute-based Access Controls (ABAC) enable dynamic, attribute-driven permissions for both users and machine identities. ABAC enforces fine-grained, context-aware access controls using metadata attributes—stored as key-value pairs—either attached to identities or provided during authentication.
<CardGroup cols={2}>
<Card title="Users" icon="square-1" href="./managing-user-metadata">
Manage user metadata manually or automatically via SAML logins.
</Card>
<Card title="Machine Identities" icon="square-2" href="./managing-machine-identity-attributes">
Set metadata manually like users and access additional attributes provided during machine authentication (for example, OIDC claims).
</Card>
</CardGroup>

View File

@ -1,65 +0,0 @@
---
title: "Attribute-based Access Controls"
description: "Learn how to use ABAC to manage permissions based on identity attributes."
---
Infisical's Attribute-based Access Controls (ABAC) allow for dynamic, attribute-driven permissions for both user and machine identities.
ABAC policies use metadata attributes—stored as key-value pairs on identities—to enforce fine-grained permissions that are context aware.
In ABAC, access controls are defined using metadata attributes, such as location or department, which can be set directly on user or machine identities.
During policy execution, these attributes are evaluated, and determine whether said actor can access the requested resource or perform the requested operation.
## Project-level Permissions
Attribute-based access controls are currently available for polices defined on projects. You can set ABAC permissions to control access to environments, folders, secrets, and secret tags.
### Setting Metadata on Identities
<Tabs>
<Tab title="Manually Configure Metadata">
<Steps>
<Step title="Navigate to the Access Control page on the organization sidebar and select an identity (user or machine).">
<img src="/images/platform/access-controls/add-metadata-step1.png" />
</Step>
<Step title="On the Identity Page, click the pencil icon to edit the selected identity.">
<img src="/images/platform/access-controls/add-metadata-step2.png" />
</Step>
<Step title="Add metadata via key-value pairs and update the identity.">
<img src="/images/platform/access-controls/add-metadata-step3.png" />
</Step>
</Steps>
</Tab>
<Tab title="Automatically Populate Metadata">
For organizations using SAML for login, Infisical automatically maps metadata attributes from SAML assertions to user identities.
This makes it easy to create policies that dynamically adapt based on the SAML users attributes.
</Tab>
</Tabs>
## Defining ABAC Policies
<img src="/images/platform/access-controls/example-abac-1.png" />
ABAC policies make use of identity metadata to define dynamic permissions. Each attribute must start and end with double curly-brackets `{{ <attribute-name> }}`.
The following attributes are available within project permissions:
- **User ID**: `{{ identity.id }}`
- **Username**: `{{ identity.username }}`
- **Metadata Attributes**: `{{ identity.metadata.<metadata-key-name> }}`
During policy execution, these placeholders are replaced by their actual values prior to evaluation.
### Example Use Case
#### Location-based Access Control
Suppose you want to restrict access to secrets within a specific folder based on a user's geographic region.
You could assign a `location` attribute to each user (e.g., `identity.metadata.location`).
You could then structure your folders to align with this attribute and define permissions accordingly.
For example, a policy might restrict access to folders matching the user's location attribute in the following pattern:
```
/appA/{{ identity.metadata.location }}
```
Using this structure, users can only access folders that correspond to their configured `location` attribute.
Consequently, if a users attribute changes due to relocation, no policies need to be changed to gain access to the folders associated with their new location.

View File

@ -18,7 +18,7 @@ To make sure that users and machine identities are only accessing the resources
<Card
title="Attribute-based Access Control"
href="./attribute-based-access-controls"
href="/documentation/platform/access-controls/abac"
icon="address-book"
color="#000000"
>

View File

@ -114,6 +114,13 @@ using the Universal Auth authentication method.
that is to exchange the **Client ID** and **Client Secret** of the identity for an access token
by making a request to the `/api/v1/auth/universal-auth/login` endpoint.
<Tip>
Choose the correct base URL based on your region:
- For Infisical Cloud US users: `https://app.infisical.com`
- For Infisical Cloud EU users: `https://eu.infisical.com`
</Tip>
#### Sample request
```bash Request

View File

@ -66,7 +66,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
<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.
Are you using Docker or Kubernetes for your deployment? If you are using Docker or Kubernetes, please follow the instructions in the [Using HSM's in your Deployment](#using-hsms-in-your-deployment) section.
</Warning>
Configuring the HSM on Infisical requires setting a set of environment variables:
@ -94,165 +94,447 @@ For organizations that work with US government agencies, FIPS compliance is almo
</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.
## Using HSMs In Your Deployment
<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`.
<Tab title="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.
```bash
mkdir /etc/luna-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`.
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.
```bash
mkdir /etc/luna-docker
```
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.
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.
```bash
cp -r /<path-to-where-your-luna-client-is-located> /etc/luna-docker
```
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.
</Step>
```bash
cp -r /<path-to-where-your-luna-client-is-located> /etc/luna-docker
```
<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.
</Step>
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.
<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.
An example config file will look like this:
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.
```Chrystoki.conf
Chrystoki2 = {
# This path points to the mounted path, /usr/safenet/lunaclient
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
}
An example config file will look like this:
Luna = {
DefaultTimeOut = 500000;
PEDTimeout1 = 100000;
PEDTimeout2 = 200000;
PEDTimeout3 = 20000;
KeypairGenTimeOut = 2700000;
CloningCommandTimeOut = 300000;
CommandTimeOutPedSet = 720000;
}
```Chrystoki.conf
Chrystoki2 = {
# This path points to the mounted path, /usr/safenet/lunaclient
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
}
CardReader = {
LunaG5Slots = 0;
RemoteCommand = 1;
}
Luna = {
DefaultTimeOut = 500000;
PEDTimeout1 = 100000;
PEDTimeout2 = 200000;
PEDTimeout3 = 20000;
KeypairGenTimeOut = 2700000;
CloningCommandTimeOut = 300000;
CommandTimeOutPedSet = 720000;
}
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;
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;
}
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;
}
```
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>
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.
<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.
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>
<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
```
```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).
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.
</Step>
</Steps>
After following these steps, your Docker setup will be ready to use HSM encryption.
</Tab>
</Tabs>
</Tab>
<Tab title="Kubernetes">
When you are deploying Infisical with the [Kubernetes self-hosting option](/self-hosting/deployment-options/kubernetes-helm), you can still use HSM encryption, but you need to ensure that the HSM client files are present in the container.
<Tabs>
<Tab title="Thales Luna Cloud HSM">
<Note>
This is only supported on helm chart version `1.4.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information.
</Note>
<Steps>
<Step title="Create HSM client folder">
When using Kubernetes, 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 Kubernetes.
```bash
mkdir /etc/hsm-client
```
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-hsm-client-is-located> /etc/hsm-client
```
</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/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers.
An example config file will look like this:
```Chrystoki.conf
Chrystoki2 = {
# This path points to the mounted path, /hsm-client
LibUNIX64 = /hsm-client/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 = /hsm-client/plugins;
MutexFolder = /hsm-client/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 = /hsm-client/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="Creating Persistent Volume Claim (PVC)">
You need to create a Persistent Volume Claim (PVC) to mount the HSM client files to the Infisical deployment.
```bash
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: infisical-data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
EOF
```
The above command will create a PVC named `infisical-data-pvc` with a storage size of `500Mi`. You can change the storage size if needed.
Next we need to create a temporary pod with the PVC mounted as a volume, allowing us to copy the HSM client files into this mounted storage.
```bash
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: hsm-setup-pod
spec:
containers:
- name: setup
image: busybox
command: ["/bin/sh", "-c", "sleep 3600"]
volumeMounts:
- name: hsm-data
mountPath: /data
volumes:
- name: hsm-data
persistentVolumeClaim:
claimName: infisical-data-pvc
EOF
```
The above command will create a pod named `hsm-setup-pod` with a busybox image. The pod will sleep for 3600 seconds _(one hour)_, which is enough time to upload the HSM client files to the PVC.
Ensure that the pod is running and is healthy by running the following command:
```bash
kubectl wait --for=condition=Ready pod/hsm-setup-pod --timeout=60s
```
Next we need to copy the HSM client files into the PVC.
```bash
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
kubectl cp ./hsm-client/ hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
```
Finally, we are ready to delete the temporary pod, as we have successfully uploaded the HSM client files to the PVC. This step may take a few minutes to complete.
```bash
kubectl delete pod hsm-setup-pod
```
</Step>
<Step title="Updating your environment variables">
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
We need to update the secret with the following environment variables:
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
The following is an example of the secret that you should update:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: infisical-secrets
type: Opaque
stringData:
# ... Other environment variables ...
HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
HSM_PIN: "<your-hsm-device-pin>"
HSM_SLOT: "<hsm-device-slot>"
HSM_KEY_LABEL: "<your-key-label>"
```
Save the file after updating the environment variables, and apply the secret changes
```bash
kubectl apply -f ./secret-file-name.yaml
```
</Step>
<Step title="Updating the Deployment">
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption.
```yaml
# ... The rest of the values.yaml file ...
image:
repository: infisical/infisical-fips # Very important: Must use "infisical/infisical-fips"
tag: "v0.117.1-postgres"
pullPolicy: IfNotPresent
extraVolumeMounts:
- name: hsm-data
mountPath: /hsm-client # The path we will mount the HSM client files to
subPath: ./hsm-client
extraVolumes:
- name: hsm-data
persistentVolumeClaim:
claimName: infisical-data-pvc # The PVC we created in the previous step
# ... The rest of the values.yaml file ...
```
</Step>
<Step title="Upgrading the Helm Chart">
After updating the values.yaml file, you need to upgrade the Helm chart in order for the changes to take effect.
```bash
helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml
```
</Step>
<Step title="Restarting the Deployment">
After upgrading the Helm chart, you need to restart the deployment in order for the changes to take effect.
```bash
kubectl rollout restart deployment/infisical-infisical
```
</Step>
</Steps>
After following these steps, your Kubernetes setup will be ready to use HSM encryption.
</Tab>
</Tabs>
</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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

View File

@ -43,8 +43,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
<Note>Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.</Note>
<Note>
Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
![Configure Details](/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png)

View File

@ -46,7 +46,11 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
- **Tags**: Optional tags to add to secrets synced by Infisical.
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.
<Note>
Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Secrets Manager Sync, then click **Next**.
![Configure Details](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png)

View File

@ -6,7 +6,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
**Prerequisites:**
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure App Configuration.
- Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration)
<Note>
The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal
@ -50,6 +50,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
- **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**.
![Configure Details](/images/secret-syncs/azure-app-configuration/app-config-details.png)

View File

@ -6,7 +6,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
**Prerequisites:**
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure Key Vault.
- Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
<Note>
The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal
@ -52,6 +52,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**.
![Configure Details](/images/secret-syncs/azure-key-vault/vault-details.png)

View File

@ -47,6 +47,7 @@ description: "Learn how to configure a Databricks Sync for Infisical."
Databricks does not support importing secrets.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Databricks Sync, then click **Next**.
![Configure Details](/images/secret-syncs/databricks/databricks-details.png)

View File

@ -43,6 +43,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
![Configure Details](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png)

View File

@ -63,6 +63,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
GitHub does not support importing secrets.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your GitHub Sync, then click **Next**.
![Configure Details](/images/secret-syncs/github/github-details.png)

View File

@ -56,6 +56,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical."
Humanitec does not support importing secrets.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Humanitec Sync, then click **Next**.
![Configure Details](/images/secret-syncs/humanitec/humanitec-details.png)

View File

@ -103,7 +103,7 @@ With standby regions and automated failovers in place, Infisical Cloud faces min
Infisical hires external third parties to perform regular security assessment and penetration testing of the platform.
Most recently, Infisical commissioned cybersecurity firm [Oneleet](https://www.oneleet.com) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.).
Most recently, Infisical commissioned cybersecurity firm [Cure53](https://cure53.de/) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.).
Please email security@infisical.com to request any reports including a letter of attestation for the conducted penetration test.

View File

@ -150,7 +150,14 @@
"pages": [
"documentation/platform/access-controls/overview",
"documentation/platform/access-controls/role-based-access-controls",
"documentation/platform/access-controls/attribute-based-access-controls",
{
"group": "Attribute based access controls",
"pages": [
"documentation/platform/access-controls/abac/overview",
"documentation/platform/access-controls/abac/managing-user-metadata",
"documentation/platform/access-controls/abac/managing-machine-identity-attributes"
]
},
"documentation/platform/access-controls/additional-privileges",
"documentation/platform/access-controls/temporary-access",
"documentation/platform/access-controls/access-requests",
@ -311,7 +318,8 @@
"group": "Guides",
"pages": [
"self-hosting/guides/mongo-to-postgres",
"self-hosting/guides/custom-certificates"
"self-hosting/guides/custom-certificates",
"self-hosting/guides/automated-bootstrapping"
]
},
{
@ -341,6 +349,7 @@
"cli/commands/dynamic-secrets",
"cli/commands/ssh",
"cli/commands/gateway",
"cli/commands/bootstrap",
"cli/commands/export",
"cli/commands/token",
"cli/commands/service-token",

View File

@ -0,0 +1,150 @@
---
title: "Programmatic Provisioning"
description: "Learn how to provision and configure Infisical instances programmatically without UI interaction"
---
Infisical's Automated Bootstrapping feature enables you to provision and configure an Infisical instance without using the UI, allowing for complete automation through static configuration files, API calls, or CLI commands. This is especially valuable for enterprise environments where automated deployment and infrastructure-as-code practices are essential.
## Overview
The Automated Bootstrapping workflow automates the following processes:
- Creating an admin user account
- Initializing an organization for the entire instance
- Establishing an **instance admin machine identity** with full administrative permissions
- Returning the machine identity credentials for further automation
## Key Concepts
- **Instance Initialization**: Infisical requires [configuration variables](/self-hosting/configuration/envars) to be set during launch, after which the bootstrap process can be triggered.
- **Instance Admin Machine Identity**: The bootstrapping process creates a machine identity with instance-level admin privileges, which can be used to programmatically manage all aspects of the Infisical instance.
![Instance Admin Identity](/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png)
- **Token Auth**: The instance admin machine identity uses [Token Auth](/documentation/platform/identities/token-auth), providing a JWT token that can be used directly to make authenticated requests to the Infisical API.
## Prerequisites
- An Infisical instance launched with all required configuration variables
- Access to the Infisical CLI or the ability to make API calls to the instance
- Network connectivity to the Infisical instance
## Bootstrap Methods
You can bootstrap an Infisical instance using either the API or the CLI.
<Tabs>
<Tab title="Using the API">
Make a POST request to the bootstrap endpoint:
```
POST: http://your-infisical-instance.com/api/v1/admin/bootstrap
{
"email": "admin@example.com",
"password": "your-secure-password",
"organization": "your-org-name"
}
```
Example using curl:
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"your-secure-password","organization":"your-org-name"}' \
http://your-infisical-instance.com/api/v1/admin/bootstrap
```
</Tab>
<Tab title="Using the CLI">
Use the [Infisical CLI](/cli/commands/bootstrap) to bootstrap the instance and extract the token for immediate use in automation:
```bash
infisical bootstrap --domain="http://localhost:8080" --email="admin@example.com" --password="your-secure-password" --organization="your-org-name" | jq ".identity.credentials.token"
```
This example command pipes the output through `jq` to extract only the machine identity token, making it easy to capture and use directly in automation scripts or export as an environment variable for tools like Terraform.
</Tab>
</Tabs>
## API Response Structure
The bootstrap process returns a JSON response with details about the created user, organization, and machine identity:
```json
{
"identity": {
"credentials": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
},
"id": "db224792-ed18-4277-9dae-57e752315854",
"name": "Instance Admin Identity"
},
"message": "Successfully bootstrapped instance",
"organization": {
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
"name": "dog",
"slug": "dog-v-e5l"
},
"user": {
"email": "admin@example.com",
"firstName": "Admin",
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
"lastName": "User",
"superAdmin": true,
"username": "admin@example.com"
}
}
```
## Using the Instance Admin Machine Identity Token
The bootstrap process automatically creates a machine identity with Token Auth configured. The returned token has instance-level admin privileges (the highest level of access) and should be treated with the same security considerations as a root credential.
The token enables full programmatic control of your Infisical instance and can be used in the following ways:
### 1. Infrastructure Automation
Store the token securely for use with infrastructure automation tools. Due to the sensitive nature of this token, ensure it's protected using appropriate secret management practices:
#### Kubernetes Secret (with appropriate RBAC restrictions)
```yaml
apiVersion: v1
kind: Secret
metadata:
name: infisical-admin-credentials
type: Opaque
data:
token: <base64-encoded-token>
```
#### Environment Variable for Terraform
```bash
export INFISICAL_TOKEN=your-access-token
terraform apply
```
### 2. Programmatic Resource Management
Use the token to authenticate API calls for creating and managing Infisical resources. The token works exactly like any other Token Auth access token in the Infisical API:
```bash
curl -X POST \
-H "Authorization: Bearer ${INFISICAL_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"projectName": "New Project",
"projectDescription": "A project created via API",
"slug": "new-project-slug",
"template": "default",
"type": "SECRET_MANAGER"
}' \
https://your-infisical-instance.com/api/v2/projects
```
## Important Notes
- **Security Warning**: The instance admin machine identity has the highest level of privileges in your Infisical deployment. The token should be treated with the utmost security and handled like a root credential. Unauthorized access to this token could compromise your entire Infisical instance.
- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities
- The instance admin permission of the generated identity can be revoked later in the server admin panel if needed
- The generated admin user account can still be used for UI access if needed, or can be removed if you prefer to manage everything through the machine identity
- This process is designed to work with future Crossplane providers and the existing Terraform provider for full infrastructure-as-code capabilities
- All necessary configuration variables should be set during the initial launch of the Infisical instance

View File

@ -14,21 +14,11 @@
git
lazygit
go
python312Full
nodejs_20
nodePackages.prettier
infisical
];
env = {
GOROOT = "${pkgs.go}/share/go";
};
shellHook = ''
export GOPATH="$(pwd)/.go"
mkdir -p "$GOPATH"
'';
};
};
}

View File

@ -1,9 +1,9 @@
import { ReactNode } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Select, SelectItem } from "@app/components/v2";
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
@ -116,6 +116,44 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
</>
)}
{AdditionalSyncOptionsFieldsComponent}
<Controller
control={control}
name="syncOptions.disableSecretDeletion"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-sync-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p className="w-[11rem]">
Disable Secret Deletion{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, Infisical will <span className="font-semibold">not</span>{" "}
remove secrets from the destination during a sync.
</p>
<p className="mt-4">
Enable this option if you intend to manage some secrets manually outside
of Infisical.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
);
}}
/>
{/* <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl

View File

@ -36,6 +36,7 @@ export const SecretSyncReviewFields = () => {
secretPath,
syncOptions: {
// appendSuffix, prependPrefix,
disableSecretDeletion,
initialSyncBehavior
},
destination,
@ -111,6 +112,11 @@ export const SecretSyncReviewFields = () => {
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
{AdditionalSyncOptionsFieldsComponent}
{disableSecretDeletion && (
<SecretSyncLabel label="Secret Deletion">
<Badge variant="primary">Disabled</Badge>
</SecretSyncLabel>
)}
</div>
</div>
<div className="flex flex-col gap-3">

View File

@ -7,7 +7,8 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
additionalSyncOptions?: T
) => {
const baseSyncOptionsSchema = z.object({
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
disableSecretDeletion: z.boolean().optional().default(false)
// scott: removed temporarily for evaluation of template formatting
// prependPrefix: z
// .string()

View File

@ -1,7 +1,9 @@
export {
useAdminDeleteUser,
useAdminGrantServerAdminAccess,
useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser,
useRemoveUserServerAdminAccess,
useUpdateAdminSlackConfig,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy

View File

@ -70,6 +70,40 @@ export const useAdminDeleteUser = () => {
});
};
export const useAdminRemoveIdentitySuperAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (identityId: string) => {
await apiRequest.delete(
`/api/v1/admin/identity-management/identities/${identityId}/super-admin-access`
);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getIdentities]
});
}
});
};
export const useRemoveUserServerAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userId: string) => {
await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}/admin-access`);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
}
});
};
export const useAdminGrantServerAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({

View File

@ -1,6 +1,7 @@
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types";
import {
@ -10,7 +11,6 @@ import {
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
import { Identity } from "@app/hooks/api/identities/types";
export const adminStandaloneKeys = {
getUsers: "get-users",

Some files were not shown because too many files have changed in this diff Show More