1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-17 15:08:32 +00:00

Compare commits

...

35 Commits

Author SHA1 Message Date
4bf16f68fc fix: correctly copy user email to clipboard 2025-01-13 17:03:08 -08:00
2a86e6f4d1 add cache contro to cloudflare pages integration 2025-01-13 17:17:39 -05:00
194fbb79f2 Merge pull request from Infisical/daniel/custom-cors
feat(api): custom cors settings
2025-01-13 14:36:19 -05:00
faaba8deb7 add metabase link to on call guide 2025-01-13 11:16:49 -05:00
ae21b157a9 Merge pull request from Infisical/misc/added-proper-error-propagation-for-aws-kms
misc: added error propagation for aws kms
2025-01-13 23:46:57 +08:00
6167c70a74 Merge pull request from Infisical/daniel/push-secret-docs
docs: small k8s docs improvements
2025-01-13 10:39:45 -05:00
0ecf75cbdb misc: scoped down to AWS-related errors 2025-01-13 23:18:24 +08:00
3f8aa0fa4b misc: added error propagation for aws kms 2025-01-13 23:00:40 +08:00
c08fbbdab2 Update app.ts 2025-01-13 13:59:25 +01:00
a4aa65bb81 add on call to hand book 2025-01-13 01:27:50 -05:00
258d19cbe4 Merge pull request from Infisical/daniel/shared-secret-audit-logs
feat(audit-logs): shared secrets audit logs
2025-01-10 22:29:05 +01:00
de91356127 Update envars.mdx 2025-01-10 22:24:57 +01:00
ccb07942de feat(api): custom cors settings 2025-01-10 22:23:19 +01:00
3d278b0925 feat(audit-logs): shared secrets audit logs 2025-01-10 21:37:10 +01:00
d7ffa70906 Merge pull request from Infisical/misc/suppot-array-value-for-oidc-aud-field
misc: add support for array values in OIDC aud field
2025-01-10 13:32:09 -05:00
b8fa7c5bb6 Merge pull request from Infisical/misc/fix-shared-secret-within-org-and-login-redirect
misc: resolve shared secret within org and added login redirect
2025-01-11 02:16:48 +08:00
2baacfcd8f misc: add support for array values 2025-01-11 02:07:04 +08:00
31c11f7d2a Merge pull request from Infisical/vmatsiiako-typo-patch-1
Update infisical-dynamic-secret-crd.mdx
2025-01-10 12:43:40 -05:00
c5f06dece4 Merge pull request from Infisical/misc/made-is-active-optional-for-integration-patch
misc: made is active optional for integration patch
2025-01-11 00:26:21 +08:00
662e79ac98 Merge pull request from Infisical/daniel/audit-log-timestamp-bug
fix(audit-logs): time conversion bug
2025-01-10 17:23:58 +01:00
17249d603b fix: add delete secrets event type to frontend 2025-01-10 17:15:26 +01:00
9bdff9c504 fix: explicit postgres time conversion 2025-01-10 17:15:14 +01:00
4552ce6ca4 Merge pull request from Infisical/misc/add-secret-key-indicator-for-failed-integrations-when-possible
misc: add secret key indicator for failed AWS integration syncs
2025-01-10 11:10:25 -05:00
ba4b8801eb Merge pull request from akhilmhdh/fix/broken-secret-creation
Resolve self signed error for mssql
2025-01-10 11:01:54 -05:00
36a5f728a1 misc: add secret key indicator for failed AWS integration syncs 2025-01-11 00:00:17 +08:00
502429d914 misc: resolve shared secret within org and added login redirect 2025-01-10 23:55:21 +08:00
27abfa4fff Merge pull request from Infisical/misc/add-pagination-handling-for-gitlab-groups-fetch
misc: add pagination handling for gitlab groups fetch
2025-01-10 19:15:38 +08:00
4bc9bca287 removed undefined type 2025-01-10 19:08:49 +08:00
612c29225d misc: add pagination handling for gitlab groups fetch 2025-01-10 19:06:10 +08:00
=
4d43accc8a fix: did same resolution for dynamic secret ops as well 2025-01-10 15:12:04 +05:30
3c89a69410 Update infisical-dynamic-secret-crd.mdx 2025-01-10 01:39:12 -08:00
=
e741b63e63 fix: resolved mssql self signed error 2025-01-10 14:59:49 +05:30
9cde1995c7 Merge pull request from Infisical/fix/address-indefinite-hang-for-run-watch-failure
fix: address infinite hang when infisical run watch fails
2025-01-10 11:54:51 +08:00
02dc23425c fix: address infinite hang when infisical run watch fails 2025-01-10 02:29:39 +08:00
60749cfc43 misc: made is active optional for integration patch 2025-01-09 02:19:24 +08:00
33 changed files with 466 additions and 68 deletions
backend/src
cli/packages/cmd
company
docs
integrations/platforms/kubernetes
self-hosting/configuration
frontend/src
const.ts
hooks/api
pages
organization
AuditLogsPage/components
UserDetailsByIDPage/components
public/ViewSharedSecretByIDPage

@ -100,10 +100,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
// Filter by date range
if (startDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
}
if (endDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
}
// we timeout long running queries to prevent DB resource issues (2 minutes)

@ -31,7 +31,7 @@ export type TListProjectAuditLogDTO = {
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -229,7 +229,10 @@ export enum EventType {
GET_APP_CONNECTION = "get-app-connection",
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection"
DELETE_APP_CONNECTION = "delete-app-connection",
CREATE_SHARED_SECRET = "create-shared-secret",
DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret"
}
interface UserActorMetadata {
@ -252,6 +255,8 @@ interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
interface UnknownUserActorMetadata {}
export interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
@ -267,6 +272,11 @@ export interface PlatformActor {
metadata: PlatformActorMetadata;
}
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
metadata: UnknownUserActorMetadata;
}
export interface IdentityActor {
type: ActorType.IDENTITY;
metadata: IdentityActorMetadata;
@ -1907,6 +1917,35 @@ interface DeleteAppConnectionEvent {
};
}
interface CreateSharedSecretEvent {
type: EventType.CREATE_SHARED_SECRET;
metadata: {
id: string;
accessType: string;
name?: string;
expiresAfterViews?: number;
usingPassword: boolean;
expiresAt: string;
};
}
interface DeleteSharedSecretEvent {
type: EventType.DELETE_SHARED_SECRET;
metadata: {
id: string;
name?: string;
};
}
interface ReadSharedSecretEvent {
type: EventType.READ_SHARED_SECRET;
metadata: {
id: string;
name?: string;
accessType: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -2083,4 +2122,7 @@ export type Event =
| GetAppConnectionEvent
| CreateAppConnectionEvent
| UpdateAppConnectionEvent
| DeleteAppConnectionEvent;
| DeleteAppConnectionEvent
| CreateSharedSecretEvent
| DeleteSharedSecretEvent
| ReadSharedSecretEvent;

@ -34,6 +34,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
const db = knex({
client: providerInputs.client,
connection: {
@ -43,7 +45,16 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
user: providerInputs.username,
password: providerInputs.password,
ssl,
pool: { min: 0, max: 1 }
pool: { min: 0, max: 1 },
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
options: isMsSQLClient
? {
trustServerCertificate: !providerInputs.ca,
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
}
: undefined
},
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
});

@ -1,7 +1,9 @@
import { KMSServiceException } from "@aws-sdk/client-kms";
import { STSServiceException } from "@aws-sdk/client-sts";
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@ -71,7 +73,16 @@ export const externalKmsServiceFactory = ({
switch (provider.type) {
case KmsProviders.Aws:
{
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }).catch((error) => {
if (error instanceof STSServiceException || error instanceof KMSServiceException) {
throw new InternalServerError({
message: error.message ? `AWS error: ${error.message}` : ""
});
}
throw error;
});
// if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput);

@ -180,6 +180,8 @@ export const secretRotationQueueFactory = ({
provider.template.client === TDbProviderClients.MsSqlServer
? ({
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
// when ca is provided use that
trustServerCertificate: !ca,
cryptoCredentialsDetails: ca ? { ca } : {}
} as Record<string, unknown>)
: undefined;

@ -199,7 +199,29 @@ const envSchema = z
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional())
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(
z
.string()
.optional()
.transform((val) => {
if (!val) return undefined;
return JSON.parse(val) as string[];
})
),
CORS_ALLOWED_HEADERS: zpStr(
z
.string()
.optional()
.transform((val) => {
if (!val) return undefined;
return JSON.parse(val) as string[];
})
)
})
// To ensure that basic encryption is always possible.
.refine(

@ -87,7 +87,16 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register<FastifyCorsOptions>(cors, {
credentials: true,
origin: appCfg.SITE_URL || true
...(appCfg.CORS_ALLOWED_ORIGINS?.length
? {
origin: [...appCfg.CORS_ALLOWED_ORIGINS, ...(appCfg.SITE_URL ? [appCfg.SITE_URL] : [])]
}
: {
origin: appCfg.SITE_URL || true
}),
...(appCfg.CORS_ALLOWED_HEADERS?.length && {
allowedHeaders: appCfg.CORS_ALLOWED_HEADERS
})
});
await server.register(addErrorsToResponseSchemas);

@ -32,13 +32,21 @@ export const getUserAgentType = (userAgent: string | undefined) => {
export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auditLogInfo", null);
server.addHook("onRequest", async (req) => {
if (!req.auth) return;
const userAgent = req.headers["user-agent"] ?? "";
const payload = {
ipAddress: req.realIp,
userAgent,
userAgentType: getUserAgentType(userAgent)
} as typeof req.auditLogInfo;
if (!req.auth) {
payload.actor = {
type: ActorType.UNKNOWN_USER,
metadata: {}
};
req.auditLogInfo = payload;
return;
}
if (req.auth.actor === ActorType.USER) {
payload.actor = {
type: ActorType.USER,

@ -131,7 +131,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
body: z.object({
app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
isActive: z.boolean().optional().describe(INTEGRATION.UPDATE.isActive),
secretPath: z
.string()
.trim()

@ -1,6 +1,7 @@
import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSharingAccessType } from "@app/lib/types";
import {
publicEndpointLimit,
@ -88,6 +89,21 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
orgId: req.permission?.orgId
});
if (sharedSecret.secret?.orgId) {
await server.services.auditLog.createAuditLog({
orgId: sharedSecret.secret.orgId,
...req.auditLogInfo,
event: {
type: EventType.READ_SHARED_SECRET,
metadata: {
id: req.params.id,
name: sharedSecret.secret.name || undefined,
accessType: sharedSecret.secret.accessType
}
}
});
}
return sharedSecret;
}
});
@ -151,6 +167,23 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SHARED_SECRET,
metadata: {
accessType: req.body.accessType,
expiresAt: req.body.expiresAt,
expiresAfterViews: req.body.expiresAfterViews,
name: req.body.name,
id: sharedSecret.id,
usingPassword: !!req.body.password
}
}
});
return { id: sharedSecret.id };
}
});
@ -181,6 +214,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
sharedSecretId
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_SHARED_SECRET,
metadata: {
id: sharedSecretId,
name: deletedSharedSecret.name || undefined
}
}
});
return { ...deletedSharedSecret };
}
});

@ -39,7 +39,8 @@ export enum ActorType { // would extend to AWS, Azure, ...
SERVICE = "service",
IDENTITY = "identity",
Machine = "machine",
SCIM_CLIENT = "scimClient"
SCIM_CLIENT = "scimClient",
UNKNOWN_USER = "unknownUser"
}
// This will be null unless the token-type is JWT

@ -2,3 +2,11 @@ import picomatch from "picomatch";
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => {
if (Array.isArray(fieldValue)) {
return fieldValue.some((entry) => entry === policyValue || picomatch.isMatch(entry, policyValue));
}
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
};

@ -27,7 +27,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TOrgBotDALFactory } from "../org/org-bot-dal";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import {
TAttachOidcAuthDTO,
TGetOidcAuthDTO,
@ -148,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
if (
!identityOidcAuth.boundAudiences
.split(", ")
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
.some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue))
) {
throw new UnauthorizedError({
message: "Access denied: OIDC audience not allowed."

@ -1289,7 +1289,10 @@ const syncSecretsAWSSecretManager = async ({
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
for await (const [key, value] of Object.entries(secrets)) {
await processAwsSecret(key, value.value, value.secretMetadata);
await processAwsSecret(key, value.value, value.secretMetadata).catch((error) => {
error.secretKey = key;
throw error;
});
}
} else {
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
@ -3711,7 +3714,8 @@ const syncSecretsCloudflarePages = async ({
}>(`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
Accept: "application/json",
"Cache-Control": "no-cache"
}
})
).data.result.deployment_configs[integration.targetEnvironment as string].env_vars;

@ -1,3 +1,5 @@
import { AxiosResponse } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
@ -11,19 +13,27 @@ const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken:
const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL;
let teams: Team[] = [];
const res = (
await request.get<{ name: string; id: string }[]>(`${gitLabApiUrl}/v4/groups`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
let page: number = 1;
while (page > 0) {
// eslint-disable-next-line no-await-in-loop
const { data, headers }: AxiosResponse<{ name: string; id: string }[]> = await request.get(
`${gitLabApiUrl}/v4/groups?page=${page}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
})
).data;
);
teams = res.map((t) => ({
name: t.name,
id: t.id.toString()
}));
page = Number(headers["x-next-page"] ?? "");
teams = teams.concat(
data.map((t) => ({
name: t.name,
id: t.id.toString()
}))
);
}
return teams;
};

@ -206,8 +206,13 @@ export const secretSharingServiceFactory = ({
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
if (accessType === SecretSharingAccessType.Organization && orgId === undefined) {
throw new UnauthorizedError();
}
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) {
throw new ForbiddenRequestError();
}
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
// or can be safely sent to the client.

@ -971,6 +971,8 @@ export const secretQueueFactory = ({
});
}
const { secretKey } = (err as { secretKey: string }) || {};
const message =
// eslint-disable-next-line no-nested-ternary
(err instanceof AxiosError
@ -979,6 +981,8 @@ export const secretQueueFactory = ({
: err?.message
: (err as Error)?.message) || "Unknown error occurred.";
const errorLog = `${secretKey ? `[Secret Key: ${secretKey}] ` : ""}${message}`;
await auditLogService.createAuditLog({
projectId,
actor: await $generateActor(actorId, isManual),
@ -989,7 +993,7 @@ export const secretQueueFactory = ({
isSynced: false,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: message
syncMessage: errorLog
}
}
});
@ -1001,13 +1005,13 @@ export const secretQueueFactory = ({
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
syncMessage: message,
syncMessage: errorLog,
isSynced: false
});
integrationsFailedToSync.push({
integrationId: integration.id,
syncMessage: message
syncMessage: errorLog
});
}
}

@ -419,22 +419,23 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt
for {
<-recheckSecretsChannel
watchMutex.Lock()
func() {
watchMutex.Lock()
defer watchMutex.Unlock()
newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token)
if err != nil {
log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets")
continue
}
newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token)
if err != nil {
log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets")
return
}
if newEnvironmentVariables.ETag != currentETag {
runCommandWithWatcher(newEnvironmentVariables)
} else {
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
}
watchMutex.Unlock()
if newEnvironmentVariables.ETag != currentETag {
runCommandWithWatcher(newEnvironmentVariables)
} else {
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
}
}()
}
}

@ -0,0 +1,22 @@
---
title: "On call summary template"
sidebarTitle: "Summary template"
---
```plain
Date: MM/DD/YY-MM/DD/YY
Notable incidents:
- [<open/resolved>] <details of the incident including who was impacted. what you did to mitigate/patch the issue>
- Action items:
- <what can we do to prevent this from happening in the future?>
Notable support:
- [Customer company name] <details of the support inquiry>
- Action items:
- <what actions should be taken/has been taken to resolve this>
- <what can we do to prevent this from happening in the future?>
Comments:
<Any comments you have from your on call shift. Were there any pain points you experienced, etc?>
```

@ -0,0 +1,78 @@
---
title: "On call rotation"
sidebarTitle: "On call rotation"
description: "Learn about call rotation at Infisical"
---
Infisical is mission-critical software, which means minimizing service disruptions is a top priority.
To make sure we can react to any issues that come up, we have an on-call rotation that helps us to provide responsive, 24x7x365 support to our customers.
Being part of the on-call rotation is an opportunity to deepen the understanding of our infrastructure, deployment pipelines, and customer-facing systems.
Having this broader understanding of our system not only helps us design better software but also enhances the overall stability of our platform.
### On-Call Overview
**Rotation Details**
Each engineer will be on call once a week, from **Thursday to Thursday**, including weekends.
During this time, the on-call engineer is expected to be available at all times to respond to service disruption alerts.
While being on call, you are responsible for acting as the first line of defense for critical incidents and answering customer support inquiries.
During your working hours, you must respond to all support tickets or involve relevant team members with sufficient context.
Outside of working hours, you are expected to be available for any high-severity pager alerts and critical support inquiries by customers.
### Responsibilities While On Call
During your working hours, prioritize the following in this order:
1. **Responding to Alerts:**
- Monitor and respond promptly to all PagerDuty alerts.
- Investigate incidents, determine root causes, and mitigate issues.
- Refer to runbooks or any relevant documentation to resolve alarms quickly.
2. **Customer Support:**
- Actively monitor all support inquiries in [**Pylon**](https://app.usepylon.com/issues) and respond to incoming tickets.
- Debug and resolve customer issues. If you encounter a problem outside your expertise, collaborate with the relevant teammates to resolve it. This is an opportunity to learn and build context for future incidents.
3. **Sprint work:**
- Since the current on-call workload does not require all of your working hours, you are expected to work on the sprint items assigned to you.
If the on-call workload increases significantly, inform Maidul to make adjustments.
4. **Continuous Improvement:**
- Take note of recurring patterns, inefficiencies, and opportunities where we can automate to reduce on-call burdens in the future.
<Warning>
Outside of working hours, you are expected to be available and respond to any high-severity pager alerts and critical support inquiries by customers.
</Warning>
### Before You Get On Call
- **Set Up PagerDuty:** Ensure you have the PagerDuty mobile app installed, configured, and notifications enabled for Infisical services.
- **Access Required Tools:** Verify access to internal network, runbooks on Notion, [https://grafana.infisical.com](https://grafana.infisical.com/), access to aws accounts and any other access you may require.
- **AWS Permissions:** You will be granted sufficient AWS permissions before the start of your on-call shift in case you need to access production accounts.
### At the End of Your Shift
- Post an on-call summary in the Slack channel `#on-call-summaries` at the end of your shift using the following [template](/documentation/engineering/oncall-summery-template). Include notable findings, support inquires and incidents you encountered.
This will helps the rest of the team stay in the loop and open discussions on how to prevent similar issues in the future.
- Do a **handoff meeting/slack huddle** with the next engineer on call to summarize any outstanding work, unresolved issues, or any incidents that require follow-up. Ensure the next on-call engineer is fully briefed so they can pick up where you left off. **Include Maidul in this hand off call.**
### When to escalate an incident
If you are paged for incident that you cannot resolve after attempting to debug and mitigate the issue, you should not hesitate to escalate and page others in.
Its better to get help sooner rather than later to minimize the impact on customers.
- **Paging relevant teammate:** If youve tried resolving an issue on your own and need additional help, page another engineer who might be relevant through PagerDuty.
- **Escalating to Maidul:** You can page Maidul at any time if you think it would be helpful.
### How to be successful on you rotations
- Be on top of all changes that get merged into main. This will help you be aware of any changes that might cause issues.
- When responding to support inquiries, double check your replies and make sure they are well written and typo-free. Always acknowledge inquiries quickly to make customers feel valued, and suggest a meeting or huddle if you need more clarity on their issues.
- When customers raise support inquiries, always consider what could have been done to make the inquiry self-serve. Could adding a tooltip next to the relevant feature provide clarity? Maybe the documentation could be more detailed or better organized?
- Document all of your notable support/findings/incidents/feature requests during on call so that it is easy to create your on call summary at the end of your on call shift.
### Resources
- [Pylon for support tickets](https://app.usepylon.com/issues)
- [AWS Portal](https://infisical.awsapps.com/start/)
- [View metrics on Grafana](https://grafana.infisical.com/)
- [Metabase](https://analytics.internal.infisical.com/)
- [Run books](https://www.notion.so/Runbooks-19e534883d6b4621b8c712194edbb687?pvs=21)
- [On call summary template](/documentation/engineering/oncall-summery-template)

@ -62,7 +62,13 @@
"handbook/time-off",
"handbook/hiring",
"handbook/meetings",
"handbook/talking-to-customers"
"handbook/talking-to-customers",
{
"group": "Engineering",
"pages": [
"documentation/engineering/oncall"
]
}
]
}
],

@ -11,7 +11,7 @@ This means any Pod, Deployment, or other Kubernetes resource can make use of dyn
This CRD offers the following features:
- **Generate a dynamic secret lease** in Infisical and track its lifecycle.
- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret.
- **Automatically rotate** the dyanmic secret value before it expires to make sure your cluster always has valid credentials.
- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials.
- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload.
### Prerequisites

@ -34,6 +34,27 @@ Used to configure platform-specific security and operational settings
this to `false`.
</ParamField>
## CORS
Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain.
The following environment variables can be used to configure the Infisical Rest API to allow or restrict access to resources from different origins.
<ParamField query="CORS_ALLOWED_ORIGINS" type="string" optional>
Specify a list of origins that are allowed to access the Infisical API.
An example value would be `CORS_ALLOWED_ORIGINS=["https://example.com"]`.
Defaults to the same value as your `SITE_URL` environment variable.
</ParamField>
<ParamField query="CORS_ALLOWED_METHODS" type="string" optional>
Array of HTTP methods allowed for CORS requests.
Defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header.
</ParamField>
## Data Layer
The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks
@ -72,7 +93,7 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
</Expandable>
</ParamField>
## Email service
## Email Service
Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features.

@ -58,7 +58,8 @@ export const leaveConfirmDefaultMessage =
"Your changes will be lost if you leave the page. Are you sure you want to continue?";
export enum SessionStorageKeys {
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN"
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL"
}
export const secretTagsColors = [

@ -3,6 +3,7 @@ import { EventType, UserAgentType } from "./enums";
export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_SECRETS]: "List secrets",
[EventType.GET_SECRET]: "Read secret",
[EventType.DELETE_SECRETS]: "Delete secrets",
[EventType.CREATE_SECRET]: "Create secret",
[EventType.UPDATE_SECRET]: "Update secret",
[EventType.DELETE_SECRET]: "Delete secret",
@ -81,7 +82,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
"Update certificate template EST configuration",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
[EventType.INTEGRATION_SYNCED]: "Integration sync"
[EventType.INTEGRATION_SYNCED]: "Integration sync",
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
[EventType.READ_SHARED_SECRET]: "Read shared secret"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

@ -2,7 +2,8 @@ export enum ActorType {
PLATFORM = "platform",
USER = "user",
SERVICE = "service",
IDENTITY = "identity"
IDENTITY = "identity",
UNKNOWN_USER = "unknownUser"
}
export enum UserAgentType {
@ -17,6 +18,7 @@ export enum UserAgentType {
export enum EventType {
GET_SECRETS = "get-secrets",
DELETE_SECRETS = "delete-secrets",
GET_SECRET = "get-secret",
CREATE_SECRET = "create-secret",
UPDATE_SECRET = "update-secret",
@ -94,5 +96,8 @@ export enum EventType {
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
INTEGRATION_SYNCED = "integration-synced",
CREATE_SHARED_SECRET = "create-shared-secret",
DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret"
}

@ -50,7 +50,11 @@ export interface PlatformActor {
metadata: object;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor;
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor | UnknownUserActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;

@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { apiRequest } from "@app/config/request";
import { SessionStorageKeys } from "@app/const";
import { organizationKeys } from "../organization/queries";
import { setAuthToken } from "../reactQuery";
@ -86,6 +87,21 @@ export const useSelectOrganization = () => {
SecurityClient.setProviderAuthToken("");
}
if (data.token && !data.isMfaEnabled) {
// We check if there is a pending callback after organization login success and redirect to it if valid
const loginRedirectInfo = sessionStorage.getItem(
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL
);
sessionStorage.removeItem(SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL);
if (loginRedirectInfo) {
const { expiry, data: redirectUrl } = JSON.parse(loginRedirectInfo);
if (new Date() < new Date(expiry)) {
window.location.assign(redirectUrl);
}
}
}
return data;
},
onSuccess: () => {

@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary */
import { useEffect, useState } from "react";
import { useState } from "react";
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -49,7 +49,6 @@ export const LogsFilter = ({
isOrgAuditLogs,
className,
control,
setValue,
reset,
watch
}: Props) => {
@ -63,12 +62,6 @@ export const LogsFilter = ({
const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? "");
useEffect(() => {
if (workspacesInOrg.length) {
setValue("project", workspacesInOrg[0]);
}
}, [workspaces]);
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
@ -129,6 +122,7 @@ export const LogsFilter = ({
>
<FilterableSelect
value={value}
isClearable
onChange={onChange}
placeholder="Select a project..."
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))}

@ -1,4 +1,7 @@
import { Td, Tr } from "@app/components/v2";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Td, Tooltip, Tr } from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types";
@ -37,6 +40,17 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
<p>Machine Identity</p>
</Td>
);
case ActorType.UNKNOWN_USER:
return (
<Td>
<div className="flex items-center gap-2">
<p>Unknown User</p>
<Tooltip content="This action was performed by a user who was not authenticated at the time.">
<FontAwesomeIcon className="text-mineshaft-400" icon={faQuestionCircle} />
</Tooltip>
</div>
</Td>
);
default:
return <Td />;
}

@ -133,7 +133,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText("");
navigator.clipboard.writeText(membership.user.username);
setCopyTextUsername("Copied");
}}
>

@ -1,10 +1,13 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useParams, useSearch } from "@tanstack/react-router";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { addSeconds, formatISO } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { SessionStorageKeys } from "@app/const";
import { ROUTE_PATHS } from "@app/const/routes";
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
@ -36,7 +39,6 @@ export const ViewSharedSecretByIDPage = () => {
select: (el) => el.key
});
const [password, setPassword] = useState<string>();
const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey);
const {
@ -50,10 +52,47 @@ export const ViewSharedSecretByIDPage = () => {
password
});
const navigate = useNavigate();
const isUnauthorized =
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 401;
const isForbidden =
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 403;
const isInvalidCredential =
((error as AxiosError)?.response?.data as { message: string })?.message ===
"Invalid credentials";
useEffect(() => {
if (isUnauthorized && !isInvalidCredential) {
// persist current URL in session storage so that we can come back to this after successful login
sessionStorage.setItem(
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 60)),
data: window.location.href
})
);
createNotification({
type: "info",
text: "Login is required in order to access the shared secret."
});
navigate({
to: "/login"
});
}
if (isForbidden) {
createNotification({
type: "error",
text: "You do not have access to this shared secret."
});
}
}, [error]);
const shouldShowPasswordPrompt =
isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret);
const isValidatingPassword = Boolean(password) && isFetching;
@ -111,7 +150,7 @@ export const ViewSharedSecretByIDPage = () => {
{!error && fetchSecret?.secret && (
<SecretContainer secret={fetchSecret.secret} secretKey={key} />
)}
{error && !isInvalidCredential && <SecretErrorContainer />}
{error && !isInvalidCredential && !isUnauthorized && <SecretErrorContainer />}
</>
)}
<div className="m-auto my-8 flex w-full">

@ -2,6 +2,8 @@ import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage";
const SharedSecretByIDPageQuerySchema = z.object({
@ -9,9 +11,18 @@ const SharedSecretByIDPageQuerySchema = z.object({
});
export const Route = createFileRoute("/shared/secret/$secretId")({
component: ViewSharedSecretByIDPage,
validateSearch: zodValidator(SharedSecretByIDPageQuerySchema),
component: ViewSharedSecretByIDPage,
search: {
middlewares: [stripSearchParams({ key: "" })]
},
beforeLoad: async ({ context }) => {
// we load the auth token because the view shared secret screen serves both public and authenticated users
await context.queryClient
.ensureQueryData({
queryKey: authKeys.getAuthToken,
queryFn: fetchAuthToken
})
.catch(() => undefined);
}
});