Merge branch 'main' into ENG-2633

This commit is contained in:
x
2025-04-24 21:23:38 -04:00
15 changed files with 343 additions and 32 deletions

View File

@@ -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<string, string>; // 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

View File

@@ -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,

View File

@@ -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<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
@@ -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

View File

@@ -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({

View File

@@ -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<TUserEncryptionKeys>(
@@ -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<TUserEncryptionKeys>(
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,

View File

@@ -44,6 +44,7 @@ export enum SmtpTemplates {
SecretRotationFailed = "secretRotationFailed.handlebars",
ProjectAccessRequest = "projectAccess.handlebars",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
ServiceTokenExpired = "serviceTokenExpired.handlebars"
}

View File

@@ -0,0 +1,20 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization admin has bypassed SSO</title>
</head>
<body>
<h2>Infisical</h2>
<p>The organization admin {{email}} has bypassed enforced SSO login.</p>
<p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you'd like to disable Admin SSO Bypass, please visit <a href="{{siteUrl}}/organization/settings">Organization Settings</a> > Security.</p>
{{emailFooter}}
</body>
</html>

View File

@@ -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",

View File

@@ -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.
<Info> After making any changes to the `infisical.rb` file, always run `infisical-ctl reconfigure` to apply them. </Info>
### 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://<username>:<password>@<host>:5432/<database>'
infisical_core['REDIS_URL'] = 'redis://<host>: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. |

View File

@@ -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
<Steps>
<Step title="Install the Infisical Package">
Select your Linux distribution to get started. Only AMD64-based systems are supported at this time, ARM support is coming soon.
<Tabs>
<Tab title="Debian/Ubuntu">
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/).
</Tab>
<Tab title="RedHat/CentOS/Amazon Linux">
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/).
</Tab>
</Tabs>
Verify the installation:
```bash
infisical-ctl help
```
</Step>
<Step title="Create the Configuration File">
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://<db-username>:<db-password>@<db-host>:<db-port>/<db-name>'
infisical_core['REDIS_URL'] = 'redis://<redis-host>:<redis-port>'
```
See the full list of options in our [configuration documentation](/self-hosting/configuration/envars).
</Step>
<Step title="Start Infisical">
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
```
</Step>
</Steps>
## 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.

View File

@@ -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.
</Card>
</CardGroup>
{/* <CardGroup cols={2}>
<Card
title="Native Deployment"
<Card
title="Linux package"
color="#000000"
icon="box"
href="deployment-options/native/standalone-binary"
href="deployment-options/native/linux-package/installation"
>
Install Infisical on your Debian-based system without containers using our standalone binary.
</Card>
<Card
title="Native Deployment, High Availability"
color="#000000"
icon="boxes-stacked"
href="deployment-options/native/high-availability"
>
Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box.
</Card>
</CardGroup> */}
Install Infisical on your system without containers using our Linux package.
</Card>

View File

@@ -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",

View File

@@ -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",

View File

@@ -718,6 +718,11 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet
}
interface OrgAdminBypassSSOEvent {
type: EventType.ORG_ADMIN_BYPASS_SSO;
metadata: Record<string, string>; // 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

View File

@@ -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 = () => {
<div className="space-x-4">
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting || currentWorkspace?.hasDeleteProtection}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
<Tooltip
className="max-w-sm"
content="This project is protected from deletion. To delete it, disable delete protection first."
isDisabled={!currentWorkspace?.hasDeleteProtection}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting || currentWorkspace?.hasDeleteProtection}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
</Tooltip>
)}
</ProjectPermissionCan>
{!isOnlyAdminMember && (