Compare commits

..

54 Commits

Author SHA1 Message Date
4c8bf9bd92 Update values.yaml 2025-06-06 20:16:50 +04:00
a6554deb80 Update connection.go 2025-06-06 20:14:03 +04:00
4bd1eb6f70 Update helm-charts/infisical-gateway/CHANGELOG.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-06 04:12:04 +04:00
022ecf75e1 fix(gateway): handle malformed URL's 2025-06-06 04:02:24 +04:00
ce170a6a47 Merge pull request #3740 from Infisical/daniel/gateway-helm-bump
helm(infisical-gateway): bump CLI image version to latest
2025-06-05 16:43:54 -04:00
cb8e36ae15 helm(infisical-gateway): bump CLI image version to latest 2025-06-06 00:41:35 +04:00
16ce1f441e Merge pull request #3731 from Infisical/daniel/gateway-auth-methods
feat(identities/kubernetes-auth): gateway as token reviewer
2025-06-05 16:33:24 -04:00
8043b61c9f Merge pull request #3730 from Infisical/org-access-control-no-access-display
improvement(org-access-control): Add org access control no access display
2025-06-05 13:27:38 -07:00
d374ff2093 Merge pull request #3732 from Infisical/ENG-2809
Add {{environment}} support for key schemas
2025-06-05 16:27:22 -04:00
eb7c533261 Update identity-kubernetes-auth-service.ts 2025-06-06 00:26:01 +04:00
9a935c9177 Lint 2025-06-05 16:07:00 -04:00
9d24eb15dc Feedback 2025-06-05 16:01:56 -04:00
ed4882dfac fix: simplify gateway http copy logic 2025-06-05 23:50:46 +04:00
7acd7fd522 Merge pull request #3737 from akhilmhdh/feat/limit-project-create
feat: added lock for project create
2025-06-06 00:53:13 +05:30
2148b636f5 Merge branch 'main' into ENG-2809 2025-06-05 15:10:22 -04:00
=
e40b4a0a4b feat: added lock for project create 2025-06-06 00:31:21 +05:30
311bf8b515 Merge pull request #3734 from Infisical/gateway-netowkr
Added networking docs to cover gateway
2025-06-05 10:47:01 -04:00
78c4c3e847 Update overview.mdx 2025-06-05 18:43:46 +04:00
b8aa36be99 cleanup and minor requested changes 2025-06-05 18:40:54 +04:00
594445814a docs(identity/kubernetes-auth): added docs for gateway as reviewer 2025-06-05 18:40:34 +04:00
a467b13069 Merge pull request #3728 from Infisical/condition-eq-comma-check
improvement(permissions): Prevent comma separated values with eq and neq checks
2025-06-05 19:48:38 +05:30
c425c03939 cleanup 2025-06-05 17:44:41 +04:00
9cc17452fa address greptile 2025-06-05 01:23:28 -04:00
93ba6f7b58 add netowkring docs 2025-06-05 01:18:21 -04:00
0fcb66e9ab Merge pull request #3733 from Infisical/improve-smtp-rate-limits
improvement(smtp-rate-limit): trim and substring keys and default to realIp
2025-06-04 23:11:41 -04:00
135f425fcf improvement: trim and substring keys and default to realIp 2025-06-04 20:00:53 -07:00
9c149cb4bf Merge pull request #3726 from Infisical/email-rate-limit
Improvement: add more aggresive rate limiting on smtp endpoints
2025-06-04 19:14:09 -07:00
ce45c1a43d improvements: address feedback 2025-06-04 19:05:22 -07:00
1a14c71564 Greptile review fixes 2025-06-04 21:41:21 -04:00
e7fe2ea51e Fix lint issues 2025-06-04 21:35:17 -04:00
caa129b565 requested changes 2025-06-05 05:23:30 +04:00
30d7e63a67 Add {{environment}} support for key schemas 2025-06-04 21:20:16 -04:00
a4c21d85ac Update identity-kubernetes-auth-router.ts 2025-06-05 05:07:58 +04:00
c34a139b19 cleanup 2025-06-05 05:02:58 +04:00
f2a55da9b6 Update .infisicalignore 2025-06-05 04:49:50 +04:00
a3584d6a8a Merge branch 'heads/main' into daniel/gateway-auth-methods 2025-06-05 04:49:35 +04:00
36f1559e5e cleanup 2025-06-05 04:45:57 +04:00
07902f7db9 feat(identities/kubernetes-auth): use gateway as token reviewer 2025-06-05 04:42:15 +04:00
6fddecdf82 Merge pull request #3729 from akhilmhdh/feat/ui-change-for-approval-replication
feat: updated ui for replication approval
2025-06-04 19:05:13 -04:00
99e2c85f8f Merge pull request #3718 from Infisical/filter-org-members-by-role
improvement(org-users-table): Add filter by roles to org users table
2025-06-04 16:01:43 -07:00
6e1504dc73 Merge pull request #3727 from Infisical/update-github-radar-image
improvement(github-radar-app): update image
2025-06-04 18:29:41 -04:00
=
07d930f608 feat: small text changes 2025-06-05 03:54:09 +05:30
1101707d8b improvement: add org access control no access display 2025-06-04 15:15:12 -07:00
=
696bbcb072 feat: updated ui for replication approval 2025-06-05 03:44:54 +05:30
54435d0ad9 improvements: prevent comma separated value usage with eq and neq checks 2025-06-04 14:21:36 -07:00
6c52847dec improvement: update image 2025-06-04 13:48:33 -07:00
698260cba6 improvement: add more aggresive rate limiting on smtp endpoints 2025-06-04 13:27:08 -07:00
caeda09b21 Merge pull request #3725 from Infisical/doc/spire
doc: add oidc auth doc for spire
2025-06-04 12:59:49 -04:00
1201baf35c doc: add oidc auth doc for spire 2025-06-04 15:42:43 +00:00
5d5f843a9f Merge pull request #3724 from Infisical/fix/secretRequestUIOverflows
Fix broken UI for secret requests due to long secret values
2025-06-04 21:08:03 +05:30
01ea22f167 move bounty progam to invite only - low quality reports 2025-06-04 10:58:03 -04:00
0fbf8efd3a improvement: add filter by roles to org users table 2025-06-03 14:36:47 -07:00
6ae7b5e996 cleanup 2025-06-03 22:24:27 +04:00
400157a468 feat(cli): gateway auth methods 2025-06-03 21:35:54 +04:00
97 changed files with 2329 additions and 985 deletions

View File

@ -40,3 +40,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:578
cli/detect/config/gitleaks.toml:gcp-api-key:579
cli/detect/config/gitleaks.toml:gcp-api-key:581
cli/detect/config/gitleaks.toml:gcp-api-key:582
backend/src/services/smtp/smtp-service.ts:generic-api-key:79

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode");
if (!hasTokenReviewModeColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.string("tokenReviewMode").notNullable().defaultTo("api");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode");
if (hasTokenReviewModeColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.dropColumn("tokenReviewMode");
});
}
}

View File

@ -31,7 +31,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
gatewayId: z.string().uuid().nullable().optional(),
accessTokenPeriod: z.coerce.number().default(0)
accessTokenPeriod: z.coerce.number().default(0),
tokenReviewMode: z.string().default("api")
});
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;

View File

@ -2,7 +2,7 @@ import axios from "axios";
import https from "https";
import { InternalServerError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
@ -43,6 +43,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
return res;
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost: inputs.targetHost,
targetPort: inputs.targetPort,
relayHost,

View File

@ -3,7 +3,7 @@ import handlebars from "handlebars";
import knex from "knex";
import { z } from "zod";
import { withGatewayProxy } from "@app/lib/gateway";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@ -185,6 +185,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await gatewayCallback("localhost", port);
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost: providerInputs.host,
targetPort: providerInputs.port,
relayHost,

View File

@ -4,7 +4,7 @@ import knex, { Knex } from "knex";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@ -196,6 +196,7 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
await gatewayCallback("localhost", port);
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost: providerInputs.host,
targetPort: providerInputs.port,
relayHost,

View File

@ -117,6 +117,7 @@ export const OCIVaultSyncFns = {
syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
environment,
destinationConfig: { compartmentOcid, vaultOcid, keyOcid }
} = secretSync;
@ -213,7 +214,7 @@ export const OCIVaultSyncFns = {
// Update and delete secrets
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
// Only update / delete active secrets
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {

View File

@ -10,7 +10,8 @@ export const PgSqlLock = {
KmsRootKeyInit: 2025,
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`)
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`)
} as const;
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;

View File

@ -400,6 +400,8 @@ export const KUBERNETES_AUTH = {
caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt:
"Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
tokenReviewMode:
"The mode to use for token review. Must be one of: 'api', 'gateway'. If gateway is selected, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding.",
allowedNamespaces:
"The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
@ -417,6 +419,8 @@ export const KUBERNETES_AUTH = {
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt:
"Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
tokenReviewMode:
"The mode to use for token review. Must be one of: 'api', 'gateway'. If gateway is selected, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding.",
allowedNamespaces:
"The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",

View File

@ -0,0 +1,411 @@
/* eslint-disable no-await-in-loop */
import crypto from "node:crypto";
import net from "node:net";
import quicDefault, * as quicModule from "@infisical/quic";
import axios from "axios";
import https from "https";
import { BadRequestError } from "../errors";
import { logger } from "../logger";
import {
GatewayProxyProtocol,
IGatewayProxyOptions,
IGatewayProxyServer,
TGatewayTlsOptions,
TPingGatewayAndVerifyDTO
} from "./types";
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RETRY_DELAY = 1000; // 1 second
const quic = quicDefault || quicModule;
const parseSubjectDetails = (data: string) => {
const values: Record<string, string> = {};
data.split("\n").forEach((el) => {
const [key, value] = el.split("=");
values[key.trim()] = value.trim();
});
return values;
};
const createQuicConnection = async (
relayHost: string,
relayPort: number,
tlsOptions: TGatewayTlsOptions,
identityId: string,
orgId: string
) => {
const client = await quic.QUICClient.createQUICClient({
host: relayHost,
port: relayPort,
config: {
ca: tlsOptions.ca,
cert: tlsOptions.cert,
key: tlsOptions.key,
applicationProtos: ["infisical-gateway"],
verifyPeer: true,
verifyCallback: async (certs) => {
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
const isValidServerCertificate = serverCertificate.verify(caCertificate.publicKey);
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
return quic.native.CryptoError.CertificateUnknown;
}
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
return quic.native.CryptoError.CertificateExpired;
}
const formatedRelayHost =
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
},
maxIdleTimeout: 90000,
keepAliveIntervalTime: 30000
},
crypto: {
ops: {
randomBytes: async (data) => {
crypto.getRandomValues(new Uint8Array(data));
}
}
}
});
return client;
};
export const pingGatewayAndVerify = async ({
relayHost,
relayPort,
tlsOptions,
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
}: TPingGatewayAndVerifyDTO) => {
let lastError: Error | null = null;
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({
message: (err as Error)?.message,
error: err as Error
});
});
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try {
const stream = quicClient.connection.newStream("bidi");
const pingWriter = stream.writable.getWriter();
await pingWriter.write(Buffer.from("PING\n"));
pingWriter.releaseLock();
// Read PONG response
const reader = stream.readable.getReader();
const { value, done } = await reader.read();
if (done) {
throw new Error("Gateway closed before receiving PONG");
}
const response = Buffer.from(value).toString();
if (response !== "PONG\n" && response !== "PONG") {
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
}
reader.releaseLock();
return;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
await new Promise((resolve) => {
setTimeout(resolve, DEFAULT_RETRY_DELAY);
});
}
} finally {
await quicClient.destroy();
}
}
logger.error(lastError);
throw new BadRequestError({
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
});
};
const setupProxyServer = async ({
targetPort,
targetHost,
tlsOptions,
relayHost,
relayPort,
identityId,
orgId,
protocol = GatewayProxyProtocol.Tcp,
httpsAgent
}: {
targetHost: string;
targetPort: number;
relayPort: number;
relayHost: string;
tlsOptions: TGatewayTlsOptions;
identityId: string;
orgId: string;
protocol?: GatewayProxyProtocol;
httpsAgent?: https.Agent;
}): Promise<IGatewayProxyServer> => {
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({
error: err as Error
});
});
const proxyErrorMsg = [""];
return new Promise((resolve, reject) => {
const server = net.createServer();
let streamClosed = false;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientConn) => {
try {
clientConn.setKeepAlive(true, 30000); // 30 seconds
clientConn.setNoDelay(true);
const stream = quicClient.connection.newStream("bidi");
const forwardWriter = stream.writable.getWriter();
let command: string;
if (protocol === GatewayProxyProtocol.Http) {
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
command = `FORWARD-HTTP ${targetUrl}`;
logger.debug(`Using HTTP proxy mode: ${command.trim()}`);
// extract ca certificate from httpsAgent if present
if (httpsAgent && targetHost.startsWith("https://")) {
const agentOptions = httpsAgent.options;
if (agentOptions && agentOptions.ca) {
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
const caB64 = Buffer.from(caCert as string).toString("base64");
command += ` ca=${caB64}`;
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
command += ` verify=${rejectUnauthorized}`;
logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`);
}
}
command += "\n";
} else if (protocol === GatewayProxyProtocol.Tcp) {
// For TCP mode, send FORWARD-TCP with host:port
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
} else {
throw new BadRequestError({
message: `Invalid protocol: ${protocol as string}`
});
}
await forwardWriter.write(Buffer.from(command));
forwardWriter.releaseLock();
// Set up bidirectional copy
const setupCopy = () => {
// Client to QUIC
// eslint-disable-next-line
(async () => {
const writer = stream.writable.getWriter();
// Create a handler for client data
clientConn.on("data", (chunk) => {
writer.write(chunk).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
});
// Handle client connection close
clientConn.on("end", () => {
if (!streamClosed) {
try {
writer.close().catch((err) => {
logger.debug(err, "Error closing writer (already closed)");
});
} catch (error) {
logger.debug(error, "Error in writer close");
}
}
});
clientConn.on("error", (clientConnErr) => {
writer.abort(clientConnErr?.message).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
});
})();
// QUIC to Client
void (async () => {
try {
const reader = stream.readable.getReader();
let reading = true;
while (reading) {
const { value, done } = await reader.read();
if (done) {
reading = false;
clientConn.end(); // Close client connection when QUIC stream ends
break;
}
// Write data to TCP client
const canContinue = clientConn.write(Buffer.from(value));
// Handle backpressure
if (!canContinue) {
await new Promise((res) => {
clientConn.once("drain", res);
});
}
}
} catch (err) {
proxyErrorMsg.push((err as Error)?.message);
clientConn.destroy();
}
})();
};
setupCopy();
// Handle connection closure
clientConn.on("close", () => {
if (!streamClosed) {
streamClosed = true;
stream.destroy().catch((err) => {
logger.debug(err, "Stream already destroyed during close event");
});
}
});
const cleanup = async () => {
try {
clientConn?.destroy();
} catch (err) {
logger.debug(err, "Error destroying client connection");
}
if (!streamClosed) {
streamClosed = true;
try {
await stream.destroy();
} catch (err) {
logger.debug(err, "Error destroying stream (might be already closed)");
}
}
};
clientConn.on("error", (clientConnErr) => {
logger.error(clientConnErr, "Client socket error");
cleanup().catch((err) => {
logger.error(err, "Client conn cleanup");
});
});
clientConn.on("end", () => {
cleanup().catch((err) => {
logger.error(err, "Client conn end");
});
});
} catch (err) {
logger.error(err, "Failed to establish target connection:");
clientConn.end();
reject(err);
}
});
server.on("error", (err) => {
reject(err);
});
server.on("close", () => {
quicClient?.destroy().catch((err) => {
logger.error(err, "Failed to destroy quic client");
});
});
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Failed to get server port"));
return;
}
logger.info(`Gateway proxy started on port ${address.port} (${protocol} mode)`);
resolve({
server,
port: address.port,
cleanup: async () => {
try {
server.close();
} catch (err) {
logger.debug(err, "Error closing server");
}
try {
await quicClient?.destroy();
} catch (err) {
logger.debug(err, "Error destroying QUIC client");
}
},
getProxyError: () => proxyErrorMsg.join(",")
});
});
});
};
export const withGatewayProxy = async <T>(
callback: (port: number, httpsAgent?: https.Agent) => Promise<T>,
options: IGatewayProxyOptions
): Promise<T> => {
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId, protocol, httpsAgent } = options;
// Setup the proxy server
const { port, cleanup, getProxyError } = await setupProxyServer({
targetHost,
targetPort,
relayPort,
relayHost,
tlsOptions,
identityId,
orgId,
protocol,
httpsAgent
});
try {
// Execute the callback with the allocated port
return await callback(port, httpsAgent);
} catch (err) {
const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) {
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
}
logger.error(err, "Failed to do gateway");
let errorMessage = proxyErrorMessage || (err as Error)?.message;
if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) {
errorMessage = (err.response?.data as { message: string }).message;
}
throw new BadRequestError({ message: errorMessage });
} finally {
// Ensure cleanup happens regardless of success or failure
await cleanup();
}
};

View File

@ -1,392 +1,2 @@
/* eslint-disable no-await-in-loop */
import crypto from "node:crypto";
import net from "node:net";
import quicDefault, * as quicModule from "@infisical/quic";
import axios from "axios";
import { BadRequestError } from "../errors";
import { logger } from "../logger";
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RETRY_DELAY = 1000; // 1 second
const quic = quicDefault || quicModule;
const parseSubjectDetails = (data: string) => {
const values: Record<string, string> = {};
data.split("\n").forEach((el) => {
const [key, value] = el.split("=");
values[key.trim()] = value.trim();
});
return values;
};
type TTlsOption = { ca: string; cert: string; key: string };
const createQuicConnection = async (
relayHost: string,
relayPort: number,
tlsOptions: TTlsOption,
identityId: string,
orgId: string
) => {
const client = await quic.QUICClient.createQUICClient({
host: relayHost,
port: relayPort,
config: {
ca: tlsOptions.ca,
cert: tlsOptions.cert,
key: tlsOptions.key,
applicationProtos: ["infisical-gateway"],
verifyPeer: true,
verifyCallback: async (certs) => {
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
const isValidServerCertificate = serverCertificate.verify(caCertificate.publicKey);
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
return quic.native.CryptoError.CertificateUnknown;
}
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
return quic.native.CryptoError.CertificateExpired;
}
const formatedRelayHost =
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
},
maxIdleTimeout: 90000,
keepAliveIntervalTime: 30000
},
crypto: {
ops: {
randomBytes: async (data) => {
crypto.getRandomValues(new Uint8Array(data));
}
}
}
});
return client;
};
type TPingGatewayAndVerifyDTO = {
relayHost: string;
relayPort: number;
tlsOptions: TTlsOption;
maxRetries?: number;
identityId: string;
orgId: string;
};
export const pingGatewayAndVerify = async ({
relayHost,
relayPort,
tlsOptions,
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
}: TPingGatewayAndVerifyDTO) => {
let lastError: Error | null = null;
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({
message: (err as Error)?.message,
error: err as Error
});
});
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try {
const stream = quicClient.connection.newStream("bidi");
const pingWriter = stream.writable.getWriter();
await pingWriter.write(Buffer.from("PING\n"));
pingWriter.releaseLock();
// Read PONG response
const reader = stream.readable.getReader();
const { value, done } = await reader.read();
if (done) {
throw new Error("Gateway closed before receiving PONG");
}
const response = Buffer.from(value).toString();
if (response !== "PONG\n" && response !== "PONG") {
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
}
reader.releaseLock();
return;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
await new Promise((resolve) => {
setTimeout(resolve, DEFAULT_RETRY_DELAY);
});
}
} finally {
await quicClient.destroy();
}
}
logger.error(lastError);
throw new BadRequestError({
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
});
};
interface TProxyServer {
server: net.Server;
port: number;
cleanup: () => Promise<void>;
getProxyError: () => string;
}
const setupProxyServer = async ({
targetPort,
targetHost,
tlsOptions,
relayHost,
relayPort,
identityId,
orgId
}: {
targetHost: string;
targetPort: number;
relayPort: number;
relayHost: string;
tlsOptions: TTlsOption;
identityId: string;
orgId: string;
}): Promise<TProxyServer> => {
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({
error: err as Error
});
});
const proxyErrorMsg = [""];
return new Promise((resolve, reject) => {
const server = net.createServer();
let streamClosed = false;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientConn) => {
try {
clientConn.setKeepAlive(true, 30000); // 30 seconds
clientConn.setNoDelay(true);
const stream = quicClient.connection.newStream("bidi");
// Send FORWARD-TCP command
const forwardWriter = stream.writable.getWriter();
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
forwardWriter.releaseLock();
// Set up bidirectional copy
const setupCopy = () => {
// Client to QUIC
// eslint-disable-next-line
(async () => {
const writer = stream.writable.getWriter();
// Create a handler for client data
clientConn.on("data", (chunk) => {
writer.write(chunk).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
});
// Handle client connection close
clientConn.on("end", () => {
if (!streamClosed) {
try {
writer.close().catch((err) => {
logger.debug(err, "Error closing writer (already closed)");
});
} catch (error) {
logger.debug(error, "Error in writer close");
}
}
});
clientConn.on("error", (clientConnErr) => {
writer.abort(clientConnErr?.message).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
});
})();
// QUIC to Client
void (async () => {
try {
const reader = stream.readable.getReader();
let reading = true;
while (reading) {
const { value, done } = await reader.read();
if (done) {
reading = false;
clientConn.end(); // Close client connection when QUIC stream ends
break;
}
// Write data to TCP client
const canContinue = clientConn.write(Buffer.from(value));
// Handle backpressure
if (!canContinue) {
await new Promise((res) => {
clientConn.once("drain", res);
});
}
}
} catch (err) {
proxyErrorMsg.push((err as Error)?.message);
clientConn.destroy();
}
})();
};
setupCopy();
// Handle connection closure
clientConn.on("close", () => {
if (!streamClosed) {
streamClosed = true;
stream.destroy().catch((err) => {
logger.debug(err, "Stream already destroyed during close event");
});
}
});
const cleanup = async () => {
try {
clientConn?.destroy();
} catch (err) {
logger.debug(err, "Error destroying client connection");
}
if (!streamClosed) {
streamClosed = true;
try {
await stream.destroy();
} catch (err) {
logger.debug(err, "Error destroying stream (might be already closed)");
}
}
};
clientConn.on("error", (clientConnErr) => {
logger.error(clientConnErr, "Client socket error");
cleanup().catch((err) => {
logger.error(err, "Client conn cleanup");
});
});
clientConn.on("end", () => {
cleanup().catch((err) => {
logger.error(err, "Client conn end");
});
});
} catch (err) {
logger.error(err, "Failed to establish target connection:");
clientConn.end();
reject(err);
}
});
server.on("error", (err) => {
reject(err);
});
server.on("close", () => {
quicClient?.destroy().catch((err) => {
logger.error(err, "Failed to destroy quic client");
});
});
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Failed to get server port"));
return;
}
logger.info("Gateway proxy started");
resolve({
server,
port: address.port,
cleanup: async () => {
try {
server.close();
} catch (err) {
logger.debug(err, "Error closing server");
}
try {
await quicClient?.destroy();
} catch (err) {
logger.debug(err, "Error destroying QUIC client");
}
},
getProxyError: () => proxyErrorMsg.join(",")
});
});
});
};
interface ProxyOptions {
targetHost: string;
targetPort: number;
relayHost: string;
relayPort: number;
tlsOptions: TTlsOption;
identityId: string;
orgId: string;
}
export const withGatewayProxy = async <T>(
callback: (port: number) => Promise<T>,
options: ProxyOptions
): Promise<T> => {
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server
const { port, cleanup, getProxyError } = await setupProxyServer({
targetHost,
targetPort,
relayPort,
relayHost,
tlsOptions,
identityId,
orgId
});
try {
// Execute the callback with the allocated port
return await callback(port);
} catch (err) {
const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) {
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
}
logger.error(err, "Failed to do gateway");
let errorMessage = proxyErrorMessage || (err as Error)?.message;
if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) {
errorMessage = (err.response?.data as { message: string }).message;
}
throw new BadRequestError({ message: errorMessage });
} finally {
// Ensure cleanup happens regardless of success or failure
await cleanup();
}
};
export { pingGatewayAndVerify, withGatewayProxy } from "./gateway";
export { GatewayHttpProxyActions, GatewayProxyProtocol } from "./types";

View File

@ -0,0 +1,42 @@
import net from "node:net";
import https from "https";
export type TGatewayTlsOptions = { ca: string; cert: string; key: string };
export enum GatewayProxyProtocol {
Http = "http",
Tcp = "tcp"
}
export enum GatewayHttpProxyActions {
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token"
}
export interface IGatewayProxyOptions {
targetHost: string;
targetPort: number;
relayHost: string;
relayPort: number;
tlsOptions: TGatewayTlsOptions;
identityId: string;
orgId: string;
protocol: GatewayProxyProtocol;
httpsAgent?: https.Agent;
}
export type TPingGatewayAndVerifyDTO = {
relayHost: string;
relayPort: number;
tlsOptions: TGatewayTlsOptions;
maxRetries?: number;
identityId: string;
orgId: string;
};
export interface IGatewayProxyServer {
server: net.Server;
port: number;
cleanup: () => Promise<void>;
getProxyError: () => string;
}

View File

@ -11,7 +11,7 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
return {
errorResponseBuilder: (_, context) => {
throw new RateLimitError({
message: `Rate limit exceeded. Please try again in ${context.after}`
message: `Rate limit exceeded. Please try again in ${Math.ceil(context.ttl / 1000)} seconds`
});
},
timeWindow: 60 * 1000,
@ -113,3 +113,12 @@ export const requestAccessLimit: RateLimitOptions = {
max: 10,
keyGenerator: (req) => req.realIp
};
export const smtpRateLimit = ({
keyGenerator = (req) => req.realIp
}: Pick<RateLimitOptions, "keyGenerator"> = {}): RateLimitOptions => ({
timeWindow: 40 * 1000,
hook: "preValidation",
max: 2,
keyGenerator
});

View File

@ -8,6 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { IdentityKubernetesAuthTokenReviewMode } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({
@ -18,6 +19,7 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
accessTokenTrustedIps: true,
createdAt: true,
updatedAt: true,
tokenReviewMode: true,
identityId: true,
kubernetesHost: true,
allowedNamespaces: true,
@ -124,6 +126,10 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
tokenReviewMode: z
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
.default(IdentityKubernetesAuthTokenReviewMode.Api)
.describe(KUBERNETES_AUTH.ATTACH.tokenReviewMode),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
@ -157,10 +163,22 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.default(0)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
.superRefine((data, ctx) => {
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
ctx.addIssue({
path: ["gatewayId"],
code: z.ZodIssueCode.custom,
message: "When token review mode is set to Gateway, a gateway must be selected"
});
}
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
ctx.addIssue({
path: ["accessTokenTTL"],
code: z.ZodIssueCode.custom,
message: "Access Token TTL cannot be greater than Access Token Max TTL."
});
}
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
@ -247,6 +265,10 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
tokenReviewMode: z
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.tokenReviewMode),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
@ -280,10 +302,26 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
.superRefine((data, ctx) => {
if (
data.tokenReviewMode &&
data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway &&
!data.gatewayId
) {
ctx.addIssue({
path: ["gatewayId"],
code: z.ZodIssueCode.custom,
message: "When token review mode is set to Gateway, a gateway must be selected"
});
}
if (data.accessTokenMaxTTL && data.accessTokenTTL ? data.accessTokenTTL > data.accessTokenMaxTTL : false) {
ctx.addIssue({
path: ["accessTokenTTL"],
code: z.ZodIssueCode.custom,
message: "Access Token TTL cannot be greater than Access Token Max TTL."
});
}
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema

View File

@ -1,7 +1,7 @@
import { z } from "zod";
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
import { inviteUserRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@ -11,7 +11,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/signup",
config: {
rateLimit: inviteUserRateLimit
rateLimit: smtpRateLimit()
},
method: "POST",
schema: {
@ -81,7 +81,10 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/signup-resend",
config: {
rateLimit: inviteUserRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) =>
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp
})
},
method: "POST",
schema: {

View File

@ -2,9 +2,9 @@ import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
@ -47,7 +47,9 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/projects/:projectId/grant-admin-access",
config: {
rateLimit: writeLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp)
})
},
schema: {
params: z.object({

View File

@ -2,10 +2,10 @@ import { z } from "zod";
import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
@ -80,7 +80,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/email/password-reset",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
})
},
schema: {
body: z.object({
@ -224,7 +226,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/email/password-setup",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp)
})
},
schema: {
response: {
@ -233,6 +237,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.password.sendPasswordSetupEmail(req.permission);
@ -267,6 +272,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
await server.services.password.setupPassword(req.body, req.permission);

View File

@ -2,7 +2,7 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
@ -12,7 +12,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/me/emails/code",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp
})
},
schema: {
body: z.object({

View File

@ -3,7 +3,7 @@ import { z } from "zod";
import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -13,7 +13,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
url: "/email/signup",
method: "POST",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
})
},
schema: {
body: z.object({

View File

@ -20,8 +20,9 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@ -33,6 +34,7 @@ import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/su
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
IdentityKubernetesAuthTokenReviewMode,
TAttachKubernetesAuthDTO,
TCreateTokenReviewResponse,
TGetKubernetesAuthDTO,
@ -72,19 +74,25 @@ export const identityKubernetesAuthServiceFactory = ({
gatewayId: string;
targetHost: string;
targetPort: number;
caCert?: string;
reviewTokenThroughGateway: boolean;
},
gatewayCallback: (host: string, port: number) => Promise<T>
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
): Promise<T> => {
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
const callbackResult = await withGatewayProxy(
async (port) => {
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
const res = await gatewayCallback("https://localhost", port);
async (port, httpsAgent) => {
const res = await gatewayCallback(
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
port,
httpsAgent
);
return res;
},
{
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
targetHost: inputs.targetHost,
targetPort: inputs.targetPort,
relayHost,
@ -95,7 +103,12 @@ export const identityKubernetesAuthServiceFactory = ({
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
},
// we always pass this, because its needed for both tcp and http protocol
httpsAgent: new https.Agent({
ca: inputs.caCert,
rejectUnauthorized: Boolean(inputs.caCert)
})
}
);
@ -129,22 +142,29 @@ export const identityKubernetesAuthServiceFactory = ({
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
}
let tokenReviewerJwt = "";
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
tokenReviewerJwt = decryptor({
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
}).toString();
} else {
// if no token reviewer is provided means the incoming token has to act as reviewer
tokenReviewerJwt = serviceAccountJwt;
}
const tokenReviewCallbackRaw = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
let tokenReviewerJwt = "";
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
tokenReviewerJwt = decryptor({
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
}).toString();
} else {
// if no token reviewer is provided means the incoming token has to act as reviewer
tokenReviewerJwt = serviceAccountJwt;
}
let { kubernetesHost } = identityKubernetesAuth;
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
}
let servername = identityKubernetesAuth.kubernetesHost;
if (servername.startsWith("https://") || servername.startsWith("http://")) {
servername = new RE2("^https?:\\/\\/").replace(servername, "");
}
// get the last colon index, if it has a port, remove it, including the colon
const lastColonIndex = servername.lastIndexOf(":");
if (lastColonIndex !== -1) {
servername = servername.substring(0, lastColonIndex);
}
const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
const baseUrl = port ? `${host}:${port}` : host;
const res = await axios
@ -165,11 +185,10 @@ export const identityKubernetesAuthServiceFactory = ({
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: Boolean(caCert),
servername: kubernetesHost
servername
})
}
)
@ -192,18 +211,137 @@ export const identityKubernetesAuthServiceFactory = ({
return res.data;
};
const [k8sHost, k8sPort] = kubernetesHost.split(":");
const tokenReviewCallbackThroughGateway = async (
host: string = identityKubernetesAuth.kubernetesHost,
port?: number,
httpsAgent?: https.Agent
) => {
logger.info(
{
host,
port
},
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
);
const data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
const baseUrl = port ? `${host}:${port}` : host;
const res = await axios
.post<TCreateTokenReviewResponse>(
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
tokenReviewCallback
{
headers: {
"Content-Type": "application/json",
"x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
...(httpsAgent ? { httpsAgent } : {})
}
)
: await tokenReviewCallback();
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
let { message } = err?.response?.data as unknown as { message?: string };
if (!message && typeof err.response.data === "string") {
message = err.response.data;
}
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
throw err;
});
return res.data;
};
let data: TCreateTokenReviewResponse | undefined;
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
const { kubernetesHost } = identityKubernetesAuth;
let urlString = kubernetesHost;
if (!kubernetesHost.startsWith("http://") && !kubernetesHost.startsWith("https://")) {
urlString = `https://${kubernetesHost}`;
}
const url = new URL(urlString);
let { port: k8sPort } = url;
const { protocol, hostname: k8sHost } = url;
const cleanedProtocol = new RE2(/[^a-zA-Z0-9]/g).replace(protocol, "").toLowerCase();
if (!["https", "http"].includes(cleanedProtocol)) {
throw new BadRequestError({
message: "Invalid Kubernetes host URL, must start with http:// or https://"
});
}
if (!k8sPort) {
k8sPort = cleanedProtocol === "https" ? "443" : "80";
}
if (!identityKubernetesAuth.gatewayId) {
throw new BadRequestError({
message: "Gateway ID is required when token review mode is set to Gateway"
});
}
data = await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http)
targetPort: k8sPort ? Number(k8sPort) : 443,
caCert,
reviewTokenThroughGateway: true
},
tokenReviewCallbackThroughGateway
);
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
let { kubernetesHost } = identityKubernetesAuth;
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
}
const [k8sHost, k8sPort] = kubernetesHost.split(":");
data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443,
reviewTokenThroughGateway: false
},
tokenReviewCallbackRaw
)
: await tokenReviewCallbackRaw();
} else {
throw new BadRequestError({
message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}`
});
}
if (!data) {
throw new BadRequestError({
message: "Failed to review token"
});
}
if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
@ -298,6 +436,7 @@ export const identityKubernetesAuthServiceFactory = ({
kubernetesHost,
caCert,
tokenReviewerJwt,
tokenReviewMode,
allowedNamespaces,
allowedNames,
allowedAudience,
@ -384,6 +523,7 @@ export const identityKubernetesAuthServiceFactory = ({
{
identityId: identityMembershipOrg.identityId,
kubernetesHost,
tokenReviewMode,
allowedNamespaces,
allowedNames,
allowedAudience,
@ -410,6 +550,7 @@ export const identityKubernetesAuthServiceFactory = ({
kubernetesHost,
caCert,
tokenReviewerJwt,
tokenReviewMode,
allowedNamespaces,
allowedNames,
allowedAudience,
@ -492,6 +633,7 @@ export const identityKubernetesAuthServiceFactory = ({
const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost,
tokenReviewMode,
allowedNamespaces,
allowedNames,
allowedAudience,

View File

@ -5,11 +5,17 @@ export type TLoginKubernetesAuthDTO = {
jwt: string;
};
export enum IdentityKubernetesAuthTokenReviewMode {
Api = "api",
Gateway = "gateway"
}
export type TAttachKubernetesAuthDTO = {
identityId: string;
kubernetesHost: string;
caCert: string;
tokenReviewerJwt?: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@ -26,6 +32,7 @@ export type TUpdateKubernetesAuthDTO = {
kubernetesHost?: string;
caCert?: string;
tokenReviewerJwt?: string | null;
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;

View File

@ -30,7 +30,7 @@ import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -259,16 +259,17 @@ export const projectServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
const plan = await licenseService.getPlan(organization.id);
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
// case: limit imposed on number of workspaces allowed
// case: number of workspaces used exceeds the number of workspaces allowed
throw new BadRequestError({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
const results = await (trx || projectDAL).transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]);
const plan = await licenseService.getPlan(organization.id);
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
// case: limit imposed on number of workspaces allowed
// case: number of workspaces used exceeds the number of workspaces allowed
throw new BadRequestError({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) {

View File

@ -127,6 +127,7 @@ export const OnePassSyncFns = {
syncSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
environment,
destinationConfig: { vaultId }
} = secretSync;
@ -164,7 +165,7 @@ export const OnePassSyncFns = {
for await (const [key, variable] of Object.entries(items)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
if (!(key in secretMap)) {
try {

View File

@ -294,7 +294,7 @@ const deleteParametersBatch = async (
export const AwsParameterStoreSyncFns = {
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig, syncOptions } = secretSync;
const { destinationConfig, syncOptions, environment } = secretSync;
const ssm = await getSSM(secretSync);
@ -391,7 +391,7 @@ export const AwsParameterStoreSyncFns = {
const [key, parameter] = entry;
// eslint-disable-next-line no-continue
if (!matchesSchema(key, syncOptions.keySchema)) continue;
if (!matchesSchema(key, environment?.slug || "", syncOptions.keySchema)) continue;
if (!(key in secretMap) || !secretMap[key].value) {
parametersToDelete.push(parameter);

View File

@ -57,7 +57,11 @@ const sleep = async () =>
setTimeout(resolve, 1000);
});
const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string): Promise<TAwsSecretsRecord> => {
const getSecretsRecord = async (
client: SecretsManagerClient,
environment: string,
keySchema?: string
): Promise<TAwsSecretsRecord> => {
const awsSecretsRecord: TAwsSecretsRecord = {};
let hasNext = true;
let nextToken: string | undefined;
@ -72,7 +76,7 @@ const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string
if (output.SecretList) {
output.SecretList.forEach((secretEntry) => {
if (secretEntry.Name && matchesSchema(secretEntry.Name, keySchema)) {
if (secretEntry.Name && matchesSchema(secretEntry.Name, environment, keySchema)) {
awsSecretsRecord[secretEntry.Name] = secretEntry;
}
});
@ -307,11 +311,11 @@ const processTags = ({
export const AwsSecretsManagerSyncFns = {
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig, syncOptions } = secretSync;
const { destinationConfig, syncOptions, environment } = secretSync;
const client = await getSecretsManagerClient(secretSync);
const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema);
const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema);
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
@ -401,7 +405,7 @@ export const AwsSecretsManagerSyncFns = {
for await (const secretKey of Object.keys(awsSecretsRecord)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secretKey, syncOptions.keySchema)) continue;
if (!matchesSchema(secretKey, environment?.slug || "", syncOptions.keySchema)) continue;
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
try {
@ -468,7 +472,11 @@ export const AwsSecretsManagerSyncFns = {
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
const client = await getSecretsManagerClient(secretSync);
const awsSecretsRecord = await getSecretsRecord(client, secretSync.syncOptions.keySchema);
const awsSecretsRecord = await getSecretsRecord(
client,
secretSync.environment?.slug || "",
secretSync.syncOptions.keySchema
);
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
const { destinationConfig } = secretSync;
@ -503,11 +511,11 @@ export const AwsSecretsManagerSyncFns = {
}
},
removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig, syncOptions } = secretSync;
const { destinationConfig, syncOptions, environment } = secretSync;
const client = await getSecretsManagerClient(secretSync);
const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema);
const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema);
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
for await (const secretKey of Object.keys(awsSecretsRecord)) {

View File

@ -141,7 +141,7 @@ export const azureAppConfigurationSyncFactory = ({
for await (const key of Object.keys(azureAppConfigSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
const azureSecret = azureAppConfigSecrets[key];
if (

View File

@ -194,7 +194,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
for await (const deleteSecretKey of deleteSecrets.filter(
(secret) =>
matchesSchema(secret, secretSync.syncOptions.keySchema) &&
matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
!setSecrets.find((setSecret) => setSecret.key === secret)
)) {
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, {

View File

@ -118,7 +118,7 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec
for await (const secret of Object.keys(camundaSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
if (!(secret in secretMap) || !secretMap[secret].value) {
try {

View File

@ -117,7 +117,7 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
for await (const secret of databricksSecretKeys) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
if (!(secret.key in secretMap)) {
await deleteDatabricksSecrets({

View File

@ -155,7 +155,7 @@ export const GcpSyncFns = {
for await (const key of Object.keys(gcpSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
try {
if (!(key in secretMap) || !secretMap[key].value) {

View File

@ -223,8 +223,9 @@ export const GithubSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const encryptedSecret of encryptedSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
// eslint-disable-next-line no-continue
continue;
if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret);

View File

@ -68,6 +68,7 @@ export const HCVaultSyncFns = {
syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
environment,
destinationConfig: { mount, path },
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
@ -97,7 +98,7 @@ export const HCVaultSyncFns = {
for await (const [key] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
if (!(key in secretMap)) {
delete variables[key];

View File

@ -200,8 +200,9 @@ export const HumanitecSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const humanitecSecret of humanitecSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(humanitecSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
// eslint-disable-next-line no-continue
continue;
if (!secretMap[humanitecSecret.key]) {
await deleteSecret(secretSync, humanitecSecret);

View File

@ -1,5 +1,5 @@
import { AxiosError } from "axios";
import RE2 from "re2";
import handlebars from "handlebars";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
@ -68,13 +68,17 @@ type TSyncSecretDeps = {
};
// Add schema to secret keys
const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
const addSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => {
if (!schema) return unprocessedSecretMap;
const processedSecretMap: TSecretMap = {};
for (const [key, value] of Object.entries(unprocessedSecretMap)) {
const newKey = new RE2("{{secretKey}}").replace(schema, key);
const newKey = handlebars.compile(schema)({
secretKey: key,
environment
});
processedSecretMap[newKey] = value;
}
@ -82,10 +86,17 @@ const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMa
};
// Strip schema from secret keys
const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
const stripSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => {
if (!schema) return unprocessedSecretMap;
const [prefix, suffix] = schema.split("{{secretKey}}");
const compiledSchemaPattern = handlebars.compile(schema)({
secretKey: "{{secretKey}}", // Keep secretKey
environment
});
const parts = compiledSchemaPattern.split("{{secretKey}}");
const prefix = parts[0];
const suffix = parts[parts.length - 1];
const strippedMap: TSecretMap = {};
@ -103,21 +114,40 @@ const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecret
};
// Checks if a key matches a schema
export const matchesSchema = (key: string, schema?: string): boolean => {
export const matchesSchema = (key: string, environment: string, schema?: string): boolean => {
if (!schema) return true;
const [prefix, suffix] = schema.split("{{secretKey}}");
if (prefix === undefined || suffix === undefined) return true;
const compiledSchemaPattern = handlebars.compile(schema)({
secretKey: "{{secretKey}}", // Keep secretKey
environment
});
return key.startsWith(prefix) && key.endsWith(suffix);
// This edge-case shouldn't be possible
if (!compiledSchemaPattern.includes("{{secretKey}}")) {
return key === compiledSchemaPattern;
}
const parts = compiledSchemaPattern.split("{{secretKey}}");
const prefix = parts[0];
const suffix = parts[parts.length - 1];
if (prefix === "" && suffix === "") return true;
// If prefix is empty, key must end with suffix
if (prefix === "") return key.endsWith(suffix);
// If suffix is empty, key must start with prefix
if (suffix === "") return key.startsWith(prefix);
return key.startsWith(prefix) && key.endsWith(suffix) && key.length >= prefix.length + suffix.length;
};
// Filter only for secrets with keys that match the schema
const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => {
const filterForSchema = (secretMap: TSecretMap, environment: string, schema?: string): TSecretMap => {
const filteredMap: TSecretMap = {};
for (const [key, value] of Object.entries(secretMap)) {
if (matchesSchema(key, schema)) {
if (matchesSchema(key, environment, schema)) {
filteredMap[key] = value;
}
}
@ -131,7 +161,7 @@ export const SecretSyncFns = {
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
@ -255,14 +285,16 @@ export const SecretSyncFns = {
);
}
return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema);
const filtered = filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
const stripped = stripSchema(filtered, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
return stripped;
},
removeSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:

View File

@ -28,10 +28,30 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
keySchema: z
.string()
.optional()
.refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), {
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
})
.refine(
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = ["{{environment}}"];
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
.join("|");
const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`);
const contentIsValid = allowedContentRegex.test(val);
// Check if {{secretKey}} is present
const secretKeyRegex = new RE2(/\{\{secretKey\}\}/);
const secretKeyIsPresent = secretKeyRegex.test(val);
return contentIsValid && secretKeyIsPresent;
},
{
message:
"Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
.describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema),
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
});

View File

@ -127,7 +127,7 @@ export const TeamCitySyncFns = {
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
if (!(key in secretMap)) {
try {

View File

@ -232,8 +232,11 @@ export const TerraformCloudSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for (const terraformCloudVariable of terraformCloudVariables) {
// eslint-disable-next-line no-continue
if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue;
if (
!matchesSchema(terraformCloudVariable.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)
)
// eslint-disable-next-line no-continue
continue;
if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) {
await deleteVariable(secretSync, terraformCloudVariable);

View File

@ -291,8 +291,9 @@ export const VercelSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const vercelSecret of vercelSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!matchesSchema(vercelSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
// eslint-disable-next-line no-continue
continue;
if (!secretMap[vercelSecret.key]) {
await deleteSecret(secretSync, vercelSecret);

View File

@ -128,6 +128,7 @@ export const WindmillSyncFns = {
syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
environment,
destinationConfig: { path },
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
@ -171,7 +172,7 @@ export const WindmillSyncFns = {
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
if (!(key in secretMap)) {
try {

View File

@ -14,7 +14,7 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.9.1
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.5.92
github.com/infisical/go-sdk v0.5.95
github.com/infisical/infisical-kmip v0.3.5
github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a

View File

@ -294,6 +294,10 @@ github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7P
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE=
github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
github.com/infisical/go-sdk v0.5.94 h1:wKBj+KpJEe+ZzOJ7koXQZDR0dLL9bt0Kqgf/1q+7tG4=
github.com/infisical/go-sdk v0.5.94/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
github.com/infisical/go-sdk v0.5.95 h1:so0YwPofbT7j6Ao8Xcxee/o3ia33meuEVDU2vWr9yfs=
github.com/infisical/go-sdk v0.5.95/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=

View File

@ -7,16 +7,76 @@ import (
"os/exec"
"os/signal"
"runtime"
"sync/atomic"
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/gateway"
"github.com/Infisical/infisical-merge/packages/util"
infisicalSdk "github.com/infisical/go-sdk"
"github.com/pkg/errors"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
func getInfisicalSdkInstance(cmd *cobra.Command) (infisicalSdk.InfisicalClientInterface, context.CancelFunc, error) {
ctx, cancel := context.WithCancel(cmd.Context())
infisicalClient := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
})
token, err := util.GetInfisicalToken(cmd)
if err != nil {
cancel()
return nil, nil, err
}
// if the --token param is set, we use it directly for authentication
if token != nil {
infisicalClient.Auth().SetAccessToken(token.Token)
return infisicalClient, cancel, nil
}
// if the --token param is not set, we use the auth-method flag to determine the authentication method, and perform the appropriate login flow based on that
authMethod, err := cmd.Flags().GetString("auth-method")
if err != nil {
cancel()
return nil, nil, err
}
authMethodValid, strategy := util.IsAuthMethodValid(authMethod, false)
if !authMethodValid {
util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", authMethod))
}
sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd)
authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){
util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin,
util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin,
util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin,
util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin,
util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin,
util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin,
util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin,
util.AuthStrategy.JWT_AUTH: sdkAuthenticator.HandleJwtAuthLogin,
}
_, err = authStrategies[strategy]()
if err != nil {
cancel()
return nil, nil, err
}
return infisicalClient, cancel, nil
}
var gatewayCmd = &cobra.Command{
Use: "gateway",
Short: "Run the Infisical gateway or manage its systemd service",
@ -26,13 +86,18 @@ var gatewayCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse token flag")
}
if token == nil {
util.HandleError(fmt.Errorf("Token not found"))
infisicalClient, cancelSdk, err := getInfisicalSdkInstance(cmd)
if err != nil {
util.HandleError(err, "unable to get infisical client")
}
defer cancelSdk()
var accessToken atomic.Value
accessToken.Store(infisicalClient.Auth().GetAccessToken())
if accessToken.Load().(string) == "" {
util.HandleError(errors.New("no access token found"))
}
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
@ -41,13 +106,14 @@ var gatewayCmd = &cobra.Command{
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sigStopCh := make(chan bool, 1)
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
ctx, cancelCmd := context.WithCancel(cmd.Context())
defer cancelCmd()
go func() {
<-sigCh
close(sigStopCh)
cancel()
cancelCmd()
cancelSdk()
// If we get a second signal, force exit
<-sigCh
@ -55,6 +121,34 @@ var gatewayCmd = &cobra.Command{
os.Exit(1)
}()
var gatewayInstance *gateway.Gateway
// Token refresh goroutine - runs every 10 seconds
go func() {
tokenRefreshTicker := time.NewTicker(10 * time.Second)
defer tokenRefreshTicker.Stop()
for {
select {
case <-tokenRefreshTicker.C:
if ctx.Err() != nil {
return
}
newToken := infisicalClient.Auth().GetAccessToken()
if newToken != "" && newToken != accessToken.Load().(string) {
accessToken.Store(newToken)
if gatewayInstance != nil {
gatewayInstance.UpdateIdentityAccessToken(newToken)
}
}
case <-ctx.Done():
return
}
}
}()
// Main gateway retry loop with proper context handling
retryTicker := time.NewTicker(5 * time.Second)
defer retryTicker.Stop()
@ -64,7 +158,7 @@ var gatewayCmd = &cobra.Command{
log.Info().Msg("Shutting down gateway")
return
}
gatewayInstance, err := gateway.NewGateway(token.Token)
gatewayInstance, err := gateway.NewGateway(accessToken.Load().(string))
if err != nil {
util.HandleError(err)
}
@ -126,7 +220,7 @@ var gatewayInstallCmd = &cobra.Command{
}
if token == nil {
util.HandleError(fmt.Errorf("Token not found"))
util.HandleError(errors.New("Token not found"))
}
domain, err := cmd.Flags().GetString("domain")
@ -183,7 +277,7 @@ var gatewayRelayCmd = &cobra.Command{
}
if relayConfigFilePath == "" {
util.HandleError(fmt.Errorf("Missing config file"))
util.HandleError(errors.New("Missing config file"))
}
gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath)
@ -198,7 +292,19 @@ var gatewayRelayCmd = &cobra.Command{
}
func init() {
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayCmd.Flags().String("token", "", "connect with Infisical using machine identity access token. if not provided, you must set the auth-method flag")
gatewayCmd.Flags().String("auth-method", "", "login method [universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]. if not provided, you must set the token flag")
gatewayCmd.Flags().String("client-id", "", "client id for universal auth")
gatewayCmd.Flags().String("client-secret", "", "client secret for universal auth")
gatewayCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods")
gatewayCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth")
gatewayCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
gatewayCmd.Flags().String("jwt", "", "JWT for jwt-based auth methods [oidc-auth, jwt-auth]")
gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")

View File

@ -49,13 +49,13 @@ func startKmipServer(cmd *cobra.Command, args []string) {
var identityClientSecret string
if strategy == util.AuthStrategy.UNIVERSAL_AUTH {
identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME})
if err != nil {
util.HandleError(err, "Unable to parse identity client ID")
}
identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME})
if err != nil {
util.HandleError(err, "Unable to parse identity client secret")
}

View File

@ -49,97 +49,6 @@ type params struct {
keyLength uint32
}
func handleUniversalAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
clientId, err := util.GetCmdFlagOrEnv(cmd, "client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
clientSecret, err := util.GetCmdFlagOrEnv(cmd, "client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret)
}
func handleKubernetesAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
serviceAccountTokenPath, err := util.GetCmdFlagOrEnv(cmd, "service-account-token-path", util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath)
}
func handleAzureAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().AzureAuthLogin(identityId, "")
}
func handleGcpIdTokenAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().GcpIdTokenAuthLogin(identityId)
}
func handleGcpIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
serviceAccountKeyFilePath, err := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath)
}
func handleAwsIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().AwsIamAuthLogin(identityId)
}
func handleOidcAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
jwt, err := util.GetCmdFlagOrEnv(cmd, "oidc-jwt", util.INFISICAL_OIDC_AUTH_JWT_NAME)
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().OidcAuthLogin(identityId, jwt)
}
func formatAuthMethod(authMethod string) string {
return strings.ReplaceAll(authMethod, "-", " ")
}
@ -154,8 +63,22 @@ var loginCmd = &cobra.Command{
Use: "login",
Short: "Login into your Infisical account",
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
PreRunE: func(cmd *cobra.Command, args []string) error {
// daniel: oidc-jwt is deprecated in favor of `jwt`. we backfill the `jwt` flag with the value of `oidc-jwt` if it's set.
if cmd.Flags().Changed("oidc-jwt") && !cmd.Flags().Changed("jwt") {
oidcJWT, err := cmd.Flags().GetString("oidc-jwt")
if err != nil {
return err
}
err = cmd.Flags().Set("jwt", oidcJWT)
if err != nil {
return err
}
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
presetDomain := config.INFISICAL_URL
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
@ -310,17 +233,19 @@ var loginCmd = &cobra.Command{
Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
} else {
authStrategies := map[util.AuthStrategyType]func(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error){
util.AuthStrategy.UNIVERSAL_AUTH: handleUniversalAuthLogin,
util.AuthStrategy.KUBERNETES_AUTH: handleKubernetesAuthLogin,
util.AuthStrategy.AZURE_AUTH: handleAzureAuthLogin,
util.AuthStrategy.GCP_ID_TOKEN_AUTH: handleGcpIdTokenAuthLogin,
util.AuthStrategy.GCP_IAM_AUTH: handleGcpIamAuthLogin,
util.AuthStrategy.AWS_IAM_AUTH: handleAwsIamAuthLogin,
util.AuthStrategy.OIDC_AUTH: handleOidcAuthLogin,
sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd)
authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){
util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin,
util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin,
util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin,
util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin,
util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin,
util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin,
util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin,
}
credential, err := authStrategies[strategy](cmd, infisicalClient)
credential, err := authStrategies[strategy]()
if err != nil {
euErrorMessage := ""
@ -518,14 +443,18 @@ func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file")
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
loginCmd.Flags().String("method", "user", "login method [user, universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]")
loginCmd.Flags().String("client-id", "", "client id for universal auth")
loginCmd.Flags().String("client-secret", "", "client secret for universal auth")
loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods")
loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth")
loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication")
loginCmd.Flags().String("jwt", "", "jwt for jwt-based auth methods [oidc-auth, jwt-auth]")
loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication. Deprecated, use --jwt instead")
loginCmd.Flags().MarkDeprecated("oidc-jwt", "use --jwt instead")
}
func DomainOverridePrompt() (bool, error) {

View File

@ -4,11 +4,19 @@ import (
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog/log"
@ -18,9 +26,13 @@ func handleConnection(ctx context.Context, quicConn quic.Connection) {
log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String())
// Use WaitGroup to track all streams
var wg sync.WaitGroup
contextWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
for {
// Accept the first stream, which we'll use for commands
stream, err := quicConn.AcceptStream(ctx)
stream, err := quicConn.AcceptStream(contextWithTimeout)
if err != nil {
log.Printf("Failed to accept QUIC stream: %v", err)
break
@ -44,7 +56,12 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
// Use buffered reader for better handling of fragmented data
reader := bufio.NewReader(stream)
defer stream.Close()
defer func() {
log.Info().Msgf("Closing stream %d", streamID)
if stream != nil {
stream.Close()
}
}()
for {
msg, err := reader.ReadBytes('\n')
@ -89,6 +106,39 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
CopyDataFromQuicToTcp(stream, destTarget)
log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String())
return
case "FORWARD-HTTP":
argParts := bytes.Split(args, []byte(" "))
if len(argParts) == 0 {
log.Error().Msg("FORWARD-HTTP requires target URL")
return
}
targetURL := string(argParts[0])
if !isValidURL(targetURL) {
log.Error().Msgf("Invalid target URL: %s", targetURL)
return
}
// Parse optional parameters
var caCertB64, verifyParam string
for _, part := range argParts[1:] {
partStr := string(part)
if strings.HasPrefix(partStr, "ca=") {
caCertB64 = strings.TrimPrefix(partStr, "ca=")
} else if strings.HasPrefix(partStr, "verify=") {
verifyParam = strings.TrimPrefix(partStr, "verify=")
}
}
log.Info().Msgf("Starting HTTP proxy to: %s", targetURL)
if err := handleHTTPProxy(stream, reader, targetURL, caCertB64, verifyParam); err != nil {
log.Error().Msgf("HTTP proxy error: %v", err)
}
return
case "PING":
if _, err := stream.Write([]byte("PONG\n")); err != nil {
log.Error().Msgf("Error writing PONG response: %v", err)
@ -100,11 +150,142 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
}
}
}
func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, caCertB64 string, verifyParam string) error {
transport := &http.Transport{
DisableKeepAlives: false,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
}
if strings.HasPrefix(targetURL, "https://") {
tlsConfig := &tls.Config{}
if caCertB64 != "" {
caCert, err := base64.StdEncoding.DecodeString(caCertB64)
if err == nil {
caCertPool := x509.NewCertPool()
if caCertPool.AppendCertsFromPEM(caCert) {
tlsConfig.RootCAs = caCertPool
log.Info().Msg("Using provided CA certificate from gateway client")
} else {
log.Error().Msg("Failed to parse provided CA certificate")
}
} else {
log.Error().Msgf("Failed to decode CA certificate: %v", err)
}
}
if verifyParam != "" {
tlsConfig.InsecureSkipVerify = verifyParam == "false"
log.Info().Msgf("TLS verification set to: %s", verifyParam)
}
transport.TLSClientConfig = tlsConfig
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
// Loop to handle multiple HTTP requests on the same stream
for {
req, err := http.ReadRequest(reader)
if err != nil {
if errors.Is(err, io.EOF) {
log.Info().Msg("Client closed HTTP connection")
return nil
}
return fmt.Errorf("failed to read HTTP request: %v", err)
}
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
actionHeader := req.Header.Get("x-infisical-action")
if actionHeader != "" {
if actionHeader == "inject-k8s-sa-auth-token" {
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
continue // Continue to next request instead of returning
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
}
req.Header.Del("x-infisical-action")
}
// Build full target URL
var targetFullURL string
if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") {
baseURL := strings.TrimSuffix(targetURL, "/")
targetFullURL = baseURL + req.URL.Path
if req.URL.RawQuery != "" {
targetFullURL += "?" + req.URL.RawQuery
}
} else {
baseURL := strings.TrimSuffix("http://"+targetURL, "/")
targetFullURL = baseURL + req.URL.Path
if req.URL.RawQuery != "" {
targetFullURL += "?" + req.URL.RawQuery
}
}
// create the request to the target
proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body)
if err != nil {
log.Error().Msgf("Failed to create proxy request: %v", err)
stream.Write([]byte(buildHttpInternalServerError("failed to create proxy request")))
continue // Continue to next request
}
proxyReq.Header = req.Header.Clone()
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
resp, err := client.Do(proxyReq)
if err != nil {
log.Error().Msgf("Failed to reach target: %v", err)
stream.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error()))))
continue // Continue to next request
}
// Write the entire response (status line, headers, body) to the stream
// http.Response.Write handles this for "Connection: close" correctly.
// For other connection tokens, manual removal might be needed if they cause issues with QUIC.
// For a simple proxy, this is generally sufficient.
resp.Header.Del("Connection") // Good practice for proxies
log.Info().Msgf("Writing response to stream: %s", resp.Status)
if err := resp.Write(stream); err != nil {
log.Error().Err(err).Msg("Failed to write response to stream")
resp.Body.Close()
return fmt.Errorf("failed to write response to stream: %w", err)
}
resp.Body.Close()
// Check if client wants to close connection
if req.Header.Get("Connection") == "close" {
log.Info().Msg("Client requested connection close")
return nil
}
}
}
func buildHttpInternalServerError(message string) string {
return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message)
}
type CloseWrite interface {
CloseWrite() error
}
func isValidURL(str string) bool {
u, err := url.Parse(str)
return err == nil && u.Scheme != "" && u.Host != ""
}
func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) {
// Create a WaitGroup to wait for both copy operations
var wg sync.WaitGroup

View File

@ -54,6 +54,10 @@ func NewGateway(identityToken string) (Gateway, error) {
}, nil
}
func (g *Gateway) UpdateIdentityAccessToken(accessToken string) {
g.httpClient.SetAuthToken(accessToken)
}
func (g *Gateway) ConnectWithRelay() error {
relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient)
if err != nil {

View File

@ -5,7 +5,9 @@ import (
"os"
"os/exec"
infisicalSdk "github.com/infisical/go-sdk"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
type AuthStrategyType string
@ -18,6 +20,7 @@ var AuthStrategy = struct {
GCP_IAM_AUTH AuthStrategyType
AWS_IAM_AUTH AuthStrategyType
OIDC_AUTH AuthStrategyType
JWT_AUTH AuthStrategyType
}{
UNIVERSAL_AUTH: "universal-auth",
KUBERNETES_AUTH: "kubernetes",
@ -26,6 +29,7 @@ var AuthStrategy = struct {
GCP_IAM_AUTH: "gcp-iam",
AWS_IAM_AUTH: "aws-iam",
OIDC_AUTH: "oidc-auth",
JWT_AUTH: "jwt-auth",
}
var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{
@ -36,6 +40,7 @@ var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{
AuthStrategy.GCP_IAM_AUTH,
AuthStrategy.AWS_IAM_AUTH,
AuthStrategy.OIDC_AUTH,
AuthStrategy.JWT_AUTH,
}
func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) {
@ -84,3 +89,120 @@ func EstablishUserLoginSession() LoggedInUserDetails {
return loggedInUserDetails
}
type SdkAuthenticator struct {
infisicalClient infisicalSdk.InfisicalClientInterface
cmd *cobra.Command
}
func NewSdkAuthenticator(infisicalClient infisicalSdk.InfisicalClientInterface, cmd *cobra.Command) *SdkAuthenticator {
return &SdkAuthenticator{
infisicalClient: infisicalClient,
cmd: cmd,
}
}
func (a *SdkAuthenticator) HandleUniversalAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
clientId, err := GetCmdFlagOrEnv(a.cmd, "client-id", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
clientSecret, err := GetCmdFlagOrEnv(a.cmd, "client-secret", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret)
}
func (a *SdkAuthenticator) HandleJwtAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().JwtAuthLogin(identityId, jwt)
}
func (a *SdkAuthenticator) HandleKubernetesAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
serviceAccountTokenPath, err := GetCmdFlagOrEnv(a.cmd, "service-account-token-path", []string{INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath)
}
func (a *SdkAuthenticator) HandleAzureAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().AzureAuthLogin(identityId, "")
}
func (a *SdkAuthenticator) HandleGcpIdTokenAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().GcpIdTokenAuthLogin(identityId)
}
func (a *SdkAuthenticator) HandleGcpIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
serviceAccountKeyFilePath, err := GetCmdFlagOrEnv(a.cmd, "service-account-key-file-path", []string{INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath)
}
func (a *SdkAuthenticator) HandleAwsIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().AwsIamAuthLogin(identityId)
}
func (a *SdkAuthenticator) HandleOidcAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME, INFISICAL_OIDC_AUTH_JWT_NAME})
if err != nil {
return infisicalSdk.MachineIdentityCredential{}, err
}
return a.infisicalClient.Auth().OidcAuthLogin(identityId, jwt)
}

View File

@ -24,7 +24,10 @@ const (
INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME = "INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH"
// OIDC Auth
INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT"
INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT" // deprecated in favor of INFISICAL_JWT
// JWT AUTH
INFISICAL_JWT_NAME = "INFISICAL_JWT"
// Generic env variable used for auth methods that require a machine identity ID
INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID"

View File

@ -292,13 +292,18 @@ func GetEnvVarOrFileContent(envName string, filePath string) (string, error) {
return fileContent, nil
}
func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
func GetCmdFlagOrEnv(cmd *cobra.Command, flag string, envNames []string) (string, error) {
value, flagsErr := cmd.Flags().GetString(flag)
if flagsErr != nil {
return "", flagsErr
}
if value == "" {
value = os.Getenv(envName)
for _, env := range envNames {
value = strings.TrimSpace(os.Getenv(env))
if value != "" {
break
}
}
}
if value == "" {
return "", fmt.Errorf("please provide %s flag", flag)

View File

@ -89,22 +89,3 @@ The relay system provides secure tunneling:
- Gateways only accept connections to approved resources
- Each connection requires explicit project authorization
- Resources remain private to their assigned organization
## Security Measures
### Certificate Lifecycle
- Certificates have limited validity periods
- Automatic certificate rotation
- Immediate certificate revocation capabilities
### Monitoring and Verification
1. **Continuous Verification**:
- Regular heartbeat checks
- Certificate chain validation
- Connection state monitoring
2. **Security Controls**:
- Automatic connection termination on verification failure
- Audit logging of all access attempts
- Machine identity based authentication

View File

@ -0,0 +1,168 @@
---
title: "Networking"
description: "Network configuration and firewall requirements for Infisical Gateway"
---
The Infisical Gateway requires outbound network connectivity to establish secure communication with Infisical's relay infrastructure.
This page outlines the required ports, protocols, and firewall configurations needed for optimal gateway usage.
## Network Architecture
The gateway uses a relay-based architecture to establish secure connections:
1. **Gateway** connects outbound to **Relay Servers** using UDP/QUIC protocol
2. **Relay Servers** facilitate secure communication between Gateway and Infisical Cloud
3. All traffic is end-to-end encrypted using mutual TLS over QUIC
## Required Network Connectivity
### Outbound Connections (Required)
The gateway requires the following outbound connectivity:
| Protocol | Destination | Ports | Purpose |
|----------|-------------|-------|---------|
| UDP | Relay Servers | 49152-65535 | Allocated relay communication (TLS) |
| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and relay allocation |
### Relay Server IP Addresses
Your firewall must allow outbound connectivity to the following Infisical relay servers on dynamically allocated ports.
<Tabs>
<Tab title="Infisical cloud (US)">
```
54.235.197.91:49152-65535
18.215.196.229:49152-65535
3.222.120.233:49152-65535
34.196.115.157:49152-65535
```
</Tab>
<Tab title="Infisical cloud (EU)">
```
3.125.237.40:49152-65535
52.28.157.98:49152-65535
3.125.176.90:49152-65535
```
</Tab>
<Tab title="Infisical dedicated">
Please contact your Infisical account manager for dedicated relay server IP addresses.
</Tab>
</Tabs>
<Warning>
These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice.
</Warning>
## Protocol Details
### QUIC over UDP
The gateway uses QUIC (Quick UDP Internet Connections) for primary communication:
- **Port 5349**: STUN/TURN over TLS (secure relay communication)
- **Built-in features**: Connection migration, multiplexing, reduced latency
- **Encryption**: TLS 1.3 with certificate pinning
## Understanding Firewall Behavior with UDP
Unlike TCP connections, UDP is a stateless protocol, and depending on your organization's firewall configuration, you may need to adjust network rules accordingly.
When the gateway sends UDP packets to a relay server, the return responses need to be allowed back through the firewall.
Modern firewalls handle this through "connection tracking" (also called "stateful inspection"), but the behavior can vary depending on your firewall configuration.
### Connection Tracking
Modern firewalls automatically track UDP connections and allow return responses. This is the preferred configuration as it:
- Automatically handles return responses
- Reduces firewall rule complexity
- Avoids the need for manual IP whitelisting
In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicitly define return traffic manually.
## Common Network Scenarios
### Corporate Firewalls
For corporate environments with strict egress filtering:
1. **Whitelist relay IP addresses** (listed above)
2. **Allow UDP port 5349** outbound
3. **Configure connection tracking** for UDP return traffic
4. **Allow ephemeral port range** 49152-65535 for return traffic if connection tracking is disabled
### Cloud Environments (AWS/GCP/Azure)
Configure security groups to allow:
- **Outbound UDP** to relay IPs on port 5349
- **Outbound HTTPS** to app.infisical.com/eu.infisical.com on port 443
- **Inbound UDP** on ephemeral ports (if not using stateful rules)
## 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 every 5 seconds if the connection is lost
- **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention
- **Multiple relay servers**: If one relay server is unavailable, the gateway can connect to alternative relay servers
- **Persistent sessions**: Existing connections are maintained where possible during brief network interruptions
- **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 QUIC instead of TCP?">
QUIC (Quick UDP Internet Connections) provides several advantages over traditional TCP for gateway communication:
- **Faster connection establishment**: QUIC combines transport and security handshakes, reducing connection setup time
- **Built-in encryption**: TLS 1.3 is integrated into the protocol, ensuring all traffic is encrypted by default
- **Connection migration**: QUIC connections can survive IP address changes (useful for NAT rebinding)
- **Reduced head-of-line blocking**: Multiple data streams can be multiplexed without blocking each other
- **Better performance over unreliable networks**: Advanced congestion control and packet loss recovery
- **Lower latency**: Optimized for real-time communication between gateway and cloud services
While TCP is stateful and easier for firewalls to track, QUIC's performance benefits outweigh the additional firewall configuration requirements.
</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 UDP** to relay servers on ports 49152-65535
- **Outbound HTTPS** to Infisical API endpoints
- **Return responses** are handled by connection tracking or explicit IP whitelisting
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 the required UDP ports?">
If your firewall has strict UDP restrictions:
1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses
2. **Use explicit IP whitelisting** if connection tracking is disabled
3. **Consider network policy exceptions** for the gateway host
4. **Monitor firewall logs** to identify which specific rules are blocking traffic
The gateway requires UDP connectivity to function - TCP-only configurations are not supported.
</Accordion>
<Accordion title="How many relay servers does the gateway connect to?">
The gateway connects to **one relay server at a time**:
- **Single active connection**: Only one relay connection is established per gateway instance
- **Automatic failover**: If the current relay becomes unavailable, the gateway will connect to an alternative relay
- **Load distribution**: Different gateway instances may connect to different relay servers for load balancing
- **No manual selection**: The Infisical API automatically assigns the optimal relay server based on availability and proximity
You should whitelist all relay IP addresses to ensure proper failover functionality.
</Accordion>
<Accordion title="Can the relay servers decrypt traffic going through them?">
No, relay servers cannot decrypt any traffic passing through them:
- **End-to-end encryption**: All traffic between the gateway and Infisical Cloud is encrypted using mutual TLS with certificate pinning
- **Relay acts as a tunnel**: The relay server only forwards encrypted packets - it has no access to encryption keys
- **No data storage**: Relay servers do not store any traffic or network-identifiable information
- **Certificate isolation**: Each organization has its own private PKI system, ensuring complete tenant isolation
The relay infrastructure is designed as a secure forwarding mechanism, similar to a VPN tunnel, where the relay provider cannot see the contents of the traffic flowing through it.
</Accordion>

View File

@ -32,7 +32,7 @@ For detailed installation instructions, refer to the Infisical [CLI Installation
To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway.
Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable.
### Deployment process
### Get started
<Steps>
<Step title="Create a Gateway Identity">
@ -128,7 +128,7 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
</Tab>
<Tab title="Development (direct)">
<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 --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)

View File

@ -52,10 +52,11 @@ Infisical is able to authenticate and interact with the TokenReview API by using
In the following steps, we explore how to create and use identities for your applications in Kubernetes to access the Infisical API using the Kubernetes Auth authentication method.
<Steps>
<Step title="Obtaining the token reviewer JWT for Infisical">
<Tabs>
<Tab title="Option 1: Reviewer JWT Token">
<Accordion title="Option 1: Reviewer JWT Token">
<Note>
**When to use this option**: Choose this approach when you want centralized authentication management. Only one service account needs special permissions, and your application service accounts remain unchanged.
@ -126,41 +127,91 @@ In the following steps, we explore how to create and use identities for your app
```
Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2.
</Accordion>
</Tab>
<Tab title="Option 2: Client JWT as Reviewer JWT Token">
<Accordion title="Option 2: Client JWT as Reviewer JWT Token">
<Note>
**When to use this option**: Choose this approach to eliminate long-lived tokens. This option simplifies Infisical configuration but requires each application service account to have elevated permissions.
</Note>
<Note>
**When to use this option**: Choose this approach to eliminate long-lived tokens. This option simplifies Infisical configuration but requires each application service account to have elevated permissions.
</Note>
The self-validation method eliminates the need for a separate long-lived reviewer JWT by using the same token for both authentication and validation. Instead of creating a dedicated reviewer service account, you'll grant the necessary permissions to each application service account.
The self-validation method eliminates the need for a separate long-lived reviewer JWT by using the same token for both authentication and validation. Instead of creating a dedicated reviewer service account, you'll grant the necessary permissions to each application service account.
For each service account that needs to authenticate with Infisical, add the `system:auth-delegator` role:
For each service account that needs to authenticate with Infisical, add the `system:auth-delegator` role:
```yaml client-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-client-binding-[your-app-name]
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: [your-app-service-account]
namespace: [your-app-namespace]
```
```yaml client-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-client-binding-[your-app-name]
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: [your-app-service-account]
namespace: [your-app-namespace]
```
```
kubectl apply -f client-role-binding.yaml
```
```
kubectl apply -f client-role-binding.yaml
```
When configuring Kubernetes Auth in Infisical, leave the **Token Reviewer JWT** field empty. Infisical will use the client's own token for validation.
</Accordion>
<Accordion title="Option 3: Use Gateway as Reviewer">
<Note>
**When to use this option**: Choose this approach when you have a gateway deployed in your Kubernetes Cluster and wish to eliminate long-lived tokens. This approach simplifies Infisical Kubernetes Auth configuration, and only one service account will need to have the elevated `system:auth-delegator` ClusterRole binding.
</Note>
When configuring Kubernetes Auth in Infisical, leave the **Token Reviewer JWT** field empty. Infisical will use the client's own token for validation.
</Tab>
</Tabs>
</Step>
<Info>
**Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is
available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please
contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an
enterprise license.
</Info>
<Steps>
<Step title="Deploying a gateway">
To deploy a gateway in your Kubernetes cluster, follow our [Gateway deployment guide using helm](/documentation/platform/gateways/overview).
</Step>
<Step title="Grant the gateway the system:auth-delegator ClusterRole binding">
To grant the gateway the `system:auth-delegator` ClusterRole binding, you can use the following command:
```yaml gateway-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-token-reviewer-role-binding
namespace: default # Replace with your namespace if not default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: infisical-gateway # The name of the gateway service account
namespace: default # Replace with your namespace if not default
```
```bash
kubectl apply -f gateway-role-binding.yaml
```
<Tip>
The gateway service account name is `infisical-gateway` by default if deployed using Helm.
</Tip>
</Step>
<Step title="Configure the Kubernetes Auth authentication method for the identity">
To configure your Kubernetes Auth method to use the gateway as the token reviewer, set the `Review Method` to "Gateway as Reviewer", and select the gateway you want to use as the token reviewer.
![identities organization create kubernetes auth method](/images/platform/identities/identities-kubernetes-auth-gateway-as-reviewer.png)
</Step>
</Steps>
</Accordion>
</Step>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.

View File

@ -0,0 +1,177 @@
---
title: SPIFFE/SPIRE
description: "Learn how to authenticate SPIRE workloads with Infisical using OpenID Connect (OIDC)."
---
**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect.
## Diagram
The following sequence diagram illustrates the OIDC Auth workflow for authenticating SPIRE workloads with Infisical.
```mermaid
sequenceDiagram
participant Client as SPIRE Workload
participant Agent as SPIRE Agent
participant Server as SPIRE Server
participant Infis as Infisical
Client->>Agent: Step 1: Request JWT-SVID
Agent->>Server: Validate workload and fetch signing key
Server-->>Agent: Return signing material
Agent-->>Client: Return JWT-SVID with verifiable claims
Note over Client,Infis: Step 2: Login Operation
Client->>Infis: Send JWT-SVID to /api/v1/auth/oidc-auth/login
Note over Infis,Server: Step 3: Query verification
Infis->>Server: Request JWT public key using OIDC Discovery
Server-->>Infis: Return public key
Note over Infis: Step 4: JWT validation
Infis->>Client: Return short-lived access token
Note over Client,Infis: Step 5: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates a SPIRE workload by verifying the JWT-SVID and checking that it meets specific requirements (e.g. it is issued by a trusted SPIRE server) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The SPIRE workload requests a JWT-SVID from the local SPIRE Agent.
2. The SPIRE Agent validates the workload's identity and requests signing material from the SPIRE Server.
3. The SPIRE Agent returns a JWT-SVID containing the workload's SPIFFE ID and other claims.
4. The JWT-SVID is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint.
5. Infisical fetches the public key that was used to sign the JWT-SVID from the SPIRE Server using OIDC Discovery.
6. Infisical validates the JWT-SVID using the public key provided by the SPIRE Server and checks that the subject, audience, and claims of the token matches with the set criteria.
7. If all is well, Infisical returns a short-lived access token that the workload can use to make authenticated requests to the Infisical API.
<Note>Infisical needs network-level access to the SPIRE Server's OIDC Discovery endpoint.</Note>
## Prerequisites
Before following this guide, ensure you have:
- A running SPIRE deployment with both SPIRE Server and SPIRE Agent configured
- OIDC Discovery Provider deployed alongside your SPIRE Server
- Workload registration entries created in SPIRE for the workloads that need to access Infisical
- Network connectivity between Infisical and your OIDC Discovery Provider endpoint
For detailed SPIRE setup instructions, refer to the [SPIRE documentation](https://spiffe.io/docs/latest/spire-about/).
## OIDC Discovery Provider Setup
To enable JWT-SVID verification with Infisical, you need to deploy the OIDC Discovery Provider alongside your SPIRE Server. The OIDC Discovery Provider runs as a separate service that exposes the necessary OIDC endpoints.
In Kubernetes deployments, this is typically done by adding an `oidc-discovery-provider` container to your SPIRE Server StatefulSet:
```yaml
- name: spire-oidc
image: ghcr.io/spiffe/oidc-discovery-provider:1.12.2
args:
- -config
- /run/spire/oidc/config/oidc-discovery-provider.conf
ports:
- containerPort: 443
name: spire-oidc-port
```
The OIDC Discovery Provider will expose the OIDC Discovery endpoint at `https://<spire-oidc-host>/.well-known/openid_configuration`, which Infisical will use to fetch the public keys for JWT-SVID verification.
<Note>For detailed setup instructions, refer to the [SPIRE OIDC Discovery Provider documentation](https://github.com/spiffe/spire/tree/main/support/oidc-discovery-provider).</Note>
## Guide
In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method with SPIFFE/SPIRE.
<Steps>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png)
<Warning>Restrict access by configuring the Subject, Audiences, and Claims fields</Warning>
Here's some more guidance on each field:
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the SPIRE Server. This will be used to fetch the public key needed for verifying the provided JWT-SVID. This should be set to your SPIRE Server's OIDC Discovery endpoint, typically `https://<spire-server-host>:<port>/.well-known/openid_configuration`
- Issuer: The unique identifier of the SPIRE Server issuing the JWT-SVID. This value is used to verify the iss (issuer) claim in the JWT-SVID to ensure the token is issued by a trusted SPIRE Server. This should match your SPIRE Server's configured issuer, typically `https://<spire-server-host>:<port>`
- CA Certificate: The PEM-encoded CA certificate for establishing secure communication with the SPIRE Server endpoints. This should contain the CA certificate that signed your SPIRE Server's TLS certificate.
- Subject: The expected SPIFFE ID that is the subject of the JWT-SVID. The format of the sub field for SPIRE JWT-SVIDs follows the SPIFFE ID format: `spiffe://<trust-domain>/<workload-path>`. For example: `spiffe://example.org/workload/api-server`
- Audiences: A list of intended recipients for the JWT-SVID. This value is checked against the aud (audience) claim in the token. When workloads request JWT-SVIDs from SPIRE, they specify an audience (e.g., `infisical` or your service name). Configure this to match what your workloads use.
- Claims: Additional information or attributes that should be present in the JWT-SVID for it to be valid. Standard SPIRE JWT-SVID claims include `sub` (SPIFFE ID), `aud` (audience), `exp` (expiration), and `iat` (issued at). You can also configure custom claims if your SPIRE Server includes additional metadata.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an access token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
<Tip>SPIRE JWT-SVIDs contain standard claims like `sub` (SPIFFE ID), `aud` (audience), `exp`, and `iat`. The audience is typically specified when requesting the JWT-SVID (e.g., `spire-agent api fetch jwt -audience infisical`).</Tip>
<Info>The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded SPIFFE IDs whenever possible for better security.</Info>
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Using JWT-SVID to authenticate with Infisical">
Here's an example of how a workload can use its JWT-SVID to authenticate with Infisical and retrieve secrets:
```bash
#!/bin/bash
# Obtain JWT-SVID from SPIRE Agent
JWT_SVID=$(spire-agent api fetch jwt -audience infisical -socketPath /run/spire/sockets/agent.sock | grep -A1 "token(" | tail -1)
# Authenticate with Infisical using the JWT-SVID
ACCESS_TOKEN=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "{\"identityId\":\"<your-identity-id>\",\"jwt\":\"$JWT_SVID\"}" \
https://app.infisical.com/api/v1/auth/oidc-auth/login | jq -r '.accessToken')
# Use the access token to retrieve secrets
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://app.infisical.com/api/v3/secrets/raw?workspaceSlug=<project-slug>&environment=<env-slug>&secretPath=/"
```
<Note>
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
</Note>
<Tip>
JWT-SVIDs from SPIRE have their own expiration time (typically short-lived). Ensure your application handles both JWT-SVID renewal from SPIRE and access token renewal from Infisical appropriately.
</Tip>
</Step>
</Steps>

View File

@ -4,33 +4,36 @@ sidebarTitle: "Networking"
description: "Network configuration details for Infisical Cloud"
---
## Overview
When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services.
## Egress IP Addresses
## Infisical IP Addresses
Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services.
Infisical Cloud operates from multiple regions. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the IP addresses that Infisical uses when making outbound requests to your services.
### US Region
<Tabs>
<Tab title="US Region">
```
3.213.63.16
54.164.68.7
```
</Tab>
<Tab title="EU Region">
```
3.77.89.19
3.125.209.189
```
</Tab>
<Tab title="Dedicated Cloud">
For dedicated Infisical deployments, please contact your account manager for the specific IP addresses used in your dedicated environment.
</Tab>
</Tabs>
To allow connections from Infisical US, add these IP addresses to your ingress rules:
<Warning>
These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice.
</Warning>
- `3.213.63.16`
- `54.164.68.7`
## What These IP Addresses Are Used For
### EU Region
To allow connections from Infisical EU, add these IP addresses to your ingress rules:
- `3.77.89.19`
- `3.125.209.189`
## Common Use Cases
You may need to allow Infisicals egress IPs if your services require inbound connections for:
- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials
- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services
- Secret integrations - When syncing secrets with third-party services like Azure Key Vault
- Native authentication with machine identities - When using methods like Kubernetes authentication
These IP addresses represent the source IPs you'll see when Infisical Cloud makes connections to your infrastructure. All outbound traffic from Infisical Cloud originates from these IP addresses, ensuring predictable source IP addresses for your firewall rules.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View File

@ -46,7 +46,7 @@ description: "Learn how to configure a 1Password Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over 1Password when keys conflict.
- **Import Secrets (Prioritize 1Password)**: Imports secrets from the destination endpoint before syncing, prioritizing values from 1Password over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -43,7 +43,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -48,7 +48,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -51,7 +51,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -39,7 +39,7 @@ description: "Learn how to configure a Camunda Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict.
- **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -46,7 +46,7 @@ description: "Learn how to configure a Databricks Sync for Infisical."
<Note>
Databricks does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -42,7 +42,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -62,7 +62,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
<Note>
GitHub does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -54,7 +54,7 @@ description: "Learn how to configure a Hashicorp Vault Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Hashicorp Vault when keys conflict.
- **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -55,7 +55,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical."
<Note>
Humanitec does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -57,7 +57,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict.
- **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -101,6 +101,10 @@ Key Schemas transform your secret keys by applying a prefix, suffix, or format p
Any destination secrets which do not match the schema will not get deleted or updated by Infisical.
Key Schemas use handlebars syntax to define dynamic values. Here's a full list of available variables:
- `{{secretKey}}` - The key of the secret
- `{{environment}}` - The environment which the secret is in (e.g. dev, staging, prod)
**Example:**
- Infisical key: `SECRET_1`
- Schema: `INFISICAL_{{secretKey}}`

View File

@ -48,7 +48,7 @@ description: "Learn how to configure a TeamCity Sync for Infisical."
<Note>
Infisical only syncs secrets from within the target scope; inherited secrets will not be imported.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -56,7 +56,7 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical."
<Note>
Terraform Cloud does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -43,7 +43,7 @@ description: "Learn how to configure a Vercel Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict.
- **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -44,7 +44,7 @@ description: "Learn how to configure a Windmill Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Windmill when keys conflict.
- **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>

View File

@ -1,80 +0,0 @@
---
title: "Bug bounty program"
description: " Learn about our bug bounty program and how to report vulnerabilities."
---
The Infisical Bug Bounty Program is our way of recognizing and rewarding the work of security researchers who help keep our platform secure. By reporting vulnerabilities or potential risks, you help us protect secrets, infrastructure, and the organizations who rely on us.
We value reports that help identify vulnerabilities that affect the integrity of secrets, prevent unauthorized access to environments, or expose flaws in our authentication or authorization flows.
### How to Report
- Send reports to **security@infisical.com** with clear steps to reproduce, impact, and (if possible) a proof-of-concept.
- You will receive follow ups from our team if we deam your report to be a legitimate vulnerability or need further clarification. We do not respond to spam, auto generated reports, inaccurate claims, or submissions that are clearly out of scope.
### What's in Scope?
- Vulnerabilities in our cloud-hosted platform (e.g., `app.infisical.com`, `eu.infisical.com`)
- Security issues in the open source Infisical codebase, as maintained in our official GitHub repository
- Authentication bypass, privilege escalation, or access to secrets/data without authorization
### Reward Guidelines
Bounties are based on severity, impact, and exploitability, as well as whether the report introduces a new vulnerability class or helps improve an existing fix.
| Severity | Examples | Typical Reward (USD currency) |
| --- | --- | --- |
| **Critical** | Full unauthorized access to secrets, authentication bypass, cross-tenant access, RCE, full compromise, etc | $2,000 - $5,000 |
| **High** | Privilege escalation, project-level access without authorization, persistent DoS | $750 - $2,000 |
| **Medium** | Info disclosure, scoped DoS (e.g. ReDoS with auth), or minor access control issues | $100 - $1,000 |
| **Low / Informational** | Missing headers, CSP warnings, theoretical flaws, self-hosting misconfigurations | Recognition only |
We may award lower amounts for:
- Duplicate class vulnerabilities already under review
- Patch bypasses of previously rewarded issues
- Vulnerabilities requiring unrealistic attacker conditions
All final reward amounts are determined at Infisical's discretion based on impact, report quality, and how actionable the issue is.
### Out of Scope
- Social engineering or phishing (including email hyperlink injection without code execution)
- Rate limiting issues on non-sensitive endpoints
- Denial-of-service attacks that require authentication and don't impact core service availability
- Findings based on outdated or forked code not maintained by the Infisical team
- Vulnerabilities in third-party dependencies unless they result in a direct risk to Infisical users
### Responsible Disclosure
We ask that researchers:
- Avoid accessing data that isn't yours
- Do not publicly disclose without coordination
- Use testing accounts where possible
- Give us a reasonable window to investigate and patch before going public
Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally.
### Program Conduct and Enforcement
We value professional and collaborative interaction with security researchers. To maintain the integrity of our bug bounty program, we expect all participants to adhere to the following guidelines:
- Maintain professional communication in all interactions
- Do not threaten public disclosure of vulnerabilities before we've had reasonable time to investigate and address the issue
- Do not attempt to extort or coerce compensation through threats
- Follow the responsible disclosure process outlined in this document
- Do not use automated scanning tools without prior permission
Violations of these guidelines may result in:
1. **Warning**: For minor violations, we may issue a warning explaining the violation and requesting compliance with program guidelines.
2. **Temporary Ban**: Repeated minor violations or more serious violations may result in a temporary suspension from the program.
3. **Permanent Ban**: Severe violations such as threats, extortion attempts, or unauthorized public disclosure will result in permanent removal from the Infisical Bug Bounty Program.
We reserve the right to reject reports, withhold bounties, and remove participants from the program at our discretion for conduct that undermines the collaborative spirit of security research.
Infisical is committed to working respectfully with security researchers who follow these guidelines, and we strive to recognize and reward valuable contributions that help protect our platform and users.

View File

@ -117,7 +117,3 @@ Whether or not Infisical or your employees can access data in the Infisical inst
It should be noted that, even on Infisical Cloud, it is physically impossible for employees of Infisical to view the values of secrets if users have not explicitly granted Infisical access to their project (i.e. opted out of zero-knowledge).
Please email security@infisical.com if you have any specific inquiries about employee data and security policies.
## Bug Bounty Program
We run a [Bug Bounty Program](/internals/bug-bounty) to recognize and reward security researchers who help make Infisical more secure.
If you've found a vulnerability, please review the program details for scope, disclosure guidelines, and reward tiers.

View File

@ -233,7 +233,8 @@
"group": "Gateway",
"pages": [
"documentation/platform/gateways/overview",
"documentation/platform/gateways/gateway-security"
"documentation/platform/gateways/gateway-security",
"documentation/platform/gateways/networking"
]
},
"documentation/platform/project-templates",
@ -343,7 +344,8 @@
"documentation/platform/identities/oidc-auth/github",
"documentation/platform/identities/oidc-auth/circleci",
"documentation/platform/identities/oidc-auth/gitlab",
"documentation/platform/identities/oidc-auth/terraform-cloud"
"documentation/platform/identities/oidc-auth/terraform-cloud",
"documentation/platform/identities/oidc-auth/spire"
]
},
@ -1843,7 +1845,6 @@
},
"internals/components",
"internals/security",
"internals/bug-bounty",
"internals/service-tokens"
]
},

View File

@ -78,11 +78,14 @@ export default function CodeInputStep({
const resendVerificationEmail = async () => {
setIsResendingVerificationEmail(true);
setIsLoading(true);
await mutateAsync({ email });
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 2000);
try {
await mutateAsync({ email });
} finally {
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 1000);
}
};
return (

View File

@ -131,7 +131,27 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="When a secret is synced, its key will be injected into the key schema before it reaches the destination. This is useful for organization."
tooltipText={
<div className="flex flex-col gap-3">
<span>
When a secret is synced, values will be injected into the key schema before it
reaches the destination. This is useful for organization.
</span>
<div className="flex flex-col">
<span>Available keys:</span>
<ul className="list-disc pl-4 text-sm">
<li>
<code>{"{{secretKey}}"}</code> - The key of the secret
</li>
<li>
<code>{"{{environment}}"}</code> - The environment which the secret is in
(e.g. dev, staging, prod)
</li>
</ul>
</div>
</div>
}
isError={Boolean(error)}
isOptional
errorText={error?.message}

View File

@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
.string()
.optional()
.refine(
(val) =>
!val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val),
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = ["{{environment}}"];
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
.join("|");
const allowedContentRegex = new RegExp(
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
);
const contentIsValid = allowedContentRegex.test(val);
const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length;
return contentIsValid && secretKeyCount === 1;
},
{
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
"Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
});

View File

@ -843,7 +843,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}) => {
const {
data: { identityKubernetesAuth }
@ -860,7 +861,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}
);
@ -950,7 +952,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}) => {
const {
data: { identityKubernetesAuth }
@ -967,7 +970,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}
);

View File

@ -379,10 +379,16 @@ export type DeleteIdentityAzureAuthDTO = {
identityId: string;
};
export enum IdentityKubernetesAuthTokenReviewMode {
Api = "api",
Gateway = "gateway"
}
export type IdentityKubernetesAuth = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@ -399,6 +405,7 @@ export type AddIdentityKubernetesAuthDTO = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt?: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@ -417,6 +424,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
identityId: string;
kubernetesHost?: string;
tokenReviewerJwt?: string | null;
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;

View File

@ -96,7 +96,7 @@ export const useUpdateOrgRole = () => {
data: { role }
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
...dto,
permissions: permissions?.length ? packRules(permissions) : undefined
permissions: permissions ? packRules(permissions) : undefined
});
return role;

View File

@ -8,6 +8,11 @@ export const formatReservedPaths = (secretPath: string) => {
return secretPath;
};
export const parsePathFromReplicatedPath = (secretPath: string) => {
const i = secretPath.indexOf(ReservedFolders.SecretReplication);
return secretPath.slice(0, i);
};
export const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};

View File

@ -22,8 +22,12 @@ export const VerifyEmailPage = () => {
*/
const sendVerificationEmail = async () => {
if (email) {
await mutateAsync({ email });
setStep(2);
try {
await mutateAsync({ email });
setStep(2);
} catch {
setLoading(false);
}
}
};

View File

@ -5,6 +5,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
@ -72,6 +73,8 @@ export const AccessManagementPage = () => {
}
];
const hasNoAccess = tabSections.every((tab) => tab.isHidden);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
@ -126,6 +129,7 @@ export const AccessManagementPage = () => {
))}
</Tabs>
</div>
{hasNoAccess && <OrgPermissionGuardBanner />}
</div>
);
};

View File

@ -33,13 +33,19 @@ import {
useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth
} from "@app/hooks/api";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import {
IdentityKubernetesAuthTokenReviewMode,
IdentityTrustedIp
} from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
tokenReviewMode: z
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
.default(IdentityKubernetesAuthTokenReviewMode.Api),
kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string().optional(),
gatewayId: z.string().optional().nullable(),
@ -62,7 +68,15 @@ const schema = z
)
.min(1)
})
.required();
.superRefine((data, ctx) => {
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
ctx.addIssue({
path: ["gatewayId"],
code: z.ZodIssueCode.custom,
message: "When token review mode is set to Gateway, a gateway must be selected"
});
}
});
export type FormData = z.infer<typeof schema>;
@ -100,11 +114,14 @@ export const IdentityKubernetesAuthForm = ({
control,
handleSubmit,
reset,
watch,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api,
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
@ -128,6 +145,7 @@ export const IdentityKubernetesAuthForm = ({
useEffect(() => {
if (data) {
reset({
tokenReviewMode: data.tokenReviewMode,
kubernetesHost: data.kubernetesHost,
tokenReviewerJwt: data.tokenReviewerJwt,
allowedNames: data.allowedNames,
@ -148,6 +166,7 @@ export const IdentityKubernetesAuthForm = ({
});
} else {
reset({
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api,
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
@ -173,6 +192,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenMaxTTL,
accessTokenNumUsesLimit,
gatewayId,
tokenReviewMode,
accessTokenTrustedIps
}: FormData) => {
try {
@ -189,6 +209,7 @@ export const IdentityKubernetesAuthForm = ({
caCert,
identityId,
gatewayId: gatewayId || null,
tokenReviewMode,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -205,6 +226,7 @@ export const IdentityKubernetesAuthForm = ({
allowedAudience: allowedAudience || "",
gatewayId: gatewayId || null,
caCert: caCert || "",
tokenReviewMode,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -228,6 +250,8 @@ export const IdentityKubernetesAuthForm = ({
}
};
const tokenReviewMode = watch("tokenReviewMode");
return (
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
@ -235,6 +259,7 @@ export const IdentityKubernetesAuthForm = ({
[
"kubernetesHost",
"tokenReviewerJwt",
"tokenReviewMode",
"gatewayId",
"accessTokenTTL",
"accessTokenMaxTTL",
@ -269,21 +294,113 @@ export const IdentityKubernetesAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
<div className="flex w-full items-center gap-2">
<div className="w-full flex-1">
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
if (v === null) {
setValue(
"tokenReviewMode",
IdentityKubernetesAuthTokenReviewMode.Api,
{
shouldDirty: true,
shouldTouch: true
}
);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => {
onChange(null);
}}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
</div>
<Controller
control={control}
name="tokenReviewMode"
render={({ field }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="The method of which tokens are reviewed. If you select Gateway as Reviewer, the selected gateway will be used to review tokens with. If this option is enabled, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding."
label="Review Mode"
>
<Select value={field.value} onValueChange={field.onChange}>
<SelectItem value="gateway">Gateway as Reviewer</SelectItem>
<SelectItem value="api">Manual Token Reviewer JWT (API)</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
{tokenReviewMode === "api" && (
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue=""
@ -300,61 +417,6 @@ export const IdentityKubernetesAuthForm = ({
)}
/>
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(null)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller
control={control}
name="allowedNames"

View File

@ -159,6 +159,7 @@ export const AddOrgMemberModal = ({
text: "Failed to invite user to org",
type: "error"
});
return;
}
if (serverDetails?.emailConfigured) {

View File

@ -1,8 +1,11 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faChevronRight,
faEllipsis,
faFilter,
faMagnifyingGlass,
faSearch,
faUsers
@ -19,7 +22,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
EmptyState,
IconButton,
Input,
@ -75,6 +82,10 @@ enum OrgMembersOrderBy {
Email = "email"
}
type Filter = {
roles: string[];
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
const navigate = useNavigate();
const { subscription } = useSubscription();
@ -184,17 +195,29 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setUserTablePreference("orgMembersTable", PreferenceKey.PerPage, newPerPage);
};
const [filter, setFilter] = useState<Filter>({
roles: []
});
const filteredUsers = useMemo(
() =>
members
?.filter(
({ user: u, inviteEmail }) =>
?.filter(({ user: u, inviteEmail, role, roleId }) => {
if (
filter.roles.length &&
!filter.roles.includes(role === "custom" ? findRoleFromId(roleId)!.slug : role)
) {
return false;
}
return (
u?.firstName?.toLowerCase().includes(search.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(search.toLowerCase()) ||
u?.username?.toLowerCase().includes(search.toLowerCase()) ||
u?.email?.toLowerCase().includes(search.toLowerCase()) ||
inviteEmail?.toLowerCase().includes(search.toLowerCase())
)
);
})
.sort((a, b) => {
const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
@ -217,7 +240,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase());
}),
[members, search, orderDirection, orderBy]
[members, search, orderDirection, orderBy, filter]
);
const handleSort = (column: OrgMembersOrderBy) => {
@ -236,14 +259,81 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setPage
});
const handleRoleToggle = useCallback(
(roleSlug: string) =>
setFilter((state) => {
const currentRoles = state.roles || [];
if (currentRoles.includes(roleSlug)) {
return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) };
}
return { ...state, roles: [...currentRoles, roleSlug] };
}),
[]
);
const isTableFiltered = Boolean(filter.roles.length);
return (
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Users"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-0">
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Roles
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Users
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleRoleToggle(slug);
}}
key={id}
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
</DropdownMenuContent>
</DropdownMenu>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>

View File

@ -163,6 +163,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
text: "Failed to add user to project",
type: "error"
});
return;
}
handlePopUpToggle("addMember", false);
reset();

View File

@ -226,6 +226,23 @@ const ConditionSchema = z
: el.rhs.trim().startsWith("/")
),
{ message: "Invalid Secret Path. Must start with '/'" }
)
.refine(
(val) =>
val
.filter((el) => el.operator === PermissionConditionOperators.$EQ)
.every((el) => !el.rhs.includes(",")),
{ message: '"Equal" checks cannot contain comma separated values. Use "IN" operator instead.' }
)
.refine(
(val) =>
val
.filter((el) => el.operator === PermissionConditionOperators.$NEQ)
.every((el) => !el.rhs.includes(",")),
{
message:
'"Not Equal" checks cannot contain comma separated values. Use "IN" operator with "Forbid" instead.'
}
);
export const projectRoleFormSchema = z.object({

View File

@ -224,8 +224,7 @@ export const SecretApprovalRequest = () => {
createdAt,
reviewers,
status,
committerUser,
isReplicated: isReplication
committerUser
} = secretApproval;
const isReviewed = reviewers.some(
({ status: reviewStatus, userId }) =>
@ -244,13 +243,15 @@ export const SecretApprovalRequest = () => {
>
<div className="mb-1">
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
{generateCommitText(commits)}
{secretApproval.isReplicated
? `${commits.length} secret pending import`
: generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
</div>
<span className="text-xs text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email}){isReplication && " via replication"}
{committerUser?.email})
{!isReviewed && status === "open" && " - Review required"}
</span>
</div>

View File

@ -12,7 +12,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tag, Tooltip } from "@app/components/v2";
import { SecretInput, Tag, Tooltip } from "@app/components/v2";
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
export type Props = {
@ -86,10 +86,10 @@ export const SecretApprovalRequestChangeItem = ({
{op === CommitType.UPDATE || op === CommitType.DELETE ? (
<div className="flex w-full cursor-default flex-col rounded-md border border-red-600/60 bg-red-600/10 p-4 xl:w-1/2">
<div className="mb-4 flex flex-row justify-between">
<span className="text-md font-medium">Legacy Secret</span>
<span className="text-md font-medium">Previous Secret</span>
<div className="rounded-full bg-red px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
Deprecated
Previous
</div>
</div>
<div className="mb-2">
@ -104,27 +104,22 @@ export const SecretApprovalRequestChangeItem = ({
Rotated Secret value will not be affected
</span>
) : (
<div
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
className="relative flex max-w-[100vh] flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 px-2"
>
<div className="relative">
<SecretInput
isReadOnly
isVisible={isOldSecretValueVisible}
value={secretVersion?.secretValue}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
/>
<div
className={`flex break-all font-mono ${isOldSecretValueVisible || !secretVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
className="absolute right-1 top-1"
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
>
{isOldSecretValueVisible
? secretVersion?.secretValue || "EMPTY"
: secretVersion?.secretValue
? secretVersion?.secretValue?.split("").map(() => "•")
: "EMPTY"}{" "}
<FontAwesomeIcon
icon={isOldSecretValueVisible ? faEyeSlash : faEye}
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
/>
</div>
{secretVersion?.secretValue && (
<div className="flex h-10 w-10 items-center justify-center pl-1">
<FontAwesomeIcon
icon={isOldSecretValueVisible ? faEyeSlash : faEye}
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
/>
</div>
)}
</div>
)}
</div>
@ -191,8 +186,7 @@ export const SecretApprovalRequestChangeItem = ({
</div>
) : (
<div className="text-md flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 text-mineshaft-300 xl:w-1/2">
{" "}
Secret not existent in the previous version.
Secret did not exist in the previous version.
</div>
)}
{op === CommitType.UPDATE || op === CommitType.CREATE ? (
@ -201,7 +195,7 @@ export const SecretApprovalRequestChangeItem = ({
<span className="text-md font-medium">New Secret</span>
<div className="rounded-full bg-green-600 px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
Current
New
</div>
</div>
<div className="mb-2">
@ -216,27 +210,22 @@ export const SecretApprovalRequestChangeItem = ({
Rotated Secret value will not be affected
</span>
) : (
<div
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
className="relative flex max-w-[100vh] flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 px-2"
>
<div className="relative">
<SecretInput
isReadOnly
isVisible={isNewSecretValueVisible}
value={newVersion?.secretValue}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
/>
<div
className={`flex break-all font-mono ${isNewSecretValueVisible || !newVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
className="absolute right-1 top-1"
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
>
{isNewSecretValueVisible
? newVersion?.secretValue || "EMPTY"
: newVersion?.secretValue
? newVersion?.secretValue?.split("").map(() => "•")
: "EMPTY"}{" "}
<FontAwesomeIcon
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
/>
</div>
{newVersion?.secretValue && (
<div className="flex h-10 w-10 items-center justify-center pl-1">
<FontAwesomeIcon
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
/>
</div>
)}
</div>
)}
</div>
@ -302,7 +291,7 @@ export const SecretApprovalRequestChangeItem = ({
) : (
<div className="text-md flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 text-mineshaft-300 xl:w-1/2">
{" "}
Secret not existent in the new version.
Secret did not exist in the previous version.
</div>
)}
</div>

View File

@ -30,19 +30,24 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { useUser } from "@app/context";
import { useUser, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useGetSecretApprovalRequestDetails,
useGetSecretImports,
useUpdateSecretApprovalReviewStatus
} from "@app/hooks/api";
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
import { formatReservedPaths } from "@app/lib/fn/string";
import { formatReservedPaths, parsePathFromReplicatedPath } from "@app/lib/fn/string";
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
export const generateCommitText = (commits: { op: CommitType }[] = []) => {
export const generateCommitText = (commits: { op: CommitType }[] = [], isReplicated = false) => {
if (isReplicated) {
return <span>{commits.length} secret pending import</span>;
}
const score: Record<string, number> = {};
commits.forEach(({ op }) => {
score[op] = (score?.[op] || 0) + 1;
@ -74,7 +79,6 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
<span style={{ color: "#F83030" }}> deleted</span>
</span>
);
return text;
};
@ -105,6 +109,7 @@ export const SecretApprovalRequestChanges = ({
workspaceId
}: Props) => {
const { user: userSession } = useUser();
const { currentWorkspace } = useWorkspace();
const {
data: secretApprovalRequestDetails,
isSuccess: isSecretApprovalRequestSuccess,
@ -112,6 +117,20 @@ export const SecretApprovalRequestChanges = ({
} = useGetSecretApprovalRequestDetails({
id: approvalRequestId
});
const approvalSecretPath = parsePathFromReplicatedPath(
secretApprovalRequestDetails?.secretPath || ""
);
const { data: secretImports } = useGetSecretImports({
environment: secretApprovalRequestDetails?.environment || "",
projectId: currentWorkspace.id,
path: approvalSecretPath
});
const replicatedImport = secretApprovalRequestDetails?.isReplicated
? secretImports?.find(
(el) => secretApprovalRequestDetails?.secretPath?.includes(el.id) && el.isReplication
)
: undefined;
const {
mutateAsync: updateSecretApprovalRequestStatus,
@ -226,34 +245,16 @@ export const SecretApprovalRequestChanges = ({
: secretApprovalRequestDetails.status}
</span>
</div>
<div className="flex flex-grow flex-col">
<div className="text-lg">
{generateCommitText(secretApprovalRequestDetails.commits)}
{secretApprovalRequestDetails.isReplicated && (
<span className="text-sm text-bunker-300"> (replication)</span>
<div className="flex-grow flex-col">
<div className="text-xl">
{generateCommitText(
secretApprovalRequestDetails.commits,
secretApprovalRequestDetails.isReplicated
)}
</div>
<div className="text-sm text-bunker-300">
<p className="inline">
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
{secretApprovalRequestDetails.commits.length} secret values in
</p>
<p className="mx-1 inline rounded bg-primary-600/40 px-1 py-1 text-primary-300">
{secretApprovalRequestDetails.environment}
</p>
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
</p>
<p
className="cursor-default truncate pb-0.5 pl-2 text-sm"
style={{ maxWidth: "10rem" }}
>
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
</p>
</div>
<div className="flex items-center space-x-2 text-xs text-gray-400">
By {secretApprovalRequestDetails?.committerUser?.firstName} (
{secretApprovalRequestDetails?.committerUser?.email})
</div>
</div>
{!hasMerged &&
@ -367,6 +368,82 @@ export const SecretApprovalRequestChanges = ({
</DropdownMenu>
)}
</div>
<div className="mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div>
{secretApprovalRequestDetails.isReplicated ? (
<div className="text-sm text-bunker-300">
A secret import in
<p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
style={{ padding: "2px 4px" }}
>
{secretApprovalRequestDetails?.environment}
</p>
<div className="mr-2 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
</p>
<Tooltip content={approvalSecretPath}>
<p
className="cursor-default truncate pb-0.5 pl-2 text-sm"
style={{ maxWidth: "15rem" }}
>
{approvalSecretPath}
</p>
</Tooltip>
</div>
has pending changes to be accepted from its source at{" "}
<p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
style={{ padding: "2px 4px" }}
>
{replicatedImport?.importEnv?.slug}
</p>
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
</p>
<Tooltip content={replicatedImport?.importPath}>
<p
className="cursor-default truncate pb-0.5 pl-2 text-sm"
style={{ maxWidth: "15rem" }}
>
{replicatedImport?.importPath}
</p>
</Tooltip>
</div>
. Approving these changes will add them to that import.
</div>
) : (
<div className="text-sm text-bunker-300">
<p className="inline">Secret(s) in</p>
<p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
style={{ padding: "2px 4px" }}
>
{secretApprovalRequestDetails?.environment}
</p>
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
</p>
<Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}>
<p
className="cursor-default truncate pb-0.5 pl-2 text-sm"
style={{ maxWidth: "20rem" }}
>
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
</p>
</Tooltip>
</div>
<p className="inline">
have pending changes. Approving these changes will add them to that environment
and path.
</p>
</div>
)}
</div>
</div>
<div className="flex flex-col space-y-4">
{secretApprovalRequestDetails.commits.map(
({ op, secretVersion, secret, ...newVersion }, index) => (

View File

@ -1,3 +1,12 @@
## 0.0.3 (June 6, 2025)
* Minor fix for handling malformed URLs for HTTP forwarding
## 0.0.2 (June 6, 2025)
* Bumped default CLI image version from 0.41.1 -> 0.41.8.
* This new image version supports using the gateway as a token reviewer for the Identity Kubernetes Auth method.
## 0.0.1 (May 1, 2025)
* Initial helm release

View File

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
version: 0.0.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.0.1"
appVersion: "0.0.3"

View File

@ -1,6 +1,6 @@
image:
pullPolicy: IfNotPresent
tag: "0.41.1"
tag: "0.41.82"
secret:
# The secret that contains the environment variables to be used by the gateway, such as INFISICAL_API_URL and TOKEN