Compare commits

...

68 Commits

Author SHA1 Message Date
DanielHougaard
7e432a4297 Update Helm chart to version v0.9.2 2025-05-07 18:27:13 +00:00
Daniel Hougaard
d4e5d2c7ed Merge pull request #3540 from Infisical/daniel/generators
feat(k8s): generator support
2025-05-07 22:10:22 +04:00
Sheen
0c2e0bb0f9 Merge pull request #3560 from Infisical/misc/add-default-old-space-config
misc: add default old space config
2025-05-08 01:46:46 +08:00
Sheen Capadngan
e2a414ffff misc: add default old space config 2025-05-08 01:39:56 +08:00
=
0ca3c2bb68 feat: added password generator crd to samples 2025-05-07 22:50:49 +05:30
Daniel Hougaard
083581b51a Merge pull request #3554 from Infisical/feat/new-project-properties-for-tf-management
feat: adjustments to properties and validation
2025-05-07 20:22:23 +04:00
x032205
40e976133c Merge pull request #3528 from Infisical/ENG-2647
feat(admin): Invalidate Cache
2025-05-07 11:50:57 -04:00
x032205
ad2f002822 Merge pull request #3558 from Infisical/pki-docs-patch
docs fix
2025-05-07 11:06:24 -04:00
x032205
8842dfe5d1 docs fix 2025-05-07 11:01:19 -04:00
Sheen
b1eea4ae9c Merge pull request #3556 from Infisical/misc/remove-unnecessary-key-encryption-for-service-token
misc: removed unnecessary key encryption for service token
2025-05-07 16:41:51 +08:00
Sheen Capadngan
a8e0a8aca3 misc: removed unnecessary key encryption for service token 2025-05-07 16:36:10 +08:00
=
b37058d0e2 feat: switched to is fetching 2025-05-07 11:30:31 +05:30
x032205
334a05d5f1 fix lint 2025-05-06 18:08:08 -04:00
x032205
12c813928c fix polling 2025-05-06 18:00:24 -04:00
x032205
521fef6fca Merge branch 'main' into ENG-2647 2025-05-06 17:00:40 -04:00
=
8f8236c445 feat: simplied the caching panel logic and fixed permission issue 2025-05-07 01:37:26 +05:30
x032205
3cf5c534ff Merge pull request #3553 from Infisical/pki-docs-patch
patch(docs): mint.json update
2025-05-06 15:54:31 -04:00
Sheen Capadngan
2b03c295f9 feat: adjustments to properties and validation 2025-05-07 03:51:22 +08:00
x032205
4fc7a52941 patch(docs): mint.json update 2025-05-06 15:38:10 -04:00
Maidul Islam
0d2b3adec7 Merge pull request #3551 from Infisical/maidul98-patch-11
Add Conduct and Enforcement to bug bounty
2025-05-06 14:50:17 -04:00
Maidul Islam
e695203c05 Update docs/internals/bug-bounty.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-06 14:49:38 -04:00
Maidul Islam
f9d76aae5d Update bug-bounty.mdx 2025-05-06 14:46:42 -04:00
Daniel Hougaard
1c280759d1 Merge pull request #3548 from Infisical/daniel/self-hosted-secret-scanning
docs: secret scanning self hosted documentation
2025-05-06 22:27:00 +04:00
Daniel Hougaard
6005dce44d fix: allow secret scanning from all self-hosted orgs 2025-05-06 22:16:29 +04:00
Daniel Hougaard
f7f7d2d528 fix: typo 2025-05-06 08:24:59 +04:00
Daniel Hougaard
57342cf2a0 docs: secret scanning self hosted documentation 2025-05-06 08:14:05 +04:00
Maidul Islam
d530604b51 Merge pull request #3547 from Infisical/add-host-to-envar
Add missing HOST environment var
2025-05-05 20:46:20 -04:00
Maidul Islam
229c7c0dcf Add missing HOST environment var
Added missing HOST environment var
2025-05-05 20:43:45 -04:00
Maidul Islam
6a79830e01 Update bug-bounty.mdx 2025-05-05 17:32:18 -04:00
x032205
722067f86c Merge pull request #3514 from Infisical/ENG-2685
feat(pki): Store Secret Key Alongside Certificate + Endpoints to Fetch PK / Cert Bundle
2025-05-05 16:12:48 -04:00
x032205
86bb2659b5 small ui tweaks 2025-05-05 16:07:04 -04:00
x032205
dc59f226b6 swapped polling to react query 2025-05-05 15:58:45 -04:00
Scott Wilson
cd9792822b Merge pull request #3545 from Infisical/fix-dns-resolve-fallback
fix(external-connections): Use DNS Lookup as Fallback for DNS Resolve
2025-05-05 12:37:26 -07:00
x032205
9175c1dffa Merge branch 'main' into ENG-2647 2025-05-05 15:27:25 -04:00
x032205
f6e802c017 review fixes: docs + frontend 2025-05-05 15:07:57 -04:00
x032205
b6e6a3c6be docs changes 2025-05-05 14:50:54 -04:00
Andrey Lyubavin
54927454bf ui fetch private key if permission allows it 2025-05-05 14:37:20 -04:00
Andrey Lyubavin
1ce06891a5 ui tweak for role policies 2025-05-05 13:43:38 -04:00
Andrey Lyubavin
3a8154eddc Merge branch 'main' into ENG-2685 2025-05-05 13:37:43 -04:00
Daniel Hougaard
1e4dfd0c7c fix(k8s/generators): update base crds 2025-05-05 02:35:57 +04:00
Daniel Hougaard
34b7d28e2f requested changes 2025-05-05 02:30:59 +04:00
Daniel Hougaard
245a348517 Update generators.go 2025-05-05 02:13:12 +04:00
Daniel Hougaard
e0fc582e2e docs(k8s/generators): docs and minor fix 2025-05-05 02:09:21 +04:00
Daniel Hougaard
68ef897b6a fix: logs and rbac 2025-05-05 01:39:30 +04:00
Daniel Hougaard
1b060e76de Update kustomization.yaml 2025-05-05 01:08:22 +04:00
Daniel Hougaard
9f7599b2a1 feat(k8s): generators 2025-05-05 00:59:11 +04:00
x
9cbe70a6f3 lint fixes 2025-05-02 20:10:30 -04:00
x
f49fb534ab review fixes 2025-05-02 19:50:55 -04:00
x
6eea4c8364 frontend tweaks 2025-05-02 19:20:02 -04:00
x
1e206ee441 Merge branch 'main' into ENG-2647 2025-05-02 19:03:08 -04:00
x
85c1a1081e checkpoint 2025-05-02 18:43:07 -04:00
x
877485b45a queue job 2025-05-02 15:23:35 -04:00
x
d13e685a81 emphasize that secrets cache is encrypted in frontend 2025-05-02 13:04:22 -04:00
x
9849a5f136 switched to applyJitter functions 2025-05-02 13:00:37 -04:00
x
26773a1444 merge 2025-05-02 12:57:28 -04:00
x
a6f280197b spelling fix 2025-05-01 17:37:54 -04:00
x
346d2f213e improvements + review fixes 2025-05-01 17:33:24 -04:00
x
9f1ac77afa invalidate cache 2025-05-01 16:34:29 -04:00
x
1268bc1238 coderabbit review fixes 2025-04-30 17:50:23 -04:00
x
07e4bc8eed review fixes 2025-04-30 17:46:05 -04:00
x
235be96ded tweaks 2025-04-30 14:53:57 -04:00
x
30471bfcad Merge branch 'main' into ENG-2685 2025-04-30 13:41:14 -04:00
x
eedffffc38 review fixes 2025-04-30 02:07:07 -04:00
x
9f487ad026 frontend type fixes 2025-04-30 00:53:31 -04:00
x
c70b9e665e more tweaks and type fix 2025-04-30 00:39:10 -04:00
x
d460e96052 Merge branch 'main' into ENG-2685 2025-04-30 00:34:37 -04:00
x
e475774910 made certificates store PK and chain in relation to the main table, added /bundle endpoints, new audit log and permission entries 2025-04-30 00:33:46 -04:00
x
e81c49500b get certificate private key endpoint + migrations 2025-04-29 20:34:39 -04:00
101 changed files with 2616 additions and 209 deletions

View File

@@ -171,6 +171,7 @@ ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
ENV NODE_OPTIONS="--max-old-space-size=1024"
WORKDIR /backend

View File

@@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV NODE_OPTIONS="--max-old-space-size=1024"
WORKDIR /backend

View File

@@ -1,4 +1,8 @@
import RE2 from "re2";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import { Lock } from "@app/lib/red-lock";
export const mockKeyStore = (): TKeyStoreFactory => {
@@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
delete store[key];
return 1;
},
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
let totalDeleted = 0;
const keys = Object.keys(store);
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
for (const key of batch) {
if (regex.test(key)) {
delete store[key];
totalDeleted += 1;
}
}
// eslint-disable-next-line no-await-in-loop
await delayMs(Math.max(0, applyJitter(delay, jitter)));
}
return totalDeleted;
},
getItem: async (key) => {
const value = store[key];
if (typeof value === "string") {

View File

@@ -0,0 +1,33 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateBody)) {
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
t.binary("encryptedCertificateChain").nullable();
});
}
if (!(await knex.schema.hasTable(TableName.CertificateSecret))) {
await knex.schema.createTable(TableName.CertificateSecret, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("certId").notNullable().unique();
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
t.binary("encryptedPrivateKey").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateSecret)) {
await knex.schema.dropTable(TableName.CertificateSecret);
}
if (await knex.schema.hasTable(TableName.CertificateBody)) {
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
t.dropColumn("encryptedCertificateChain");
});
}
}

View File

@@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
certId: z.string().uuid(),
encryptedCertificate: zodBuffer
encryptedCertificate: zodBuffer,
encryptedCertificateChain: zodBuffer.nullable().optional()
});
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;

View File

@@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const CertificateSecretsSchema = z.object({
@@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
certId: z.string().uuid(),
pk: z.string(),
sk: z.string()
encryptedPrivateKey: zodBuffer
});
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;

View File

@@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({
description: z.string().nullable().optional(),
type: z.string(),
enforceCapitalization: z.boolean().default(false),
hasDeleteProtection: z.boolean().default(true).nullable().optional()
hasDeleteProtection: z.boolean().default(false).nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -1,11 +1,11 @@
import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns";
import {
SecretScanningResolvedStatus,
SecretScanningRiskStatus
} from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
body: z.object({ organizationId: z.string().trim() }),
response: {
200: z.object({
sessionId: z.string()
sessionId: z.string(),
gitAppSlug: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const appCfg = getConfig();
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
if (!canUseSecretScanning(req.auth.orgId)) {
throw new BadRequestError({
message: "Secret scanning is temporarily unavailable."
});

View File

@@ -224,6 +224,8 @@ export enum EventType {
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
@@ -1790,6 +1792,24 @@ interface GetCertBody {
};
}
interface GetCertPrivateKey {
type: EventType.GET_CERT_PRIVATE_KEY;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface GetCertBundle {
type: EventType.GET_CERT_BUNDLE;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
@@ -2824,6 +2844,8 @@ export type Event =
| DeleteCert
| RevokeCert
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert

View File

@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionCertificateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
ReadPrivateKey = "read-private-key"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
@@ -232,7 +240,7 @@ export type ProjectPermissionSet =
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
@@ -478,7 +486,7 @@ const GeneralPermissionSchema = [
}),
z.object({
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
"Describe what action an entity can take."
)
}),
@@ -688,7 +696,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
@@ -708,6 +715,17 @@ const buildAdminPermissionRules = () => {
);
});
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.ReadPrivateKey
],
ProjectPermissionSub.Certificates
);
can(
[
ProjectPermissionSshHostActions.Edit,
@@ -965,10 +983,10 @@ const buildMemberPermissionRules = () => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete
],
ProjectPermissionSub.Certificates
);
@@ -1041,7 +1059,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);

View File

@@ -0,0 +1,11 @@
import { getConfig } from "@app/lib/config/env";
export const canUseSecretScanning = (orgId: string) => {
const appCfg = getConfig();
if (!appCfg.isCloud) {
return true;
}
return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId);
};

View File

@@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors";
import { TGitAppDALFactory } from "./git-app-dal";
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
import { TSecretScanningDALFactory } from "./secret-scanning-dal";
import { canUseSecretScanning } from "./secret-scanning-fns";
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
import {
SecretScanningRiskStatus,
@@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TInstallAppSessionDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const sessionId = crypto.randomBytes(16).toString("hex");
await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId });
return { sessionId };
return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG };
};
const linkInstallationToOrg = async ({
@@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
if (canUseSecretScanning(actorOrgId)) {
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
@@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({
)
);
}
return { installatedApp };
};
@@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({
};
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
const appCfg = getConfig();
const { commits, repository, installation, pusher } = payload;
if (!commits || !repository || !installation || !pusher) {
return;
@@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({
});
if (!installationLink) return;
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
if (canUseSecretScanning(installationLink.orgId)) {
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },

View File

@@ -1,6 +1,8 @@
import { Redis } from "ioredis";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import { Redlock, Settings } from "@app/lib/red-lock";
export const PgSqlLock = {
@@ -48,6 +50,13 @@ export const KeyStoreTtls = {
AccessTokenStatusUpdateInSeconds: 120
};
type TDeleteItems = {
pattern: string;
batchSize?: number;
delay?: number;
jitter?: number;
};
type TWaitTillReady = {
key: string;
waitingCb?: () => void;
@@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => {
const deleteItem = async (key: string) => redis.del(key);
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
let cursor = "0";
let totalDeleted = 0;
do {
// Await in loop is needed so that Redis is not overwhelmed
// eslint-disable-next-line no-await-in-loop
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
cursor = nextCursor;
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
const pipeline = redis.pipeline();
for (const key of batch) {
pipeline.unlink(key);
}
// eslint-disable-next-line no-await-in-loop
await pipeline.exec();
totalDeleted += batch.length;
console.log("BATCH DONE");
// eslint-disable-next-line no-await-in-loop
await delayMs(Math.max(0, applyJitter(delay, jitter)));
}
} while (cursor !== "0");
return totalDeleted;
};
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
@@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => {
// eslint-disable-next-line
await new Promise((resolve) => {
waitingCb?.();
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
setTimeout(resolve, Math.max(0, applyJitter(delay, jitter)));
});
attempts += 1;
// eslint-disable-next-line
@@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => {
setExpiry,
setItemWithExpiry,
deleteItem,
deleteItems,
incrementBy,
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
return redisLock.acquire(resources, duration, settings);

View File

@@ -1,3 +1,7 @@
import RE2 from "re2";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import { Lock } from "@app/lib/red-lock";
import { TKeyStoreFactory } from "./keystore";
@@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
delete store[key];
return 1;
},
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
let totalDeleted = 0;
const keys = Object.keys(store);
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
for (const key of batch) {
if (regex.test(key)) {
delete store[key];
totalDeleted += 1;
}
}
// eslint-disable-next-line no-await-in-loop
await delayMs(Math.max(0, applyJitter(delay, jitter)));
}
return totalDeleted;
},
getItem: async (key) => {
const value = store[key];
if (typeof value === "string") {

View File

@@ -1619,7 +1619,8 @@ export const CERTIFICATES = {
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
certificate: "The certificate body of the certificate.",
certificateChain: "The certificate chain of the certificate.",
serialNumberRes: "The serial number of the certificate."
serialNumberRes: "The serial number of the certificate.",
privateKey: "The private key of the certificate."
}
};

View File

@@ -146,6 +146,7 @@ const envSchema = z
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
SECRET_SCANNING_GIT_APP_SLUG: zpStr(z.string().default("infisical-radar")),
// LICENSE
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),

View File

@@ -0,0 +1,4 @@
export const delay = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});

View File

@@ -25,6 +25,7 @@ import {
TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types";
import { CacheType } from "@app/services/super-admin/super-admin-types";
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
export enum QueueName {
@@ -49,7 +50,8 @@ export enum QueueName {
AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync",
SecretRotationV2 = "secret-rotation-v2"
SecretRotationV2 = "secret-rotation-v2",
InvalidateCache = "invalidate-cache"
}
export enum QueueJobs {
@@ -81,7 +83,8 @@ export enum QueueJobs {
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
InvalidateCache = "invalidate-cache"
}
export type TQueueJobTypes = {
@@ -234,6 +237,14 @@ export type TQueueJobTypes = {
name: QueueJobs.SecretRotationV2SendNotification;
payload: TSecretRotationSendNotificationJobPayload;
};
[QueueName.InvalidateCache]: {
name: QueueJobs.InvalidateCache;
payload: {
data: {
type: CacheType;
};
};
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = {
max: 30, // conservative default
keyGenerator: (req) => req.realIp
};
export const invalidateCacheLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
hook: "preValidation",
max: 1,
keyGenerator: (req) => req.realIp
};

View File

@@ -0,0 +1,8 @@
import { FastifyReply } from "fastify";
export const addNoCacheHeaders = (reply: FastifyReply) => {
void reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
void reply.header("Pragma", "no-cache");
void reply.header("Expires", "0");
void reply.header("Surrogate-Control", "no-store");
};

View File

@@ -126,6 +126,7 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
@@ -241,6 +242,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
import { slackServiceFactory } from "@app/services/slack/slack-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
@@ -610,6 +612,11 @@ export const registerRoutes = async (
queueService
});
const invalidateCacheQueue = invalidateCacheQueueFactory({
keyStore,
queueService
});
const userService = userServiceFactory({
userDAL,
userAliasDAL,
@@ -721,7 +728,8 @@ export const registerRoutes = async (
keyStore,
licenseService,
kmsService,
microsoftTeamsService
microsoftTeamsService,
invalidateCacheQueue
});
const orgAdminService = orgAdminServiceFactory({
@@ -812,6 +820,7 @@ export const registerRoutes = async (
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
const certificateSecretDAL = certificateSecretDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db);
@@ -820,6 +829,7 @@ export const registerRoutes = async (
const certificateService = certificateServiceFactory({
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
@@ -891,6 +901,7 @@ export const registerRoutes = async (
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,

View File

@@ -4,13 +4,14 @@ import { z } from "zod";
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerAdminRouter = async (server: FastifyZodProvider) => {
@@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/invalidate-cache",
config: {
rateLimit: invalidateCacheLimit
},
schema: {
body: z.object({
type: z.nativeEnum(CacheType)
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
await server.services.superAdmin.invalidateCache(req.body.type);
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.InvalidateCache,
distinctId: getTelemetryDistinctId(req),
properties: {
...req.auditLogInfo
}
});
return {
message: "Cache invalidation job started"
};
}
});
server.route({
method: "GET",
url: "/invalidating-cache-status",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
invalidating: z.boolean()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const invalidating = await server.services.superAdmin.checkIfInvalidatingCache();
return {
invalidating
};
}
});
};

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
@@ -5,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addNoCacheHeaders } from "@app/server/lib/caching";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -64,6 +66,111 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
});
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
server.route({
method: "GET",
url: "/:serialNumber/private-key",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate private key",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
}),
response: {
200: z.string().trim()
}
},
handler: async (req, reply) => {
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CERT_PRIVATE_KEY,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
return certPrivateKey;
}
});
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
server.route({
method: "GET",
url: "/:serialNumber/bundle",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate bundle including the certificate, chain, and private key.",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
}
},
handler: async (req, reply) => {
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
await server.services.certificate.getCertBundle({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CERT_BUNDLE,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
return {
certificate,
certificateChain,
serialNumber,
privateKey
};
}
});
server.route({
method: "POST",
url: "/issue-certificate",
@@ -411,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
}
@@ -429,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.DELETE_CERT,
type: EventType.GET_CERT_BODY,
metadata: {
certId: cert.id,
cn: cert.commonName,

View File

@@ -170,7 +170,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template),
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
shouldCreateDefaultEnvs: z.boolean().optional().default(true)
}),
response: {
200: z.object({
@@ -190,7 +191,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId,
template: req.body.template,
type: req.body.type
type: req.body.type,
createDefaultEnvs: req.body.shouldCreateDefaultEnvs
});
await server.services.telemetry.sendPostHogEvents({
@@ -272,7 +274,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
}),
response: {
200: projectWithEnv

View File

@@ -6,7 +6,11 @@ import { z } from "zod";
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -21,6 +25,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
@@ -75,6 +80,7 @@ type TCertificateAuthorityServiceFactoryDep = {
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
@@ -96,6 +102,7 @@ export const certificateAuthorityServiceFactory = ({
certificateTemplateDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
@@ -1157,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
@@ -1373,6 +1383,23 @@ export const certificateAuthorityServiceFactory = ({
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
@@ -1396,7 +1423,16 @@ export const certificateAuthorityServiceFactory = ({
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
@@ -1414,17 +1450,9 @@ export const certificateAuthorityServiceFactory = ({
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
certificateChain: certificateChainPem,
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
@@ -1487,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
}

View File

@@ -1,6 +1,11 @@
import crypto from "node:crypto";
import * as x509 from "@peculiar/x509";
import { CrlReason } from "./certificate-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
switch (crlReason) {
@@ -46,3 +51,73 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.map((cert) => cert.toString("pem"))
.join("\n")
.trim();
/**
* Return the public and private key of certificate
* Note: credentials are returned as PEM strings
*/
export const getCertificateCredentials = async ({
certId,
projectId,
certificateSecretDAL,
projectDAL,
kmsService
}: TGetCertificateCredentialsDTO) => {
const certificateSecret = await certificateSecretDAL.findOne({ certId });
if (!certificateSecret)
throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` });
const keyId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedPrivateKey = await kmsDecryptor({
cipherTextBlob: certificateSecret.encryptedPrivateKey
});
try {
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" });
const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString();
const pkObj = crypto.createPublicKey(skObj);
const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString();
return {
certificateSecret,
certPrivateKey,
certPublicKey
};
} catch (error) {
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
}
};
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
// Otherwise we'll fallback to manually building the chain
export const buildCertificateChain = async ({
caCert,
caCertChain,
encryptedCertificateChain,
kmsService,
kmsId
}: TBuildCertificateChainDTO) => {
if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
return null;
}
let certificateChain = `${caCert}\n${caCertChain}`.trim();
if (encryptedCertificateChain) {
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
}
return certificateChain;
};

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
export const certificateSecretDALFactory = (db: TDbClient) => {
const certSecretOrm = ormify(db, TableName.CertificateSecret);
return certSecretOrm;
};

View File

@@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
@@ -15,11 +18,21 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { revocationReasonToCrlCode } from "./certificate-fns";
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import {
CertStatus,
TDeleteCertDTO,
TGetCertBodyDTO,
TGetCertBundleDTO,
TGetCertDTO,
TGetCertPrivateKeyDTO,
TRevokeCertDTO
} from "./certificate-types";
type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
@@ -34,6 +47,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
export const certificateServiceFactory = ({
certificateDAL,
certificateSecretDAL,
certificateBodyDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
@@ -59,7 +73,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
return {
cert,
@@ -67,6 +84,48 @@ export const certificateServiceFactory = ({
};
};
/**
* Get certificate private key.
*/
const getCertPrivateKey = async ({
serialNumber,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetCertPrivateKeyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: ca.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
ca,
cert,
certPrivateKey
};
};
/**
* Delete certificate with serial number [serialNumber]
*/
@@ -83,7 +142,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Delete,
ProjectPermissionSub.Certificates
);
const deletedCert = await certificateDAL.deleteById(cert.id);
@@ -118,7 +180,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Delete,
ProjectPermissionSub.Certificates
);
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
@@ -165,7 +230,10 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
@@ -192,19 +260,107 @@ export const certificateServiceFactory = ({
kmsService
});
const certificateChain = await buildCertificateChain({
caCert,
caCertChain,
kmsId: certificateManagerKeyId,
kmsService,
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
return {
certificate: certObj.toString("pem"),
certificateChain: `${caCert}\n${caCertChain}`.trim(),
certificateChain,
serialNumber: certObj.serialNumber,
cert,
ca
};
};
/**
* Return certificate body and certificate chain for certificate with
* serial number [serialNumber]
*/
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate
});
const certObj = new x509.X509Certificate(decryptedCert);
const certificate = certObj.toString("pem");
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChain = await buildCertificateChain({
caCert,
caCertChain,
kmsId: certificateManagerKeyId,
kmsService,
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: ca.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
certificate,
certificateChain,
privateKey: certPrivateKey,
serialNumber,
cert,
ca
};
};
return {
getCert,
getCertPrivateKey,
deleteCert,
revokeCert,
getCertBody
getCertBody,
getCertBundle
};
};

View File

@@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509";
import { TProjectPermission } from "@app/lib/types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
export enum CertStatus {
ACTIVE = "active",
REVOKED = "revoked"
@@ -73,3 +77,27 @@ export type TRevokeCertDTO = {
export type TGetCertBodyDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertPrivateKeyDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertBundleDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertificateCredentialsDTO = {
certId: string;
projectId: string;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TBuildCertificateChainDTO = {
caCert?: string;
caCertChain?: string;
encryptedCertificateChain?: Buffer;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
kmsId: string;
};

View File

@@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSecretActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub
@@ -948,7 +949,10 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
const cas = await certificateAuthorityDAL.find({ projectId });

View File

@@ -0,0 +1,49 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { CacheType } from "./super-admin-types";
export type TInvalidateCacheQueueFactoryDep = {
queueService: TQueueServiceFactory;
keyStore: Pick<TKeyStoreFactory, "deleteItems" | "setItemWithExpiry" | "deleteItem">;
};
export type TInvalidateCacheQueueFactory = ReturnType<typeof invalidateCacheQueueFactory>;
export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => {
const startInvalidate = async (dto: {
data: {
type: CacheType;
};
}) => {
await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, {
removeOnComplete: true,
removeOnFail: true,
jobId: `invalidate-cache-${dto.data.type}`
});
};
queueService.start(QueueName.InvalidateCache, async (job) => {
try {
const {
data: { type }
} = job.data;
await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails)
if (type === CacheType.ALL || type === CacheType.SECRETS)
await keyStore.deleteItems({ pattern: "secret-manager:*" });
await keyStore.deleteItem("invalidating-cache");
} catch (err) {
logger.error(err, "Failed to invalidate cache");
await keyStore.deleteItem("invalidating-cache");
}
});
return {
startInvalidate
};
};

View File

@@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import {
CacheType,
LoginMethod,
TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO,
@@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
kmsRootConfigDAL: TKmsRootConfigDALFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
invalidateCacheQueue: TInvalidateCacheQueueFactory;
};
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
@@ -64,7 +67,7 @@ export let getServerCfg: () => Promise<
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const superAdminServiceFactory = ({
serverCfgDAL,
@@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({
identityAccessTokenDAL,
identityTokenAuthDAL,
identityOrgMembershipDAL,
microsoftTeamsService
microsoftTeamsService,
invalidateCacheQueue
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({
await kmsService.updateEncryptionStrategy(strategy);
};
const invalidateCache = async (type: CacheType) => {
await invalidateCacheQueue.startInvalidate({
data: { type }
});
};
const checkIfInvalidatingCache = async () => {
return (await keyStore.getItem("invalidating-cache")) !== null;
};
return {
initServerCfg,
updateServerCfg,
@@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({
getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser,
deleteIdentitySuperAdminAccess,
deleteUserSuperAdminAccess
deleteUserSuperAdminAccess,
invalidateCache,
checkIfInvalidatingCache
};
};

View File

@@ -44,3 +44,8 @@ export enum LoginMethod {
LDAP = "ldap",
OIDC = "oidc"
}
export enum CacheType {
ALL = "all",
SECRETS = "secrets"
}

View File

@@ -21,7 +21,8 @@ export enum PostHogEventTypes {
IssueSshHostUserCert = "Issue SSH Host User Certificate",
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
SignCert = "Sign PKI Certificate",
IssueCert = "Issue PKI Certificate"
IssueCert = "Issue PKI Certificate",
InvalidateCache = "Invalidate Cache"
}
export type TSecretModifiedEvent = {
@@ -203,6 +204,13 @@ export type TIssueCertificateEvent = {
};
};
export type TInvalidateCacheEvent = {
event: PostHogEventTypes.InvalidateCache;
properties: {
userAgent?: string;
};
};
export type TPostHogEvent = { distinctId: string } & (
| TSecretModifiedEvent
| TAdminInitEvent
@@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & (
| TIssueSshHostHostCertEvent
| TSignCertificateEvent
| TIssueCertificateEvent
| TInvalidateCacheEvent
);

View File

@@ -0,0 +1,8 @@
---
title: "Get Certificate Bundle"
openapi: "GET /api/v1/pki/certificates/{serialNumber}/bundle"
---
<Note>
You must have the certificate `read-private-key` permission in order to call this endpoint.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Get Certificate Private Key"
openapi: "GET /api/v1/pki/certificates/{serialNumber}/private-key"
---

View File

@@ -7,6 +7,113 @@ The Infisical Secret Scanner allows you to keep an overview and stay alert of ex
To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes.
<Accordion title="Self-hosting">
To setup secret scanning on your own instance of Infisical, you can follow the steps below.
<Steps>
<Step title="Create a GitHub App">
Create a new GitHub app in your GitHub organization or personal [Developer Settings](https://github.com/settings/apps).
![Create GitHub App](/images/platform/secret-scanning/github-create-app.png)
### Configure the GitHub App
To configure the GitHub app to work with Infisical, you'll need to modify the following settings:
- **Homepage URL**: Required to be set. Set it to the URL of your Infisical instance. (e.g. `https://app.infisical.com`)
- **Setup URL**: Set this to `https://<your-infisical-instance.com>/organization/secret-scanning`
- **Webhook URL**: Set this to `https://<your-infisical-instance.com>/api/v1/secret-scanning/webhook`
- **Webhook Secret**: Set this to a random string. This is used to verify the webhook request from Infisical. Use `openssl rand -base64 32` in your terminal to generate a random secret.
<Note>
Remember to save the webhook secret as you will need it in the next step.
</Note>
![GitHub App Settings](/images/platform/secret-scanning/github-configure-app.png)
### Configure the GitHub App Permissions
The GitHub app needs the following permissions:
Repository permissions:
- `Checks`: Read and Write
- `Contents`: Read-only
- `Issues`: Read and Write
- `Pull Requests`: Read and Write
- `Metadata`: Read-only (enabled by default)
![Github App Repository Permissions](/images/platform/secret-scanning/github-repo-permissions.png)
Subscribed events:
- `Check run`
- `Pull request`
- `Push`
![Github App Subscribed Events](/images/platform/secret-scanning/github-subscribed-events.png)
### Create the GitHub App
Now you can create the GitHub app by clicking on the "Create GitHub App" button.
<Note>
If you want other Github users to be able to install the app, you need to tick the "Any account" option under "Where can this GitHub App be installed?"
</Note>
![Create GitHub App](/images/platform/secret-scanning/github-create-app-button.png)
</Step>
<Step title="Retrieve the GitHub App ID">
After clicking the "Create GitHub App" button, you will be redirected to the GitHub settings page. Here you can copy the "App ID" and save it for later when you need to configure your environment variables for your Infisical instance.
![Github App ID](/images/platform/secret-scanning/github-app-copy-app-id.png)
</Step>
<Step title="Retrieve your GitHub App slug">
The GitHub App slug is the name of the app you created in a slug friendly format. You can find the slug in the URL of the app you created.
![Github App Slug](/images/platform/secret-scanning/github-app-copy-slug.png)
</Step>
<Step title="Create a new GitHub App private key">
Create a new app private key by clicking on the "Generate a private key" button under the "Private keys" section.
Once you click the "Generate a private key" button, the private key will be downloaded to your computer. Save this file for later as you will need the private key when configuring Infisical.
![Github App Private Key](/images/platform/secret-scanning/github-app-create-private-key.png)
<Note>
Remember to save the private key as you will need it in the next step.
</Note>
</Step>
<Step title="Configure your Infisical instance">
Now you can configure your Infisical instance by setting the following environment variables:
- `SECRET_SCANNING_GIT_APP_ID`: The App ID of your GitHub App.
- `SECRET_SCANNING_GIT_APP_SLUG`: The slug of your GitHub App.
- `SECRET_SCANNING_PRIVATE_KEY`: The private key of your GitHub App that you created in a previous step.
- `SECRET_SCANNING_WEBHOOK_SECRET`: The webhook secret of your GitHub App that you created in a previous step.
</Step>
</Steps>
After restarting your Infisical instance, you should be able to use the secret scanning feature within your organization. Follow the steps below to add the GitHub App to your Infisical organization.
</Accordion>
## Install the Infisical Radar GitHub App
To install the GitHub App, press the "Integrate With GitHub" button in the top right corner of your Infisical Secret Scanning dashboard.
![Integrate With GitHub](/images/platform/secret-scanning/infisical-connect-secret-scanner.png)
Next, you'll be prompted to select which organization you'd like to install the app into. Select the organization you'd like to install the app into by clicking the organization in the menu.
![Select Organization](/images/platform/secret-scanning/github-select-org-2.png)
Select the repositories you'd like to scan for secrets and press the "Install" button.
![Select Repositories](/images/platform/secret-scanning/github-select-repos.png)
## Code Scanning
![Scanning Overview](/images/platform/secret-scanning/overview.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD.
hostAPI: https://app.infisical.com/api
# Optional, defaults to no replacement.
@@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
<Accordion title="resyncInterval">
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
The `resyncInterval` is a string-formatted duration that defines the time between each resync. The field is optional, and will default to no automatic resync if not defined.
If you don't want to automatically reconcile the InfisicalPushSecret CRD on an interval, you can remove the `resyncInterval` field entirely from your InfisicalPushSecret CRD.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
@@ -459,6 +461,126 @@ Using Go templates, you can format, combine, and create new key-value pairs of s
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
</Accordion>
## Using generators to push secrets
Generators are a feature of the Infisical secrets operator that allows you to generate secrets on-reconcile and push them to Infisical. This is useful for secret rotation purposes, and fully operator-managed secrets.
A generator is a custom resource that is installed on the cluster that defines the logic for generating a secret.
Generators don't keep track of the secrets they generate, which means that on each reconciliation, a new value will be created and pushed.
For this reason you may want to disable automatic reconciliation of the InfisicalPushSecret CRD. You can do this by removing `resyncInterval` from the InfisicalPushSecret CRD.
**Supported generators**:
- `Password`: Generates a random password of string format.
- `UUID`: Generates a random v4 UUID.
To use a generator, you must specify at least one generator in the `push.generators[]` field. An example of a generator usage can be seen here:
<Accordion title="push.generators[]">
Define a generator in the `push.generators[]` field.
<Accordion title="push.generators[].destinationSecretName">
The name of the secret that will be created in Infisical.
</Accordion>
<Accordion title="push.generators[].generatorRef">
The reference to the generator resource.
Valid fields:
- `kind`: The kind of the generator resource, must match the generator kind.
- `name`: The name of the generator resource.
</Accordion>
</Accordion>
```yaml
push:
secret:
secretName: push-secret-source-secret
secretNamespace: dev
generators:
- destinationSecretName: password-generator # Name of the secret that will be created in Infisical
generatorRef:
kind: Password|UUID # Kind of the resource, must match the generator kind.
name: custom-generator # Name of the generator resource
```
<Tabs>
<Tab title="Password Generator">
The Password generator is a custom resource that is installed on the cluster that defines the logic for generating a password.
<Accordion title="Spec definition">
- `kind`: The kind of the generator resource, must match the generator kind. For the Password generator, the kind is `Password`.
- `generator.passwordSpec`: The spec of the password generator.
<Accordion title="generator.passwordSpec">
- `length`: The length of the password.
- `digits`: The number of digits in the password.
- `symbols`: The number of symbols in the password.
- `symbolCharacters`: The characters to use for the symbols in the password.
- `noUpper`: Whether to include uppercase letters in the password.
- `allowRepeat`: Whether to allow repeating characters in the password.
</Accordion>
</Accordion>
```yaml password-cluster-generator.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: password-generator
spec:
kind: Password
generator:
passwordSpec:
length: 10
digits: 5
symbols: 5
symbolCharacters: "-_$@"
noUpper: false
allowRepeat: true
```
Example InfisicalPushSecret CRD using the Password generator:
```yaml infisical-push-secret-crd.yaml
push:
generators:
- destinationSecretName: password-generator-test
generatorRef:
kind: Password
name: password-generator
```
</Tab>
<Tab title="UUID Generator">
The UUID generator is a custom resource that is installed on the cluster that defines the logic for generating a UUID.
<Accordion title="Spec definition">
- `kind`: The kind of the generator resource, must match the generator kind. For the UUID generator, the kind is `UUID`.
- `generator.uuidSpec`: The spec of the UUID generator. For UUID's, this can be left empty.
</Accordion>
```yaml uuid-cluster-generator.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: uuid-generator
spec:
kind: UUID
generator:
uuidSpec:
```
Example InfisicalPushSecret CRD using the UUID generator:
```yaml infisical-push-secret-crd.yaml
push:
generators:
- destinationSecretName: uuid-generator-test
generatorRef:
kind: UUID
name: uuid-generator
```
</Tab>
</Tabs>
## Applying the InfisicalPushSecret CRD to your cluster
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.

View File

@@ -41,7 +41,7 @@ All final reward amounts are determined at Infisical's discretion based on impac
### Out of Scope
- Social engineering or phishing
- 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
@@ -57,4 +57,24 @@ We ask that researchers:
- 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.
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

@@ -252,11 +252,12 @@ Supports conditions and permission inversion
#### Subject: `certificates`
| Action | Description |
| -------- | ----------------------------- |
| `read` | View certificates |
| `create` | Issue new certificates |
| `delete` | Revoke or remove certificates |
| Action | Description |
| -------------------- | ----------------------------- |
| `read` | View certificates |
| `read-private-key` | Read certificate private key |
| `create` | Issue new certificates |
| `delete` | Revoke or remove certificates |
#### Subject: `certificate-templates`

View File

@@ -1454,6 +1454,8 @@
"api-reference/endpoints/certificates/revoke",
"api-reference/endpoints/certificates/delete",
"api-reference/endpoints/certificates/cert-body",
"api-reference/endpoints/certificates/bundle",
"api-reference/endpoints/certificates/private-key",
"api-reference/endpoints/certificates/issue-certificate",
"api-reference/endpoints/certificates/sign-certificate"
]

View File

@@ -29,6 +29,19 @@ Used to configure platform-specific security and operational settings
Specifies the internal port on which the application listens.
</ParamField>
<ParamField query="HOST" type="string" default="localhost" optional>
Specifies the network interface Infisical will bind to when accepting incoming connections.
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
Example values:
- `localhost` (default, same as `127.0.0.1`)
- `0.0.0.0` (all interfaces, accessible externally)
- `192.168.1.100` (specific interface IP)
</ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
Telemetry helps us improve Infisical but if you want to disable it you may set
this to `false`.
@@ -612,6 +625,26 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
</ParamField>
</Accordion>
## Secret Scanning
<Accordion title="GitHub">
<ParamField query="SECRET_SCANNING_GIT_APP_ID" type="string" default="none" optional>
The App ID of your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_GIT_APP_SLUG" type="string" default="none" optional>
The slug of your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_PRIVATE_KEY" type="string" default="none" optional>
A private key for your GitHub App.
</ParamField>
<ParamField query="SECRET_SCANNING_WEBHOOK_SECRET" type="string" default="none" optional>
The webhook secret of your GitHub App.
</ParamField>
</Accordion>
## Observability
You can configure Infisical to collect and expose telemetry data for analytics and monitoring.

View File

@@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types";
export {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,

View File

@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionCertificateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
ReadPrivateKey = "read-private-key"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
@@ -268,7 +276,7 @@ export type ProjectPermissionSet =
)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]

View File

@@ -10,6 +10,7 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,

View File

@@ -3,6 +3,7 @@ export {
useAdminGrantServerAdminAccess,
useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser,
useInvalidateCache,
useRemoveUserServerAdminAccess,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy

View File

@@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import {
RootKeyEncryptionStrategy,
TCreateAdminUserDTO,
TInvalidateCacheDTO,
TServerConfig,
TUpdateServerConfigDTO
} from "./types";
@@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => {
}
});
};
export const useInvalidateCache = () => {
const queryClient = useQueryClient();
return useMutation<void, object, TInvalidateCacheDTO>({
mutationFn: async (dto) => {
await apiRequest.post("/api/v1/admin/invalidate-cache", dto);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() });
}
});
};

View File

@@ -8,6 +8,7 @@ import {
AdminGetIdentitiesFilters,
AdminGetUsersFilters,
AdminIntegrationsConfig,
TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
@@ -22,8 +23,10 @@ export const adminQueryKeys = {
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getIdentities: (filters: AdminGetIdentitiesFilters) =>
[adminStandaloneKeys.getIdentities, { filters }] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
};
export const fetchServerConfig = async () => {
@@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => {
}
});
};
export const useGetInvalidatingCacheStatus = (enabled = true) => {
return useQuery({
queryKey: adminQueryKeys.getInvalidateCache(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetInvalidatingCacheStatus>(
"/api/v1/admin/invalidating-cache-status"
);
return data.invalidating;
},
enabled,
refetchInterval: (data) => (data ? 3000 : false)
});
};

View File

@@ -24,6 +24,7 @@ export type TServerConfig = {
enabledLoginMethods: LoginMethod[];
authConsentContent?: string;
pageFrameContent?: string;
invalidatingCache: boolean;
};
export type TUpdateServerConfigDTO = {
@@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
export enum CacheType {
ALL = "all",
SECRETS = "secrets"
}
export type TInvalidateCacheDTO = {
type: CacheType;
};
export type TGetInvalidatingCacheStatus = {
invalidating: boolean;
};

View File

@@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_CERT]: "Delete certificate",
[EventType.REVOKE_CERT]: "Revoke certificate",
[EventType.GET_CERT_BODY]: "Get certificate body",
[EventType.GET_CERT_PRIVATE_KEY]: "Get certificate private key",
[EventType.GET_CERT_BUNDLE]: "Get certificate bundle",
[EventType.CREATE_PKI_ALERT]: "Create PKI alert",
[EventType.GET_PKI_ALERT]: "Get PKI alert",
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",

View File

@@ -78,6 +78,8 @@ export enum EventType {
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",

View File

@@ -620,6 +620,24 @@ interface GetCertBody {
};
}
interface GetCertPrivateKey {
type: EventType.GET_CERT_PRIVATE_KEY;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface GetCertBundle {
type: EventType.GET_CERT_BUNDLE;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
@@ -881,6 +899,8 @@ export type Event =
| DeleteCert
| RevokeCert
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert

View File

@@ -6,7 +6,8 @@ import { TCertificate } from "./types";
export const certKeys = {
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"]
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
};
export const useGetCert = (serialNumber: string) => {
@@ -38,3 +39,19 @@ export const useGetCertBody = (serialNumber: string) => {
enabled: Boolean(serialNumber)
});
};
export const useGetCertBundle = (serialNumber: string) => {
return useQuery({
queryKey: certKeys.getCertBundle(serialNumber),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificate: string;
certificateChain: string;
serialNumber: string;
privateKey: string;
}>(`/api/v1/pki/certificates/${serialNumber}/bundle`);
return data;
},
enabled: Boolean(serialNumber)
});
};

View File

@@ -10,15 +10,17 @@ import {
} from "./types";
export const useCreateNewInstallationSession = () => {
return useMutation<{ sessionId: string }, object, { organizationId: string }>({
mutationFn: async (opt) => {
const { data } = await apiRequest.post(
"/api/v1/secret-scanning/create-installation-session/organization",
opt
);
return data;
return useMutation<{ sessionId: string; gitAppSlug: string }, object, { organizationId: string }>(
{
mutationFn: async (opt) => {
const { data } = await apiRequest.post(
"/api/v1/secret-scanning/create-installation-session/organization",
opt
);
return data;
}
}
});
);
};
export const useUpdateRiskStatus = () => {

View File

@@ -31,6 +31,7 @@ import {
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
import { AuthPanel } from "./components/AuthPanel";
import { CachingPanel } from "./components/CachingPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { UserPanel } from "./components/UserPanel";
@@ -42,7 +43,8 @@ enum TabSections {
Integrations = "integrations",
Users = "users",
Identities = "identities",
Kmip = "kmip"
Kmip = "kmip",
Caching = "caching"
}
enum SignUpModes {
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>User Identities</Tab>
<Tab value={TabSections.Identities}>Machine Identities</Tab>
<Tab value={TabSections.Caching}>Caching</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>
@@ -408,6 +411,9 @@ export const OverviewPage = () => {
<TabPanel value={TabSections.Identities}>
<IdentityPanel />
</TabPanel>
<TabPanel value={TabSections.Caching}>
<CachingPanel />
</TabPanel>
</Tabs>
</div>
)}

View File

@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
import { faRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
import { useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useInvalidateCache } from "@app/hooks/api";
import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries";
import { CacheType } from "@app/hooks/api/admin/types";
export const CachingPanel = () => {
const { mutateAsync: invalidateCache } = useInvalidateCache();
const { user } = useUser();
const [type, setType] = useState<CacheType | null>(null);
const [shouldPoll, setShouldPoll] = useState(false);
const {
data: invalidationStatus,
isFetching,
refetch
} = useGetInvalidatingCacheStatus(shouldPoll);
const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus));
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"invalidateCache"
] as const);
const handleInvalidateCacheSubmit = async () => {
if (!type || isInvalidating) return;
try {
await invalidateCache({ type });
createNotification({ text: `Began invalidating ${type} cache`, type: "success" });
setShouldPoll(true);
handlePopUpClose("invalidateCache");
} catch (err) {
console.error(err);
createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" });
}
};
useEffect(() => {
if (isInvalidating) return;
if (shouldPoll) {
setShouldPoll(false);
createNotification({ text: "Successfully invalidated cache", type: "success" });
}
}, [isInvalidating, shouldPoll]);
useEffect(() => {
refetch().then((v) => setShouldPoll(v.data || false));
}, []);
return (
<>
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-3">
<span className="text-xl font-semibold text-mineshaft-100">Secrets Cache</span>
{isInvalidating && (
<Badge
variant="danger"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
Invalidating Cache
</Badge>
)}
</div>
<span className="max-w-xl text-sm text-mineshaft-400">
The encrypted secrets cache encompasses all secrets stored within the system and
provides a temporary, secure storage location for frequently accessed credentials.
</span>
</div>
<Button
colorSchema="danger"
onClick={() => {
setType(CacheType.SECRETS);
handlePopUpOpen("invalidateCache");
}}
isDisabled={!user.superAdmin || isInvalidating}
>
Invalidate Secrets Cache
</Button>
</div>
<DeleteActionModal
isOpen={popUp.invalidateCache.isOpen}
title={`Are you sure you want to invalidate ${type} cache?`}
subTitle="This action is permanent and irreversible. The cache invalidation process may take several minutes to complete."
onChange={(isOpen) => handlePopUpToggle("invalidateCache", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleInvalidateCacheSubmit}
/>
</>
);
};

View File

@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { PkiCollectionSection } from "../AlertingPage/components";
import { CertificatesSection } from "./components";
@@ -17,7 +22,7 @@ export const CertificatesPage = () => {
ProjectPermissionSub.PkiCollections
);
const canAccessCerts = permission.can(
ProjectPermissionActions.Read,
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
@@ -40,7 +45,7 @@ export const CertificatesPage = () => {
)}
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
<CertificatesSection />

View File

@@ -1,5 +1,11 @@
import { Modal, ModalContent } from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { useGetCertBody } from "@app/hooks/api";
import { useGetCertBundle } from "@app/hooks/api/certificates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertificateContent } from "./CertificateContent";
@@ -10,10 +16,29 @@ type Props = {
};
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const { data } = useGetCertBody(
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""
const { permission } = useProjectPermission();
const serialNumber =
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "";
const canReadPrivateKey = permission.can(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
// useGetCertBundle fails unless user has the correct permissions
const { data: bundleData } = useGetCertBundle(serialNumber);
const { data: bodyData } = useGetCertBody(serialNumber);
const data:
| {
certificate: string;
certificateChain: string;
serialNumber: string;
privateKey?: string;
}
| undefined = canReadPrivateKey ? bundleData : bodyData;
return (
<Modal
isOpen={popUp?.certificateCert?.isOpen}
@@ -27,6 +52,7 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
serialNumber={data.serialNumber}
certificate={data.certificate}
certificateChain={data.certificateChain}
privateKey={data.privateKey}
/>
) : (
<div />

View File

@@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { useDeleteCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -50,7 +54,7 @@ export const CertificatesSection = () => {
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
I={ProjectPermissionCertificateActions.Create}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (

View File

@@ -30,7 +30,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -110,7 +114,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
@@ -131,7 +135,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
@@ -152,7 +156,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
@@ -173,7 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (

View File

@@ -108,7 +108,7 @@ export const SecretScanningPage = withPermission(
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession({ organizationId });
window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`;
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
};
return (

View File

@@ -10,10 +10,6 @@ import { AxiosError } from "axios";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
encryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
@@ -28,7 +24,7 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
import { useCreateServiceToken } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const apiTokenExpiry = [
@@ -97,7 +93,6 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
const [newToken, setToken] = useState("");
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
const createServiceToken = useCreateServiceToken();
const hasServiceToken = Boolean(newToken);
@@ -118,26 +113,13 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
try {
if (!currentWorkspace?.id) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: key,
key: randomBytes
});
const { serviceToken } = await createServiceToken.mutateAsync({
encryptedKey: ciphertext,
iv,
tag,
encryptedKey: "",
iv: "",
tag: "",
scopes,
expiresIn: Number(expiresIn),
name,

View File

@@ -114,7 +114,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
<div
key={el.id}
className={twMerge(
"relative bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md",
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
draggedItem === rootIndex ? "opacity-50" : ""
)}
@@ -179,7 +179,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
</div>
)}
</div>
<div className="flex text-gray-300">
<div className="flex gap-4 text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }, index) => {

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
} from "@app/context";
@@ -32,6 +33,14 @@ const GeneralPolicyActionSchema = z.object({
create: z.boolean().optional()
});
const CertificatePolicyActionSchema = z.object({
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Read]: z.boolean().optional(),
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional()
});
const SecretPolicyActionSchema = z.object({
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
@@ -219,7 +228,7 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
@@ -371,7 +380,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.CertificateTemplates,
@@ -507,6 +515,25 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.Certificates) {
const canRead = action.includes(ProjectPermissionCertificateActions.Read);
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0].read = true;
if (canEdit) formVal[subject]![0].edit = true;
if (canCreate) formVal[subject]![0].create = true;
if (canDelete) formVal[subject]![0].delete = true;
if (canReadPrivateKey)
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true;
return;
}
if (subject === ProjectPermissionSub.Project) {
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
@@ -1014,10 +1041,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Certificates]: {
title: "Certificates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
{ label: "Read", value: ProjectPermissionCertificateActions.Read },
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey },
{ label: "Create", value: ProjectPermissionCertificateActions.Create },
{ label: "Modify", value: ProjectPermissionCertificateActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete }
]
},
[ProjectPermissionSub.CertificateTemplates]: {

View File

@@ -13,9 +13,9 @@ 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: v0.9.1
version: v0.9.2
# 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: "v0.9.1"
appVersion: "v0.9.2"

View File

@@ -0,0 +1,97 @@
{{- if .Values.installCRDs }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: clustergenerators.secrets.infisical.com
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
labels:
{{- include "secrets-operator.labels" . | nindent 4 }}
spec:
group: secrets.infisical.com
names:
kind: ClusterGenerator
listKind: ClusterGeneratorList
plural: clustergenerators
singular: clustergenerator
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: ClusterGenerator represents a cluster-wide generator
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
properties:
generator:
description: Generator the spec for this generator, must match the kind.
properties:
passwordSpec:
description: PasswordSpec controls the behavior of the password
generator.
properties:
allowRepeat:
default: false
description: set allowRepeat to true to allow repeating characters.
type: boolean
digits:
description: digits specifies the number of digits in the generated
password. If omitted it defaults to 25% of the length of the
password
type: integer
length:
default: 24
description: Length of the password to be generated. Defaults
to 24
type: integer
noUpper:
default: false
description: Set noUpper to disable uppercase characters
type: boolean
symbolCharacters:
description: symbolCharacters specifies the special characters
that should be used in the generated password.
type: string
symbols:
description: symbols specifies the number of symbol characters
in the generated password. If omitted it defaults to 25% of
the length of the password
type: integer
type: object
uuidSpec:
description: UUIDSpec controls the behavior of the uuid generator.
type: object
type: object
kind:
description: Kind the kind of this generator.
enum:
- Password
- UUID
type: string
required:
- kind
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
{{- end }}

View File

@@ -143,6 +143,34 @@ spec:
type: string
push:
properties:
generators:
items:
properties:
destinationSecretName:
type: string
generatorRef:
properties:
kind:
allOf:
- enum:
- Password
- UUID
- enum:
- Password
- UUID
description: Specify the Kind of the generator resource
type: string
name:
type: string
required:
- kind
- name
type: object
required:
- destinationSecretName
- generatorRef
type: object
type: array
secret:
properties:
secretName:
@@ -168,8 +196,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- secret
type: object
resyncInterval:
type: string
@@ -199,7 +225,6 @@ spec:
required:
- destination
- push
- resyncInterval
type: object
status:
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret

View File

@@ -81,6 +81,18 @@ rules:
- tokenreviews
verbs:
- create
- apiGroups:
- secrets.infisical.com
resources:
- clustergenerators
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:

View File

@@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.9.1
tag: v0.9.2
resources:
limits:
cpu: 500m

View File

@@ -0,0 +1,152 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GeneratorKind represents a kind of generator.
// +kubebuilder:validation:Enum=Password;UUID
type GeneratorKind string
const (
GeneratorKindPassword GeneratorKind = "Password"
GeneratorKindUUID GeneratorKind = "UUID"
)
type ClusterGeneratorSpec struct {
// Kind the kind of this generator.
Kind GeneratorKind `json:"kind"`
// Generator the spec for this generator, must match the kind.
Generator GeneratorSpec `json:"generator,omitempty"`
}
type GeneratorSpec struct {
// +kubebuilder:validation:Optional
PasswordSpec *PasswordSpec `json:"passwordSpec,omitempty"`
// +kubebuilder:validation:Optional
UUIDSpec *UUIDSpec `json:"uuidSpec,omitempty"`
}
// ClusterGenerator represents a cluster-wide generator
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
type ClusterGenerator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ClusterGeneratorSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// ClusterGeneratorList contains a list of ClusterGenerator resources.
type ClusterGeneratorList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ClusterGenerator `json:"items"`
}
// ! UUID Generator
// UUIDSpec controls the behavior of the uuid generator.
type UUIDSpec struct{}
// UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216).
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type UUID struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec UUIDSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// UUIDList contains a list of UUID resources.
type UUIDList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []UUID `json:"items"`
}
// ! Password Generator
// PasswordSpec controls the behavior of the password generator.
type PasswordSpec struct {
// Length of the password to be generated.
// Defaults to 24
// +kubebuilder:validation:Optional
// +kubebuilder:default=24
Length int `json:"length"`
// digits specifies the number of digits in the generated
// password. If omitted it defaults to 25% of the length of the password
Digits *int `json:"digits,omitempty"`
// symbols specifies the number of symbol characters in the generated
// password. If omitted it defaults to 25% of the length of the password
Symbols *int `json:"symbols,omitempty"`
// symbolCharacters specifies the special characters that should be used
// in the generated password.
SymbolCharacters *string `json:"symbolCharacters,omitempty"`
// Set noUpper to disable uppercase characters
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
NoUpper bool `json:"noUpper"`
// set allowRepeat to true to allow repeating characters.
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
AllowRepeat bool `json:"allowRepeat"`
}
// Password generates a random password based on the
// configuration parameters in spec.
// You can specify the length, characterset and other attributes.
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
type Password struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PasswordSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// PasswordList contains a list of Password resources.
type PasswordList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Password `json:"items"`
}
func init() {
SchemeBuilder.Register(&Password{}, &PasswordList{})
SchemeBuilder.Register(&UUID{}, &UUIDList{})
SchemeBuilder.Register(&ClusterGenerator{}, &ClusterGeneratorList{})
}

View File

@@ -29,9 +29,28 @@ type InfisicalPushSecretSecretSource struct {
Template *SecretTemplate `json:"template,omitempty"`
}
type SecretPush struct {
type GeneratorRef struct {
// Specify the Kind of the generator resource
// +kubebuilder:validation:Enum=Password;UUID
// +kubebuilder:validation:Required
Secret InfisicalPushSecretSecretSource `json:"secret"`
Kind GeneratorKind `json:"kind"`
// +kubebuilder:validation:Required
Name string `json:"name"`
}
type SecretPushGenerator struct {
// +kubebuilder:validation:Required
DestinationSecretName string `json:"destinationSecretName"`
// +kubebuilder:validation:Required
GeneratorRef GeneratorRef `json:"generatorRef"`
}
type SecretPush struct {
// +kubebuilder:validation:Optional
Secret *InfisicalPushSecretSecretSource `json:"secret,omitempty"`
// +kubebuilder:validation:Optional
Generators []SecretPushGenerator `json:"generators,omitempty"`
}
// InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
@@ -52,7 +71,8 @@ type InfisicalPushSecretSpec struct {
// +kubebuilder:validation:Required
Push SecretPush `json:"push"`
ResyncInterval string `json:"resyncInterval"`
// +kubebuilder:validation:Optional
ResyncInterval *string `json:"resyncInterval,omitempty"`
// Infisical host to pull secrets from
// +kubebuilder:validation:Optional

View File

@@ -96,6 +96,80 @@ func (in *CaReference) DeepCopy() *CaReference {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGenerator.
func (in *ClusterGenerator) DeepCopy() *ClusterGenerator {
if in == nil {
return nil
}
out := new(ClusterGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ClusterGenerator) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterGeneratorList) DeepCopyInto(out *ClusterGeneratorList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ClusterGenerator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorList.
func (in *ClusterGeneratorList) DeepCopy() *ClusterGeneratorList {
if in == nil {
return nil
}
out := new(ClusterGeneratorList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ClusterGeneratorList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterGeneratorSpec) DeepCopyInto(out *ClusterGeneratorSpec) {
*out = *in
in.Generator.DeepCopyInto(&out.Generator)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorSpec.
func (in *ClusterGeneratorSpec) DeepCopy() *ClusterGeneratorSpec {
if in == nil {
return nil
}
out := new(ClusterGeneratorSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamicSecretDetails) DeepCopyInto(out *DynamicSecretDetails) {
*out = *in
@@ -143,6 +217,46 @@ func (in *GcpIamAuthDetails) DeepCopy() *GcpIamAuthDetails {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GeneratorRef) DeepCopyInto(out *GeneratorRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorRef.
func (in *GeneratorRef) DeepCopy() *GeneratorRef {
if in == nil {
return nil
}
out := new(GeneratorRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
*out = *in
if in.PasswordSpec != nil {
in, out := &in.PasswordSpec, &out.PasswordSpec
*out = new(PasswordSpec)
(*in).DeepCopyInto(*out)
}
if in.UUIDSpec != nil {
in, out := &in.UUIDSpec, &out.UUIDSpec
*out = new(UUIDSpec)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec.
func (in *GeneratorSpec) DeepCopy() *GeneratorSpec {
if in == nil {
return nil
}
out := new(GeneratorSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericAwsIamAuth) DeepCopyInto(out *GenericAwsIamAuth) {
*out = *in
@@ -483,6 +597,11 @@ func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) {
out.Destination = in.Destination
in.Authentication.DeepCopyInto(&out.Authentication)
in.Push.DeepCopyInto(&out.Push)
if in.ResyncInterval != nil {
in, out := &in.ResyncInterval, &out.ResyncInterval
*out = new(string)
**out = **in
}
out.TLS = in.TLS
}
@@ -746,10 +865,107 @@ func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Password) DeepCopyInto(out *Password) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Password.
func (in *Password) DeepCopy() *Password {
if in == nil {
return nil
}
out := new(Password)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Password) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PasswordList) DeepCopyInto(out *PasswordList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Password, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordList.
func (in *PasswordList) DeepCopy() *PasswordList {
if in == nil {
return nil
}
out := new(PasswordList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PasswordList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PasswordSpec) DeepCopyInto(out *PasswordSpec) {
*out = *in
if in.Digits != nil {
in, out := &in.Digits, &out.Digits
*out = new(int)
**out = **in
}
if in.Symbols != nil {
in, out := &in.Symbols, &out.Symbols
*out = new(int)
**out = **in
}
if in.SymbolCharacters != nil {
in, out := &in.SymbolCharacters, &out.SymbolCharacters
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordSpec.
func (in *PasswordSpec) DeepCopy() *PasswordSpec {
if in == nil {
return nil
}
out := new(PasswordSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretPush) DeepCopyInto(out *SecretPush) {
*out = *in
in.Secret.DeepCopyInto(&out.Secret)
if in.Secret != nil {
in, out := &in.Secret, &out.Secret
*out = new(InfisicalPushSecretSecretSource)
(*in).DeepCopyInto(*out)
}
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make([]SecretPushGenerator, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush.
@@ -762,6 +978,22 @@ func (in *SecretPush) DeepCopy() *SecretPush {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretPushGenerator) DeepCopyInto(out *SecretPushGenerator) {
*out = *in
out.GeneratorRef = in.GeneratorRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPushGenerator.
func (in *SecretPushGenerator) DeepCopy() *SecretPushGenerator {
if in == nil {
return nil
}
out := new(SecretPushGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) {
*out = *in
@@ -848,6 +1080,79 @@ func (in *TLSConfig) DeepCopy() *TLSConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UUID) DeepCopyInto(out *UUID) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUID.
func (in *UUID) DeepCopy() *UUID {
if in == nil {
return nil
}
out := new(UUID)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *UUID) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UUIDList) DeepCopyInto(out *UUIDList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]UUID, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDList.
func (in *UUIDList) DeepCopy() *UUIDList {
if in == nil {
return nil
}
out := new(UUIDList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *UUIDList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UUIDSpec) DeepCopyInto(out *UUIDSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDSpec.
func (in *UUIDSpec) DeepCopy() *UUIDSpec {
if in == nil {
return nil
}
out := new(UUIDSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) {
*out = *in

View File

@@ -0,0 +1,90 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: clustergenerators.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: ClusterGenerator
listKind: ClusterGeneratorList
plural: clustergenerators
singular: clustergenerator
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: ClusterGenerator represents a cluster-wide generator
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
properties:
generator:
description: Generator the spec for this generator, must match the
kind.
properties:
passwordSpec:
description: PasswordSpec controls the behavior of the password
generator.
properties:
allowRepeat:
default: false
description: set allowRepeat to true to allow repeating characters.
type: boolean
digits:
description: digits specifies the number of digits in the
generated password. If omitted it defaults to 25% of the
length of the password
type: integer
length:
default: 24
description: Length of the password to be generated. Defaults
to 24
type: integer
noUpper:
default: false
description: Set noUpper to disable uppercase characters
type: boolean
symbolCharacters:
description: symbolCharacters specifies the special characters
that should be used in the generated password.
type: string
symbols:
description: symbols specifies the number of symbol characters
in the generated password. If omitted it defaults to 25%
of the length of the password
type: integer
type: object
uuidSpec:
description: UUIDSpec controls the behavior of the uuid generator.
type: object
type: object
kind:
description: Kind the kind of this generator.
enum:
- Password
- UUID
type: string
required:
- kind
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -142,6 +142,34 @@ spec:
type: string
push:
properties:
generators:
items:
properties:
destinationSecretName:
type: string
generatorRef:
properties:
kind:
allOf:
- enum:
- Password
- UUID
- enum:
- Password
- UUID
description: Specify the Kind of the generator resource
type: string
name:
type: string
required:
- kind
- name
type: object
required:
- destinationSecretName
- generatorRef
type: object
type: array
secret:
properties:
secretName:
@@ -168,8 +196,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- secret
type: object
resyncInterval:
type: string
@@ -200,7 +226,6 @@ spec:
required:
- destination
- push
- resyncInterval
type: object
status:
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret

View File

@@ -0,0 +1,69 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: passwords.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: Password
listKind: PasswordList
plural: passwords
singular: password
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Password generates a random password based on the configuration
parameters in spec. You can specify the length, characterset and other attributes.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: PasswordSpec controls the behavior of the password generator.
properties:
allowRepeat:
default: false
description: set allowRepeat to true to allow repeating characters.
type: boolean
digits:
description: digits specifies the number of digits in the generated
password. If omitted it defaults to 25% of the length of the password
type: integer
length:
default: 24
description: Length of the password to be generated. Defaults to 24
type: integer
noUpper:
default: false
description: Set noUpper to disable uppercase characters
type: boolean
symbolCharacters:
description: symbolCharacters specifies the special characters that
should be used in the generated password.
type: string
symbols:
description: symbols specifies the number of symbol characters in
the generated password. If omitted it defaults to 25% of the length
of the password
type: integer
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,42 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: uuids.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: UUID
listKind: UUIDList
plural: uuids
singular: uuid
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216).
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: UUIDSpec controls the behavior of the uuid generator.
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -5,6 +5,7 @@ resources:
- bases/secrets.infisical.com_infisicalsecrets.yaml
- bases/secrets.infisical.com_infisicalpushsecrets.yaml
- bases/secrets.infisical.com_infisicaldynamicsecrets.yaml
- bases/secrets.infisical.com_clustergenerators.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:

View File

@@ -74,6 +74,18 @@ rules:
- tokenreviews
verbs:
- create
- apiGroups:
- secrets.infisical.com
resources:
- clustergenerators
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: password-generator
spec:
kind: Password
generator:
passwordSpec:
length: 10
digits: 5
symbols: 5
symbolCharacters: "-_$@"
noUpper: false
allowRepeat: true

View File

@@ -393,7 +393,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c
// Max TTL
if infisicalDynamicSecret.Status.MaxTTL != "" {
maxTTLDuration, err := util.ConvertIntervalToDuration(infisicalDynamicSecret.Status.MaxTTL)
maxTTLDuration, err := util.ConvertIntervalToDuration(&infisicalDynamicSecret.Status.MaxTTL)
if err != nil {
return defaultNextReconcile, fmt.Errorf("unable to parse MaxTTL duration: %w", err)
}

View File

@@ -51,7 +51,7 @@ func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=clustergenerators,verbs=get;list;watch;create;update;patch;delete
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// For more details, check Reconcile and its Result here:
@@ -108,23 +108,30 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, nil
}
if infisicalPushSecretCRD.Spec.ResyncInterval != "" {
if infisicalPushSecretCRD.Spec.Push.Secret == nil && infisicalPushSecretCRD.Spec.Push.Generators == nil {
logger.Info("No secret or generators found, skipping reconciliation. Please define ")
return ctrl.Result{}, nil
}
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
if err != nil {
if err != nil {
// if resyncInterval is nil, we don't want to reconcile automatically
if infisicalPushSecretCRD.Spec.ResyncInterval != nil {
logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to convert resync interval to duration")
return ctrl.Result{}, err
}
}
requeueTime = duration
requeueTime = duration
if requeueTime != 0 {
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
} else {
logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime))
}
// Check if the resource is already marked for deletion
@@ -137,10 +144,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
// Get modified/default config
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
if requeueTime != 0 {
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to fetch infisical-config")
return ctrl.Result{}, err
}
}
if infisicalPushSecretCRD.Spec.HostAPI == "" {
@@ -152,10 +164,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" {
api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCRD)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
if requeueTime != 0 {
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to fetch CA certificate")
return ctrl.Result{}, err
}
}
logger.Info("Using custom CA certificate...")
@@ -167,17 +184,27 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
r.SetReconcileStatusCondition(ctx, &infisicalPushSecretCRD, err)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
if requeueTime != 0 {
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to reconcile Infisical Push Secret")
return ctrl.Result{}, err
}
}
// Sync again after the specified time
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
if requeueTime != 0 {
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Info("Operator will reconcile on next spec change")
return ctrl.Result{}, nil
}
}
func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
@@ -228,27 +255,67 @@ func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error
)).
Watches(
&source.Kind{Type: &corev1.Secret{}},
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
ctx := context.Background()
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
if err := r.List(ctx, pushSecrets); err != nil {
return []reconcile.Request{}
}
requests := []reconcile.Request{}
for _, pushSecret := range pushSecrets.Items {
if pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pushSecret.GetName(),
Namespace: pushSecret.GetNamespace(),
},
})
}
}
return requests
}),
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForSecret),
).
Watches(
&source.Kind{Type: &secretsv1alpha1.ClusterGenerator{}},
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForClusterGenerator),
).
Complete(r)
}
func (r *InfisicalPushSecretReconciler) findPushSecretsForClusterGenerator(o client.Object) []reconcile.Request {
ctx := context.Background()
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
if err := r.List(ctx, pushSecrets); err != nil {
return []reconcile.Request{}
}
clusterGenerator, ok := o.(*secretsv1alpha1.ClusterGenerator)
if !ok {
return []reconcile.Request{}
}
requests := []reconcile.Request{}
for _, pushSecret := range pushSecrets.Items {
if pushSecret.Spec.Push.Generators != nil {
for _, generator := range pushSecret.Spec.Push.Generators {
if generator.GeneratorRef.Name == clusterGenerator.GetName() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pushSecret.GetName(),
Namespace: pushSecret.GetNamespace(),
},
})
break
}
}
}
}
return requests
}
func (r *InfisicalPushSecretReconciler) findPushSecretsForSecret(o client.Object) []reconcile.Request {
ctx := context.Background()
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
if err := r.List(ctx, pushSecrets); err != nil {
return []reconcile.Request{}
}
requests := []reconcile.Request{}
for _, pushSecret := range pushSecrets.Items {
if pushSecret.Spec.Push.Secret != nil &&
pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pushSecret.GetName(),
Namespace: pushSecret.GetNamespace(),
},
})
}
}
return requests
}

View File

@@ -19,6 +19,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
generatorUtil "github.com/Infisical/infisical/k8-operator/packages/generator"
infisicalSdk "github.com/infisical/go-sdk"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
)
@@ -106,6 +107,52 @@ func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSec
infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables
}
func (r *InfisicalPushSecretReconciler) processGenerators(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (map[string]string, error) {
processedSecrets := make(map[string]string)
if len(infisicalPushSecret.Spec.Push.Generators) == 0 {
return processedSecrets, nil
}
for _, generator := range infisicalPushSecret.Spec.Push.Generators {
generatorRef := generator.GeneratorRef
clusterGenerator := &v1alpha1.ClusterGenerator{}
err := r.Client.Get(ctx, types.NamespacedName{Name: generatorRef.Name}, clusterGenerator)
if err != nil {
return nil, fmt.Errorf("unable to get ClusterGenerator resource [err=%s]", err)
}
if generatorRef.Kind == v1alpha1.GeneratorKindPassword {
// get the custom ClusterGenerator resource from the cluster
if clusterGenerator.Spec.Generator.PasswordSpec == nil {
return nil, fmt.Errorf("password spec is not defined in the ClusterGenerator resource")
}
password, err := generatorUtil.GeneratorPassword(*clusterGenerator.Spec.Generator.PasswordSpec)
if err != nil {
return nil, fmt.Errorf("unable to generate password [err=%s]", err)
}
processedSecrets[generator.DestinationSecretName] = password
}
if generatorRef.Kind == v1alpha1.GeneratorKindUUID {
uuid, err := generatorUtil.GeneratorUUID()
if err != nil {
return nil, fmt.Errorf("unable to generate UUID [err=%s]", err)
}
processedSecrets[generator.DestinationSecretName] = uuid
}
}
return processedSecrets, nil
}
func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) {
processedSecrets := make(map[string]string)
@@ -172,18 +219,31 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
})
}
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
})
processedSecrets := make(map[string]string)
if err != nil {
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
if infisicalPushSecret.Spec.Push.Secret != nil {
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
})
if err != nil {
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
}
processedSecrets, err = r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
if err != nil {
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
}
}
processedSecrets, err := r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
generatorSecrets, err := r.processGenerators(ctx, infisicalPushSecret)
if err != nil {
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
return fmt.Errorf("unable to process generators [err=%s]", err)
}
for key, value := range generatorSecrets {
processedSecrets[key] = value
}
destination := infisicalPushSecret.Spec.Destination

View File

@@ -4,10 +4,12 @@ go 1.21
require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/aws/smithy-go v1.20.3
github.com/infisical/go-sdk v0.4.4
github.com/lestrrat-go/jwx/v2 v2.1.4
github.com/onsi/ginkgo/v2 v2.6.0
github.com/onsi/gomega v1.24.1
github.com/sethvargo/go-password v0.3.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
sigs.k8s.io/controller-runtime v0.14.4
@@ -34,7 +36,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -85,7 +86,7 @@ require (
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.6.0
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect

View File

@@ -338,6 +338,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

View File

@@ -0,0 +1 @@
package generator

View File

@@ -0,0 +1,76 @@
package generator
import (
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/sethvargo/go-password/password"
)
const (
defaultLength = 24
defaultSymbolChars = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
digitFactor = 0.25
symbolFactor = 0.25
)
func generateSafePassword(
passLen int,
symbols int,
symbolCharacters string,
digits int,
noUpper bool,
allowRepeat bool,
) (string, error) {
gen, err := password.NewGenerator(&password.GeneratorInput{
Symbols: symbolCharacters,
})
if err != nil {
return "", err
}
return gen.Generate(
passLen,
digits,
symbols,
noUpper,
allowRepeat,
)
}
func GeneratorPassword(spec v1alpha1.PasswordSpec) (string, error) {
symbolCharacters := defaultSymbolChars
if spec.SymbolCharacters != nil && *spec.SymbolCharacters != "" {
symbolCharacters = *spec.SymbolCharacters
}
passwordLength := defaultLength
if spec.Length != 0 {
passwordLength = spec.Length
}
digits := int(float32(passwordLength) * digitFactor)
if spec.Digits != nil {
digits = *spec.Digits
}
symbols := int(float32(passwordLength) * symbolFactor)
if spec.Symbols != nil {
symbols = *spec.Symbols
}
pass, err := generateSafePassword(
passwordLength,
symbols,
symbolCharacters,
digits,
spec.NoUpper,
spec.AllowRepeat,
)
if err != nil {
return "", err
}
return pass, nil
}

View File

@@ -0,0 +1,10 @@
package generator
import (
"github.com/google/uuid"
)
func GeneratorUUID() (string, error) {
uuid := uuid.New().String()
return uuid, nil
}

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