diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 91464bc0b..a31200a1b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -234,6 +234,7 @@ export enum EventType { GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project", + ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", @@ -1907,6 +1908,11 @@ interface OrgAdminAccessProjectEvent { }; // no metadata yet } +interface OrgAdminBypassSSOEvent { + type: EventType.ORG_ADMIN_BYPASS_SSO; + metadata: Record; // no metadata yet +} + interface CreateCertificateTemplateEstConfig { type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; metadata: { @@ -2656,6 +2662,7 @@ export type Event = | GetProjectKmsBackupEvent | LoadProjectKmsBackupEvent | OrgAdminAccessProjectEvent + | OrgAdminBypassSSOEvent | CreateCertificateTemplate | UpdateCertificateTemplate | GetCertificateTemplate diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 54c6b96b2..2c1b768fb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -596,7 +596,14 @@ export const registerRoutes = async ( kmsService }); - const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService }); + const loginService = authLoginServiceFactory({ + userDAL, + smtpService, + tokenService, + orgDAL, + totpService, + auditLogService + }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 0e0f999dd..e576d6768 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -3,6 +3,8 @@ import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; @@ -11,6 +13,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; @@ -28,7 +31,15 @@ import { TOauthTokenExchangeDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; -import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type"; +import { + ActorType, + AuthMethod, + AuthModeJwtTokenPayload, + AuthModeMfaJwtTokenPayload, + AuthTokenType, + MfaMethod +} from "./auth-type"; +import { removeTrailingSlash } from "@app/lib/fn"; type TAuthLoginServiceFactoryDep = { userDAL: TUserDALFactory; @@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = { tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; totpService: Pick; + auditLogService: Pick; }; export type TAuthLoginFactory = ReturnType; @@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({ tokenService, smtpService, orgDAL, - totpService + totpService, + auditLogService }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({ mfaMethod: decodedToken.mfaMethod }); + // In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced) + if ( + selectedOrg.authEnforced && + selectedOrg.bypassOrgAuthEnabled && + !isAuthMethodSaml(decodedToken.authMethod) && + decodedToken.authMethod !== AuthMethod.OIDC + ) { + await auditLogService.createAuditLog({ + orgId: organizationId, + ipAddress, + userAgent, + userAgentType: getUserAgentType(userAgent), + actor: { + type: ActorType.USER, + metadata: { + email: user.email, + userId: user.id, + username: user.username + } + }, + event: { + type: EventType.ORG_ADMIN_BYPASS_SSO, + metadata: {} + } + }); + + // Notify all admins via email (besides the actor) + const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin); + const adminEmails = orgAdmins + .filter((admin) => admin.user.id !== user.id) + .map((admin) => admin.user.email) + .filter(Boolean) as string[]; + + if (adminEmails.length > 0) { + await smtpService.sendMail({ + recipients: adminEmails, + subjectLine: "Security Alert: Admin SSO Bypass", + substitutions: { + email: user.email, + timestamp: new Date().toISOString(), + ip: ipAddress, + userAgent, + siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com") + }, + template: SmtpTemplates.OrgAdminBreakglassAccess + }); + } + } + return { ...tokens, isMfaEnabled: false diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 07ed90bef..8bfa50b64 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -787,13 +787,19 @@ export const kmsServiceFactory = ({ return projectDataKey; } } + } catch (error) { + logger.error( + error, + `getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]` + ); + throw error; } finally { await lock?.release(); } } if (!project.kmsSecretManagerEncryptedDataKey) { - throw new Error("Missing project data key"); + throw new BadRequestError({ message: "Missing project data key" }); } const kmsDecryptor = await decryptWithKmsKey({ diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 02bf58321..54b0e1b0f 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + OrgMembershipRole, TableName, TOrganizations, TOrganizationsInsert, @@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => { try { - const conn = tx || db; + const conn = tx || db.replicaNode(); const members = await conn(TableName.OrgMembership) - // .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( @@ -251,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => { + try { + const conn = tx || db.replicaNode(); + const members = await conn(TableName.OrgMembership) + .where(`${TableName.OrgMembership}.orgId`, orgId) + .where(`${TableName.OrgMembership}.role`, role) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + conn.ref("id").withSchema(TableName.OrgMembership), + conn.ref("inviteEmail").withSchema(TableName.OrgMembership), + conn.ref("orgId").withSchema(TableName.OrgMembership), + conn.ref("role").withSchema(TableName.OrgMembership), + conn.ref("roleId").withSchema(TableName.OrgMembership), + conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("username").withSchema(TableName.Users), + conn.ref("email").withSchema(TableName.Users), + conn.ref("firstName").withSchema(TableName.Users), + conn.ref("lastName").withSchema(TableName.Users), + conn.ref("id").withSchema(TableName.Users).as("userId"), + conn.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }); + + return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { username, email, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find org members by role" }); + } + }; + const findOrgGhostUser = async (orgId: string) => { try { const member = await db @@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => { findAllOrgsByUserId, ghostUserExists, findOrgMembersByUsername, + findOrgMembersByRole, findOrgGhostUser, create, updateById, diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 25f5f3949..550e1bb07 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -44,6 +44,7 @@ export enum SmtpTemplates { SecretRotationFailed = "secretRotationFailed.handlebars", ProjectAccessRequest = "projectAccess.handlebars", OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars", + OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars", ServiceTokenExpired = "serviceTokenExpired.handlebars" } diff --git a/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars new file mode 100644 index 000000000..cc97ff201 --- /dev/null +++ b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars @@ -0,0 +1,20 @@ + + + + + + Organization admin has bypassed SSO + + + +

Infisical

+

The organization admin {{email}} has bypassed enforced SSO login.

+

Timestamp: {{timestamp}}

+

IP address: {{ip}}

+

User agent: {{userAgent}}

+

If you'd like to disable Admin SSO Bypass, please visit Organization Settings > Security.

+ + {{emailFooter}} + + + diff --git a/docs/mint.json b/docs/mint.json index 9f5b3098e..c1e3a7f6b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -313,6 +313,13 @@ "self-hosting/deployment-options/kubernetes-helm" ] }, + { + "group": "Linux Package", + "pages": [ + "self-hosting/deployment-options/native/linux-package/installation", + "self-hosting/deployment-options/native/linux-package/commands-configuration" + ] + }, "self-hosting/guides/upgrading-infisical", "self-hosting/configuration/envars", "self-hosting/configuration/requirements", diff --git a/docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx b/docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx new file mode 100644 index 000000000..61be021b6 --- /dev/null +++ b/docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx @@ -0,0 +1,38 @@ +--- +title: "Configurations" +description: "Learn how to configure and manage the Infisical Linux package" +--- + +## Configuration Overview + +All configuration for the Infisical Linux package is managed through a single file called `infisical.rb`, located in the `/etc/infisical` directory. +This file defines all necessary settings, including encryption keys, database connections, and environment-specific settings. + + After making any changes to the `infisical.rb` file, always run `infisical-ctl reconfigure` to apply them. + +### Example Configuration + +```ruby infisical.rb +# Important: Replace these values with secure keys in production +infisical_core['ENCRYPTION_KEY'] = '6c1fe4e407b8911c104518103505b218' +infisical_core['AUTH_SECRET'] = '5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=' + +# Database connection strings +infisical_core['DB_CONNECTION_URI'] = 'postgres://:@:5432/' +infisical_core['REDIS_URL'] = 'redis://:6379' +``` + +For a full list of supported configuration variables, refer to the [configuration variables documentation](/self-hosting/configuration/envars). + +## All `infisical-ctl` Commands + +The Infisical Linux package includes the `infisical-ctl` command-line tool, which allows you to manage your deployment. +The available commands are listed below. + +| Command | Description | +|-----------------------------|-----------------------------------------------------------------------------| +| `infisical-ctl reconfigure` | Applies changes from `infisical.rb` and restarts the Infisical services. | +| `infisical-ctl start` | Starts the Infisical services. | +| `infisical-ctl stop` | Stops all running Infisical services. | +| `infisical-ctl status` | Displays the current status of the Infisical services. | +| `infisical-ctl tail` | Streams real-time logs from the Infisical application. | \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/native/linux-package/installation.mdx b/docs/self-hosting/deployment-options/native/linux-package/installation.mdx new file mode 100644 index 000000000..30ef83242 --- /dev/null +++ b/docs/self-hosting/deployment-options/native/linux-package/installation.mdx @@ -0,0 +1,122 @@ +--- +title: "Installation" +description: "Learn how to deploy Infisical using the Linux package" +--- + +Infisical can be deployed on Linux virtual machines without the need for containers using our standalone Linux packages. +These packages are available in both .deb (for Debian-based systems) and .rpm (for RHEL-based systems) formats. +The installation includes the Infisical service, along with a CLI tool (infisical-ctl) to help you manage configurations, startup, and application logging. +This approach is ideal for environments where containerization isn't desired, while still providing a lightweight deployment option. + +## Prerequisites + +This installation method only provides the Infisical application. You are responsible for configuring both PostgreSQL and Redis, either by using managed services (e.g., AWS RDS, Azure Database, GCP Cloud SQL/Memorystore) or by deploying them manually in your on-prem environment. +Please ensure you have the following before beginning installation of Infisical: + +- A Linux server running a Debian/Ubuntu or RHEL-based distribution +- A running PostgreSQL database instance (version 14 and up) +- A running Redis database instance (versions 6.x or 7.x) + +## Installation Steps + + + + + Select your Linux distribution to get started. Only AMD64-based systems are supported at this time, ARM support is coming soon. + + + + + Add the Infisical repository: + ```bash + curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.deb.sh' | sudo -E bash + ``` + + Install Infisical: + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + + + + Add the Infisical repository: + ```bash + curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.rpm.sh' | sudo -E bash + ``` + + Install Infisical: + ```bash + sudo yum install infisical-core + ``` + + > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + + + + + Verify the installation: + ```bash + infisical-ctl help + ``` + + + + Create an `infisical.rb` file at `/etc/infisical`. This file contains your database connection strings and other runtime settings. + + ```ruby + # Important: Replace with secure values in production + infisical_core['ENCRYPTION_KEY'] = '6c1fe4e407b8911c104518103505b218' + infisical_core['AUTH_SECRET'] = '5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=' + + # Example database connection strings + infisical_core['DB_CONNECTION_URI'] = 'postgres://:@:/' + infisical_core['REDIS_URL'] = 'redis://:' + ``` + + See the full list of options in our [configuration documentation](/self-hosting/configuration/envars). + + + + 1. Start the Infisical service: + ```bash + infisical-ctl reconfigure + ``` + The server runs on port `8080` by default (customizable in `infisical.rb`). + + 2. Check the service status: + ```bash + infisical-ctl status + ``` + + View the service logs in real-time: + ```bash + infisical-ctl tail + ``` + + + + +## Platform Support + +### Microsoft Windows +Infisical is built for Linux-based systems. It is not supported on Microsoft Windows, and we do not plan to support it in the near future. For Windows users, consider running Infisical in a virtual machine or WSL2 environment. + +### Unsupported Linux Distributions and Unix-like Systems +Infisical is not tested or officially supported on the following: + +- Arch Linux +- Fedora +- FreeBSD +- Gentoo +- macOS + +We recommend sticking to officially supported distributions for the best experience. + +## Linux vs Containerized Deployments + +Infisical is a stateless application, which means it can be easily scaled and redeployed without maintaining internal state between instances. + +If your use case requires rolling updates, self-healing, or auto-scaling, we recommend deploying Infisical in a containerized environment such as Kubernetes/OpenShift, or using managed container orchestration services like AWS ECS or Google Cloud Run. +These platforms offer built-in capabilities for high availability and help simplify operational overhead for your deployment. \ No newline at end of file diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index a7ea50e39..acb692711 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -33,21 +33,10 @@ Choose from a number of deployment options listed below to get started. Use our Helm chart to Install Infisical on your Kubernetes cluster. -{/* - - Install Infisical on your Debian-based system without containers using our standalone binary. - - - Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box. - - */} + Install Infisical on your system without containers using our Linux package. + diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 159e840ec..229f822da 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -84,6 +84,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item", [EventType.DELETE_PKI_COLLECTION_ITEM]: "Delete PKI collection item", [EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project", + [EventType.ORG_ADMIN_BYPASS_SSO]: "Org admin bypassed SSO enforcement", [EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template", [EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template", [EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 15adb0272..d465fb820 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -90,6 +90,7 @@ export enum EventType { ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item", DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item", ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project", + ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index a18974f2e..2524f84f4 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -718,6 +718,11 @@ interface OrgAdminAccessProjectEvent { }; // no metadata yet } +interface OrgAdminBypassSSOEvent { + type: EventType.ORG_ADMIN_BYPASS_SSO; + metadata: Record; // no metadata yet +} + interface CreateCertificateTemplate { type: EventType.CREATE_CERTIFICATE_TEMPLATE; metadata: { @@ -885,6 +890,7 @@ export type Event = | AddPkiCollectionItem | DeletePkiCollectionItem | OrgAdminAccessProjectEvent + | OrgAdminBypassSSOEvent | CreateCertificateTemplate | UpdateCertificateTemplate | GetCertificateTemplate diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx index 2f11523c4..ffb9e93fc 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal } from "@app/components/v2"; +import { Button, DeleteActionModal, Tooltip } from "@app/components/v2"; import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal"; import { ProjectPermissionActions, @@ -142,16 +142,22 @@ export const DeleteProjectSection = () => {
{(isAllowed) => ( - + + )} {!isOnlyAdminMember && (