Merge remote-tracking branch 'origin/main' into feat/pki-ENG-3666

This commit is contained in:
Carlos Monastyrski
2025-09-22 15:52:41 -03:00
128 changed files with 5873 additions and 2385 deletions

View File

@@ -63,6 +63,8 @@ jobs:
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
DD_GIT_REPOSITORY_URL=${{ github.server_url }}/${{ github.repository }}
DD_GIT_COMMIT_SHA=${{ github.sha }}
infisical-fips-standalone:
name: Build infisical standalone image postgres

View File

@@ -35,7 +35,7 @@ jobs:
run: kubectl create namespace infisical-gateway
- name: Create gateway secret
run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token -n infisical-gateway
run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token --from-literal=INFISICAL_RELAY_NAME=my-test-relay -n infisical-gateway
- name: Run chart-testing (install)
run: |

View File

@@ -173,6 +173,12 @@ COPY --from=frontend-runner /app ./backend/frontend-build
ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG DD_GIT_REPOSITORY_URL
ENV DD_GIT_REPOSITORY_URL $DD_GIT_REPOSITORY_URL
ARG DD_GIT_COMMIT_SHA
ENV DD_GIT_COMMIT_SHA $DD_GIT_COMMIT_SHA
ENV PORT 8080
ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false

View File

@@ -0,0 +1,31 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
if (hasAllowedNames) t.string("allowedNames", 1000).notNullable().alter();
if (hasAllowedNamespaces) t.string("allowedNamespaces", 1000).notNullable().alter();
if (hasAllowedAudience) t.string("allowedAudience", 1000).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
if (hasAllowedNames) t.string("allowedNames", 255).notNullable().alter();
if (hasAllowedNamespaces) t.string("allowedNamespaces", 255).notNullable().alter();
if (hasAllowedAudience) t.string("allowedAudience", 255).notNullable().alter();
});
}
}

View File

@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema
@@ -50,6 +51,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const stringifiedPermissions = JSON.stringify(
packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))
);
const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@@ -61,7 +66,23 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
},
data: {
...req.body,
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
@@ -106,6 +127,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const stringifiedPermissions = req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined;
const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@@ -114,11 +139,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined
permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role };
}
});
@@ -155,6 +195,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
actor: req.permission.type,
roleId: req.params.roleId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role };
}
});

View File

@@ -1,6 +1,7 @@
import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -42,6 +43,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod,
req.permission.orgId
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_ORG_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: JSON.stringify(req.body.permissions)
}
}
});
return { role };
}
});
@@ -116,6 +133,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod,
req.permission.orgId
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_ORG_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
}
}
});
return { role };
}
});
@@ -146,6 +179,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod,
req.permission.orgId
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_ORG_ROLE,
metadata: { roleId: role.id, slug: role.slug, name: role.name }
}
});
return { role };
}
});

View File

@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
@@ -52,6 +53,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@@ -63,9 +66,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
},
data: {
...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role };
}
});
@@ -112,6 +132,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@@ -120,9 +141,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role };
}
});
@@ -161,6 +199,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
roleId: req.params.roleId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role };
}
});

View File

@@ -1,9 +1,10 @@
import { z } from "zod";
import { RelaysSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -89,14 +90,59 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
throw new BadRequestError({
message: "Org relay registration is not yet supported"
});
return server.services.relay.registerRelay({
...req.body,
identityId: req.permission.id,
orgId: req.permission.orgId
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
}
});
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: RelaysSchema.array()
}
},
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.getRelays({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: RelaysSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.deleteRelay({
id: req.params.id,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});

View File

@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
@@ -52,6 +53,8 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@@ -63,9 +66,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
},
data: {
...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role };
}
});
@@ -112,6 +132,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@@ -120,9 +141,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role };
}
});
@@ -161,6 +199,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
actor: req.permission.type,
roleId: req.params.roleId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role };
}
});

View File

@@ -777,6 +777,20 @@ export const accessApprovalRequestServiceFactory = ({
.map((appUser) => appUser.email)
.filter((email): email is string => !!email);
const approvalPath = `/projects/secret-management/${project.id}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
await notificationService.createUserNotifications(
approverUsersForEmail.map((approver) => ({
userId: approver.id,
orgId: actorOrgId,
type: NotificationType.ACCESS_POLICY_BYPASSED,
title: "Secret Access Policy Bypassed",
body: `**${actingUser.firstName} ${actingUser.lastName}** (${actingUser.email}) has accessed a secret in **${policy.secretPath || "/"}** in the **${environment?.name || permissionEnvironment}** environment for project **${project.name}** without obtaining the required approval.`,
link: approvalPath
}))
);
if (recipientEmails.length > 0) {
await smtpService.sendMail({
recipients: recipientEmails,
@@ -788,7 +802,7 @@ export const accessApprovalRequestServiceFactory = ({
bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/",
environment: environment?.name || permissionEnvironment,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
approvalUrl,
requestType: "access"
},
template: SmtpTemplates.AccessSecretRequestBypassed

View File

@@ -486,9 +486,21 @@ export enum EventType {
UPDATE_PROJECT = "update-project",
DELETE_PROJECT = "delete-project",
CREATE_PROJECT_ROLE = "create-project-role",
UPDATE_PROJECT_ROLE = "update-project-role",
DELETE_PROJECT_ROLE = "delete-project-role",
CREATE_ORG_ROLE = "create-org-role",
UPDATE_ORG_ROLE = "update-org-role",
DELETE_ORG_ROLE = "delete-org-role",
CREATE_SECRET_REMINDER = "create-secret-reminder",
GET_SECRET_REMINDER = "get-secret-reminder",
DELETE_SECRET_REMINDER = "delete-secret-reminder"
DELETE_SECRET_REMINDER = "delete-secret-reminder",
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value"
}
export const filterableSecretEvents: EventType[] = [
@@ -599,6 +611,7 @@ interface CreateSecretEvent {
secretKey: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
secretTags?: string[];
};
}
@@ -613,6 +626,7 @@ interface CreateSecretBatchEvent {
secretPath?: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
secretTags?: string[];
}>;
};
}
@@ -626,6 +640,7 @@ interface UpdateSecretEvent {
secretKey: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
secretTags?: string[];
};
}
@@ -640,6 +655,7 @@ interface UpdateSecretBatchEvent {
secretVersion: number;
secretMetadata?: TSecretMetadata;
secretPath?: string;
secretTags?: string[];
}>;
};
}
@@ -3581,6 +3597,96 @@ interface ProjectDeleteEvent {
};
}
interface DashboardListSecretsEvent {
type: EventType.DASHBOARD_LIST_SECRETS;
metadata: {
environment: string;
secretPath: string;
numberOfSecrets: number;
secretIds: string[];
};
}
interface DashboardGetSecretValueEvent {
type: EventType.DASHBOARD_GET_SECRET_VALUE;
metadata: {
secretId: string;
secretKey: string;
environment: string;
secretPath: string;
};
}
interface DashboardGetSecretVersionValueEvent {
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE;
metadata: {
secretId: string;
version: string;
};
}
interface ProjectRoleCreateEvent {
type: EventType.CREATE_PROJECT_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
description?: string | null;
permissions: string;
};
}
interface ProjectRoleUpdateEvent {
type: EventType.UPDATE_PROJECT_ROLE;
metadata: {
roleId: string;
slug?: string;
name?: string;
description?: string | null;
permissions?: string;
};
}
interface ProjectRoleDeleteEvent {
type: EventType.DELETE_PROJECT_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
};
}
interface OrgRoleCreateEvent {
type: EventType.CREATE_ORG_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
description?: string | null;
permissions: string;
};
}
interface OrgRoleUpdateEvent {
type: EventType.UPDATE_ORG_ROLE;
metadata: {
roleId: string;
slug?: string;
name?: string;
description?: string | null;
permissions?: string;
};
}
interface OrgRoleDeleteEvent {
type: EventType.DELETE_ORG_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -3905,4 +4011,13 @@ export type Event =
| ProjectDeleteEvent
| SecretReminderCreateEvent
| SecretReminderGetEvent
| SecretReminderDeleteEvent;
| SecretReminderDeleteEvent
| DashboardListSecretsEvent
| DashboardGetSecretValueEvent
| DashboardGetSecretVersionValueEvent
| ProjectRoleCreateEvent
| ProjectRoleUpdateEvent
| ProjectRoleDeleteEvent
| OrgRoleCreateEvent
| OrgRoleUpdateEvent
| OrgRoleDeleteEvent;

View File

@@ -16,7 +16,7 @@ import {
PutUserPolicyCommand,
RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { AssumeRoleCommand, GetSessionTokenCommand, STSClient } from "@aws-sdk/client-sts";
import { z } from "zod";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
@@ -26,9 +26,12 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
import { AwsIamAuthType, AwsIamCredentialType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
// AWS STS duration constants (in seconds)
const AWS_STS_MIN_DURATION = 900;
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername;
@@ -120,6 +123,58 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs);
try {
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
if (providerInputs.method === AwsIamAuthType.AccessKey) {
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: providerInputs.accessKey,
secretAccessKey: providerInputs.secretAccessKey
}
});
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
return true;
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
const appCfg = getConfig();
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
? {
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
}
: undefined
});
await stsClient.send(
new AssumeRoleCommand({
RoleArn: providerInputs.roleArn,
RoleSessionName: `infisical-validation-${crypto.nativeCrypto.randomUUID()}`,
DurationSeconds: AWS_STS_MIN_DURATION,
ExternalId: projectId
})
);
return true;
}
if (providerInputs.method === AwsIamAuthType.IRSA) {
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher
});
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
return true;
}
}
const client = await $getClient(providerInputs, projectId);
const isConnected = await client
.send(new GetUserCommand({}))
@@ -137,7 +192,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
});
return isConnected;
} catch (err) {
const sensitiveTokens = [];
const sensitiveTokens: string[] = [];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
@@ -163,102 +218,269 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
};
metadata: { projectId: string };
}) => {
const { inputs, usernameTemplate, metadata, identity } = data;
const { inputs, usernameTemplate, metadata, identity, expireAt } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs, metadata.projectId);
const username = generateUsername(usernameTemplate, identity);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
try {
let stsClient: STSClient;
let entityId: string;
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
const additionalTags = providerInputs.tags.map((tag) => ({
Key: tag.key,
Value: tag.value
}));
awsTags.push(...additionalTags);
const currentTime = Date.now();
const requestedDuration = Math.floor((expireAt - currentTime) / 1000);
if (requestedDuration <= 0) {
throw new BadRequestError({ message: "Expiration time must be in the future" });
}
let durationSeconds: number;
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
durationSeconds = requestedDuration;
const appCfg = getConfig();
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
? {
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
}
: undefined
});
const assumeRoleRes = await stsClient.send(
new AssumeRoleCommand({
RoleArn: providerInputs.roleArn,
RoleSessionName: `infisical-temp-cred-${crypto.nativeCrypto.randomUUID()}`,
DurationSeconds: durationSeconds,
ExternalId: metadata.projectId
})
);
if (
!assumeRoleRes.Credentials?.AccessKeyId ||
!assumeRoleRes.Credentials?.SecretAccessKey ||
!assumeRoleRes.Credentials?.SessionToken
) {
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
}
entityId = `assume-role-${alphaNumericNanoId(8)}`;
return {
entityId,
data: {
ACCESS_KEY: assumeRoleRes.Credentials.AccessKeyId,
SECRET_ACCESS_KEY: assumeRoleRes.Credentials.SecretAccessKey,
SESSION_TOKEN: assumeRoleRes.Credentials.SessionToken
}
};
}
if (providerInputs.method === AwsIamAuthType.AccessKey) {
durationSeconds = requestedDuration;
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: providerInputs.accessKey,
secretAccessKey: providerInputs.secretAccessKey
}
});
const sessionTokenRes = await stsClient.send(
new GetSessionTokenCommand({
DurationSeconds: durationSeconds
})
);
if (
!sessionTokenRes.Credentials?.AccessKeyId ||
!sessionTokenRes.Credentials?.SecretAccessKey ||
!sessionTokenRes.Credentials?.SessionToken
) {
throw new BadRequestError({ message: "Failed to get session token - verify credentials and permissions" });
}
entityId = `session-token-${alphaNumericNanoId(8)}`;
return {
entityId,
data: {
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
}
};
}
if (providerInputs.method === AwsIamAuthType.IRSA) {
durationSeconds = requestedDuration;
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher
});
const sessionTokenRes = await stsClient.send(
new GetSessionTokenCommand({
DurationSeconds: durationSeconds
})
);
if (
!sessionTokenRes.Credentials?.AccessKeyId ||
!sessionTokenRes.Credentials?.SecretAccessKey ||
!sessionTokenRes.Credentials?.SessionToken
) {
throw new BadRequestError({
message: "Failed to get session token - verify IRSA credentials and permissions"
});
}
entityId = `irsa-session-${alphaNumericNanoId(8)}`;
return {
entityId,
data: {
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
}
};
}
throw new BadRequestError({ message: "Unsupported authentication method for temporary credentials" });
} catch (err) {
const sensitiveTokens: string[] = [];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
let errorMessage = (err as Error)?.message || "Unknown error";
if (err && typeof err === "object" && "name" in err && "$metadata" in err) {
const awsError = err as { name?: string; message?: string; $metadata?: object };
if (awsError.name) {
errorMessage = `${awsError.name}: ${errorMessage}`;
}
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create temporary credentials: ${sanitizedErrorMessage}`
});
}
}
try {
const createUserRes = await client.send(
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: awsTags,
UserName: username
})
);
if (providerInputs.credentialType === AwsIamCredentialType.IamUser) {
const client = await $getClient(providerInputs, metadata.projectId);
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
if (userGroups) {
await Promise.all(
userGroups
.split(",")
.filter(Boolean)
.map((group) =>
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
)
);
const username = generateUsername(usernameTemplate, identity);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
const additionalTags = providerInputs.tags.map((tag) => ({
Key: tag.key,
Value: tag.value
}));
awsTags.push(...additionalTags);
}
if (policyArns) {
await Promise.all(
policyArns
.split(",")
.filter(Boolean)
.map((policyArn) =>
client.send(
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
)
)
);
}
if (policyDocument) {
await client.send(
new PutUserPolicyCommand({
UserName: createUserRes.User.UserName,
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
PolicyDocument: policyDocument
try {
const createUserRes = await client.send(
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: awsTags,
UserName: username
})
);
}
const createAccessKeyRes = await client.send(
new CreateAccessKeyCommand({
UserName: createUserRes.User.UserName
})
);
if (!createAccessKeyRes.AccessKey)
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
return {
entityId: username,
data: {
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
USERNAME: username
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
if (userGroups) {
await Promise.all(
userGroups
.split(",")
.filter(Boolean)
.map((group) =>
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
)
);
}
};
} catch (err) {
const sensitiveTokens = [username];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
if (policyArns) {
await Promise.all(
policyArns
.split(",")
.filter(Boolean)
.map((policyArn) =>
client.send(
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
)
)
);
}
if (policyDocument) {
await client.send(
new PutUserPolicyCommand({
UserName: createUserRes.User.UserName,
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
PolicyDocument: policyDocument
})
);
}
const createAccessKeyRes = await client.send(
new CreateAccessKeyCommand({
UserName: createUserRes.User.UserName
})
);
if (!createAccessKeyRes.AccessKey)
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
return {
entityId: username,
data: {
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
USERNAME: username
}
};
} catch (err) {
const sensitiveTokens = [username];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
throw new BadRequestError({ message: "Invalid credential type specified" });
};
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs);
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
return { entityId };
}
const client = await $getClient(providerInputs, metadata.projectId);
const username = entityId;

View File

@@ -32,6 +32,11 @@ export enum AwsIamAuthType {
IRSA = "irsa"
}
export enum AwsIamCredentialType {
IamUser = "iam-user",
TemporaryCredentials = "temporary-credentials"
}
export enum ElasticSearchAuthTypes {
User = "user",
ApiKey = "api-key"
@@ -203,6 +208,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
z.discriminatedUnion("method", [
z.object({
method: z.literal(AwsIamAuthType.AccessKey),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1),
@@ -215,6 +221,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
}),
z.object({
method: z.literal(AwsIamAuthType.AssumeRole),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
roleArn: z.string().trim().min(1, "Role ARN required"),
region: z.string().trim().min(1),
awsPath: z.string().trim().optional(),
@@ -226,6 +233,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
}),
z.object({
method: z.literal(AwsIamAuthType.IRSA),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
region: z.string().trim().min(1),
awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(),

View File

@@ -395,7 +395,8 @@ export const gatewayV2ServiceFactory = ({
relayId: gateway.relayId,
orgId: gateway.orgId,
orgName: gateway.orgName,
gatewayId
gatewayId,
gatewayName: gateway.name
});
return {
@@ -508,7 +509,8 @@ export const gatewayV2ServiceFactory = ({
const relayCredentials = await relayService.getCredentialsForGateway({
relayName,
orgId,
gatewayId: gateway.id
gatewayId: gateway.id,
gatewayName: gateway.name
});
return {

View File

@@ -160,7 +160,10 @@ export const licenseServiceFactory = ({
}
if (isValidOfflineLicense) {
onPremFeatures = contents.license.features;
onPremFeatures = {
...contents.license.features,
slug: "enterprise"
};
instanceType = InstanceType.EnterpriseOnPremOffline;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
isValidLicense = true;

View File

@@ -24,7 +24,7 @@ export type TOfflineLicense = {
export type TFeatureSet = {
_id: null;
slug: null;
slug: string | null;
tier: -1;
workspaceLimit: null;
workspacesUsed: number;

View File

@@ -58,6 +58,13 @@ export enum OrgPermissionGatewayActions {
AttachGateways = "attach-gateways"
}
export enum OrgPermissionRelayActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionIdentityActions {
Read = "read",
Create = "create",
@@ -109,6 +116,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share"
}
@@ -136,6 +144,7 @@ export type OrgPermissionSet =
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionRelayActions, OrgPermissionSubjects.Relay]
| [
OrgPermissionAppConnectionActions,
(
@@ -279,6 +288,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Relay).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionRelayActions).describe(
"Describe what action an entity can take."
)
})
]);
@@ -383,6 +398,11 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
@@ -445,6 +465,10 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,

View File

@@ -754,7 +754,8 @@ export const pitServiceFactory = ({
secrets: newSecrets.map((secret) => ({
secretId: secret.id,
secretKey: secret.secretKey,
secretVersion: secret.version
secretVersion: secret.version,
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
});
@@ -781,7 +782,8 @@ export const pitServiceFactory = ({
secrets: updatedSecrets.map((secret) => ({
secretId: secret.id,
secretKey: secret.secretKey,
secretVersion: secret.version
secretVersion: secret.version,
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
});

View File

@@ -0,0 +1 @@
export const RELAY_CONNECTING_GATEWAY_INFO = "1.3.6.1.4.1.12345.100.3";

View File

@@ -1,9 +1,13 @@
import { isIP } from "node:net";
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { TRelays } from "@app/db/schemas";
import { PgSqlLock } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
@@ -14,11 +18,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionRelayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types";
import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal";
import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal";
import { RELAY_CONNECTING_GATEWAY_INFO } from "./relay-constants";
import { TRelayDALFactory } from "./relay-dal";
export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>;
@@ -29,12 +37,16 @@ export const relayServiceFactory = ({
instanceRelayConfigDAL,
orgRelayConfigDAL,
relayDAL,
kmsService
kmsService,
licenseService,
permissionService
}: {
instanceRelayConfigDAL: TInstanceRelayConfigDALFactory;
orgRelayConfigDAL: TOrgRelayConfigDALFactory;
relayDAL: TRelayDALFactory;
kmsService: TKmsServiceFactory;
licenseService: TLicenseServiceFactory;
permissionService: TPermissionServiceFactory;
}) => {
const $getInstanceCAs = async () => {
const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => {
@@ -639,8 +651,9 @@ export const relayServiceFactory = ({
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
// san
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: host }], false)
new x509.SubjectAlternativeNameExtension([{ type: isIP(host) ? "ip" : "dns", value: host }], false)
];
const relayServerSerialNumber = createSerialNumber();
@@ -689,6 +702,7 @@ export const relayServiceFactory = ({
const $generateRelayClientCredentials = async ({
gatewayId,
gatewayName,
orgId,
orgName,
relayPkiClientCaCertificate,
@@ -697,6 +711,7 @@ export const relayServiceFactory = ({
relayPkiServerCaCertificateChain
}: {
gatewayId: string;
gatewayName: string;
orgId: string;
orgName: string;
relayPkiClientCaCertificate: Buffer;
@@ -727,6 +742,16 @@ export const relayServiceFactory = ({
const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
const clientCertSerialNumber = createSerialNumber();
const connectingGatewayInfoExtension = new x509.Extension(
RELAY_CONNECTING_GATEWAY_INFO,
false,
Buffer.from(
JSON.stringify({
name: gatewayName
})
)
);
// Build standard extensions
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
@@ -740,7 +765,8 @@ export const relayServiceFactory = ({
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
connectingGatewayInfoExtension
];
const clientCert = await x509.X509CertificateGenerator.create({
@@ -768,11 +794,13 @@ export const relayServiceFactory = ({
const getCredentialsForGateway = async ({
relayName,
orgId,
gatewayId
gatewayId,
gatewayName
}: {
relayName: string;
orgId: string;
gatewayId: string;
gatewayName: string;
}) => {
let relay: TRelays | null = await relayDAL.findOne({
orgId,
@@ -819,10 +847,10 @@ export const relayServiceFactory = ({
const relayClientSshCert = await createSshCert({
caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"),
clientPublicKey: relayClientSshPublicKey,
keyId: `relay-client-${relay.id}`,
principals: [gatewayId],
keyId: `client-${relayName}`,
principals: [gatewayId, gatewayName],
certType: SshCertType.USER,
requestedTtl: "30d"
requestedTtl: "1d"
});
return {
@@ -837,12 +865,14 @@ export const relayServiceFactory = ({
relayId,
orgId,
orgName,
gatewayId
gatewayId,
gatewayName
}: {
relayId: string;
orgId: string;
orgName: string;
gatewayId: string;
gatewayName: string;
}) => {
const relay = await relayDAL.findOne({
id: relayId
@@ -860,6 +890,7 @@ export const relayServiceFactory = ({
const instanceCAs = await $getInstanceCAs();
const relayCertificateCredentials = await $generateRelayClientCredentials({
gatewayId,
gatewayName,
orgId,
orgName,
relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate,
@@ -877,6 +908,7 @@ export const relayServiceFactory = ({
const orgCAs = await $getOrgCAs(orgId);
const relayCertificateCredentials = await $generateRelayClientCredentials({
gatewayId,
gatewayName,
orgId,
orgName,
relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate,
@@ -895,11 +927,13 @@ export const relayServiceFactory = ({
host,
name,
identityId,
actorAuthMethod,
orgId
}: {
host: string;
name: string;
identityId?: string;
actorAuthMethod?: ActorAuthMethod;
orgId?: string;
}) => {
let relay: TRelays;
@@ -908,6 +942,27 @@ export const relayServiceFactory = ({
await verifyHostInputValidity(host);
if (isOrgRelay) {
const orgLicensePlan = await licenseService.getPlan(orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message:
"Relay registration failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
});
}
const { permission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityId,
orgId,
actorAuthMethod!,
orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionRelayActions.CreateRelays,
OrgPermissionSubjects.Relay
);
relay = await relayDAL.transaction(async (tx) => {
const existingRelay = await relayDAL.findOne(
{
@@ -995,9 +1050,75 @@ export const relayServiceFactory = ({
});
};
const getRelays = async ({
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
const instanceRelays = await relayDAL.find({
orgId: null
});
const orgRelays = await relayDAL.find({
orgId: actorOrgId
});
return [...instanceRelays, ...orgRelays];
};
const deleteRelay = async ({
id,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
id: string;
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
const relay = await relayDAL.findById(id);
if (!relay || relay.orgId !== actorOrgId || relay.orgId === null) {
throw new NotFoundError({ message: "Relay not found" });
}
const deletedRelay = await relayDAL.deleteById(id);
return deletedRelay;
};
return {
registerRelay,
getCredentialsForGateway,
getCredentialsForClient
getCredentialsForClient,
getRelays,
deleteRelay
};
};

View File

@@ -1,5 +1,7 @@
import { TSecretApprovalRequests } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
@@ -11,6 +13,7 @@ type TSendApprovalEmails = {
smtpService: Pick<TSmtpService, "sendMail">;
projectId: string;
secretApprovalRequest: TSecretApprovalRequests;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export const sendApprovalEmailsFn = async ({
@@ -18,7 +21,8 @@ export const sendApprovalEmailsFn = async ({
projectDAL,
smtpService,
projectId,
secretApprovalRequest
secretApprovalRequest,
notificationService
}: TSendApprovalEmails) => {
const cfg = getConfig();
@@ -26,6 +30,17 @@ export const sendApprovalEmailsFn = async ({
const project = await projectDAL.findProjectWithOrg(projectId);
await notificationService.createUserNotifications(
policy.userApprovers.map((approver) => ({
userId: approver.userId,
orgId: project.orgId,
type: NotificationType.SECRET_CHANGE_REQUEST,
title: "Secret Change Request",
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
link: `/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}`
}))
);
// now we need to go through each of the reviewers and print out all the commits that they need to approve
for await (const reviewerUser of policy.userApprovers) {
await smtpService.sendMail({

View File

@@ -28,6 +28,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -140,6 +142,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -172,7 +175,8 @@ export const secretApprovalRequestServiceFactory = ({
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService,
folderCommitService
folderCommitService,
notificationService
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({
projectId,
@@ -1035,6 +1039,17 @@ export const secretApprovalRequestServiceFactory = ({
}
});
await notificationService.createUserNotifications(
approverUsers.map((approver) => ({
userId: approver.id,
orgId: project.orgId,
type: NotificationType.SECRET_CHANGE_POLICY_BYPASSED,
title: "Secret Change Policy Bypassed",
body: `**${requestedByUser.firstName} ${requestedByUser.lastName}** (${requestedByUser.email}) has merged a secret to **${policy.secretPath}** in the **${env.name}** environment for project **${project.name}** without obtaining the required approval.`,
link: `/projects/secret-management/${project.id}/approval`
}))
);
await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Infisical Secret Change Policy Bypassed",
@@ -1069,7 +1084,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets
secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
}))
}
});
@@ -1085,7 +1102,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets
secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
}
});
}
@@ -1104,7 +1123,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets
secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
}))
}
});
@@ -1120,7 +1141,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets
secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
}
});
}
@@ -1446,7 +1469,8 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
projectId,
notificationService
});
return secretApprovalRequest;
@@ -1813,7 +1837,8 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
projectId,
notificationService
});
return secretApprovalRequest;
};

View File

@@ -17,6 +17,8 @@ import {
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
@@ -28,6 +30,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
smtpService: Pick<TSmtpService, "sendMail">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export const secretRotationV2QueueServiceFactory = async ({
@@ -36,7 +39,8 @@ export const secretRotationV2QueueServiceFactory = async ({
secretRotationV2Service,
projectMembershipDAL,
projectDAL,
smtpService
smtpService,
notificationService
}: TSecretRotationV2QueueServiceFactoryDep) => {
const appCfg = getConfig();
@@ -152,6 +156,19 @@ export const secretRotationV2QueueServiceFactory = async ({
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({
userId: admin.userId,
orgId: project.orgId,
type: NotificationType.SECRET_ROTATION_FAILED,
title: "Secret Rotation Failed",
body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`,
link: rotationPath
}))
);
await smtpService.sendMail({
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
template: SmtpTemplates.SecretRotationFailed,
@@ -165,9 +182,7 @@ export const secretRotationV2QueueServiceFactory = async ({
secretPath: folder.path,
environment: environment.name,
projectName: project.name,
rotationUrl: encodeURI(
`${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}`
)
rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
}
});
} catch (error) {

View File

@@ -21,6 +21,8 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
@@ -52,6 +54,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
@@ -65,7 +68,8 @@ export const secretScanningV2QueueServiceFactory = async ({
kmsService,
auditLogService,
keyStore,
appConnectionDAL
appConnectionDAL,
notificationService
}: TSecretRotationV2QueueServiceFactoryDep) => {
const queueDataSourceFullScan = async (
dataSource: TSecretScanningDataSourceWithConnection,
@@ -592,16 +596,38 @@ export const secretScanningV2QueueServiceFactory = async ({
const timestamp = new Date().toISOString();
const subjectLine =
payload.status === SecretScanningScanStatus.Completed
? "Incident Alert: Secret(s) Leaked"
: `Secret Scanning Failed`;
await notificationService.createUserNotifications(
recipients.map((member) => ({
userId: member.userId,
orgId: project.orgId,
type:
payload.status === SecretScanningScanStatus.Completed
? NotificationType.SECRET_SCANNING_SECRETS_DETECTED
: NotificationType.SECRET_SCANNING_SCAN_FAILED,
title: subjectLine,
body:
payload.status === SecretScanningScanStatus.Completed
? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.`
: `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`,
link:
payload.status === SecretScanningScanStatus.Completed
? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
: `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
}))
);
await smtpService.sendMail({
recipients: recipients.map((member) => member.user.email!).filter(Boolean),
template:
payload.status === SecretScanningScanStatus.Completed
? SmtpTemplates.SecretScanningV2SecretsDetected
: SmtpTemplates.SecretScanningV2ScanFailed,
subjectLine:
payload.status === SecretScanningScanStatus.Completed
? "Incident Alert: Secret(s) Leaked"
: `Secret Scanning Failed`,
subjectLine,
substitutions:
payload.status === SecretScanningScanStatus.Completed
? {

View File

@@ -254,6 +254,8 @@ export type TQueueJobTypes = {
[QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
orgId: string;
actorId: string;
actorEmail: string;
importType: ExternalPlatforms;
data: {

View File

@@ -776,7 +776,8 @@ export const registerRoutes = async (
orgDAL,
totpService,
orgMembershipDAL,
auditLogService
auditLogService,
notificationService
});
const passwordService = authPaswordServiceFactory({
tokenService,
@@ -890,7 +891,8 @@ export const registerRoutes = async (
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
projectMembershipDAL
projectMembershipDAL,
notificationService
});
const rateLimitService = rateLimitServiceFactory({
@@ -929,7 +931,8 @@ export const registerRoutes = async (
projectRoleDAL,
groupProjectDAL,
secretReminderRecipientsDAL,
licenseService
licenseService,
notificationService
});
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
permissionService,
@@ -1096,7 +1099,9 @@ export const registerRoutes = async (
instanceRelayConfigDAL,
orgRelayConfigDAL,
relayDAL,
kmsService
kmsService,
licenseService,
permissionService
});
const gatewayV2Service = gatewayV2ServiceFactory({
@@ -1134,7 +1139,8 @@ export const registerRoutes = async (
appConnectionDAL,
licenseService,
gatewayService,
gatewayV2Service
gatewayV2Service,
notificationService
});
const secretQueueService = secretQueueFactory({
@@ -1218,7 +1224,8 @@ export const registerRoutes = async (
projectTemplateService,
groupProjectDAL,
smtpService,
reminderService
reminderService,
notificationService
});
const projectEnvService = projectEnvServiceFactory({
@@ -1352,7 +1359,8 @@ export const registerRoutes = async (
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService,
folderCommitService
folderCommitService,
notificationService
});
const secretService = secretServiceFactory({
@@ -1803,7 +1811,8 @@ export const registerRoutes = async (
secretV2BridgeService,
resourceMetadataDAL,
folderCommitService,
folderVersionDAL
folderVersionDAL,
notificationService
});
const migrationService = externalMigrationServiceFactory({
@@ -2054,7 +2063,8 @@ export const registerRoutes = async (
queueService,
projectDAL,
projectMembershipDAL,
smtpService
smtpService,
notificationService
});
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
@@ -2066,7 +2076,8 @@ export const registerRoutes = async (
smtpService,
kmsService,
keyStore,
appConnectionDAL
appConnectionDAL,
notificationService
});
const secretScanningV2Service = secretScanningV2ServiceFactory({

View File

@@ -1,16 +1,16 @@
import { ForbiddenError } from "@casl/ability";
import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
import { SecretFoldersSchema, SecretImportsSchema, SecretType, UsersSchema } from "@app/db/schemas";
import { RemindersSchema } from "@app/db/schemas/reminders";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types";
import { secretsLimit } from "@app/server/config/rateLimiter";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -111,6 +111,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
SecretRotationV2Schema,
z.object({
secrets: secretRawSchema
.omit({ secretValue: true })
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
@@ -124,7 +125,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array()
.optional(),
secrets: secretRawSchema
.omit({ secretValue: true })
.extend({
isEmpty: z.boolean(),
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
@@ -219,7 +222,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>>[number] & { isEmpty: boolean })[]
| undefined;
let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined;
@@ -426,43 +431,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({
viewSecretValue: true,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
isInternal: true
});
secrets = (
await server.services.secret.getSecretsRawMultiEnv({
viewSecretValue: true,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
isInternal: true
})
).map((secret) => ({ ...secret, isEmpty: !secret.secretValue }));
}
}
if (secrets?.length || secretRotations?.length) {
for await (const environment of environments) {
const secretCountFromEnv =
(secrets?.filter((secret) => secret.environment === environment).length ?? 0) +
(secretRotations
?.filter((rotation) => rotation.environment.slug === environment)
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
const secretIds = [
...new Set(
[
...(secrets?.filter((secret) => secret.environment === environment) ?? []),
...(secretRotations
?.filter((rotation) => rotation.environment.slug === environment)
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
].map((secret) => secret.id)
)
];
if (secretCountFromEnv) {
if (secretIds) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountFromEnv
numberOfSecrets: secretIds.length,
secretIds
}
}
});
@@ -473,7 +486,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: {
numberOfSecrets: secretCountFromEnv,
numberOfSecrets: secretIds.length,
projectId,
environment,
secretPath,
@@ -584,7 +597,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
viewSecretValue: booleanSchema.default(true),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
@@ -606,7 +618,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
SecretRotationV2Schema,
z.object({
secrets: secretRawSchema
.omit({ secretValue: true })
.extend({
isEmpty: z.boolean(),
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
@@ -619,7 +633,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array()
.optional(),
secrets: secretRawSchema
.omit({ secretValue: true })
.extend({
isEmpty: z.boolean(),
secretReminderRecipients: z
.object({
user: UsersSchema.pick({ id: true, email: true, username: true }),
@@ -715,12 +731,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
isEmpty: boolean;
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
})[]
| undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
| (Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>[number] & {
secrets: (NonNullable<
Awaited<
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
>[number]["secrets"][number] & {
isEmpty: boolean;
}
> | null)[];
})[]
| undefined;
let totalImportCount: number | undefined;
@@ -822,19 +847,31 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
);
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
{
projectId,
search,
orderBy,
orderDirection,
environments: [environment],
secretPath,
limit: remainingLimit,
offset: adjustedOffset
},
req.permission
);
secretRotations = (
await server.services.secretRotationV2.getDashboardSecretRotations(
{
projectId,
search,
orderBy,
orderDirection,
environments: [environment],
secretPath,
limit: remainingLimit,
offset: adjustedOffset
},
req.permission
)
).map((rotation) => ({
...rotation,
secrets: rotation.secrets.map((secret) =>
secret
? {
...secret,
isEmpty: !secret.secretValue
}
: secret
)
}));
await server.services.auditLog.createAuditLog({
projectId,
@@ -919,7 +956,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
viewSecretValue: req.query.viewSecretValue,
viewSecretValue: true,
throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId,
environment,
@@ -943,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets = rawSecrets.map((secret) => ({
...secret,
isEmpty: !secret.secretValue,
reminder: reminders[secret.id] ?? null
}));
}
@@ -977,19 +1015,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}));
if (secrets?.length || secretRotations?.length) {
const secretCount =
(secrets?.length ?? 0) +
(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
const secretIds = [
...new Set(
[
...(secrets ?? []),
...(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
].map((secret) => secret.id)
)
];
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCount
numberOfSecrets: secretIds.length,
secretIds
}
}
});
@@ -1000,7 +1044,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: {
numberOfSecrets: secretCount,
numberOfSecrets: secretIds.length,
projectId,
environment,
secretPath,
@@ -1060,6 +1104,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array()
.optional(),
secrets: secretRawSchema
.omit({ secretValue: true })
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
@@ -1145,18 +1190,20 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
);
for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
const envSecrets = secrets.filter((secret) => secret.environment === environment);
const secretCountForEnv = envSecrets.length;
if (secretCountForEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountForEnv
numberOfSecrets: secretCountForEnv,
secretIds: envSecrets.map((secret) => secret.id)
}
}
});
@@ -1259,6 +1306,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
@@ -1310,6 +1358,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
@@ -1345,11 +1394,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
numberOfSecrets: secrets.length,
secretIds: secrets.map((secret) => secret.id)
}
}
});
@@ -1373,4 +1423,256 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
return { secrets };
}
});
server.route({
method: "GET",
url: "/secret-value",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
secretKey: z.string().trim(),
isOverride: z
.enum(["true", "false"])
.transform((value) => value === "true")
.optional()
}),
response: {
200: z.object({
valueOverride: z.string().optional(),
value: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, environment, secretKey, isOverride } = req.query;
// TODO (scott): just get the secret instead of searching for it in list
const { secrets } = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
viewSecretValue: true,
throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search: secretKey,
includeTagsInSearch: true,
includeMetadataInSearch: true
});
if (isOverride) {
const personalSecret = secrets.find(
(secret) => secret.type === SecretType.Personal && secret.secretKey === secretKey
);
if (!personalSecret)
throw new BadRequestError({
message: `Could not find personal secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
if (personalSecret)
return {
valueOverride: personalSecret.secretValue
};
}
const sharedSecret = secrets.find(
(secret) => secret.type === SecretType.Shared && secret.secretKey === secretKey
);
if (!sharedSecret)
throw new BadRequestError({
message: `Could not find secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
// only audit if not personal
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_GET_SECRET_VALUE,
metadata: {
environment: req.query.environment,
secretPath: req.query.secretPath,
secretKey,
secretId: sharedSecret.id
}
}
});
return { value: sharedSecret.secretValue };
}
});
server.route({
url: "/secret-imports",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: secretRawSchema.omit({ secretValue: true }).extend({ isEmpty: z.boolean() }).array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
projectId: req.query.projectId,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment: req.query.environment,
secretPath: req.query.path,
numberOfSecrets: importedSecrets.length,
secretIds: importedSecrets.map((secret) => secret.id)
}
}
});
return {
secrets: importedSecrets.map((importData) => ({
...importData,
secrets: importData.secrets.map((secret) => ({
...secret,
isEmpty: !secret.secretValue
}))
}))
};
}
});
server.route({
method: "GET",
url: "/secret-versions/:secretId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
secretId: z.string()
}),
querystring: z.object({
offset: z.coerce.number(),
limit: z.coerce.number()
}),
response: {
200: z.object({
secretVersions: secretRawSchema
.omit({ secretValue: true })
.extend({
secretValueHidden: z.boolean()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretVersions = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
limit: req.query.limit,
offset: req.query.offset,
secretId: req.params.secretId
});
return { secretVersions };
}
});
server.route({
method: "GET",
url: "/secret-versions/:secretId/value/:version",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
secretId: z.string(),
version: z.string()
}),
response: {
200: z.object({
value: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { version, secretId } = req.params;
const [secretVersion] = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretId,
secretVersions: [version]
});
if (!secretVersion)
throw new NotFoundError({
message: `Could not find secret version "${version}" for secret with ID "${secretId}`
});
await server.services.auditLog.createAuditLog({
projectId: secretVersion.workspace,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE,
metadata: {
secretId,
version
}
}
});
return { value: secretVersion.secretValue };
}
});
};

View File

@@ -3,8 +3,10 @@ import { z } from "zod";
import { UserNotificationsSchema } from "@app/db/schemas/user-notifications";
import { UnauthorizedError } from "@app/lib/errors";
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 { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerNotificationRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -97,6 +99,16 @@ export const registerNotificationRouter = async (server: FastifyZodProvider) =>
...req.body
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.NotificationUpdated,
distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: {
notificationId: req.params.notificationId,
...req.body
}
});
return { notification };
}
});

View File

@@ -627,7 +627,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretId: secret.id,
secretKey: req.params.secretName,
secretVersion: secret.version,
secretMetadata: req.body.secretMetadata
secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
}
}
});
@@ -780,7 +781,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretId: secret.id,
secretKey: req.params.secretName,
secretVersion: secret.version,
secretMetadata: req.body.secretMetadata
secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
}
}
});
@@ -2154,7 +2156,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretId: secret.id,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
}
@@ -2288,7 +2291,6 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
return { approval: secretOperation.approval };
}
const { secrets } = secretOperation;
const secretMetadataMap = new Map(
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
);
@@ -2308,7 +2310,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
}
@@ -2328,7 +2331,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
}

View File

@@ -478,7 +478,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id,
secretKey: req.params.secretName,
secretVersion: secret.version,
secretMetadata: req.body.secretMetadata
secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
}
}
});
@@ -621,7 +622,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id,
secretKey: req.params.secretName,
secretVersion: secret.version,
secretMetadata: req.body.secretMetadata
secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
}
}
});
@@ -762,7 +764,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
hide: false,
tags: [ApiDocsTags.Secrets],
body: z.object({
projectSlug: z.string().trim(),
projectId: z.string().trim(),
sourceEnvironment: z.string().trim(),
sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
destinationEnvironment: z.string().trim(),
@@ -911,7 +913,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
}
@@ -1063,7 +1066,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
}))
}
}
@@ -1262,7 +1266,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { secretName } = req.params;
const { secretPath, environment, projectId } = req.query;
const { tree, value } = await server.services.secret.getSecretReferenceTree({
const { tree, value, secret } = await server.services.secret.getSecretReferenceTree({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
@@ -1273,6 +1277,21 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment
});
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRET,
metadata: {
environment,
secretPath,
secretId: secret.id,
secretKey: secretName,
secretVersion: secret.version
}
}
});
return { tree, value };
}
});

View File

@@ -266,9 +266,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
appConnection.orgId,
actor.authMethod,
appConnection.orgId
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
@@ -316,9 +316,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
appConnection.orgId,
actor.authMethod,
appConnection.orgId
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
@@ -475,9 +475,9 @@ export const appConnectionServiceFactory = ({
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
appConnection.orgId,
actor.authMethod,
appConnection.orgId
actor.orgId
);
if (appConnection.projectId) {
@@ -633,9 +633,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
appConnection.orgId,
actor.authMethod,
appConnection.orgId
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
@@ -803,9 +803,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
appConnection.orgId,
actor.authMethod,
appConnection.orgId
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(

View File

@@ -14,6 +14,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
import { TOrgDALFactory } from "../org/org-dal";
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
@@ -47,6 +49,7 @@ type TAuthLoginServiceFactoryDep = {
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
orgMembershipDAL: TOrgMembershipDALFactory;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
@@ -57,7 +60,8 @@ export const authLoginServiceFactory = ({
orgDAL,
orgMembershipDAL,
totpService,
auditLogService
auditLogService,
notificationService
}: TAuthLoginServiceFactoryDep) => {
/*
* Private
@@ -71,6 +75,16 @@ export const authLoginServiceFactory = ({
if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]);
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
await notificationService.createUserNotifications([
{
userId: user.id,
type: NotificationType.LOGIN_FROM_NEW_DEVICE,
title: "Login From New Device",
body: `A new device with IP **${ip}** and User Agent **${userAgent}** has logged into your account.`
}
]);
if (user.email) {
await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin,
@@ -563,6 +577,18 @@ export const authLoginServiceFactory = ({
.filter(Boolean) as string[];
if (adminEmails.length > 0) {
await notificationService.createUserNotifications(
orgAdmins
.filter((admin) => admin.user.id !== user.id)
.map((admin) => ({
userId: admin.user.id,
orgId: organizationId,
type: NotificationType.ADMIN_SSO_BYPASS,
title: "Security Alert: Admin SSO Bypass",
body: `The org admin **${user.email}** has bypassed enforced SSO login.`
}))
);
await smtpService.sendMail({
recipients: adminEmails,
subjectLine: "Security Alert: Admin SSO Bypass",

View File

@@ -5,6 +5,8 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -42,6 +44,7 @@ export type TExternalMigrationQueueFactoryDep = {
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
@@ -62,9 +65,12 @@ export const externalMigrationQueueFactory = ({
folderDAL,
folderCommitService,
folderVersionDAL,
resourceMetadataDAL
resourceMetadataDAL,
notificationService
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
orgId: string;
actorId: string;
actorEmail: string;
importType: ExternalPlatforms;
data: {
@@ -87,9 +93,19 @@ export const externalMigrationQueueFactory = ({
};
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
const { data, actorEmail, importType } = job.data;
const { data, actorEmail, importType, actorId, orgId } = job.data;
try {
await notificationService.createUserNotifications([
{
userId: actorId,
orgId,
type: NotificationType.IMPORT_STARTED,
title: "Import Started",
body: `An import from **${importType}** to Infisical has been started.`
}
]);
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import started",
@@ -137,6 +153,16 @@ export const externalMigrationQueueFactory = ({
);
}
await notificationService.createUserNotifications([
{
userId: actorId,
orgId,
type: NotificationType.IMPORT_SUCCESSFUL,
title: "Import Successful",
body: `An import from **${importType}** to Infisical has successfully completed.`
}
]);
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import successful",
@@ -146,6 +172,17 @@ export const externalMigrationQueueFactory = ({
template: SmtpTemplates.ExternalImportSuccessful
});
} catch (err) {
await notificationService.createUserNotifications([
{
userId: actorId,
orgId,
type: NotificationType.IMPORT_FAILED,
title: "Import Failed",
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
body: `An import from **${importType}** to Infisical has failed: ${(err as any)?.message || "Unknown error"}.`
}
]);
await smtpService.sendMail({
recipients: [job.data.actorEmail],
subjectLine: "Infisical import failed",

View File

@@ -73,6 +73,8 @@ export const externalMigrationServiceFactory = ({
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
await externalMigrationQueue.startImport({
actorId: user.id,
orgId: actorOrgId,
actorEmail: user.email!,
importType: ExternalPlatforms.EnvKey,
data: {
@@ -131,6 +133,8 @@ export const externalMigrationServiceFactory = ({
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
await externalMigrationQueue.startImport({
actorId: user.id,
orgId: actorOrgId,
actorEmail: user.email!,
importType: ExternalPlatforms.Vault,
data: {

View File

@@ -84,69 +84,70 @@ export const identityUaServiceFactory = ({
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
if (identityUa.lockoutEnabled) {
try {
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 500, {
retryCount: 3,
retryDelay: 300,
retryJitter: 100
});
} catch (e) {
logger.info(
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
);
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
let lockout: LockoutObject | undefined;
if (lockoutRaw) {
lockout = JSON.parse(lockoutRaw) as LockoutObject;
}
if (lockout && lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
if (!identityMembershipOrg) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
const clientSecretPrefix = clientSecret.slice(0, 4);
const clientSecretInfo = await identityUaClientSecretDAL.find({
identityUAId: identityUa.id,
isClientSecretRevoked: false,
clientSecretPrefix
});
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
for await (const info of clientSecretInfo) {
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
if (isMatch) {
validClientSecretInfo = info;
break;
}
}
try {
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
if (!validClientSecretInfo) {
if (identityUa.lockoutEnabled) {
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
try {
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
retryCount: 3,
retryDelay: 300,
retryJitter: 100
});
let lockout: LockoutObject | undefined;
if (lockoutRaw) {
lockout = JSON.parse(lockoutRaw) as LockoutObject;
}
if (lockout && lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
if (!identityMembershipOrg) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
const clientSecretPrefix = clientSecret.slice(0, 4);
const clientSecretInfo = await identityUaClientSecretDAL.find({
identityUAId: identityUa.id,
isClientSecretRevoked: false,
clientSecretPrefix
});
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
for await (const info of clientSecretInfo) {
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
if (isMatch) {
validClientSecretInfo = info;
break;
}
}
if (!validClientSecretInfo) {
if (identityUa.lockoutEnabled) {
if (!lockout) {
// Re-fetch the latest lockout data while holding the lock
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
if (lockoutRawNew) {
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
} else {
lockout = {
lockedOut: false,
failedAttempts: 0
};
}
if (lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
lockout.failedAttempts += 1;
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
lockout.lockedOut = true;
@@ -157,110 +158,121 @@ export const identityUaServiceFactory = ({
lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
JSON.stringify(lockout)
);
}
throw new UnauthorizedError({ message: "Invalid credentials" });
} else if (lockout) {
await keyStore.deleteItem(LOCKOUT_KEY);
}
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (Number(clientSecretTTL) > 0) {
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
const currentDate = new Date();
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationTime) {
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Access denied due to expired client secret"
});
} catch (e) {
if (lock === undefined) {
logger.info(
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
);
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
}
throw e;
} finally {
if (lock) {
await lock.release();
}
}
}
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
// number of times client secret can be used for
// a login operation reached
throw new UnauthorizedError({ message: "Invalid credentials" });
} else if (lockout) {
// If credentials are valid, clear any existing lockout record
await keyStore.deleteItem(LOCKOUT_KEY);
}
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (Number(clientSecretTTL) > 0) {
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
const currentDate = new Date();
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationTime) {
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Access denied due to client secret usage limit reached"
message: "Access denied due to expired client secret"
});
}
}
const accessTokenTTLParams =
Number(identityUa.accessTokenPeriod) === 0
? {
accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
}
: {
accessTokenTTL: identityUa.accessTokenPeriod,
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
accessTokenMaxTTL: 1000000000
};
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityUa.identityId,
isAccessTokenRevoked: false,
identityUAClientSecretId: uaClientSecretDoc.id,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
accessTokenPeriod: identityUa.accessTokenPeriod,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
...accessTokenTTLParams
},
tx
);
return newToken;
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
// number of times client secret can be used for
// a login operation reached
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Access denied due to client secret usage limit reached"
});
}
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
const accessTokenTTLParams =
Number(identityUa.accessTokenPeriod) === 0
? {
accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
}
: {
accessTokenTTL: identityUa.accessTokenPeriod,
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
accessTokenMaxTTL: 1000000000
};
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityUa.identityId,
clientSecretId: validClientSecretInfo.id,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} 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
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
isAccessTokenRevoked: false,
identityUAClientSecretId: uaClientSecretDoc.id,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
accessTokenPeriod: identityUa.accessTokenPeriod,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
...accessTokenTTLParams
},
tx
);
return {
accessToken,
identityUa,
validClientSecretInfo,
identityAccessToken,
identityMembershipOrg,
...accessTokenTTLParams
};
} finally {
if (lock) await lock.release();
}
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityUa.identityId,
clientSecretId: validClientSecretInfo.id,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} 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
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return {
accessToken,
identityUa,
validClientSecretInfo,
identityAccessToken,
identityMembershipOrg,
...accessTokenTTLParams
};
};
const attachUniversalAuth = async ({

View File

@@ -1,6 +1,21 @@
export enum NotificationType {
ACCESS_APPROVAL_REQUEST = "access-approval-request",
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated"
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated",
ACCESS_POLICY_BYPASSED = "access-policy-bypassed",
SECRET_CHANGE_REQUEST = "secret-change-request",
SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed",
SECRET_ROTATION_FAILED = "secret-rotation-failed",
SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected",
SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed",
LOGIN_FROM_NEW_DEVICE = "login-from-new-device",
ADMIN_SSO_BYPASS = "admin-sso-bypass",
IMPORT_STARTED = "import-started",
IMPORT_SUCCESSFUL = "import-successful",
IMPORT_FAILED = "import-failed",
DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin",
PROJECT_ACCESS_REQUEST = "project-access-request",
PROJECT_INVITATION = "project-invitation",
SECRET_SYNC_FAILED = "secret-sync-failed"
}
export interface TCreateUserNotificationDTO {

View File

@@ -5,6 +5,8 @@ import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
@@ -20,6 +22,7 @@ type TOrgAdminServiceFactoryDep = {
>;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
@@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({
projectDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
smtpService
smtpService,
notificationService
}: TOrgAdminServiceFactoryDep) => {
const listOrgProjects = async ({
actor,
@@ -130,23 +134,34 @@ export const orgAdminServiceFactory = ({
});
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
const filteredProjectMembers = projectMembers
.filter(
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
)
.map((el) => el.user.email!)
.filter(Boolean);
const projectAdmins = projectMembers.filter(
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
);
const mappedProjectAdmins = projectAdmins.map((el) => el.user.email!).filter(Boolean);
const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username;
if (filteredProjectMembers.length) {
await smtpService.sendMail({
template: SmtpTemplates.OrgAdminProjectDirectAccess,
recipients: filteredProjectMembers,
subjectLine: "Organization Admin Project Direct Access Issued",
substitutions: {
projectName: project.name,
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
}
});
if (actorEmail) {
await notificationService.createUserNotifications(
projectAdmins.map((member) => ({
userId: member.userId,
orgId: project.orgId,
type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN,
title: "Direct Project Access Issued",
body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.`
}))
);
if (mappedProjectAdmins.length) {
await smtpService.sendMail({
template: SmtpTemplates.OrgAdminProjectDirectAccess,
recipients: mappedProjectAdmins,
subjectLine: "Organization Admin Project Direct Access Issued",
substitutions: {
projectName: project.name,
email: actorEmail
}
});
}
}
return { isExistingMember: false, membership: updatedMembership };
};

View File

@@ -18,6 +18,8 @@ import { ms } from "@app/lib/ms";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
@@ -56,6 +58,7 @@ type TProjectMembershipServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
groupProjectDAL: TGroupProjectDALFactory;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
@@ -74,7 +77,8 @@ export const projectMembershipServiceFactory = ({
projectDAL,
projectKeyDAL,
secretReminderRecipientsDAL,
licenseService
licenseService,
notificationService
}: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({
actorId,
@@ -236,6 +240,16 @@ export const projectMembershipServiceFactory = ({
});
if (sendEmails) {
await notificationService.createUserNotifications(
orgMembers.map((member) => ({
userId: member.userId,
orgId: project.orgId,
type: NotificationType.PROJECT_INVITATION,
title: "Project Invitation",
body: `You've been invited to join the project **${project.name}**.`
}))
);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,

View File

@@ -57,6 +57,8 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
import { TOrgDALFactory } from "../org/org-dal";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
@@ -183,6 +185,7 @@ type TProjectServiceFactoryDep = {
>;
projectTemplateService: TProjectTemplateServiceFactory;
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -227,7 +230,8 @@ export const projectServiceFactory = ({
projectTemplateService,
groupProjectDAL,
smtpService,
reminderService
reminderService,
notificationService
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -1924,6 +1928,21 @@ export const projectServiceFactory = ({
projectTypeUrl = "cert-management";
}
const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
await notificationService.createUserNotifications(
projectMembers
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
.map((member) => ({
userId: member.userId,
orgId: project.orgId,
type: NotificationType.PROJECT_ACCESS_REQUEST,
title: "Project Access Request",
body: `**${userDetails.firstName} ${userDetails.lastName}** (${userDetails.email}) has requested access to the project **${project.name}**.`,
link: callbackPath
}))
);
await smtpService.sendMail({
template: SmtpTemplates.ProjectAccessRequest,
recipients: filteredProjectMembers,
@@ -1934,7 +1953,7 @@ export const projectServiceFactory = ({
projectName: project?.name,
orgName: org?.name,
note: comment,
callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
callback_url: `${appCfg.SITE_URL}${callbackPath}`
}
});
};

View File

@@ -61,6 +61,8 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
@@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
type SecretSyncActionJob = Job<
@@ -142,7 +145,8 @@ export const secretSyncQueueFactory = ({
folderCommitService,
licenseService,
gatewayService,
gatewayV2Service
gatewayV2Service,
notificationService
}: TSecretSyncQueueFactoryDep) => {
const appCfg = getConfig();
@@ -898,6 +902,19 @@ export const secretSyncQueueFactory = ({
break;
}
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({
userId: admin.userId,
orgId: project.orgId,
type: NotificationType.SECRET_SYNC_FAILED,
title: `Secret Sync Failed to ${actionLabel} Secrets`,
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
link: syncPath
}))
);
await smtpService.sendMail({
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
template: SmtpTemplates.SecretSyncFailed,
@@ -910,7 +927,7 @@ export const secretSyncQueueFactory = ({
secretPath: folder?.path,
environment: environment?.name,
projectName: project.name,
syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
syncUrl: `${appCfg.SITE_URL}${syncPath}`
}
});
};

View File

@@ -446,9 +446,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
}
})
.where((bd) => {
void bd
.whereNull(`${TableName.SecretV2}.userId`)
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
void bd.whereNull(`${TableName.SecretV2}.userId`);
// scott: removing this as we don't need to count overrides
// and there is currently a bug when you move secrets that doesn't move the override so this can skew count
// .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
})
.countDistinct(`${TableName.SecretV2}.key`);

View File

@@ -597,19 +597,27 @@ export const expandSecretReferencesFactory = ({
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
}
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return { value: "", tags: [] };
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
try {
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return { value: "", tags: [] };
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign
prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) };
return prev;
}, {});
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign
prev[secret.key] = {
value: decryptSecret(secret.encryptedValue) || "",
tags: secret.tags?.map((el) => el.slug)
};
return prev;
}, {});
secretCache[cacheKey] = decryptedSecret;
secretCache[cacheKey] = decryptedSecret;
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
} catch (error) {
secretCache[cacheKey] = {};
return { value: "", tags: [] };
}
};
const recursivelyExpandSecret = async (dto: {
@@ -622,11 +630,16 @@ export const expandSecretReferencesFactory = ({
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
if (!dto.value) return { expandedValue: "", stackTrace };
const stack = [{ ...dto, depth: 0, trace: stackTrace }];
// Track visited secrets to prevent circular references
const createSecretId = (env: string, secretPath: string, key: string) => `${env}:${secretPath}:${key}`;
const currentSecretId = createSecretId(dto.environment, dto.secretPath, dto.secretKey);
const stack = [{ ...dto, depth: 0, trace: stackTrace, visitedSecrets: new Set<string>([currentSecretId]) }];
let expandedValue = dto.value;
while (stack.length) {
const { value, secretPath, environment, depth, trace } = stack.pop()!;
const { value, secretPath, environment, depth, trace, visitedSecrets } = stack.pop()!;
// eslint-disable-next-line no-continue
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
@@ -664,6 +677,7 @@ export const expandSecretReferencesFactory = ({
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
if (!secretCache[cacheKey]) secretCache[cacheKey] = {};
secretCache[cacheKey][secretKey] = referredValue;
referencedSecretValue = referredValue.value;
@@ -683,6 +697,7 @@ export const expandSecretReferencesFactory = ({
});
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
if (!secretCache[cacheKey]) secretCache[cacheKey] = {};
secretCache[cacheKey][secretReferenceKey] = referedValue;
referencedSecretValue = referedValue.value;
@@ -700,17 +715,27 @@ export const expandSecretReferencesFactory = ({
trace
};
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue);
// Check for circular reference
const referencedSecretId = createSecretId(
referencedSecretEnvironmentSlug,
referencedSecretPath,
referencedSecretKey
);
const isCircular = visitedSecrets.has(referencedSecretId);
const newVisitedSecrets = new Set([...visitedSecrets, referencedSecretId]);
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue) && !isCircular;
if (dto.shouldStackTrace) {
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
trace?.children.push(stackTraceNode);
// if stack trace this would be child node
if (shouldExpandMore) {
stack.push({ ...node, trace: stackTraceNode });
stack.push({ ...node, trace: stackTraceNode, visitedSecrets: newVisitedSecrets });
}
} else if (shouldExpandMore) {
// if no stack trace is needed we just keep going with root node
stack.push(node);
stack.push({ ...node, visitedSecrets: newVisitedSecrets });
}
if (referencedSecretValue) {

View File

@@ -159,17 +159,14 @@ export const secretV2BridgeServiceFactory = ({
const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment)));
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx);
if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length)
throw new BadRequestError({
message: `Referenced environment not found. Missing ${diff(
uniqueReferenceEnvironmentSlugs,
referencesEnvironments.map((el) => el.slug)
).join(",")}`
});
// Filter out references to non-existent environments
const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug);
const validEnvironmentReferences = references.filter((el) => referencesEnvironmentGroupBySlug[el.environment]);
if (validEnvironmentReferences.length === 0) return;
const referredFolders = await folderDAL.findByManySecretPath(
references.map((el) => ({
validEnvironmentReferences.map((el) => ({
secretPath: el.secretPath,
envId: referencesEnvironmentGroupBySlug[el.environment][0].id
})),
@@ -177,58 +174,71 @@ export const secretV2BridgeServiceFactory = ({
);
const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`);
// Find only references that have valid folders (don't throw for missing paths)
const validReferences = validEnvironmentReferences.filter((el) => {
const folderId =
referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`]?.[0]
?.id;
return folderId;
});
if (validReferences.length === 0) return;
const referredSecrets = await secretDAL.find(
{
$complex: {
operator: "or",
value: references.map((el) => {
const folderId =
referencesFolderGroupByPath[
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
][0]?.id;
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
value: validReferences
.map((el) => {
const folderGroup =
referencesFolderGroupByPath[
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
];
if (!folderGroup || !folderGroup[0]) return null;
return {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: folderId
},
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
};
})
const folderId = folderGroup[0].id;
return {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: folderId
},
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
};
})
.filter((query) => query !== null) as Array<{
operator: "and";
value: Array<{
operator: "eq";
field: "folderId" | "key";
value: string;
}>;
}>
}
},
{ tx }
);
if (
referredSecrets.length !==
new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`))
.size // only count unique references
)
throw new BadRequestError({
message: `Referenced secret(s) not found: ${diff(
references.map((el) => el.secretKey),
referredSecrets.map((el) => el.key)
).join(",")}`
});
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => {
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: el.environment,
secretPath: el.secretPath,
secretName: el.secretKey,
secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug)
});
// Only check permissions for secrets that actually exist
referredSecrets.forEach((secret) => {
const reference = validReferences.find((ref) => ref.secretKey === secret.key);
if (reference) {
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: reference.environment,
secretPath: reference.secretPath,
secretName: reference.secretKey,
secretTags: secret.tags?.map((i) => i.slug)
});
}
});
return referredSecrets;
@@ -478,15 +488,16 @@ export const secretV2BridgeServiceFactory = ({
secret = sharedSecretToModify;
}
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.secretName,
secretTags: secret.tags.map((el) => el.slug)
})
);
if (secret.type !== SecretType.Personal)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.secretName,
secretTags: secret.tags.map((el) => el.slug)
})
);
// validate tags
// fetch all tags and if not same count throw error meaning one was invalid tags
@@ -497,17 +508,18 @@ export const secretV2BridgeServiceFactory = ({
const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags;
// now check with new ids
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.secretName,
...(tagsToCheck.length && {
secretTags: tagsToCheck.map((el) => el.slug)
if (secret.type !== SecretType.Personal)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.secretName,
...(tagsToCheck.length && {
secretTags: tagsToCheck.map((el) => el.slug)
})
})
})
);
);
if (inputSecret.newSecretName) {
const doesNewNameSecretExist = await secretDAL.findOne({
@@ -546,6 +558,14 @@ export const secretV2BridgeServiceFactory = ({
);
}
if (secretValue) {
const { nestedReferences, localReferences } = getAllSecretReferences(secretValue);
const allSecretReferences = nestedReferences.concat(
localReferences.map((el) => ({ secretKey: el, secretPath, environment }))
);
await $validateSecretReferences(projectId, permission, allSecretReferences);
}
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
@@ -706,15 +726,17 @@ export const secretV2BridgeServiceFactory = ({
})
});
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretToDelete.key,
secretTags: secretToDelete.tags?.map((el) => el.slug)
})
);
if (secretToDelete.type !== SecretType.Personal)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretToDelete.key,
secretTags: secretToDelete.tags?.map((el) => el.slug)
})
);
try {
const deletedSecret = await secretDAL.transaction(async (tx) => {
@@ -1658,7 +1680,7 @@ export const secretV2BridgeServiceFactory = ({
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
// get all tags
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
const sanitizedTagIds = [...new Set(inputSecrets.flatMap(({ tagIds = [] }) => tagIds))];
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
if (tags.length !== sanitizedTagIds.length)
throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
@@ -1906,7 +1928,7 @@ export const secretV2BridgeServiceFactory = ({
});
// get all tags
const sanitizedTagIds = secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds);
const sanitizedTagIds = [...new Set(secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds))];
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds, tx) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupByID = groupBy(tags, (i) => i.id);
@@ -2333,7 +2355,8 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
limit = 20,
offset = 0,
secretId
secretId,
secretVersions: secretVersionsFilter
}: TGetSecretVersionsDTO) => {
const secret = await secretDAL.findById(secretId);
@@ -2370,6 +2393,7 @@ export const secretV2BridgeServiceFactory = ({
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
secretId,
projectId: folder.projectId,
secretVersions: secretVersionsFilter,
findOpt: {
offset,
limit,
@@ -2939,7 +2963,7 @@ export const secretV2BridgeServiceFactory = ({
secretKey: secretName
});
return { tree: stackTrace, value: expandedValue };
return { tree: stackTrace, value: expandedValue, secret };
};
const getAccessibleSecrets = async ({
@@ -3155,6 +3179,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretById,
getAccessibleSecrets,
getSecretVersionsByIds,
findSecretIdsByFolderIdAndKeys
findSecretIdsByFolderIdAndKeys,
$validateSecretReferences
};
};

View File

@@ -159,6 +159,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
limit?: number;
offset?: number;
secretId: string;
secretVersions?: string[];
};
export type TSecretReference = { environment: string; secretPath: string; secretKey: string };

View File

@@ -2568,7 +2568,8 @@ export const secretServiceFactory = ({
actorAuthMethod,
limit = 20,
offset = 0,
secretId
secretId,
secretVersions: filterSecretVersions
}: TGetSecretVersionsDTO) => {
const secretVersionV2 = await secretV2BridgeService
.getSecretVersions({
@@ -2578,7 +2579,8 @@ export const secretServiceFactory = ({
actorAuthMethod,
limit,
offset,
secretId
secretId,
secretVersions: filterSecretVersions
})
.catch((err) => {
if ((err as Error).message === "BadRequest: Failed to find secret") {

View File

@@ -331,6 +331,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
limit?: number;
offset?: number;
secretId: string;
secretVersions?: string[];
};
export type TSecretReference = { environment: string; secretPath: string };

View File

@@ -32,7 +32,8 @@ export enum PostHogEventTypes {
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
SignCert = "Sign PKI Certificate",
IssueCert = "Issue PKI Certificate",
InvalidateCache = "Invalidate Cache"
InvalidateCache = "Invalidate Cache",
NotificationUpdated = "Notification Updated"
}
export type TSecretModifiedEvent = {
@@ -232,6 +233,14 @@ export type TInvalidateCacheEvent = {
};
};
export type TNotificationUpdatedEvent = {
event: PostHogEventTypes.NotificationUpdated;
properties: {
notificationId: string;
isRead?: boolean;
};
};
export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
| TSecretModifiedEvent
| TAdminInitEvent
@@ -251,4 +260,5 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
| TSignCertificateEvent
| TIssueCertificateEvent
| TInvalidateCacheEvent
| TNotificationUpdatedEvent
);

View File

@@ -294,11 +294,18 @@ export const userServiceFactory = ({
// Delete all user aliases since the email is changing
await userAliasDAL.delete({ userId }, tx);
// Ensure EMAIL auth method is included if not already present
const currentAuthMethods = user.authMethods || [];
const updatedAuthMethods = currentAuthMethods.includes(AuthMethod.EMAIL)
? currentAuthMethods
: [...currentAuthMethods, AuthMethod.EMAIL];
const updatedUser = await userDAL.updateById(
userId,
{
email: newEmail.toLowerCase(),
username: newEmail.toLowerCase()
username: newEmail.toLowerCase(),
authMethods: updatedAuthMethods
},
tx
);

View File

@@ -9,7 +9,7 @@ description: "Run the Infisical gateway or manage its systemd service"
infisical gateway start --name=<name> --relay=<relay-name> --auth-method=<auth-method>
```
</Tab>
<Tab title="Install gateway service">
<Tab title="Start gateway as background daemon (Linux only)">
```bash
sudo infisical gateway systemd install --token=<token> --domain=<domain> --name=<name> --relay=<relay-name>
```
@@ -25,29 +25,29 @@ The gateway system uses SSH reverse tunnels over TCP, eliminating firewall compl
<Warning>
**Deprecation and Migration Notice:** The legacy `infisical gateway` command (v1) will be removed in a future release. Please migrate to `infisical gateway start` (Gateway v2).
If you are moving from Gateway v1 to Gateway v2, this is NOT a drop-in switch. Gateway v2 creates new gateway instances with new gateway IDs. You must update any existing resources that reference gateway IDs (for example: dynamic secret configs, app connections, or other gateway-bound resources) to point to the new Gateway v2 gateway ID. Until you update those references, traffic will continue to target the old v1 gateway.
If you are moving from Gateway v1 to Gateway v2, this is NOT a drop-in switch. Gateway v2 creates new gateway instances with new gateway IDs. You must update any existing resources that reference gateway IDs (for example: dynamic secret configs, app connections, or other gateway-bound resources) to point to the new Gateway v2 gateway resource. Until you update those references, traffic will continue to target the old v1 gateway.
</Warning>
## Subcommands & flags
<Accordion title="infisical gateway start" defaultOpen="true">
Run the Infisical gateway component within your VPC. The gateway establishes an SSH reverse tunnel to the specified relay server and provides secure access to private resources.
Run the Infisical gateway component within your the network where your target resources are located. The gateway establishes an SSH reverse tunnel to the specified relay server and provides secure access to private resources within your network.
```bash
infisical gateway start --relay=<relay-name> --name=<name> --auth-method=<auth-method>
```
The gateway component:
Once started, the gateway component will:
- Establishes outbound SSH reverse tunnels to relay servers (no inbound firewall rules needed)
- Authenticates using SSH certificates issued by Infisical
- Automatically reconnects if the connection is lost
- Provides access to private resources within your network
- Establish outbound SSH reverse tunnels to relay servers (no inbound firewall rules needed)
- Authenticate using SSH certificates issued by Infisical
- Automatically reconnect if the connection is lost
- Provide access to private resources within your network
### Authentication
The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags.
The Relay supports multiple authentication methods. Below are the available authentication methods, with their respective flags.
<AccordionGroup>
<Accordion title="Universal Auth">
@@ -361,12 +361,12 @@ sudo systemctl disable infisical-gateway # Disable auto-start on boot
</Accordion>
## Legacy Gateway Commands (Deprecated)
## Legacy Gateway Commands
<Accordion title="infisical gateway (deprecated)">
<Warning>
**This command is deprecated and will be removed in a future release.**
Please migrate to `infisical gateway start` for the new TCP-based SSH tunnel architecture.
**Migration required:** If you are currently using Gateway v1 (via `infisical gateway`), moving to Gateway v2 is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID. Until you update those references, traffic will continue to target the old v1 gateway.
@@ -593,7 +593,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa
<Accordion title="infisical gateway install (deprecated)">
<Warning>
**This command is deprecated and will be removed in a future release.**
Please migrate to `infisical gateway systemd install` for the new TCP-based SSH tunnel architecture with enhanced security and better performance.
**Migration required:** If you previously installed Gateway v1 via `infisical gateway install`, moving to Gateway v2 is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID. Until you update those references, traffic will continue to target the old v1 gateway.

View File

@@ -6,88 +6,70 @@ description: "Relay-related commands for Infisical"
<Tabs>
<Tab title="Start relay">
```bash
infisical relay start --type=<type> --host=<host> --name=<name> --auth-method=<auth-method>
infisical relay start --host=<host> --name=<name> --auth-method=<auth-method>
```
</Tab>
<Tab title="Start relay as background daemon (Linux only)">
```bash
# Install systemd service
sudo infisical relay systemd install --host=<host> --name=<name> --token=<token>
# Uninstall systemd service
sudo infisical relay systemd uninstall
```
</Tab>
</Tabs>
## Description
Relay-related commands for Infisical that provide identity-aware relay infrastructure for routing encrypted traffic:
- **Relay**: Identity-aware server that routes encrypted traffic (can be instance-wide or organization-specific)
The relay system uses SSH reverse tunnels over TCP, eliminating firewall complexity and providing excellent performance for enterprise environments.
Relay-related commands for Infisical that provide identity-aware relay infrastructure for routing encrypted traffic. Relays are organization-deployed servers that route encrypted traffic between Infisical and your gateways.
## Subcommands & flags
<Accordion title="infisical relay start" defaultOpen="true">
Run the Infisical relay component. The relay handles network traffic routing and can operate in different modes.
Run the Infisical relay component. The relay handles network traffic routing between Infisical and your gateways.
```bash
infisical relay start --type=<type> --host=<host> --name=<name> --auth-method=<auth-method>
infisical relay start --host=<host> --name=<name> --auth-method=<auth-method>
```
### Flags
<Accordion title="--type">
The type of relay to run. Must be either 'instance' or 'org'.
- **`instance`**: Shared relay server that can be used by all organizations on your Infisical instance. Set up by the instance administrator. Uses `INFISICAL_RELAY_AUTH_SECRET` environment variable for authentication, which must be configured by the instance admin.
- **`org`**: Dedicated relay server that individual organizations deploy and manage in their own infrastructure. Provides enhanced security, custom geographic placement, and compliance benefits. Uses standard Infisical authentication methods.
```bash
# Organization relay (customer-deployed)
infisical relay start --type=org --host=192.168.1.100 --name=my-org-relay
# Instance relay (configured by instance admin)
INFISICAL_RELAY_AUTH_SECRET=<secret> infisical relay start --type=instance --host=10.0.1.50 --name=shared-relay
```
</Accordion>
<Accordion title="--host">
The host (IP address or hostname) of the instance where the relay is deployed. This must be a static public IP or resolvable hostname that gateways can reach.
```bash
# Example with IP address
infisical relay start --host=203.0.113.100 --type=org --name=my-relay
infisical relay start --host=203.0.113.100 --name=my-relay
# Example with hostname
infisical relay start --host=relay.example.com --type=org --name=my-relay
infisical relay start --host=relay.example.com --name=my-relay
```
</Accordion>
<Accordion title="--name">
The name of the relay.
The name of the relay. This is an arbitrary identifier for your relay instance.
```bash
# Example
infisical relay start --name=my-relay --type=org --host=192.168.1.100
infisical relay start --name=my-relay --host=192.168.1.100
```
</Accordion>
### Authentication
**Organization Relays (`--type=org`):**
Deploy your own relay server in your infrastructure for enhanced security and reduced latency. Supports all standard Infisical authentication methods documented below.
**Instance Relays (`--type=instance`):**
Shared relay servers that serve all organizations on your Infisical instance. For Infisical Cloud, these are already running and ready to use. For self-hosted deployments, they're set up by the instance administrator. Authentication is handled via the `INFISICAL_RELAY_AUTH_SECRET` environment variable.
Relays support all standard Infisical authentication methods. Choose the authentication method that best fits your environment and set the corresponding flags when starting the relay.
```bash
# Organization relay with Universal Auth (customer-deployed)
infisical relay start --type=org --host=192.168.1.100 --name=my-org-relay --auth-method=universal-auth --client-id=<client-id> --client-secret=<client-secret>
# Instance relay (configured by instance admin)
INFISICAL_RELAY_AUTH_SECRET=<secret> infisical relay start --type=instance --host=10.0.1.50 --name=shared-relay
# Example with Universal Auth
infisical relay start --host=192.168.1.100 --name=my-relay --auth-method=universal-auth --client-id=<client-id> --client-secret=<client-secret>
```
### Authentication Methods
### Available Authentication Methods
The Infisical CLI supports multiple authentication methods for organization relays. Below are the available authentication methods, with their respective flags.
The Infisical CLI supports multiple authentication methods for relays. Below are the available authentication methods, with their respective flags.
<AccordionGroup>
<Accordion title="Universal Auth">
@@ -108,7 +90,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
</ParamField>
```bash
infisical relay start --auth-method=universal-auth --client-id=<client-id> --client-secret=<client-secret> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=universal-auth --client-id=<client-id> --client-secret=<client-secret> --host=<host> --name=<name>
```
</Accordion>
@@ -132,7 +114,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
```bash
infisical relay start --auth-method=kubernetes --machine-identity-id=<machine-identity-id> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=kubernetes --machine-identity-id=<machine-identity-id> --host=<host> --name=<name>
```
</Accordion>
@@ -153,7 +135,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
```bash
infisical relay start --auth-method=azure --machine-identity-id=<machine-identity-id> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=azure --machine-identity-id=<machine-identity-id> --host=<host> --name=<name>
```
</Accordion>
@@ -174,7 +156,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
```bash
infisical relay start --auth-method=gcp-id-token --machine-identity-id=<machine-identity-id> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=gcp-id-token --machine-identity-id=<machine-identity-id> --host=<host> --name=<name>
```
</Accordion>
@@ -196,7 +178,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
</ParamField>
```bash
infisical relay start --auth-method=gcp-iam --machine-identity-id=<machine-identity-id> --service-account-key-file-path=<service-account-key-file-path> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=gcp-iam --machine-identity-id=<machine-identity-id> --service-account-key-file-path=<service-account-key-file-path> --host=<host> --name=<name>
```
</Accordion>
@@ -215,7 +197,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
</ParamField>
```bash
infisical relay start --auth-method=aws-iam --machine-identity-id=<machine-identity-id> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=aws-iam --machine-identity-id=<machine-identity-id> --host=<host> --name=<name>
```
</Accordion>
@@ -237,7 +219,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
</ParamField>
```bash
infisical relay start --auth-method=oidc-auth --machine-identity-id=<machine-identity-id> --jwt=<oidc-jwt> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=oidc-auth --machine-identity-id=<machine-identity-id> --jwt=<oidc-jwt> --host=<host> --name=<name>
```
</Accordion>
@@ -261,7 +243,7 @@ The Infisical CLI supports multiple authentication methods for organization rela
```bash
infisical relay start --auth-method=jwt-auth --jwt=<jwt> --machine-identity-id=<machine-identity-id> --type=org --host=<host> --name=<name>
infisical relay start --auth-method=jwt-auth --jwt=<jwt> --machine-identity-id=<machine-identity-id> --host=<host> --name=<name>
```
</Accordion>
@@ -277,30 +259,132 @@ The Infisical CLI supports multiple authentication methods for organization rela
</ParamField>
```bash
infisical relay start --token=<token> --type=org --host=<host> --name=<name>
infisical relay start --token=<token> --host=<host> --name=<name>
```
</Accordion>
</AccordionGroup>
### Deployment Considerations
</Accordion>
**When to use Instance Relays (`--type=instance`):**
<Accordion title="infisical relay systemd" defaultOpen="false">
Manage systemd service for Infisical relay. This allows you to install and run the relay as a systemd service on Linux systems.
### Requirements
- **Operating System**: Linux only (systemd is not supported on other operating systems)
- **Privileges**: Root/sudo privileges required for both install and uninstall operations
- **Systemd**: The system must be running systemd as the init system
- You want to get started quickly without setting up your own relay infrastructure
- You're using Infisical Cloud and want to leverage the existing relay infrastructure
- You're on a self-hosted instance where the admin has already set up shared relays
- You don't need custom geographic placement of relay servers
- You don't have specific compliance requirements that require dedicated infrastructure
- You want to minimize operational overhead by using shared infrastructure
```bash
infisical relay systemd <subcommand>
```
**When to use Organization Relays (`--type=org`):**
### Subcommands
- You need lower latency by deploying relay servers closer to your resources
- You have security requirements that mandate running infrastructure in your own environment
- You have compliance requirements such as data sovereignty or air-gapped environments
- You need custom network policies or specific networking configurations
- You have high-scale performance requirements that shared infrastructure can't meet
- You want full control over your relay infrastructure and its configuration
<Accordion title="install">
Install and enable systemd service for the relay. Must be run with sudo on Linux systems.
```bash
sudo infisical relay systemd install --host=<host> --name=<name> --token=<token> [flags]
```
#### Flags
<Accordion title="--host">
The host (IP address or hostname) of the instance where the relay is deployed. This must be a static public IP or resolvable hostname that gateways can reach.
```bash
# Example with IP address
sudo infisical relay systemd install --host=203.0.113.100 --name=my-relay --token=<token>
# Example with hostname
sudo infisical relay systemd install --host=relay.example.com --name=my-relay --token=<token>
```
</Accordion>
<Accordion title="--name">
The name of the relay.
```bash
# Example
sudo infisical relay systemd install --name=my-relay --host=192.168.1.100 --token=<token>
```
</Accordion>
<Accordion title="--token">
Connect with Infisical using machine identity access token.
```bash
# Example
sudo infisical relay systemd install --token=<machine-identity-token> --host=<host> --name=<name>
```
</Accordion>
<Accordion title="--domain">
Domain of your self-hosted Infisical instance. Optional flag for specifying a custom domain.
```bash
# Example
sudo infisical relay systemd install --domain=http://localhost:8080 --token=<token> --host=<host> --name=<name>
```
</Accordion>
#### Examples
```bash
# Install relay with token authentication
sudo infisical relay systemd install --host=192.168.1.100 --name=my-relay --token=<machine-identity-token>
# Install with custom domain
sudo infisical relay systemd install --domain=http://localhost:8080 --token=<token> --host=<host> --name=<name>
```
#### Post-installation
After successful installation, the service will be enabled but not started. To start the service:
```bash
sudo systemctl start infisical-relay
```
To check the service status:
```bash
sudo systemctl status infisical-relay
```
To view service logs:
```bash
sudo journalctl -u infisical-relay -f
```
</Accordion>
<Accordion title="uninstall">
Uninstall and remove systemd service for the relay. Must be run with sudo on Linux systems.
```bash
sudo infisical relay systemd uninstall
```
#### Examples
```bash
# Uninstall the relay systemd service
sudo infisical relay systemd uninstall
```
#### What it does
- Stops the `infisical-relay` systemd service if it's running
- Disables the service from starting on boot
- Removes the systemd service file
- Cleans up the service configuration
</Accordion>
</Accordion>

View File

@@ -173,8 +173,9 @@
"group": "Gateway",
"pages": [
"documentation/platform/gateways/overview",
"documentation/platform/gateways/gateway-security",
"documentation/platform/gateways/networking",
"documentation/platform/gateways/gateway-deployment",
"documentation/platform/gateways/relay-deployment",
"documentation/platform/gateways/security",
{
"group": "Gateway (Deprecated)",
"pages": [

View File

@@ -29,6 +29,9 @@ You can update your account email address:
1. Open the `Personal Settings` menu.
2. Navigate to the `Authentication` tab.
3. In the `Change Email` section, enter your new email address.
<Info>
If you don't currently have Email authentication enabled, it will be automatically activated when you change your email. You may disable it in the authentication settings after logging in with your new email if needed.
</Info>
![change email section](../../../images/auth-methods/personal-settings-authentication-change-email-password.png)
4. Click `Send Verification Code` to receive an 6-digit verification code at your new email address.
5. Check your new email inbox and enter the verification code.

View File

@@ -3,49 +3,93 @@ title: "AWS IAM"
description: "Learn how to dynamically generate AWS IAM Users."
---
The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users on demand based on a configured AWS policy. Infisical supports several authentication methods to connect to your AWS account, including assuming an IAM Role, using IAM Roles for Service Accounts (IRSA) on EKS, or static Access Keys.
The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users and temporary credentials on demand based on a configured AWS policy. Infisical supports several authentication methods to connect to your AWS account, including assuming an IAM Role, using IAM Roles for Service Accounts (IRSA) on EKS, or static Access Keys.
## AWS STS Duration Limits
When using **Temporary Credentials**, AWS STS has specific maximum duration limits:
- **AssumeRole operations**: Maximum 1 hour (3600 seconds) when using temporary credentials
- **GetSessionToken operations** (Access Key & IRSA): Maximum 12 hours (43200 seconds)
<Info>
**Automatic Duration Adjustment**: If you specify a TTL that exceeds these AWS limits, Infisical will automatically use the maximum allowed duration instead of failing the operation. This ensures your dynamic secrets work reliably within AWS constraints.
</Info>
## Prerequisite
Infisical needs an AWS IAM principal (a user or a role) with the required permissions to create and manage other IAM users. This principal will be responsible for the lifecycle of the dynamically generated users.
Infisical needs an AWS IAM principal (a user or a role) with the required permissions to create and manage other IAM users and temporary credentials. This principal will be responsible for the lifecycle of the dynamically generated users and temporary credentials.
<Accordion title="Required IAM Permissions">
```json
{
"Version": "2012-10-17",
"Statement": [
<Tabs>
<Tab title="IAM User">
Required permissions for creating temporary IAM users:
```json
{
"Effect": "Allow",
"Action": [
"iam:AttachUserPolicy",
"iam:CreateAccessKey",
"iam:CreateUser",
"iam:DeleteAccessKey",
"iam:DeleteUser",
"iam:DeleteUserPolicy",
"iam:DetachUserPolicy",
"iam:GetUser",
"iam:ListAccessKeys",
"iam:ListAttachedUserPolicies",
"iam:ListGroupsForUser",
"iam:ListUserPolicies",
"iam:PutUserPolicy",
"iam:AddUserToGroup",
"iam:RemoveUserFromGroup",
"iam:TagUser"
],
"Resource": ["*"]
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:AttachUserPolicy",
"iam:CreateAccessKey",
"iam:CreateUser",
"iam:DeleteAccessKey",
"iam:DeleteUser",
"iam:DeleteUserPolicy",
"iam:DetachUserPolicy",
"iam:GetUser",
"iam:ListAccessKeys",
"iam:ListAttachedUserPolicies",
"iam:ListGroupsForUser",
"iam:ListUserPolicies",
"iam:PutUserPolicy",
"iam:AddUserToGroup",
"iam:RemoveUserFromGroup",
"iam:TagUser"
],
"Resource": ["*"]
}
]
}
]
}
```
```
To minimize managing user access you can attach a resource in format
To minimize managing user access you can attach a resource in format
> arn:aws:iam::\<account-id\>:user/\<aws-scope-path\>
> arn:aws:iam::\<account-id\>:user/\<aws-scope-path\>
Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** with a path to minimize managing user access.
Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** with a path to minimize managing user access.
</Tab>
<Tab title="Temporary Credentials">
Required permissions for Access Key and Assume Role methods:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sts:GetSessionToken",
"sts:AssumeRole"
],
"Resource": ["*"]
}
]
}
```
To minimize managing user access you can attach a resource in format
> arn:aws:iam::\<account-id\>:user/\<aws-scope-path\>
Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** with a path to minimize managing user access.
</Tab>
</Tabs>
</Accordion>
@@ -170,43 +214,76 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Select *Assume Role* method.
</ParamField>
<ParamField path="Aws Role ARN" type="string" required>
The ARN of the AWS Role to assume.
<ParamField path="Credential Type" type="string" required>
Choose the credential generation approach:
- **IAM User (Default)**: Creates new temporary IAM users in your AWS account
- **Temporary Credentials**: Generates temporary credentials from your role connection
</ParamField>
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
<ParamField path="Aws Role ARN" type="string" required>
The ARN of the AWS Role to assume.
</ParamField>
<ParamField path="AWS Region" type="string" required>
The AWS data center region.
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required>
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
</ParamField>
<Tabs>
<Tab title="IAM User">
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string">
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
Allowed template variables are
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
</ParamField>
Allowed template variables are:
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
- `{{identity.name}}`: Name of the identity that is generating the secret
- `{{random N}}`: Random string of N characters
Allowed template functions are:
- `truncate`: Truncates a string to a specified length
- `replace`: Replaces a substring with another value
</ParamField>
<ParamField path="Tags" type="map<string, string>[]">
Tags to be added to the created IAM User resource.
</ParamField>
</Tab>
<Tab title="Temporary Credentials">
When **Credential Type** is set to **Temporary Credentials**:
<Info>
No additional configuration parameters are required. The generated credentials will:
- Inherit the permissions of the assumed role
- Include an AWS Session Token
- Be valid for the duration specified in Default TTL
</Info>
<Warning>
**Duration Limit**: AssumeRole temporary credentials are limited to 1 hour maximum by AWS. TTL values exceeding this limit will be automatically adjusted to 1 hour.
</Warning>
</Tab>
</Tabs>
</Step>
<Step title="Click 'Submit'">
@@ -232,6 +309,18 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
**Credentials format depends on your chosen credential type:**
**IAM User credential type:**
- AWS Username
- AWS Access Key ID
- AWS Secret Access Key
**Temporary Credentials credential type:**
- AWS Access Key ID
- AWS Secret Access Key
- AWS Session Token
![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png)
</Step>
</Steps>
@@ -342,36 +431,75 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
<ParamField path="Method" type="string" required>
Select *IRSA* method.
</ParamField>
<ParamField path="Credential Type" type="string" required>
Choose the credential generation approach:
- **IAM User**: Creates new temporary IAM users in your AWS account
- **Temporary Credentials**: Generates temporary credentials from your IRSA role connection
</ParamField>
<ParamField path="Aws Role ARN" type="string" required>
The ARN of the AWS IAM Role for the service account to assume.
</ParamField>
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
<ParamField path="AWS Region" type="string" required>
The AWS data center region.
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required>
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
Allowed template variables are
<Tabs>
<Tab title="IAM User">
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string">
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
Allowed template variables are:
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
- `{{identity.name}}`: Name of the identity that is generating the secret
- `{{random N}}`: Random string of N characters
Allowed template functions are:
- `truncate`: Truncates a string to a specified length
- `replace`: Replaces a substring with another value
</ParamField>
<ParamField path="Tags" type="map<string, string>[]">
Tags to be added to the created IAM User resource.
</ParamField>
</Tab>
<Tab title="Temporary Credentials">
When **Credential Type** is set to **Temporary Credentials**:
<Info>
No additional configuration parameters are required. The generated credentials will:
- Inherit the permissions of the assumed IRSA role
- Include an AWS Session Token
- Be valid for the duration specified in Default TTL
</Info>
<Note>
**Duration Limit**: IRSA temporary credentials support up to 12 hours maximum via GetSessionToken. TTL values exceeding this limit will be automatically adjusted.
</Note>
</Tab>
</Tabs>
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
@@ -429,6 +557,12 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Select *Access Key* method.
</ParamField>
<ParamField path="Credential Type" type="string" required>
Choose the credential generation approach:
- **IAM User**: Creates new temporary IAM users in your AWS account
- **Temporary Credentials**: Generates temporary credentials from your access key connection
</ParamField>
<ParamField path="AWS Access Key" type="string" required>
The managing AWS IAM User Access Key
</ParamField>
@@ -437,43 +571,66 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
The managing AWS IAM User Secret Key
</ParamField>
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
<ParamField path="AWS Region" type="string" required>
The AWS data center region.
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required>
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
</ParamField>
<Tabs>
<Tab title="IAM User">
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string">
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas.
</ParamField>
Allowed template variables are
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
</ParamField>
Allowed template variables are:
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
- `{{identity.name}}`: Name of the identity that is generating the secret
- `{{random N}}`: Random string of N characters
<ParamField path="Tags" type="map[string]string">
Tags to be added to the created IAM User resource.
</ParamField>
Allowed template functions are:
- `truncate`: Truncates a string to a specified length
- `replace`: Replaces a substring with another value
</ParamField>
<ParamField path="Tags" type="map[string]string">
Tags to be added to the created IAM User resource.
</ParamField>
</Tab>
<Tab title="Temporary Credentials">
When **Credential Type** is set to **Temporary Credentials**:
<Info>
No additional configuration parameters are required. The generated credentials will:
- Inherit the permissions of your access key connection
- Include an AWS Session Token
- Be valid for the duration specified in Default TTL
</Info>
<Note>
**Duration Limit**: Access Key temporary credentials support up to 12 hours maximum via GetSessionToken. TTL values exceeding this limit will be automatically adjusted.
</Note>
</Tab>
</Tabs>
</Step>
@@ -500,6 +657,18 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
**Credentials format depends on your chosen credential type:**
**IAM User credential type:**
- AWS Username
- AWS Access Key ID
- AWS Secret Access Key
**Temporary Credentials credential type:**
- AWS Access Key ID
- AWS Secret Access Key
- AWS Session Token
![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png)
</Step>
</Steps>

View File

@@ -0,0 +1,265 @@
---
title: "Gateway Deployment"
description: "Complete guide to deploying Infisical Gateways including network configuration and firewall requirements"
---
Infisical Gateways enables secure communication between your private resources and the Infisical platform without exposing inbound ports in your network.
This guide covers everything you need to deploy and configure Infisical Gateways.
## Deployment Steps
To successfully deploy an Infisical Gateway for use, follow these steps in order.
<Steps>
<Step title="Provision a Machine Identity">
Create a machine identity with the correct permissions to create and manage gateways. This identity is used by the gateway to authenticate with Infisical and should be provisioned in advance.
The gateway supports several [machine identity auth methods](/documentation/platform/identities/machine-identities), as listed below. Choose the one that best fits your environment and set the corresponding environment variables when deploying the gateway.
<AccordionGroup>
<Accordion title="Universal Auth">
Simple and secure authentication using client ID and client secret.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=universal-auth`
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id>`
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret>`
</Accordion>
<Accordion title="Token Auth">
Direct authentication using a machine identity access token.
**Environment Variables:**
- `INFISICAL_TOKEN=<token>`
</Accordion>
<Accordion title="Native Kubernetes">
Authentication using Kubernetes service account tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=kubernetes`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="Native AWS IAM">
Authentication using AWS IAM roles.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=aws-iam`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="Native GCP ID Token">
Authentication using GCP identity tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=gcp-id-token`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="GCP IAM">
Authentication using GCP service account keys.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=gcp-iam`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
- `INFISICAL_GCP_SERVICE_ACCOUNT_KEY_FILE_PATH=<path-to-key-file>`
</Accordion>
<Accordion title="Native Azure">
Authentication using Azure managed identity.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=azure`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="OIDC Auth">
Authentication using OIDC identity tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=oidc-auth`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
- `INFISICAL_JWT=<oidc-jwt>`
</Accordion>
<Accordion title="JWT Auth">
Authentication using JWT tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=jwt-auth`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
- `INFISICAL_JWT=<jwt>`
</Accordion>
</AccordionGroup>
</Step>
<Step title="Set Up a Relay Server">
Ensure a relay server is running and accessible before you deploy any gateways. You have two options:
- **Managed relay (Infisical Cloud, US/EU only):** Managed relays are only available for Infisical Cloud instances in the US and EU regions. If you are using Infisical Cloud in these regions, you can use the provided managed relay.
- **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the <a href="/documentation/platform/gateways/relay-deployment">Relay Deployment Guide</a>.
</Step>
<Step title="Install the Infisical CLI">
Make sure the Infisical CLI is installed on the machine or environment where you plan to deploy the gateway. The CLI is required for gateway installation and management.
See the [CLI Installation Guide](/cli/overview) for instructions.
</Step>
<Step title="Configure Network & Firewall">
Ensure your network and firewall settings allow the gateway to connect to all required services. All connections are outbound only; no inbound ports need to be opened.
| Protocol | Destination | Port | Purpose |
| -------- | ------------------------------------ | ---- | ------------------------------------------ |
| TCP | Relay Server IP/Hostname | 2222 | SSH reverse tunnel establishment |
| TCP | Infisical instance host (US/EU, other) | 443 | API communication and certificate requests |
For managed relays, allow outbound traffic to the provided relay server IP/hostname. For self-hosted relays, allow outbound traffic to your own relay server address.
If you are in a corporate environment with strict egress filtering, ensure outbound TCP 2222 to relay servers and outbound HTTPS 443 to Infisical API endpoints are allowed.
</Step>
<Step title="Select a Deployment Method">
The Infisical CLI is used to install and start the gateway in your chosen environment. The CLI provides commands for both production and development scenarios, and supports a variety of options/flags to configure your deployment.
To view all available flags and equivalent environment variables for gateway deployment, see the [Gateway CLI Command Reference](/cli/commands/gateway).
<Tabs>
<Tab title="Linux Server (Production)">
For production deployments on Linux servers, install the Gateway as a systemd service so that it runs securely in the background and automatically restarts on failure or system reboot:
```bash
sudo infisical gateway systemd install --token <your-machine-identity-token> --domain <your-infisical-domain> --name <gateway-name> --relay <relay-name>
sudo systemctl start infisical-gateway
```
<Warning>
The systemd install command requires a Linux operating system with root/sudo
privileges.
</Warning>
</Tab>
<Tab title="Kubernetes (Production)">
For production deployments on Kubernetes clusters, install the Gateway using the Infisical Helm chart:
#### Install the latest Helm Chart repository
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
```
#### Create a Kubernetes Secret
The gateway supports all identity authentication methods through environment variables:
```bash
kubectl create secret generic infisical-gateway-environment \
--from-literal=INFISICAL_AUTH_METHOD=universal-auth \
--from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id> \
--from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret> \
--from-literal=INFISICAL_RELAY_NAME=<relay-name> \
--from-literal=INFISICAL_GATEWAY_NAME=<gateway-name>
```
#### Install the Gateway
```bash
helm install infisical-gateway infisical-helm-charts/infisical-gateway
```
</Tab>
<Tab title="Development & Testing">
For development or testing environments:
```bash
infisical gateway start --token <token> --relay=<relay-name> --name=<gateway-name>
```
</Tab>
</Tabs>
</Step>
<Step title="Verify Your Gateway Deployment">
After deployment, verify your gateway is working:
1. **Check logs** for "Gateway started successfully" message indicating the gateway is running and connected to the relay
2. **Verify registration** in the Infisical by visiting the Gateways section of your organization. The new gateway should appear with a recent heartbeat timestamp.
3. **Test connectivity** by creating a resource in Infisical that uses the gateway to access a private service. Verify the resource can successfully connect through the gateway.
</Step>
</Steps>
## Frequently Asked Questions
<Accordion title="Do I need to open any inbound ports on my firewall?">
No inbound ports need to be opened for gateways. The gateway only makes outbound connections:
- **Outbound SSH** to relay servers on port 2222
- **Outbound HTTPS** to Infisical API endpoints on port 443
- **SSH reverse tunnels** handle all communication - no return traffic configuration needed
This design maintains security by avoiding the need for inbound firewall rules that could expose your network to external threats.
</Accordion>
<Accordion title="How do I test network connectivity from the gateway?">
Test relay connectivity and outbound API access from the gateway:
1. Test SSH port to relay:
```bash
nc -zv <relay-ip> 2222
```
2. Test outbound API access (replace with your Infisical domain if different):
```bash
curl -I https://app.infisical.com
```
</Accordion>
<Accordion title="How do I troubleshoot relay connectivity issues?">
If the gateway cannot connect to the relay:
1. Verify the relay server is running and accessible
2. Check firewall rules allow outbound connections on port 2222
3. Confirm the relay name matches exactly
4. Test SSH port to relay:
```bash
nc -zv <relay-ip> 2222
```
</Accordion>
<Accordion title="How do I troubleshoot authentication failures?">
If you encounter authentication failures:
1. Verify machine identity credentials are correct
2. Check token expiration and renewal
3. Ensure authentication method is properly configured
</Accordion>
<Accordion title="Where can I find gateway logs?">
Check gateway logs for detailed error information:
- **systemd service:**
```bash
sudo journalctl -u infisical-gateway -f
```
- **Kubernetes:**
```bash
kubectl logs deployment/infisical-gateway
```
- **Local installation:** Logs appear in the terminal where you started the gateway
</Accordion>
<Accordion title="Where is the gateway configuration file stored?">
For systemd-based installations, the gateway's configuration file is stored at `/etc/infisical/gateway.conf`. You may reference or inspect this file for troubleshooting advanced configuration issues.
</Accordion>
<Accordion title="What happens if there is a network interruption?">
The gateway is designed to handle network interruptions gracefully:
- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers if the SSH connection is lost
- **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention
- **Persistent SSH tunnels**: SSH connections are automatically re-established when connectivity is restored
- **Certificate rotation**: The gateway handles certificate renewal automatically during reconnection
- **Graceful degradation**: The gateway logs connection issues and continues attempting to restore connectivity
No manual intervention is typically required during network interruptions.
</Accordion>

View File

@@ -1,178 +0,0 @@
---
title: "Networking"
description: "Network configuration and firewall requirements for Infisical Gateway"
---
The Infisical Gateway requires outbound network connectivity to establish secure SSH reverse tunnels with relay servers.
This page outlines the required ports, protocols, and firewall configurations needed for optimal gateway usage.
## Network Architecture
The gateway uses SSH reverse tunnels to establish secure connections with end-to-end encryption:
1. **Gateway** connects outbound to **Relay Servers** using SSH over TCP
2. **Infisical platform** establishes mTLS connections with gateways for application traffic
3. **Relay Servers** route the doubly-encrypted traffic (mTLS payload within SSH tunnels) between the platform and gateways
4. **Double encryption** ensures relay servers cannot access application data - only the platform and gateway can decrypt traffic
## Required Network Connectivity
### Outbound Connections (Required)
The gateway requires the following outbound connectivity:
| Protocol | Destination | Ports | Purpose |
| -------- | ------------------------------------ | ----- | ------------------------------------------ |
| TCP | Relay Servers | 2222 | SSH reverse tunnel establishment |
| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and certificate requests |
### Relay Server Connectivity
**For Instance Relays (Infisical Cloud):** Your firewall must allow outbound connectivity to Infisical-managed relay servers.
**For Organization Relays:** Your firewall must allow outbound connectivity to your own relay server IP addresses or hostnames.
**For Self-hosted Instance Relays:** Your firewall must allow outbound connectivity to relay servers configured by your instance administrator.
<Tabs>
<Tab title="Instance Relays (Infisical Cloud)">
Infisical provides multiple managed relay servers with static IP addresses.
You can whitelist these IPs ahead of time based on which relay server you
choose to connect to. **Firewall requirements:** Allow outbound TCP
connections to the desired relay server IP on port 2222.
</Tab>
<Tab title="Organization Relays">
You control the relay server IP addresses or hostnames when deploying your
own organization relays. **Firewall requirements:** Allow outbound TCP
connections to your relay server IP or hostname on port 2222. For example,
if your relay is at `203.0.113.100` or `relay.example.com`, allow TCP to
`203.0.113.100:2222` or `relay.example.com:2222`.
</Tab>
<Tab title="Self-hosted Instance Relays">
Contact your instance administrator for the relay server IP addresses or
hostnames configured for your deployment. **Firewall requirements:** Allow
outbound TCP connections to instance relay servers on port 2222.
</Tab>
</Tabs>
## Protocol Details
### SSH over TCP
The gateway uses SSH reverse tunnels for primary communication:
- **Port 2222**: SSH connection to relay servers
- **Built-in features**: Automatic reconnection, certificate-based authentication, encrypted tunneling
- **Encryption**: SSH with certificate-based authentication and key exchange
## Firewall Configuration for SSH
The gateway uses standard SSH over TCP, making firewall configuration straightforward.
### TCP Connection Handling
SSH connections over TCP are stateful and handled seamlessly by all modern firewalls:
- **Established connections** are automatically tracked
- **Return traffic** is allowed for established outbound connections
- **No special configuration** needed for connection tracking
- **Standard SSH protocol** that enterprise firewalls handle well
### Simplified Firewall Rules
Since SSH uses TCP, you only need simple outbound rules:
1. **Allow outbound TCP** to relay servers (IP addresses or hostnames) on port 2222
2. **Allow outbound HTTPS** to Infisical API endpoints on port 443
3. **No inbound rules required** - all connections are outbound only
## Common Network Scenarios
### Corporate Firewalls
For corporate environments with strict egress filtering:
1. **Allow outbound TCP** to relay servers (IP addresses or hostnames) on port 2222
2. **Allow outbound HTTPS** to the Infisical API server on port 443
3. **No inbound rules required** - all connections are outbound only
4. **Standard TCP rules** - simple and straightforward configuration
### Cloud Environments (AWS/GCP/Azure)
Configure security groups to allow:
- **Outbound TCP** to relay servers (IP addresses or hostnames) on port 2222
- **Outbound HTTPS** to app.infisical.com/eu.infisical.com on port 443
- **No inbound rules required** - SSH reverse tunnels are outbound only
## Frequently Asked Questions
<Accordion title="What happens if there is a network interruption?">
The gateway is designed to handle network interruptions gracefully:
- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers if the SSH connection is lost
- **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention
- **Persistent SSH tunnels**: SSH connections are automatically re-established when connectivity is restored
- **Certificate rotation**: The gateway handles certificate renewal automatically during reconnection
- **Graceful degradation**: The gateway logs connection issues and continues attempting to restore connectivity
No manual intervention is typically required during network interruptions.
</Accordion>
<Accordion title="Why does the gateway use SSH over TCP?">
SSH over TCP provides several advantages for enterprise gateway communication:
- **Firewall-friendly**: TCP is stateful and handled seamlessly by all enterprise firewalls
- **Standard protocol**: SSH is a well-established protocol that network teams are familiar with
- **Certificate-based security**: Uses SSH certificates for strong authentication without shared secrets
- **Automatic tunneling**: SSH reverse tunnels handle all the complexity of secure communication
- **Enterprise compatibility**: Works reliably across all enterprise network configurations
TCP's reliability and firewall compatibility make it ideal for enterprise environments where network policies are strictly managed.
</Accordion>
<Accordion title="Do I need to open any inbound ports on my firewall?">
No inbound ports need to be opened. The gateway only makes outbound connections:
- **Outbound SSH** to relay servers on port 2222
- **Outbound HTTPS** to Infisical API endpoints on port 443
- **SSH reverse tunnels** handle all communication - no return traffic configuration needed
This design maintains security by avoiding the need for inbound firewall rules that could expose your network to external threats.
</Accordion>
<Accordion title="What if my firewall blocks SSH connections?">
If your firewall has strict outbound restrictions:
1. **Work with your network team** to allow outbound TCP connections on port 2222 to relay servers (IP addresses or hostnames)
2. **Allow standard SSH traffic** - most enterprises already have SSH policies in place
3. **Consider network policy exceptions** for the gateway host if needed
4. **Monitor firewall logs** to identify which specific rules are blocking traffic
</Accordion>
<Accordion title="How many relay servers does the gateway connect to?">
The gateway connects to **one relay server**:
- **Single SSH connection**: Each gateway establishes one SSH reverse tunnel to its assigned relay server
- **Named relay assignment**: Gateways connect to the specific relay server specified by `--relay`
- **Automatic reconnection**: If the relay connection is lost, the gateway automatically reconnects to the same relay
- **Certificate-based authentication**: Each connection uses SSH certificates issued by Infisical for secure authentication
</Accordion>
<Accordion title="Can the relay servers decrypt traffic going through them?">
No, relay servers cannot decrypt any traffic passing through them due to end-to-end encryption:
- **Client-to-Gateway mTLS (via TLS-pinned tunnel)**: Clients connect via a proxy that establishes a TLS-pinned tunnel to the gateway; mTLS between the client and gateway is negotiated inside this tunnel, encrypting all application traffic
- **SSH tunnel encryption**: The mTLS-encrypted traffic is then transmitted through SSH reverse tunnels to relay servers
- **Double encryption**: Traffic is encrypted twice - once by client mTLS and again by SSH tunnels
- **Relay only routes traffic**: The relay server only routes the doubly-encrypted traffic without access to either encryption layer
- **No data storage**: Relay servers do not store any traffic or sensitive information
- **Certificate isolation**: Each connection uses unique certificates, ensuring complete tenant isolation
The relay infrastructure is designed as a secure routing mechanism where only the client and gateway can decrypt the actual application traffic.
</Accordion>

View File

@@ -1,19 +1,14 @@
---
title: "Gateway"
title: "Gateway Overview"
sidebarTitle: "Overview"
description: "How to access private network resources from Infisical"
---
![Architecture Overview](../../../images/platform/gateways/gateway-highlevel-diagram.png)
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
**Architecture Components:**
- **Gateway**: Lightweight agent deployed within your VPCs that provides access to private resources
- **Relay**: Infrastructure that routes encrypted traffic (instance-wide or organization-specific)
Common use cases include generating dynamic credentials or rotating credentials for private databases.
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
This is particularly useful when Infisical isn't hosted within the same network as the resources it needs to reach.
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
<Info>
Gateway is a paid feature available under the Enterprise Tier for Infisical
@@ -22,428 +17,62 @@ Common use cases include generating dynamic credentials or rotating credentials
license.
</Info>
## Core Components
The Gateway system consists of two primary components working together to enable secure network access:
<Tabs>
<Tab title="Gateway" icon="server">
A Gateway is a lightweight service that you deploy within your own network infrastructure to provide secure access to your private resources. Think of it as a secure bridge between Infisical and your internal systems.
Gateways must be deployed within the same network where your target resources are located, with direct network connectivity to the private resources you want Infisical to access.
For different networks, regions, or isolated environments, you'll need to deploy separate gateways.
**Core Functions:**
- **Network Placement**: Deployed within your VPCs, data centers, or on-premises infrastructure where your private resources live
- **Connection Model**: Only makes outbound connections to Infisical's relay servers, so no inbound firewall rules are needed
- **Security Method**: Uses SSH reverse tunnels with certificate-based authentication for maximum security
- **Resource Access**: Acts as a proxy to connect Infisical to your private databases, APIs, and other services
</Tab>
<Tab title="Relay Server" icon="route">
A Relay Server is the routing infrastructure that enables secure communication between the Infisical platform and your deployed gateways. It acts as an intermediary that never sees your actual data.
**Core Functions:**
- **Traffic Routing**: Routes encrypted traffic between the Infisical platform and your gateways without storing or inspecting the data
- **Network Isolation**: Enables secure communication without requiring direct network connections between Infisical and your private infrastructure
- **Authentication Management**: Validates SSH certificates and manages secure routing between authenticated gateways
**Deployment Options:**
To reduce operational overhead, Infisical Cloud (US/EU) provides managed relay infrastructure, though organizations can also deploy their own relays for reduced latency.
- **Infisical Managed**: Use pre-deployed relays in select regions, shared across all Infisical Cloud organizations. Each organization traffic is isolated and encrypted.
- **Self-Deployed**: Deploy your own dedicated relay servers geographically close to your infrastructure for reduced latency.
</Tab>
</Tabs>
## How It Works
The Gateway system uses SSH reverse tunnels for secure, firewall-friendly connectivity:
1. **Gateway Registration**: The gateway establishes an outbound SSH reverse tunnel to a relay server using SSH certificates issued by Infisical
2. **Relay Routing**: The relay server routes encrypted traffic between the Infisical platform and gateways
3. **Resource Access**: The Infisical platform connects to your private resources through the established gateway connections
**Key Benefits:**
- **No inbound firewall rules needed** - all connections are outbound from your network
- **Firewall-friendly** - uses standard SSH over TCP
- **Certificate-based authentication** provides enhanced security
- **Automatic reconnection** if connections are lost
## Deployment
The Infisical Gateway is integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage.
You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary.
For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview).
**Prerequisites:**
1. **Relay Server**: Before deploying gateways, you need a running relay server:
- **Infisical Cloud**: Instance relays are already available - no setup needed
- **Self-hosted**: Instance admin must set up shared instance relays, or organizations can deploy their own
2. **Machine Identity**: Configure a machine identity with appropriate permissions to create and manage gateways
Once authenticated, the Gateway establishes an SSH reverse tunnel to the specified relay server, allowing secure access to your private resources.
### Get started
<Steps>
<Step title="Create a Gateway Identity">
1. Navigate to **Organization Access Control** in your Infisical dashboard.
2. Create a dedicated machine identity for your Gateway.
3. **Best Practice:** Assign a unique identity to each Gateway for better security and management.
![Create Gateway Identity](../../../images/platform/gateways/create-identity-for-gateway.png)
</Step>
<Step title="Configure Authentication Method">
You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities).
</Step>
<Step title="Choose Your Relay Setup">
You have two options for relay infrastructure:
<Tabs>
<Tab title="Use Instance Relays (Easiest)">
**Infisical Cloud:** Instance relays are already running and available - **no setup required**. You can immediately proceed to deploy gateways using these shared relays.
**Self-hosted:** If your instance admin has set up shared instance relays, you can use them directly. If not, the instance admin can set them up:
```bash
# Instance admin sets up shared relay (one-time setup)
export INFISICAL_RELAY_AUTH_SECRET=<instance-relay-secret>
infisical relay start --type=instance --ip=<public-ip> --name=<relay-name>
```
</Tab>
<Tab title="Deploy Your Own Organization Relay">
**Available for all users:** Deploy your own dedicated relay infrastructure for enhanced control:
```bash
# Deploy organization-specific relay
infisical relay start --type=org --ip=<public-ip> --name=<relay-name> --auth-method=universal-auth --client-id=<client-id> --client-secret=<client-secret>
```
**When to choose this:**
- You need lower latency (deploy closer to your resources)
- Enhanced security requirements
- Compliance needs (data sovereignty, air-gapped environments)
- Custom network policies
</Tab>
</Tabs>
</Step>
<Step title="Deploy the Gateway">
Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production:
<Tabs>
<Tab title="Production (systemd)">
For production deployments on Linux, install the Gateway as a systemd service:
<Warning>
**Gateway v2:** The `infisical gateway systemd install` command deploys the new Gateway v2 component.
If you are migrating from Gateway v1 (legacy `infisical gateway install` command), this is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID.
</Warning>
```bash
sudo infisical gateway systemd install --token <your-machine-identity-token> --domain <your-infisical-domain> --name <gateway-name> --relay <relay-name>
sudo systemctl start infisical-gateway
```
This will install and start the Gateway as a secure systemd service that:
- Runs with restricted privileges:
- Runs as root user (required for secure token management)
- Restricted access to home directories
- Private temporary directory
- Automatically restarts on failure
- Starts on system boot
- Manages token and domain configuration securely in `/etc/infisical/gateway.conf`
<Warning>
The install command requires:
- Linux operating system
- Root/sudo privileges
- Systemd
</Warning>
</Tab>
<Tab title="Production (Helm)">
The Gateway can be installed via [Helm](https://helm.sh/). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
For production deployments on Kubernetes, install the Gateway using the Infisical Helm chart:
### Install the latest Helm Chart repository
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
```
### Update the Helm Chart repository
```bash
helm repo update
```
### Create a Kubernetes Secret containing gateway environment variables
The gateway supports all identity authentication methods through the use of environment variables.
The environment variables must be set in the `infisical-gateway-environment` Kubernetes secret.
#### Supported authentication methods
<AccordionGroup>
<Accordion title="Universal Auth">
The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" type="string" required>
Your machine identity client ID.
</ParamField>
<ParamField query="INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET" type="string" required>
Your machine identity client secret.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `universal-auth` when using Universal Auth.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment \
--from-literal=INFISICAL_AUTH_METHOD=universal-auth \
--from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id> \
--from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret> \
--from-literal=INFISICAL_RELAY_NAME=<relay-name> \
--from-literal=INFISICAL_GATEWAY_NAME=<gateway-name>
```
</Accordion>
<Accordion title="Native Kubernetes">
The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH" type="string" optional>
Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `kubernetes` when using Native Kubernetes.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=kubernetes --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>
```
</Accordion>
<Accordion title="Native Azure">
The Native Azure method is used to authenticate with Infisical when running in an Azure environment.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `azure` when using Native Azure.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=azure --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>
```
</Accordion>
<Accordion title="Native GCP ID Token">
The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `gcp-id-token` when using Native GCP ID Token.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=gcp-id-token --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>
```
</Accordion>
<Accordion title="GCP IAM">
The GCP IAM method is used to authenticate with Infisical with a GCP service account key.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_GCP_SERVICE_ACCOUNT_KEY_FILE_PATH" type="string" required>
Path to your GCP service account key file _(Must be in JSON format!)_
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `gcp-iam` when using GCP IAM.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=gcp-iam --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id> --from-literal=INFISICAL_GCP_SERVICE_ACCOUNT_KEY_FILE_PATH=<service-account-key-file-path>
```
</Accordion>
<Accordion title="Native AWS IAM">
The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `aws-iam` when using Native AWS IAM.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=aws-iam --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>
```
</Accordion>
<Accordion title="OIDC Auth">
The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_JWT" type="string" required>
The OIDC JWT from the identity provider.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `oidc-auth` when using OIDC Auth.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=oidc-auth --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id> --from-literal=INFISICAL_JWT=<oidc-jwt>
```
</Accordion>
<Accordion title="JWT Auth">
The JWT Auth method is used to authenticate with Infisical via a JWT token.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_JWT" type="string" required>
The JWT token to use for authentication.
</ParamField>
<ParamField query="INFISICAL_MACHINE_IDENTITY_ID" type="string" required>
Your machine identity ID.
</ParamField>
<ParamField query="INFISICAL_AUTH_METHOD" type="string" required>
The authentication method to use. Must be `jwt-auth` when using JWT Auth.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=jwt-auth --from-literal=INFISICAL_JWT=<jwt> --from-literal=INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>
```
</Accordion>
<Accordion title="Token Auth">
You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token.
<ParamField query="Environment Variables">
<Expandable title="properties">
<ParamField query="INFISICAL_TOKEN" type="string" required>
The machine identity access token to use for authentication.
</ParamField>
</Expandable>
</ParamField>
```bash
kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_TOKEN=<token>
```
</Accordion>
</AccordionGroup>
#### Required environment variables
In addition to the authentication method above, you **must** include these required variables:
<AccordionGroup>
<Accordion title="INFISICAL_RELAY_NAME">
The name of the relay server that this gateway should connect to.
</Accordion>
<Accordion title="INFISICAL_GATEWAY_NAME">
The name of this gateway instance.
</Accordion>
</AccordionGroup>
**Complete example with required variables:**
```bash
kubectl create secret generic infisical-gateway-environment \
--from-literal=INFISICAL_AUTH_METHOD=universal-auth \
--from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id> \
--from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret> \
--from-literal=INFISICAL_RELAY_NAME=<relay-name> \
--from-literal=INFISICAL_GATEWAY_NAME=<gateway-name>
```
#### Other environment variables
<AccordionGroup>
<Accordion title="INFISICAL_API_URL">
The API URL to use for the gateway. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`.
</Accordion>
</AccordionGroup>
### Install the Infisical Gateway Helm Chart
<Warning>
**Version mapping:** Helm chart versions `>= 1.0.0` contain the new Gateway v2 component. Helm chart versions `<= 0.0.5` contain the legacy Gateway v1 component.
If you are moving from Gateway v1 (chart `<= 0.0.5`) to Gateway v2 (chart `>= 1.0.0`), this is not in-place. Gateway v2 provisions new gateway instances with new gateway IDs. Update any resources that reference a gateway ID (for example: dynamic secret configs, app connections, or other gateway-bound resources) to use the new Gateway v2 gateway ID.
</Warning>
```bash
helm install infisical-gateway infisical-helm-charts/infisical-gateway
```
### Check the gateway logs
After installing the gateway, you can check the logs to ensure it's running as expected.
```bash
kubectl logs deployment/infisical-gateway
```
You should see the following output which indicates the gateway is running as expected.
```bash
$ kubectl logs deployment/infisical-gateway
12:43AM INF Starting gateway
12:43AM INF Starting gateway certificate renewal goroutine
12:43AM INF Successfully registered gateway and received certificates
12:43AM INF Connecting to relay server infisical-start on 152.42.218.156:2222...
12:43AM INF Relay connection established for gateway
12:43AM INF Received incoming connection, starting TLS handshake
12:43AM INF TLS handshake completed successfully
12:43AM INF Negotiated ALPN protocol: infisical-ping
12:43AM INF Starting ping handler
12:43AM INF Ping handler completed
12:43AM INF Gateway is reachable by Infisical
```
</Tab>
<Tab title="Local Installation (testing)">
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
```bash
infisical gateway start --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) --relay=<relay-name> --name=<gateway-name>
```
Alternatively, if you already have the token, use it directly with the `--token` flag:
```bash
infisical gateway start --token <your-machine-identity-token> --relay=<relay-name> --name=<gateway-name>
```
Or set it as an environment variable:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
infisical gateway start --relay=<relay-name> --name=<gateway-name>
```
</Tab>
</Tabs>
For detailed information about the gateway commands and their options, see the [gateway command documentation](/cli/commands/gateway).
<Note>
**Requirements:**
- Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical
- The gateway must be able to reach the relay server (outbound connection only)
- Replace `<relay-name>` with the name of your relay server and `<gateway-name>` with a unique name for this gateway
</Note>
</Step>
<Step title="Verify Gateway Deployment">
To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list.
![Gateway List](../../../images/platform/gateways/gateway-list.png)
</Step>
</Steps>
2. **Persistent Connection**: The gateway maintains an open TCP connection with the relay server, creating a secure channel for incoming requests
3. **Request Routing**: When Infisical needs to access your resources, requests are routed through the relay server to the already-established gateway connection
4. **Resource Access**: The gateway receives the routed requests and connects to your private resources on behalf of Infisical
## Getting Started
Ready to set up your gateway? Follow the guides below.
<Columns cols={2}>
<Card title="Gateway Deployment" href="/documentation/platform/gateways/gateway-deployment">
Deploy and configure your gateway within your network infrastructure.
</Card>
<Card title="Relay Deployment" href="/documentation/platform/gateways/relay-deployment">
Set up relay servers if using self-deployed infrastructure.
</Card>
</Columns>
<Columns cols={1}>
<Card title="Security Architecture" href="/documentation/platform/gateways/security">
Learn about the security model and implementation best practices.
</Card>
</Columns>

View File

@@ -0,0 +1,243 @@
---
title: "Relay Deployment"
description: "How to deploy Infisical Relay Servers"
---
Infisical Relay is a secure routing layer that allows Infisical to connect to your private network resources, such as databases or internal APIs, without exposing them to the public internet.
The relay acts as an intermediary, forwarding encrypted traffic between Infisical and your deployed gateways. This ensures that your sensitive data remains protected and never leaves your network unencrypted.
With this architecture, you can achieve secure, firewall-friendly access across network boundaries, making it possible for Infisical to interact with resources even in highly restricted environments.
Before diving in, it's important to determine whether you actually need to deploy your own relay server or if you can use Infisical's managed infrastructure.
## Do You Need to Deploy a Relay?
Not all users need to deploy their own relay servers. Infisical provides managed relay infrastructure in US/EU regions for Infisical Cloud users, which requires no setup or maintenance. You only need to deploy a relay if you:
- Are self-hosting Infisical
- Have a dedicated enterprise instance of Infisical (managed by Infisical)
- Require closer geographic proximity to target resources than managed relays provide for lower latency and reduced network congestion when accessing resources through the relay
- Need full control over relay infrastructure and traffic routing
If you are using Infisical Cloud and do not have specific requirements, you can use the managed relays provided by Infisical and skip the rest of this guide.
## Deployment Steps
To successfully deploy an Infisical Relay for use, follow these steps in order.
<Steps>
<Step title="Provision a Machine Identity">
Create a machine identity with the correct permissions to create and manage relays. This identity is used by the relay to authenticate with Infisical and should be provisioned in advance.
The relay supports several [machine identity auth methods](/documentation/platform/identities/machine-identities) for authentication, as listed below. Choose the one that best fits your environment and set the corresponding environment variables when deploying the relay.
<AccordionGroup>
<Accordion title="Universal Auth">
Simple and secure authentication using client ID and client secret.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=universal-auth`
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id>`
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret>`
</Accordion>
<Accordion title="Token Auth">
Direct authentication using a machine identity access token.
**Environment Variables:**
- `INFISICAL_TOKEN=<token>`
</Accordion>
<Accordion title="Native Kubernetes">
Authentication using Kubernetes service account tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=kubernetes`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="Native AWS IAM">
Authentication using AWS IAM roles.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=aws-iam`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="Native GCP ID Token">
Authentication using GCP identity tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=gcp-id-token`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="GCP IAM">
Authentication using GCP service account keys.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=gcp-iam`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
- `INFISICAL_GCP_SERVICE_ACCOUNT_KEY_FILE_PATH=<path-to-key-file>`
</Accordion>
<Accordion title="Native Azure">
Authentication using Azure managed identity.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=azure`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
</Accordion>
<Accordion title="OIDC Auth">
Authentication using OIDC identity tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=oidc-auth`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
- `INFISICAL_JWT=<oidc-jwt>`
</Accordion>
<Accordion title="JWT Auth">
Authentication using JWT tokens.
**Environment Variables:**
- `INFISICAL_AUTH_METHOD=jwt-auth`
- `INFISICAL_MACHINE_IDENTITY_ID=<machine-identity-id>`
- `INFISICAL_JWT=<jwt>`
</Accordion>
</AccordionGroup>
</Step>
<Step title="Install the Infisical CLI">
Install the Infisical CLI on the server where you plan to deploy the relay. The CLI is required for relay installation and management.
See the [CLI Installation Guide](/cli/overview) for instructions.
This server must have a static IP address or DNS name to be identifiable by the Infisical platform.
</Step>
<Step title="Configure Network & Firewall">
Ensure your network and firewall settings allow the server to accept inbound connections and make outbound connections:
**Inbound Connections Rules:**
| Protocol | Source | Port | Purpose |
| -------- | ------------------ | ---- | -------------------------------- |
| TCP | Gateways | 2222 | SSH reverse tunnel establishment |
| TCP | Infisical instance host (US/EU, other) | 8443 | Platform-to-relay communication |
**Outbound Connections Rules:**
| Protocol | Destination | Port | Purpose |
| -------- | ------------------------------------ | ---- | ------------------------------------------ |
| TCP | Infisical instance host (US/EU, other) | 443 | API communication and certificate requests |
</Step>
<Step title="Select a Deployment Method">
The Infisical CLI is used to install and start the relay in your chosen environment. The CLI provides commands for both production and development scenarios, and supports a variety of options/flags to configure your deployment.
To view all available flags and equivalent environment variables for relay deployment, see the [Relay CLI Command Reference](/cli/commands/relay).
<Tabs>
<Tab title="Linux Server">
For production deployments on Linux servers, install the Relay as a systemd service. This installation method only supports [Token Auth](/documentation/platform/identities/token-auth) at the moment.
Once you have a [Token Auth](/documentation/platform/identities/token-auth) token, set the following environment variables for relay authentication:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
```
<Warning>
The systemd install command requires a Linux operating system with root/sudo privileges.
</Warning>
```bash
sudo infisical relay systemd install \
--token <your-machine-identity-token> \
--name <relay-name> \
--domain <your-infisical-domain> \
--host <static-ip-or-dns-of-the-server>
# Start the relay service
sudo systemctl start infisical-relay
sudo systemctl enable infisical-relay
```
</Tab>
<Tab title="Other Environments">
For non-Linux systems or when you need more control over the relay process:
```bash
infisical relay start \
--type=<type> \
--host=<host> \
--name=<name> \
--auth-method=<auth-method>
```
This method supports all [machine identity auth methods](/documentation/platform/identities/machine-identities) and runs in the foreground. Suitable for production use on non-Linux systems or development environments.
Set the appropriate environment variables for your chosen auth method as described in Step 1 before running the relay start command.
</Tab>
</Tabs>
</Step>
</Steps>
## Frequently Asked Questions
<Accordion title="Can the relay servers decrypt traffic going through them?">
No, relay servers cannot decrypt any traffic passing through them due to end-to-end encryption:
- **Client-to-Gateway mTLS (via TLS-pinned tunnel)**: Clients connect via a proxy that establishes a TLS-pinned tunnel to the gateway; mTLS between the client and gateway is negotiated inside this tunnel, encrypting all application traffic
- **SSH tunnel encryption**: The mTLS-encrypted traffic is then transmitted through SSH reverse tunnels to relay servers
- **Double encryption**: Traffic is encrypted twice - once by client mTLS and again by SSH tunnels
- **Relay only routes traffic**: The relay server only routes the doubly-encrypted traffic without access to either encryption layer
The relay infrastructure is designed as a secure routing mechanism where only the client and gateway can decrypt the actual application traffic.
</Accordion>
<Accordion title="What are the benefits of deploying my own relay?">
Deploying your own relay provides several advantages:
- **Dedicated resources**: Full control over relay infrastructure and performance
- **Lower latency**: Deploy closer to your gateways for optimal performance
- **Compliance**: Meet specific data routing and compliance requirements
- **Custom network policies**: Implement organization-specific network configurations
- **Geographic proximity**: Reduce network congestion and improve response times to access resources
- **High availability**: Deploy multiple relays for redundancy and load distribution
Organization-deployed relays give you complete control over your secure communication infrastructure.
</Accordion>
<Accordion title="How do I troubleshoot connectivity issues?">
For detailed troubleshooting:
**Platform cannot connect to relay:**
- Check firewall rules allow inbound TCP with TLS on port 8443
- Test connectivity: `openssl s_client -connect <relay-ip>:8443`
**Test network connectivity:**
```bash
# Test outbound API access from relay. Replace URL with your Infisical instance if self-hosted
curl -I https://app.infisical.com
# Test TCP with TLS port from platform
openssl s_client -connect <relay-ip>:8443
```
</Accordion>
<Accordion title="What happens if my relay server goes down?">
Relay server outages affect gateway connectivity:
- **Gateway reconnection**: Gateways will automatically attempt to reconnect when the relay comes back online
- **Service interruption**: While the relay is down, the Infisical platform cannot reach gateways through that relay. As a result, any secrets or resources accessed via those gateways will be temporarily unavailable until connectivity is restored.
- **Multiple relays**: Deploy multiple relay servers for redundancy and high availability
- **Automatic restart**: Use systemd or container orchestration to automatically restart failed relay services
For production environments, consider deploying multiple relay servers to avoid single points of failure.
</Accordion>

View File

@@ -1,13 +1,9 @@
---
title: "Gateway Security Architecture"
sidebarTitle: "Architecture"
description: "Understand the security model and tenant isolation of Infisical's Gateway"
title: "Security Architecture"
description: "Security model, tenant isolation, and best practices for Infisical Gateways and Relays"
---
# Gateway Security Architecture
The Infisical Gateway enables secure access to private resources using SSH reverse tunnels, certificate-based authentication, and a comprehensive PKI (Public Key Infrastructure) system. The architecture provides end-to-end encryption and complete tenant isolation through multiple certificate authorities.
This document explains the internal security architecture and how tenant isolation is maintained.
## Security Model Overview
@@ -82,16 +78,16 @@ The platform establishes secure direct connections with gateways through a **TLS
2. **Connection Flow**:
```
Platform ←→ [SSH Reverse Tunnel] ←→ Gateway
Platform ←→ [TCP with TLS] ←→ Relay ←→ [SSH Reverse Tunnel] ←→ Gateway
```
- Gateway maintains persistent outbound SSH tunnel to relay server
- Platform connects directly to gateway through this tunnel
- TLS handshake occurs over the SSH tunnel, establishing mTLS connection
- Application traffic flows through the TLS-pinned tunnel
- Platform connects to relay server using TCP with TLS
- Relay routes encrypted traffic between platform and gateway
- TLS handshake occurs between platform and gateway through the relay
- Application traffic flows through the TLS-pinned tunnel via relay routing
3. **Security Benefits**:
- **No inbound connections**: Gateway never needs to accept incoming connections
- **Certificate-based authentication**: Uses Organization Gateway certificates for mutual TLS
- **Double encryption**: TLS traffic within SSH tunnel provides layered security
@@ -132,7 +128,6 @@ The architecture provides tenant isolation through multiple certificate authorit
- Ephemeral certificate validation ensures time-bound access
2. **Network Isolation**:
- Each organization's traffic flows through isolated certificate-authenticated channels
- Relay servers route traffic based on certificate validation without content access
- Gateway validates all incoming connections against Organization Gateway Client CA

View File

@@ -40,14 +40,10 @@ To interact with various resources in Infisical, Machine Identities can authenti
## Identity Lockout
Lockout is a feature that prevents brute-force attacks on identity login endpoints. Auth methods that support lockout include: [Universal Auth](/documentation/platform/identities/universal-auth).
Lockout is a feature that prevents brute-force attacks on identity login endpoints. Auth methods that support lockout include: [Universal Auth](/documentation/platform/identities/universal-auth), [LDAP Auth](/documentation/platform/identities/ldap-auth/general).
Supported auth methods have lockout enabled by default. If triggered, lockout temporarily disables the login endpoint for 5 minutes after 3 consecutive failed login attempts within a 30-second window. Lockout can be configured and disabled in the identity auth method settings.
<Warning>
When Lockout is enabled, a rate limit of approximately 10 requests per second is enforced on relevant authentication endpoints. This security measure employs a protective lock to mitigate parallel login attacks. If this rate limitation interferes with your operational requirements, you may consider disabling Lockout.
</Warning>
## FAQ
<AccordionGroup>

View File

@@ -366,3 +366,21 @@ password = "{{ .Value }}"
**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment`
</Accordion>
<Accordion title="dynamic_secret">
```bash
dynamic_secret "<project-slug>" "<environment-slug>" "<secret-path>" "<dynamic-secret-name>" "<lease-ttl>"
```
```bash example-redis-dynamic-secret
{{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }}
{{ .DB_USERNAME }}={{ .DB_PASSWORD }}
{{- end }}
**Function Name**: dynamic_secret
**Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date.
**Returns**: An object with keys corresponding to the dynamic secret lease credentials.
```
</Accordion>

View File

@@ -59,9 +59,10 @@ The Infisical Agent Injector supports the following annotations:
The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create.
</Accordion>
<Accordion title="org.infisical.com/inject-mode">
The inject mode annotation is used to specify the mode to use to inject the secrets into the pod. Currently only `init` mode is supported.
The inject mode annotation is used to specify the mode to use to inject the secrets into the pod.
- `init`: The init method will create an init container for the pod that will render the secrets into a shared volume mount within the pod. The agent init container will run before any other containers in the pod runs, including other init containers.
- `sidecar`: The sidecar method will create a sidecar container for the pod that will render the secrets into a shared volume mount within the pod. The agent sidecar container will run alongside the main container in the pod. This means that the secrets rendered will always be in sync with your Infisical secrets.
</Accordion>
<Accordion title="org.infisical.com/agent-config-map">
The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod.
@@ -203,7 +204,7 @@ metadata:
app: demo
annotations:
org.infisical.com/inject: "true" # Set to true for the injector to patch the pod on create/update events
org.infisical.com/inject-mode: "init" # The mode to use to inject the secrets into the pod. Currently only `init` mode is supported.
org.infisical.com/inject-mode: "init" # The mode to use to inject the secrets into the pod. init|sidecar
org.infisical.com/agent-config-map: "name-of-config-map" # The name of the config map that you created above, which contains all the settings for injecting the secrets into the pod
spec:
# ...

View File

@@ -146,6 +146,7 @@ spec:
projectSlug: <project-slug> # <-- project slug
projectId: <project-id> # <-- project id
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: "<secrets-path>" # Root is "/"
credentialsRef:
@@ -331,6 +332,7 @@ spec:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
recursive: true
...
```
@@ -526,6 +528,7 @@ spec:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
recursive: true
...
```
@@ -574,6 +577,7 @@ spec:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
recursive: true
...
```
@@ -619,6 +623,7 @@ spec:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
recursive: true
...
```
@@ -664,6 +669,7 @@ spec:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
recursive: true
...
```
@@ -711,6 +717,7 @@ spec:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
recursive: true
...
```
@@ -764,6 +771,7 @@ spec:
projectSlug: <project-slug> # <-- project slug
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: "<secrets-path>" # Root is "/"
secretName: <secret-name> # OPTIONAL: If you want to fetch a single Infisical secret, you can specify the secret name here. If not specified, all secrets in the specified scope will be fetched.
identityId: <machine-identity-id>
credentialsRef:
secretName: ldap-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials

View File

@@ -218,6 +218,15 @@ Supports conditions and permission inversion
| `delete-gateways` | Remove gateways from organization |
| `attach-gateways` | Attach gateways to resources |
#### Subject: `relay`
| Action | Description |
| --------------- | ------------------------------- |
| `list-relays` | View all organization relays |
| `create-relays` | Add new relays to organization |
| `edit-relays` | Modify existing relay settings |
| `delete-relays` | Remove relays from organization |
#### Subject: `machine-identity-auth-template`
| Action | Description |

View File

@@ -50,6 +50,7 @@ The SDK methods are organized into the following high-level categories:
4. `projects`: Creates and manages projects.
5. `environments`: Creates and manages environments.
6. `folders`: Creates and manages folders.
7. `kms`: Manages KMS keys and encryption/signing operations.
### `auth`
@@ -456,7 +457,7 @@ const project = await client.projects().create({
projectDescription: "<project-description>", // Optional
slug: "<slug-of-project-to-create>", // Optional
template: "<project-template-name>", // Optional
kmsKeyId: "kms-key-id" // Optional
kmsKeyId: "<kms-key-id>" // Optional
});
```
@@ -556,4 +557,223 @@ const folders = await client.folders().listFolders({
- `recursive` (boolean): An optional flag to list folders recursively. Defaults to `false`.
**Returns:**
- `Folder[]`: An array of folders.
- `Folder[]`: An array of folders.
### `kms`
The KMS (Key Management Service) module allows you to create and manage cryptographic keys for encryption and digital signing operations.
#### Create a new KMS key
Creating a new KMS key can be done by using the `.kms().keys().create({})` function. Keys can be created for either encryption/decryption or signing/verification operations.
##### Example for creating an encryption key
```typescript
import { InfisicalSDK, KeyUsage, EncryptionAlgorithm } from "@infisical/sdk";
const client = new InfisicalSDK();
await client.auth().universalAuth.login({
clientId: "CLIENT_ID",
clientSecret: "CLIENT_SECRET"
});
const encryptionKey = await client.kms().keys().create({
projectId: "your-project-id",
name: "my-encryption-key",
description: "Key for encrypting sensitive data",
keyUsage: KeyUsage.ENCRYPTION,
encryptionAlgorithm: EncryptionAlgorithm.AES_256_GCM
});
console.log(encryptionKey);
```
##### Example for creating a signing key
```typescript
const signingKey = await client.kms().keys().create({
projectId: "your-project-id",
name: "my-signing-key",
description: "Key for signing documents",
keyUsage: KeyUsage.SIGNING,
encryptionAlgorithm: EncryptionAlgorithm.RSA_4096
});
console.log(signingKey);
```
**Parameters:**
- `projectId` (string): The ID of your project.
- `name` (string): The name of the KMS key.
- `description` (string, optional): A description of the key's purpose.
- `keyUsage` (KeyUsage): Either `KeyUsage.ENCRYPTION` for encrypt/decrypt operations or `KeyUsage.SIGNING` for sign/verify operations.
- `encryptionAlgorithm` (EncryptionAlgorithm): The algorithm to use. Options include:
- For encryption: `AES_256_GCM`, `AES_128_GCM`, `RSA_4096`, `ECC_NIST_P256`
- For signing: `RSA_4096`, `ECC_NIST_P256`
**Returns:**
- `KmsKey`: The created KMS key object.
#### Get a KMS key by name
```typescript
const key = await client.kms().keys().getByName({
projectId: "your-project-id",
name: "my-encryption-key"
});
console.log(key);
```
**Parameters:**
- `projectId` (string): The ID of your project.
- `name` (string): The name of the KMS key to retrieve.
**Returns:**
- `KmsKey`: The KMS key object.
#### Delete a KMS key
```typescript
const deletedKey = await client.kms().keys().delete({
keyId: "<kms-key-id>"
});
console.log(deletedKey);
```
**Parameters:**
- `keyId` (string): The ID of the KMS key to delete.
**Returns:**
- `KmsKey`: The deleted KMS key object.
### `kms.encryption`
The encryption module provides operations for encrypting and decrypting data using KMS keys created with `KeyUsage.ENCRYPTION`.
#### Encrypt data
```typescript
const encrypted = await client.kms().encryption().encrypt({
keyId: "<kms-key-id>",
plaintext: "<base64-encoded-data>"
});
console.log(encrypted); // Returns the ciphertext string
```
**Parameters:**
- `keyId` (string): The ID of the encryption key.
- `plaintext` (string): The data to encrypt. This must be base64 encoded.
**Returns:**
- `string`: The encrypted ciphertext.
#### Decrypt data
```typescript
const decrypted = await client.kms().encryption().decrypt({
keyId: "<kms-key-id>",
ciphertext: "<encrypted-data>"
});
console.log(decrypted); // Returns the original plaintext
```
**Parameters:**
- `keyId` (string): The ID of the encryption key used to encrypt the data.
- `ciphertext` (string): The encrypted data to decrypt.
**Returns:**
- `string`: The decrypted plaintext.
### `kms.signing`
The signing module provides operations for digitally signing data and verifying signatures using KMS keys created with `KeyUsage.SIGNING`.
#### Sign data
```typescript
import { SigningAlgorithm } from "@infisical/sdk";
const signature = await client.kms().signing().sign({
keyId: "<kms-key-id>",
data: "<base64-encoded-data>",
signingAlgorithm: SigningAlgorithm.RSASSA_PSS_SHA_256,
isDigest: false // Optional: set to true if data is already a hash digest
});
console.log(signature);
```
**Parameters:**
- `keyId` (string): The ID of the signing key.
- `data` (string): The data to sign.
- `signingAlgorithm` (SigningAlgorithm): The signing algorithm to use. Available algorithms:
- **RSA PSS** (non-deterministic): `RSASSA_PSS_SHA_256`, `RSASSA_PSS_SHA_384`, `RSASSA_PSS_SHA_512`
- **RSA PKCS#1 v1.5** (deterministic): `RSASSA_PKCS1_V1_5_SHA_256`, `RSASSA_PKCS1_V1_5_SHA_384`, `RSASSA_PKCS1_V1_5_SHA_512`
- **ECDSA** (non-deterministic): `ECDSA_SHA_256`, `ECDSA_SHA_384`, `ECDSA_SHA_512`
- `isDigest` (boolean, optional): Whether the data is already a hash digest. Defaults to `false`.
**Returns:**
- `KmsSignDataResponse`: Object containing the signature, keyId, and signingAlgorithm.
#### Verify a signature
```typescript
const verification = await client.kms().signing().verify({
keyId: "<kms-key-id>",
data: "<base64-encoded-original-data>", // Must be base64 encoded
signature: "<data-signature>",
signingAlgorithm: SigningAlgorithm.RSASSA_PSS_SHA_256,
isDigest: false // Optional: set to true if data is already a hash digest
});
console.log(verification.signatureValid); // true or false
```
**Parameters:**
- `keyId` (string): The ID of the signing key used to create the signature.
- `data` (string): The original data that was signed (must be base64 encoded).
- `signature` (string): The signature to verify.
- `signingAlgorithm` (SigningAlgorithm): The same signing algorithm used to create the signature.
- `isDigest` (boolean, optional): Whether the data is already a hash digest. Defaults to `false`.
**Returns:**
- `KmsVerifyDataResponse`: Object containing `signatureValid` (boolean), `keyId`, and `signingAlgorithm`.
#### Get supported signing algorithms for a key
```typescript
const algorithms = await client.kms().signing().listSigningAlgorithms({
keyId: "<kms-key-id>"
});
console.log(algorithms); // Array of supported SigningAlgorithm values
```
**Parameters:**
- `keyId` (string): The ID of the KMS signing key.
**Returns:**
- `SigningAlgorithm[]`: Array of supported signing algorithms for the key.
#### Get public key
Retrieve the public key for signature verification operations.
```typescript
const publicKey = await client.kms().signing().getPublicKey({
keyId: "<kms-key-id>"
});
console.log(publicKey); // Returns the public key string
```
**Parameters:**
- `keyId` (string): The ID of the KMS signing key.
**Returns:**
- `string`: The public key in PEM format.

View File

@@ -29,17 +29,54 @@ const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
export const hasSecretReference = (value: string | undefined) =>
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
const createNodeId = (node: TSecretReferenceTraceNode): string =>
`${node.environment}:${node.secretPath}:${node.key}`;
const isCircularReference = (
node: TSecretReferenceTraceNode,
visitedPath: Set<string>
): boolean => {
const nodeId = createNodeId(node);
return visitedPath.has(nodeId);
};
const hasCircularReferences = (
node: TSecretReferenceTraceNode,
visitedPath: Set<string> = new Set()
): boolean => {
const nodeId = createNodeId(node);
if (visitedPath.has(nodeId)) {
return true;
}
const newVisitedPath = new Set([...visitedPath, nodeId]);
return node.children.some((child) => hasCircularReferences(child, newVisitedPath));
};
export const SecretReferenceNode = ({
node,
isRoot,
secretKey
secretKey,
visitedPath = new Set()
}: {
node: TSecretReferenceTraceNode;
isRoot?: boolean;
secretKey?: string;
visitedPath?: Set<string>;
}) => {
const [isOpen, setIsOpen] = useState(false);
const hasChildren = node.children.length > 0;
const nodeId = createNodeId(node);
const isCircular = !isRoot && isCircularReference(node, visitedPath);
const newVisitedPath = isCircular ? visitedPath : new Set([...visitedPath, nodeId]);
const safeChildren = isCircular
? []
: node.children.filter((child) => !isCircularReference(child, newVisitedPath));
const hasChildren = safeChildren.length > 0;
return (
<li>
@@ -77,8 +114,12 @@ export const SecretReferenceNode = ({
<Collapsible.Content className={twMerge("mt-4", style.collapsibleContent)}>
{hasChildren && (
<ul>
{node.children.map((el, index) => (
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
{safeChildren.map((el, index) => (
<SecretReferenceNode
node={el}
key={`${el.key}-${index + 1}`}
visitedPath={newVisitedPath}
/>
))}
</ul>
)}
@@ -102,6 +143,9 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const tree = data?.tree;
const secretValue = data?.value;
// Check if the tree contains circular references
const hasCirculars = tree ? hasCircularReferences(tree) : false;
useEffect(() => {
if (error instanceof AxiosError) {
const err = error?.response?.data as TApiErrors;
@@ -130,9 +174,25 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
);
}
if (tree?.children?.length === 0) {
return (
<div className="flex items-center justify-center py-4">
<span className="text-mineshaft-400">This secret does not contain references</span>
</div>
);
}
return (
<div>
<FormControl label="Expanded value">
<FormControl
label="Expanded value"
tooltipText={
hasCirculars
? "This secret contains circular references. Value shown is resolved once, with circular paths truncated in the reference tree below."
: undefined
}
tooltipClassName="max-w-md break-words"
>
<SecretInput
key="value-overriden"
isReadOnly

View File

@@ -1,5 +1,5 @@
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons";
import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
@@ -55,6 +55,8 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
secretPath?: string;
environment?: string;
containerClassName?: string;
isLoadingValue?: boolean;
isErrorLoadingValue?: boolean;
};
type ReferenceItem = {
@@ -175,13 +177,29 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
type: ReferenceType.SECRET
});
});
if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) {
suggestionsArr.push({
label: "No matches found",
slug: "__no_match__",
type: ReferenceType.SECRET
});
}
return suggestionsArr;
}, [secrets, folders, currentProject?.environments, isPopupOpen, suggestionSource.value]);
}, [
secrets,
folders,
currentProject?.environments,
isPopupOpen,
suggestionSource.value,
suggestionSource.predicate
]);
const handleSuggestionSelect = (selectIndex?: number) => {
const selectedSuggestion =
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
if (!selectedSuggestion) {
if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") {
return;
}
@@ -226,21 +244,40 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
if (isPopupOpen) {
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex + 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
let nextIndex = mod(prevIndex + 1, suggestions.length);
// Skip "no match" messages
while (
nextIndex < suggestions.length &&
suggestions[nextIndex].slug === "__no_match__"
) {
nextIndex = mod(nextIndex + 1, suggestions.length);
}
// If we only have no-match messages, don't highlight anything
if (suggestions[nextIndex]?.slug === "__no_match__") {
return -1;
}
popoverContentRef.current?.children?.[nextIndex]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
return nextIndex;
});
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex - 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
let prevIdx = mod(prevIndex - 1, suggestions.length);
// Skip "no match" messages
while (prevIdx >= 0 && suggestions[prevIdx].slug === "__no_match__") {
prevIdx = mod(prevIdx - 1, suggestions.length);
}
// If we only have no-match messages, don't highlight anything
if (suggestions[prevIdx]?.slug === "__no_match__") {
return -1;
}
popoverContentRef.current?.children?.[prevIdx]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
return prevIdx;
});
} else if (e.key === "Enter" && highlightedIndex >= 0) {
e.preventDefault();
@@ -279,11 +316,16 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
ref={handleRef}
onKeyDown={handleKeyDown}
value={value}
onFocus={() => setIsFocused.on()}
onFocus={(evt) => {
if (props.onFocus) props.onFocus(evt);
setIsFocused.on();
}}
onBlur={(evt) => {
// should not on blur when its mouse down selecting a item from suggestion
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
setIsFocused.off();
if (props.onBlur) props.onBlur(evt);
}}
onChange={(e) => onChange?.(e.target.value)}
containerClassName={containerClassName}
@@ -304,7 +346,12 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
{suggestions.map((item, i) => {
let entryIcon;
let subText;
if (item.type === ReferenceType.SECRET) {
const isNoMatchMessage = item.slug === "__no_match__";
if (isNoMatchMessage) {
entryIcon = <FontAwesomeIcon icon={faSearch} className="text-gray-400" />;
subText = "No results";
} else if (item.type === ReferenceType.SECRET) {
entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />;
subText = "Secret";
} else if (item.type === ReferenceType.ENVIRONMENT) {
@@ -315,10 +362,28 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
subText = "Folder";
}
return (
return isNoMatchMessage ? (
<div
tabIndex={0}
role="button"
role="status"
aria-label="no-match-message"
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div className="text-md relative flex w-full cursor-default select-none items-center justify-between px-2 py-2 opacity-75 outline-none transition-all">
<div className="flex w-full items-start gap-2">
<div className="mt-1 flex items-center">{entryIcon}</div>
<div className="text-md w-10/12 truncate text-left">
<span className="text-gray-400">{item.label}</span>
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
{subText}
</div>
</div>
</div>
</div>
</div>
) : (
<button
type="button"
onKeyDown={(e) => {
if (e.key === "Enter") handleSuggestionSelect(i);
}}
@@ -330,8 +395,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
handleSuggestionSelect(i);
}}
onMouseEnter={() => setHighlightedIndex(i)}
style={{ pointerEvents: "auto" }}
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
className="flex w-full items-center justify-between border-none border-mineshaft-600 bg-transparent p-0 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div
@@ -342,14 +406,14 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
<div className="flex w-full items-start gap-2">
<div className="mt-1 flex items-center">{entryIcon}</div>
<div className="text-md w-10/12 truncate text-left">
{item.label}
<span>{item.label}</span>
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
{subText}
</div>
</div>
</div>
</div>
</div>
</button>
);
})}
</div>

View File

@@ -7,7 +7,16 @@ import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPa
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
const syntaxHighlight = (
content?: string | null,
isVisible?: boolean,
isImport?: boolean,
isLoadingValue?: boolean,
isErrorLoadingValue?: boolean
) => {
if (isLoadingValue) return HIDDEN_SECRET_VALUE;
if (isErrorLoadingValue)
return <span className="ph-no-capture text-red/75">Error loading secret value.</span>;
if (isImport && !content) return "IMPORTED";
if (content === "") return "EMPTY";
if (!content) return "EMPTY";
@@ -20,7 +29,8 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
skipNext = true;
return (
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
&#36;&#123;<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#36;&#123;
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#125;
</span>
);
@@ -48,6 +58,8 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
isDisabled?: boolean;
containerClassName?: string;
canEditButNotView?: boolean;
isLoadingValue?: boolean;
isErrorLoadingValue?: boolean;
};
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
@@ -65,6 +77,8 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
isReadOnly,
onFocus,
canEditButNotView,
isLoadingValue,
isErrorLoadingValue,
...props
},
ref
@@ -83,7 +97,9 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
{syntaxHighlight(
value,
isVisible || (isSecretFocused && !valueAlwaysHidden),
isImport
isImport,
isLoadingValue,
isErrorLoadingValue
)}
</span>
</code>
@@ -114,7 +130,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
}}
value={value || ""}
{...props}
readOnly={isReadOnly}
readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
/>
</div>
</div>

View File

@@ -21,6 +21,13 @@ export enum OrgGatewayPermissionActions {
AttachGateways = "attach-gateways"
}
export enum OrgRelayPermissionActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
CreateTemplates = "create-templates",
@@ -51,6 +58,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share",
GithubOrgSync = "github-org-sync",
GithubOrgSyncManual = "github-org-sync-manual",
@@ -135,6 +143,7 @@ export type OrgPermissionSet =
OrgPermissionSubjects.MachineIdentityAuthTemplate
]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgRelayPermissionActions, OrgPermissionSubjects.Relay]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
| [
OrgPermissionAppConnectionActions,

View File

@@ -230,7 +230,23 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_ORG]: "Update Organization",
[EventType.CREATE_PROJECT]: "Create Project",
[EventType.UPDATE_PROJECT]: "Update Project",
[EventType.DELETE_PROJECT]: "Delete Project"
[EventType.DELETE_PROJECT]: "Delete Project",
[EventType.CREATE_SECRET_REMINDER]: "Create Secret Reminder",
[EventType.GET_SECRET_REMINDER]: "Get Secret Reminder",
[EventType.DELETE_SECRET_REMINDER]: "Delete Secret Reminder",
[EventType.DASHBOARD_LIST_SECRETS]: "Dashboard List Secrets",
[EventType.DASHBOARD_GET_SECRET_VALUE]: "Dashboard Get Secret Value",
[EventType.DASHBOARD_GET_SECRET_VERSION_VALUE]: "Dashboard Get Secret Version Value",
[EventType.CREATE_PROJECT_ROLE]: "Create Project Role",
[EventType.UPDATE_PROJECT_ROLE]: "Update Project Role",
[EventType.DELETE_PROJECT_ROLE]: "Delete Project Role",
[EventType.CREATE_ORG_ROLE]: "Create Org Role",
[EventType.UPDATE_ORG_ROLE]: "Update Org Role",
[EventType.DELETE_ORG_ROLE]: "Delete Org Role"
};
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {

View File

@@ -224,5 +224,21 @@ export enum EventType {
CREATE_PROJECT = "create-project",
UPDATE_PROJECT = "update-project",
DELETE_PROJECT = "delete-project"
DELETE_PROJECT = "delete-project",
CREATE_SECRET_REMINDER = "create-secret-reminder",
GET_SECRET_REMINDER = "get-secret-reminder",
DELETE_SECRET_REMINDER = "delete-secret-reminder",
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value",
CREATE_PROJECT_ROLE = "create-project-role",
UPDATE_PROJECT_ROLE = "update-project-role",
DELETE_PROJECT_ROLE = "delete-project-role",
CREATE_ORG_ROLE = "create-org-role",
UPDATE_ORG_ROLE = "update-org-role",
DELETE_ORG_ROLE = "delete-org-role"
}

View File

@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { apiRequest } from "@app/config/request";
@@ -10,13 +10,15 @@ import {
DashboardProjectSecretsOverview,
DashboardProjectSecretsOverviewResponse,
DashboardSecretsOrderBy,
DashboardSecretValue,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO
TGetDashboardProjectSecretsQuickSearchDTO,
TGetSecretValueDTO
} from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
@@ -73,6 +75,15 @@ export const dashboardKeys = {
...dashboardKeys.all(),
"accessible-secrets",
{ projectId, secretPath, environment, filterByAction }
] as const,
getSecretValuesRoot: () => [...dashboardKeys.all(), "secrets-values"] as const,
getSecretValue: ({ environment, secretPath, secretKey, isOverride }: TGetSecretValueDTO) =>
[
...dashboardKeys.getSecretValuesRoot(),
environment,
secretPath,
secretKey,
isOverride
] as const
};
@@ -174,6 +185,8 @@ export const useGetProjectSecretsOverview = (
"queryKey" | "queryFn"
>
) => {
const queryClient = useQueryClient();
return useQuery({
...options,
// wait for all values to be available
@@ -193,8 +206,8 @@ export const useGetProjectSecretsOverview = (
includeSecretRotations,
environments
}),
queryFn: () =>
fetchProjectSecretsOverview({
queryFn: async () => {
const resp = fetchProjectSecretsOverview({
secretPath,
search,
limit,
@@ -208,7 +221,14 @@ export const useGetProjectSecretsOverview = (
includeDynamicSecrets,
includeSecretRotations,
environments
}),
});
queryClient.invalidateQueries({
queryKey: dashboardKeys.getSecretValuesRoot()
});
return resp;
},
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
const { secrets, secretRotations, ...select } = data;
const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
@@ -254,7 +274,6 @@ export const useGetProjectSecretsDetails = (
search = "",
includeSecrets,
includeFolders,
viewSecretValue,
includeImports,
includeDynamicSecrets,
includeSecretRotations,
@@ -270,6 +289,8 @@ export const useGetProjectSecretsDetails = (
"queryKey" | "queryFn"
>
) => {
const queryClient = useQueryClient();
return useQuery({
...options,
// wait for all values to be available
@@ -286,7 +307,6 @@ export const useGetProjectSecretsDetails = (
limit,
orderBy,
orderDirection,
viewSecretValue,
offset,
projectId,
environment,
@@ -297,14 +317,13 @@ export const useGetProjectSecretsDetails = (
includeSecretRotations,
tags
}),
queryFn: () =>
fetchProjectSecretsDetails({
queryFn: async () => {
const resp = await fetchProjectSecretsDetails({
secretPath,
search,
limit,
orderBy,
orderDirection,
viewSecretValue,
offset,
projectId,
environment,
@@ -314,7 +333,14 @@ export const useGetProjectSecretsDetails = (
includeDynamicSecrets,
includeSecretRotations,
tags
}),
});
queryClient.invalidateQueries({
queryKey: dashboardKeys.getSecretValuesRoot()
});
return resp;
},
select: useCallback(
(data: Awaited<ReturnType<typeof fetchProjectSecretsDetails>>) => ({
...data,
@@ -471,3 +497,31 @@ export const useGetAccessibleSecrets = ({
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
});
};
export const fetchSecretValue = async (params: TGetSecretValueDTO) => {
const { data } = await apiRequest.get<DashboardSecretValue>("/api/v1/dashboard/secret-value", {
params
});
return data;
};
export const useGetSecretValue = (
params: TGetSecretValueDTO,
options?: Omit<
UseQueryOptions<
DashboardSecretValue,
unknown,
DashboardSecretValue,
ReturnType<typeof dashboardKeys.getSecretValue>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: dashboardKeys.getSecretValue(params),
queryFn: async () => fetchSecretValue(params),
staleTime: 1000 * 60,
...options
});
};

View File

@@ -112,7 +112,6 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
TGetDashboardProjectSecretsOverviewDTO,
"environments"
> & {
viewSecretValue: boolean;
environment: string;
includeImports?: boolean;
tags: Record<string, boolean>;
@@ -156,3 +155,21 @@ export type TGetAccessibleSecretsDTO = {
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};
export type TGetSecretValueDTO = {
projectId: string;
secretKey: string;
environment: string;
secretPath: string;
isOverride?: boolean;
};
export type DashboardSecretValue =
| {
value: string;
valueOverride: undefined;
}
| {
value: undefined;
valueOverride: string;
};

View File

@@ -59,6 +59,11 @@ export enum DynamicSecretAwsIamAuth {
IRSA = "irsa"
}
export enum DynamicSecretAwsIamCredentialType {
IamUser = "iam-user",
TemporaryCredentials = "temporary-credentials"
}
export type TDynamicSecretProvider =
| {
type: DynamicSecretProviders.SqlDatabase;
@@ -97,6 +102,7 @@ export type TDynamicSecretProvider =
inputs:
| {
method: DynamicSecretAwsIamAuth.AccessKey;
credentialType: DynamicSecretAwsIamCredentialType;
accessKey: string;
secretAccessKey: string;
region: string;
@@ -107,6 +113,7 @@ export type TDynamicSecretProvider =
}
| {
method: DynamicSecretAwsIamAuth.AssumeRole;
credentialType: DynamicSecretAwsIamCredentialType;
roleArn: string;
region: string;
awsPath?: string;
@@ -116,6 +123,7 @@ export type TDynamicSecretProvider =
}
| {
method: DynamicSecretAwsIamAuth.IRSA;
credentialType: DynamicSecretAwsIamCredentialType;
region: string;
awsPath?: string;
policyDocument?: string;

View File

@@ -0,0 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,17 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { relayQueryKeys } from "./queries";
export const useDeleteRelayById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => {
return apiRequest.delete(`/api/v1/relays/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: relayQueryKeys.list() });
}
});
};

View File

@@ -0,0 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TRelay } from "./types";
export const relayQueryKeys = {
list: () => ["relays"] as const
};
const fetchRelays = async (): Promise<TRelay[]> => {
const { data } = await apiRequest.get<TRelay[]>("/api/v1/relays");
return data;
};
export const useGetRelays = () => {
return useQuery({
queryKey: relayQueryKeys.list(),
queryFn: fetchRelays
});
};

View File

@@ -0,0 +1,13 @@
export type TRelay = {
id: string;
createdAt: string;
updatedAt: string;
orgId: string | null;
identityId: string | null;
name: string;
host: string;
};
export type TDeleteRelayDTO = {
id: string;
};

View File

@@ -67,7 +67,7 @@ export const useGetSecretImports = ({
const fetchImportedSecrets = async (projectId: string, environment: string, directory?: string) => {
const { data } = await apiRequest.get<{ secrets: TImportedSecrets[] }>(
"/api/v2/secret-imports/secrets",
"/api/v1/dashboard/secret-imports",
{
params: {
projectId,
@@ -132,13 +132,13 @@ export const useGetImportedSecretsSingleEnv = ({
id: encSecret.id,
env: encSecret.environment,
key: encSecret.secretKey,
value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags,
comment: encSecret.secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version
version: encSecret.version,
isEmpty: encSecret.isEmpty
};
})
}));
@@ -172,7 +172,6 @@ export const useGetImportedSecretsAllEnvs = ({
id: encSecret.id,
env: encSecret.environment,
key: encSecret.secretKey,
value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags,
comment: encSecret.secretComment,
@@ -233,7 +232,9 @@ export const useGetImportedSecretsAllEnvs = ({
return {
secret: secret?.secrets.find((s) => s.key === secretName),
environmentInfo: secret?.environmentInfo
environmentInfo: secret?.environmentInfo,
secretPath: secret?.secretPath,
environment: secret?.environment
};
}
return undefined;

View File

@@ -28,7 +28,7 @@ export type TImportedSecrets = {
environmentInfo: ProjectEnv;
secretPath: string;
folderId: string;
secrets: SecretV3Raw[];
secrets: Omit<SecretV3Raw, "secretValue">[];
};
export type TGetSecretImports = {

View File

@@ -6,6 +6,7 @@ import axios from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { useToggle } from "@app/hooks/useToggle";
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants";
import {
@@ -21,7 +22,9 @@ import {
TGetProjectSecretsKey,
TGetSecretAccessListDTO,
TGetSecretReferenceTreeDTO,
TSecretReferenceTraceNode
TGetSecretVersionValue,
TSecretReferenceTraceNode,
TSecretVersionValue
} from "./types";
export const secretKeys = {
@@ -34,6 +37,8 @@ export const secretKeys = {
}: TGetProjectSecretsKey) =>
[{ projectId, environment, secretPath, viewSecretValue }, "secrets"] as const,
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
getSecretVersionValue: (secretId: string, version: number) =>
["secret-versions", secretId, version] as const,
getSecretAccessList: ({
projectId,
environment,
@@ -67,14 +72,17 @@ export const fetchProjectSecrets = async ({
};
export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
const personalSecrets: Record<string, { id: string; value?: string; env: string }> = {};
const personalSecrets: Record<
string,
{ id: string; value?: string; env: string; isEmpty?: boolean }
> = {};
const secrets: SecretV3RawSanitized[] = [];
rawSecrets.forEach((el) => {
const decryptedSecret: SecretV3RawSanitized = {
id: el.id,
env: el.environment,
key: el.secretKey,
value: el.secretValue,
value: el.secretValueHidden ? HIDDEN_SECRET_VALUE : el.secretValue,
secretValueHidden: el.secretValueHidden,
tags: el.tags || [],
comment: el.secretComment || "",
@@ -89,14 +97,16 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
secretMetadata: el.secretMetadata,
isRotatedSecret: el.isRotatedSecret,
rotationId: el.rotationId,
reminder: el.reminder
reminder: el.reminder,
isEmpty: el.isEmpty
};
if (el.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: el.id,
value: el.secretValue,
env: el.environment
env: el.environment,
isEmpty: el.isEmpty
};
} else {
secrets.push(decryptedSecret);
@@ -109,6 +119,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
sec.idOverride = personalSecret.id;
sec.valueOverride = personalSecret.value;
sec.overrideAction = "modified";
sec.isEmpty = personalSecret.isEmpty;
sec.secretValueHidden = false;
}
});
@@ -238,7 +250,7 @@ export const useGetProjectSecretsAllEnv = ({
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
const { data } = await apiRequest.get<{ secretVersions: SecretVersions[] }>(
`/api/v1/secret/${secretId}/secret-versions`,
`/api/v1/dashboard/secret-versions/${secretId}`,
{
params: {
limit,
@@ -259,6 +271,26 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
}, [])
});
export const fetchSecretVersionValue = async (secretId: string, version: number) => {
const { data } = await apiRequest.get<TSecretVersionValue>(
`/api/v1/dashboard/secret-versions/${secretId}/value/${version}`
);
return data.value;
};
export const useGetSecretVersionValue = (
dto: TGetSecretVersionValue,
options?: Omit<
UseQueryOptions<string, unknown, string, ReturnType<typeof secretKeys.getSecretVersionValue>>,
"queryKey" | "queryFn"
>
) =>
useQuery({
queryKey: secretKeys.getSecretVersionValue(dto.secretId, dto.version),
queryFn: () => fetchSecretVersionValue(dto.secretId, dto.version),
...options
});
export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) =>
useQuery({
enabled: Boolean(dto.secretKey),

View File

@@ -47,6 +47,7 @@ export type SecretV3RawSanitized = {
isPending?: boolean;
pendingAction?: PendingAction;
reminder?: Reminder;
isEmpty?: boolean;
};
export type SecretV3Raw = {
@@ -73,6 +74,7 @@ export type SecretV3Raw = {
rotationId?: string;
secretReminderRecipients?: SecretReminderRecipient[];
reminder?: Reminder;
isEmpty?: boolean;
};
export type SecretV3RawResponse = {
@@ -137,6 +139,15 @@ export type GetSecretVersionsDTO = {
offset: number;
};
export type TGetSecretVersionValue = {
secretId: string;
version: number;
};
export type TSecretVersionValue = {
value: string;
};
export type TGetSecretAccessListDTO = {
projectId: string;
environment: string;

View File

@@ -26,7 +26,11 @@ export const Notification = ({ notification, onDelete }: Props) => {
{!notification.isRead && (
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
)}
<Tooltip content={<Markdown>{notification.title}</Markdown>} delayDuration={300}>
<Tooltip
content={<Markdown>{notification.title}</Markdown>}
delayDuration={300}
className="z-[1000]"
>
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5 text-mineshaft-100">
<Markdown components={{ p: "span" }}>{notification.title}</Markdown>
</span>

View File

@@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
<DropdownMenuContent
align="end"
side="bottom"
className="mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
className="z-[999] mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
>
<div className="flex w-full flex-col">
<div className="flex items-center justify-between border-b border-mineshaft-500 px-3 py-2">

View File

@@ -1,9 +1,9 @@
import {
faBook,
faCog,
faDoorClosed,
faInfinity,
faMoneyBill,
faNetworkWired,
faPlug,
faShare,
faTable,
@@ -124,14 +124,14 @@ export const OrgSidebar = ({ isHidden }: Props) => {
</MenuItem>
)}
</Link>
<Link to="/organization/gateways">
<Link to="/organization/networking">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDoorClosed} className="mr-4" />
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
</div>
Gateways
Networking
</div>
</MenuItem>
)}

View File

@@ -64,7 +64,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const { currentProject } = useProject();
const { data, isPending } = useListWorkspaceCertificates({
projectId: currentProject?.slug ?? "",
projectId: currentProject?.id ?? "",
offset: (page - 1) * perPage,
limit: perPage
});

View File

@@ -1,267 +0,0 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Modal,
ModalContent,
PageHeader,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgGatewayPermissionActions,
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
export const GatewayListPage = withPermission(
() => {
const [search, setSearch] = useState("");
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteGateway",
"editDetails"
] as const);
const deleteGatewayById = useDeleteGatewayById();
const deleteGatewayV2ById = useDeleteGatewayV2ById();
const handleDeleteGateway = async () => {
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
if (data.isV1) {
await deleteGatewayById.mutateAsync(data.id);
} else {
await deleteGatewayV2ById.mutateAsync(data.id);
}
handlePopUpToggle("deleteGateway");
createNotification({
type: "success",
text: "Successfully deleted gateway"
});
};
const filteredGateway = gateways?.filter((el) =>
el.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="bg-bunker-800">
<Helmet>
<title>Infisical | Gateways</title>
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title={
<div className="flex w-full items-center">
<span>Gateways</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/documentation/platform/gateways/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
}
description="Create and configure gateway to access private network resources from Infisical"
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search gateway..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Identity</Th>
<Th>
Health Check
<Tooltip
asChild={false}
className="normal-case"
content="The last known healthcheck. Triggers every 1 hour."
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
</Tooltip>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isGatewaysLoading && (
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
)}
{filteredGateway?.map((el) => (
<Tr key={el.id}>
<Td>
<div className="flex items-center gap-2">
<span>{el.name}</span>
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
Gateway v{el.isV1 ? "1" : "2"}
</span>
</div>
</Td>
<Td>{el.identity.name}</Td>
<Td>
{el.heartbeat
? formatRelative(new Date(el.heartbeat), new Date())
: "-"}
</Td>
<Td className="w-5">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={() => navigator.clipboard.writeText(el.id)}
>
Copy ID
</DropdownMenuItem>
{el.isV1 && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.EditGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => handlePopUpOpen("editDetails", el)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
className="text-red"
onClick={() => handlePopUpOpen("deleteGateway", el)}
>
Delete Gateway
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
))}
</TBody>
</Table>
<Modal
isOpen={popUp.editDetails.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
>
<ModalContent title="Edit Gateway">
<EditGatewayDetailsModal
gatewayDetails={popUp.editDetails.data}
onClose={() => handlePopUpToggle("editDetails")}
/>
</ModalContent>
</Modal>
{!isGatewaysLoading && !filteredGateway?.length && (
<EmptyState
title={
gateways?.length
? "No Gateways match search..."
: "No Gateways have been configured"
}
icon={gateways?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure you want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleDeleteGateway()}
/>
</TableContainer>
</div>
</div>
</div>
</div>
</div>
);
},
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
);

View File

@@ -1,16 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { GatewayListPage } from "./GatewayListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
)({
component: GatewayListPage,
context: () => ({
breadcrumbs: [
{
label: "Gateways"
}
]
})
});

View File

@@ -0,0 +1,25 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
export const NetworkingPage = () => {
return (
<>
<Helmet>
<title>Infisical | Networking</title>
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Networking"
description="Manage gateways and relays to securely access private network resources from Infisical"
/>
<NetworkingTabGroup />
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,266 @@
import { useState } from "react";
import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
const isHealthy = heartbeatDate && heartbeatDate >= oneHourAgo;
const tooltipContent = heartbeatDate
? `Last heartbeat: ${heartbeatDate.toLocaleString()}`
: "No heartbeat data available";
return (
<Tooltip content={tooltipContent}>
<span className={`cursor-default ${isHealthy ? "text-green-400" : "text-red-400"}`}>
{isHealthy ? "Healthy" : "Unreachable"}
</span>
</Tooltip>
);
};
export const GatewayTab = withPermission(
() => {
const [search, setSearch] = useState("");
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteGateway",
"editDetails"
] as const);
const deleteGatewayById = useDeleteGatewayById();
const deleteGatewayV2ById = useDeleteGatewayV2ById();
const handleDeleteGateway = async () => {
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
if (data.isV1) {
await deleteGatewayById.mutateAsync(data.id);
} else {
await deleteGatewayV2ById.mutateAsync(data.id);
}
handlePopUpToggle("deleteGateway");
createNotification({
type: "success",
text: "Successfully deleted gateway"
});
};
const filteredGateway = gateways?.filter((el) =>
el.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-mineshaft-100">Gateways</h3>
<a
href="https://infisical.com/docs/documentation/platform/gateways/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 py-0.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
</div>
<p className="mb-4 text-sm text-mineshaft-400">
Create and configure gateway to access private network resources from Infisical
</p>
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search gateway..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/2">Name</Th>
<Th>
Health Check
<Tooltip
asChild={false}
className="normal-case"
content="The last known healthcheck. Triggers every 1 hour."
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
</Tooltip>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isGatewaysLoading && (
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
)}
{filteredGateway?.map((el) => (
<Tr key={el.id}>
<Td>
<div className="flex items-center gap-2">
<span>{el.name}</span>
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
Gateway v{el.isV1 ? "1" : "2"}
</span>
</div>
</Td>
<Td>
<GatewayHealthStatus heartbeat={el.heartbeat} />
</Td>
<Td className="w-5">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={() => navigator.clipboard.writeText(el.id)}
>
Copy ID
</DropdownMenuItem>
{el.isV1 && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.EditGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => handlePopUpOpen("editDetails", el)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan
I={OrgGatewayPermissionActions.DeleteGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
className="text-red"
onClick={() => handlePopUpOpen("deleteGateway", el)}
>
Delete Gateway
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
))}
</TBody>
</Table>
<Modal
isOpen={popUp.editDetails.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
>
<ModalContent title="Edit Gateway">
<EditGatewayDetailsModal
gatewayDetails={popUp.editDetails.data}
onClose={() => handlePopUpToggle("editDetails")}
/>
</ModalContent>
</Modal>
{!isGatewaysLoading && !filteredGateway?.length && (
<EmptyState
title={
gateways?.length
? "No Gateways match search..."
: "No Gateways have been configured"
}
icon={gateways?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure you want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleDeleteGateway()}
/>
</TableContainer>
</div>
</div>
);
},
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
);

View File

@@ -0,0 +1 @@
export { GatewayTab } from "./GatewayTab";

View File

@@ -0,0 +1,37 @@
import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { GatewayTab } from "../GatewayTab/GatewayTab";
import { RelayTab } from "../RelayTab/RelayTab";
export const NetworkingTabGroup = () => {
const search = useSearch({
from: "/_authenticate/_inject-org-details/_org-layout/organization/networking/"
});
const tabs = [
{ name: "Gateways", key: "gateways", component: GatewayTab },
{ name: "Relays", key: "relays", component: RelayTab }
];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};

View File

@@ -0,0 +1 @@
export { NetworkingTabGroup } from "./NetworkingTabGroup";

View File

@@ -0,0 +1,198 @@
import { useState } from "react";
import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEllipsisV,
faMagnifyingGlass,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgPermissionSubjects,
OrgRelayPermissionActions
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
export const RelayTab = withPermission(
() => {
const [search, setSearch] = useState("");
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const);
const deleteRelayById = useDeleteRelayById();
const handleDeleteRelay = async () => {
const data = popUp.deleteRelay.data as { id: string };
await deleteRelayById.mutateAsync(data.id);
handlePopUpToggle("deleteRelay");
createNotification({
type: "success",
text: "Successfully deleted relay"
});
};
const filteredRelays = relays?.filter((el) =>
el.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-mineshaft-100">Relays</h3>
<a
href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment"
target="_blank"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 py-0.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
</div>
<p className="mb-4 text-sm text-mineshaft-400">
Create and configure relays to securely access private network resources from Infisical
</p>
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search relay..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Host</Th>
<Th>Created</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isRelaysLoading && (
<TableSkeleton innerKey="relay-table" columns={4} key="relay-table" />
)}
{filteredRelays?.map((el) => (
<Tr key={el.id}>
<Td>
<div className="flex items-center gap-2">
<span>{el.name}</span>
{!el.orgId && (
<Tooltip content="This is a managed relay provided by Infisical">
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
Managed
</span>
</Tooltip>
)}
</div>
</Td>
<Td>{el.host}</Td>
<Td>{formatRelative(new Date(el.createdAt), new Date())}</Td>
<Td className="w-5">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={() => navigator.clipboard.writeText(el.id)}
>
Copy ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgRelayPermissionActions.DeleteRelays}
a={OrgPermissionSubjects.Relay}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed || !el.orgId}
icon={<FontAwesomeIcon icon={faTrash} />}
className="text-red"
onClick={() => handlePopUpOpen("deleteRelay", el)}
>
Delete Relay
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isRelaysLoading && !filteredRelays?.length && (
<EmptyState
title={
relays?.length ? "No Relays match search..." : "No Relays have been configured"
}
icon={relays?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteRelay.isOpen}
title={`Are you sure you want to delete relay ${
(popUp?.deleteRelay?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteRelay", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleDeleteRelay()}
/>
</TableContainer>
</div>
</div>
);
},
{ action: OrgRelayPermissionActions.ListRelays, subject: OrgPermissionSubjects.Relay }
);

View File

@@ -0,0 +1 @@
export { RelayTab } from "./RelayTab";

View File

@@ -0,0 +1,3 @@
export { GatewayTab } from "./GatewayTab/GatewayTab";
export { NetworkingTabGroup } from "./NetworkingTabGroup/NetworkingTabGroup";
export { RelayTab } from "./RelayTab/RelayTab";

View File

@@ -0,0 +1,26 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { NetworkingPage } from "./NetworkingPage";
const NetworkingPageQueryParams = z.object({
selectedTab: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/networking/"
)({
component: NetworkingPage,
validateSearch: zodValidator(NetworkingPageQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
},
context: () => ({
breadcrumbs: [
{
label: "Networking"
}
]
})
});

View File

@@ -11,7 +11,8 @@ import {
OrgPermissionIdentityActions,
OrgPermissionKmipActions,
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSecretShareAction
OrgPermissionSecretShareAction,
OrgRelayPermissionActions
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
@@ -90,6 +91,15 @@ const orgGatewayPermissionSchema = z
})
.optional();
const orgRelayPermissionSchema = z
.object({
[OrgRelayPermissionActions.ListRelays]: z.boolean().optional(),
[OrgRelayPermissionActions.EditRelays]: z.boolean().optional(),
[OrgRelayPermissionActions.DeleteRelays]: z.boolean().optional(),
[OrgRelayPermissionActions.CreateRelays]: z.boolean().optional()
})
.optional();
const machineIdentityAuthTemplatePermissionSchema = z
.object({
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(),
@@ -147,6 +157,7 @@ export const formSchema = z.object({
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema,
gateway: orgGatewayPermissionSchema,
relay: orgRelayPermissionSchema,
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
"secret-share": secretSharingPermissionSchema
})

View File

@@ -0,0 +1,180 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgRelayPermissionActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [
{ action: OrgRelayPermissionActions.ListRelays, label: "List Relays" },
{ action: OrgRelayPermissionActions.CreateRelays, label: "Create Relays" },
{ action: OrgRelayPermissionActions.EditRelays, label: "Edit Relays" },
{ action: OrgRelayPermissionActions.DeleteRelays, label: "Delete Relays" }
] as const;
export const OrgRelayPermissionRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.relay"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.[OrgRelayPermissionActions.ListRelays]) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.FullAccess:
setValue(
"permissions.relay",
{
[OrgRelayPermissionActions.ListRelays]: true,
[OrgRelayPermissionActions.EditRelays]: true,
[OrgRelayPermissionActions.CreateRelays]: true,
[OrgRelayPermissionActions.DeleteRelays]: true
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.relay",
{
[OrgRelayPermissionActions.ListRelays]: true,
[OrgRelayPermissionActions.EditRelays]: false,
[OrgRelayPermissionActions.CreateRelays]: false,
[OrgRelayPermissionActions.DeleteRelays]: false
},
{ shouldDirty: true }
);
break;
case Permission.NoAccess:
default:
setValue(
"permissions.relay",
{
[OrgRelayPermissionActions.ListRelays]: false,
[OrgRelayPermissionActions.EditRelays]: false,
[OrgRelayPermissionActions.CreateRelays]: false,
[OrgRelayPermissionActions.DeleteRelays]: false
},
{ shouldDirty: true }
);
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td className="w-full select-none">Relays</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.relay.${action}`}
key={`permissions.relay.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={Boolean(field.value)}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.relay.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};

View File

@@ -69,6 +69,7 @@ type Props = {
| "organization-admin-console"
| "kmip"
| "gateway"
| "relay"
| "secret-share"
| "billing"
| "audit-logs"

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