diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 93c3dd147..d62a1eeb2 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,10 +1,18 @@ +import { ProjectMembershipRole } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; +import { getConfig } from "@app/lib/config/env"; +import { applyJitter } from "@app/lib/delay"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -15,7 +23,12 @@ import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types"; type TDynamicSecretLeaseQueueServiceFactoryDep = { queueService: TQueueServiceFactory; dynamicSecretLeaseDAL: Pick; - dynamicSecretDAL: Pick; + smtpService: Pick; + userDAL: Pick; + identityDAL: TIdentityDALFactory; + dynamicSecretDAL: Pick; + projectMembershipDAL: Pick; + projectDAL: Pick; dynamicSecretProviders: Record; kmsService: Pick; folderDAL: Pick; @@ -23,18 +36,24 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { export type TDynamicSecretLeaseQueueServiceFactory = { pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise; - setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise; + setLeaseRevocation: (leaseId: string, dynamicSecretId: string, expiryAt: Date) => Promise; unsetLeaseRevocation: (leaseId: string) => Promise; + queueFailedRevocation: (leaseId: string, dynamicSecretId: string) => Promise; init: () => Promise; }; +const MAX_REVOCATION_RETRY_COUNT = 10; + export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, dynamicSecretLeaseDAL, kmsService, - folderDAL + folderDAL, + projectMembershipDAL, + projectDAL, + smtpService }: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queuePg( @@ -48,10 +67,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ ); }; - const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => { + const setLeaseRevocation = async (leaseId: string, dynamicSecretId: string, expiryAt: Date) => { await queueService.queuePg( QueueJobs.DynamicSecretRevocation, - { leaseId }, + { leaseId, dynamicSecretId }, { id: leaseId, singletonKey: leaseId, @@ -68,10 +87,53 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId); }; + const queueFailedRevocation = async (leaseId: string, dynamicSecretId: string) => { + const appConfig = getConfig(); + + const retryDelaySeconds = appConfig.isDevelopmentMode ? 1 : Math.floor(applyJitter(3_600_000 * 4) / 1000); // retry every 4 hours with 20% +- jitter (convert ms to seconds for pgboss) + + await queueService.queuePg( + QueueJobs.DynamicSecretRevocation, + { leaseId, isRetry: true, dynamicSecretId }, + { + singletonKey: `${leaseId}-retry`, // avoid conflicts with scheduled revocation + retryDelay: retryDelaySeconds, + retryLimit: MAX_REVOCATION_RETRY_COUNT, // we dont want it to ever hit the limit, we want the expireInHours to take effect. + expireInHours: 23 // if we set it to 24 hours, pgboss will complain that the expireIn is too high + } + ); + }; + + const $queueDynamicSecretLeaseRevocationFailedEmail = async (leaseId: string, dynamicSecretId: string) => { + const appConfig = getConfig(); + + const delay = appConfig.isDevelopmentMode ? 1_000 * 60 : 1_000 * 60 * 15; // 1 minute in development, 15 minutes in production + + await queueService.queue( + QueueName.DynamicSecretLeaseRevocationFailedEmail, + QueueJobs.DynamicSecretLeaseRevocationFailedEmail, + { + leaseId + }, + { + jobId: `dynamic-secret-lease-revocation-failed-email-${dynamicSecretId}`, + delay, + attempts: 3, + backoff: { + type: "exponential", + delay: 1000 * 60 // 1 minute + }, + removeOnComplete: true, + removeOnFail: true + } + ); + }; + const $dynamicSecretQueueJob = async ( jobName: string, jobId: string, - data: { leaseId: string } | { dynamicSecretCfgId: string } + data: { leaseId: string; dynamicSecretId: string; isRetry?: boolean } | { dynamicSecretCfgId: string }, + retryCount?: number ): Promise => { try { if (jobName === QueueJobs.DynamicSecretRevocation) { @@ -79,7 +141,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ logger.info("Dynamic secret lease revocation started: ", leaseId, jobId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); - if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + if (!dynamicSecretLease) { + throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + } const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId); if (!folder) @@ -150,7 +214,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ } logger.info("Finished dynamic secret job", jobId); } catch (error) { - logger.error(error); + logger.error(error, "Failed to delete dynamic secret"); if (jobName === QueueJobs.DynamicSecretPruning) { const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string }; @@ -161,20 +225,97 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ } if (jobName === QueueJobs.DynamicSecretRevocation) { - const { leaseId } = data as { leaseId: string }; + const { leaseId, isRetry, dynamicSecretId } = data as { + leaseId: string; + isRetry?: boolean; + dynamicSecretId: string; + }; await dynamicSecretLeaseDAL.updateById(leaseId, { status: DynamicSecretStatus.FailedDeletion, - statusDetails: (error as Error)?.message?.slice(0, 255) + statusDetails: `${(error as Error)?.message?.slice(0, 255)} - Retrying automatically` }); + + // only add to retry queue if this is not a retry, and if the error is not a DisableRotationErrors error + if (!isRetry && !(error instanceof DisableRotationErrors)) { + // if revocation fails, we should stop the job and queue a new job to retry the revocation at a later time. + await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId); + await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId); + await queueFailedRevocation(leaseId, dynamicSecretId); + + // if its the last attempt, and the error isn't a DisableRotationErrors error, send an email to the project admins (debounced) + } else if (isRetry && !(error instanceof DisableRotationErrors)) { + if (retryCount && retryCount === MAX_REVOCATION_RETRY_COUNT) { + // if all retries fail, we should also stop the automatic revocation job. + // the ID of the revocation job is set to the leaseId, so we can use that to stop the job + + // we dont have to stop the retry job, because if we hit this point, its the last attempt and the retry job will be stopped by pgboss itself after this point, + await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId); + await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, leaseId); + + await $queueDynamicSecretLeaseRevocationFailedEmail(leaseId, dynamicSecretId); + } + } } if (error instanceof DisableRotationErrors) { if (jobId) { await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId); await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId); } + } else { + // propagate to next part + throw error; + } + } + }; + + // send alert email once all revocation attempts have failed + const $dynamicSecretLeaseRevocationFailedEmailJob = async (jobId: string, data: { leaseId: string }) => { + try { + const appCfg = getConfig(); + + const { leaseId } = data; + logger.info( + { leaseId, jobId }, + "Dynamic secret revocation failed. Notifying project admins about failed revocation." + ); + + const lease = await dynamicSecretLeaseDAL.findById(leaseId); + if (!lease) { + throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + } + + const folder = await folderDAL.findById(lease.dynamicSecret.folderId); + if (!folder) throw new NotFoundError({ message: `Failed to find folder with ${lease.dynamicSecret.folderId}` }); + + const project = await projectDAL.findById(folder.projectId); + const projectMembers = await projectMembershipDAL.findAllProjectMembers(project.id); + + const projectAdmins = projectMembers.filter((member) => + member.roles.some((role) => role.role === ProjectMembershipRole.Admin) + ); + + await smtpService.sendMail({ + recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + template: SmtpTemplates.DynamicSecretLeaseRevocationFailed, + subjectLine: "Dynamic Secret Lease Revocation Failed", + substitutions: { + dynamicSecretLeaseUrl: `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/secrets/${folder.environment.envSlug}?dynamicSecretId=${lease.dynamicSecret.id}&filterBy=dynamic&search=${lease.dynamicSecret.name}`, + dynamicSecretName: lease.dynamicSecret.name, + projectName: project.name, + environmentSlug: folder.environment.envSlug, + errorMessage: lease.statusDetails || "An unknown error occurred" + } + }); + } catch (error) { + logger.error(error, "Failed to send dynamic secret lease revocation failed email"); + if (error instanceof DisableRotationErrors) { + if (jobId) { + await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId); + await queueService.stopJobById(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId); + } + } else { + throw error; } - // propogate to next part - throw error; } }; @@ -182,14 +323,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ await $dynamicSecretQueueJob(job.name, job.id as string, job.data); }); + // we use redis for sending the email because: + // 1. we are insensitive to losing the jobs in queue in case of a disaster event + // 2. pgboss does not support exclusive job keys on v0.10.x, and upgrading to v0.11.x which supports exclusive jobs comes with a lot of breaking changes, and we would need to manually migrate our existing jobs to the new version + queueService.start(QueueName.DynamicSecretLeaseRevocationFailedEmail, async (job) => { + await $dynamicSecretLeaseRevocationFailedEmailJob(job.id as string, job.data); + }); + const init = async () => { await queueService.startPg( QueueJobs.DynamicSecretRevocation, async ([job]) => { - await $dynamicSecretQueueJob(job.name, job.id, job.data); + await $dynamicSecretQueueJob(job.name, job.id, job.data, job.retryCount); }, { - workerCount: 5, + workerCount: 10, pollingIntervalSeconds: 1 } ); @@ -210,6 +358,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ pruneDynamicSecret, setLeaseRevocation, unsetLeaseRevocation, + queueFailedRevocation, init }; }; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index cf37626c7..ea5efd502 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -178,7 +178,7 @@ export const dynamicSecretLeaseServiceFactory = ({ config }); - await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt); + await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt); return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; }; @@ -272,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({ ); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); - await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt); + await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt); const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { expireAt, externalEntityId: entityId @@ -358,11 +358,13 @@ export const dynamicSecretLeaseServiceFactory = ({ if ((revokeResponse as { error?: Error })?.error) { const { error } = revokeResponse as { error?: Error }; logger.error(error?.message, "Failed to revoke lease"); - const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { + const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { status: DynamicSecretLeaseStatus.FailedDeletion, statusDetails: error?.message?.slice(0, 255) }); - return deletedDynamicSecretLease; + // queue a job to retry the revocation at a later time + await dynamicSecretQueueService.queueFailedRevocation(dynamicSecretLease.id, dynamicSecretCfg.id); + return updatedDynamicSecretLease; } await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); diff --git a/backend/src/lib/delay/index.ts b/backend/src/lib/delay/index.ts index 32cb8ebfc..a5d4250fc 100644 --- a/backend/src/lib/delay/index.ts +++ b/backend/src/lib/delay/index.ts @@ -2,3 +2,13 @@ export const delay = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }); + +export const applyJitter = (delayMs: number) => { + const jitterFactor = 0.2; + + // generates random value in [-0.2, +0.2] range + const randomFactor = (Math.random() * 2 - 1) * jitterFactor; + const jitterAmount = randomFactor * delayMs; + + return delayMs + jitterAmount; +}; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 8cf8555f9..57409d173 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -61,6 +61,7 @@ export enum QueueName { SecretPushEventScan = "secret-push-event-scan", UpgradeProjectToGhost = "upgrade-project-to-ghost", DynamicSecretRevocation = "dynamic-secret-revocation", + DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email", CaCrlRotation = "ca-crl-rotation", CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber SecretReplication = "secret-replication", @@ -120,6 +121,7 @@ export enum QueueJobs { SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint", + DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email", InvalidateCache = "invalidate-cache", SecretScanningV2FullScan = "secret-scanning-v2-full-scan", SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan", @@ -219,11 +221,19 @@ export type TQueueJobTypes = { name: QueueJobs.TelemetryInstanceStats; payload: undefined; }; + [QueueName.DynamicSecretLeaseRevocationFailedEmail]: { + name: QueueJobs.DynamicSecretLeaseRevocationFailedEmail; + payload: { + leaseId: string; + }; + }; [QueueName.DynamicSecretRevocation]: | { name: QueueJobs.DynamicSecretRevocation; payload: { + isRetry?: boolean; leaseId: string; + dynamicSecretId: string; }; } | { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 00771168c..860912934 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1874,7 +1874,12 @@ export const registerRoutes = async ( dynamicSecretProviders, dynamicSecretDAL, folderDAL, - kmsService + kmsService, + smtpService, + userDAL, + identityDAL, + projectMembershipDAL, + projectDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, diff --git a/backend/src/services/smtp/emails/DynamicSecretLeaseRevocationFailedTemplate.tsx b/backend/src/services/smtp/emails/DynamicSecretLeaseRevocationFailedTemplate.tsx new file mode 100644 index 000000000..94e2e8f6a --- /dev/null +++ b/backend/src/services/smtp/emails/DynamicSecretLeaseRevocationFailedTemplate.tsx @@ -0,0 +1,68 @@ +import { Heading, Section, Text } from "@react-email/components"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface DynamicSecretLeaseRevocationFailedTemplateProps + extends Omit { + siteUrl: string; + dynamicSecretLeaseUrl: string; + dynamicSecretName: string; + projectName: string; + environmentSlug: string; + errorMessage: string; +} + +export const DynamicSecretLeaseRevocationFailedTemplate = ({ + siteUrl, + dynamicSecretLeaseUrl, + dynamicSecretName, + projectName, + environmentSlug, + errorMessage +}: DynamicSecretLeaseRevocationFailedTemplateProps) => { + return ( + + + Dynamic Secret Lease Revocation Failed + +
+ + One or more leases for the dynamic secret {dynamicSecretName} in project{" "} + {projectName} and environment {environmentSlug} have failed to revoke after + multiple attempts. + + + Please review the dynamic secret leases and attempt to revoke them again. + +
+ +
+ + Latest error message + + {errorMessage} +
+ +
+ View Dynamic Secret Leases +
+
+ ); +}; + +export default DynamicSecretLeaseRevocationFailedTemplate; + +DynamicSecretLeaseRevocationFailedTemplate.PreviewProps = { + errorMessage: 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "[REDACTED]" - tuple concurrently updated.', + dynamicSecretLeaseUrl: "https://infisical.com/test", + leaseId: "717d5013-7194-49d9-b6ac-6192328c2914", + dynamicSecretName: "postgres-prod-db", + projectName: "Development Team", + environmentSlug: "dev", + siteUrl: "https://infisical.com" +} as DynamicSecretLeaseRevocationFailedTemplateProps; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index cef22009a..62906764f 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -43,6 +43,7 @@ import { SubOrganizationInvitationTemplate, UnlockAccountTemplate } from "./emails"; +import DynamicSecretLeaseRevocationFailedTemplate from "./emails/DynamicSecretLeaseRevocationFailedTemplate"; export type TSmtpConfig = SMTPTransport.Options; export type TSmtpSendMail = { @@ -89,7 +90,8 @@ export enum SmtpTemplates { SecretScanningV2ScanFailed = "secretScanningV2ScanFailed", SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected", AccountDeletionConfirmation = "accountDeletionConfirmation", - HealthAlert = "healthAlert" + HealthAlert = "healthAlert", + DynamicSecretLeaseRevocationFailed = "dynamicSecretLeaseRevocationFailed" } export enum SmtpHost { @@ -137,7 +139,8 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate, [SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate, [SmtpTemplates.AccountDeletionConfirmation]: AccountDeletionConfirmationTemplate, - [SmtpTemplates.HealthAlert]: HealthAlertTemplate + [SmtpTemplates.HealthAlert]: HealthAlertTemplate, + [SmtpTemplates.DynamicSecretLeaseRevocationFailed]: DynamicSecretLeaseRevocationFailedTemplate }; export const smtpServiceFactory = (cfg: TSmtpConfig) => { diff --git a/docs/integrations/platforms/infisical-agent.mdx b/docs/integrations/platforms/infisical-agent.mdx index 43d322faa..883d248ae 100644 --- a/docs/integrations/platforms/infisical-agent.mdx +++ b/docs/integrations/platforms/infisical-agent.mdx @@ -8,12 +8,12 @@ It eliminates the need to modify application logic by enabling clients to decide ![agent diagram](/images/agent/infisical-agent-diagram.png) -### Key features: +## Key Features -- Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume -- Templating: Renders secrets via user provided templates to desired formats for applications to consume +- **Token lifecycle management**: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume +- **Templating**: Renders secrets and dynamic secret leases via user provided templates to desired formats for applications to consume -### Token renewal +## Token Renewal The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates. @@ -28,7 +28,7 @@ Every time the agent successfully retrieves a new access token, it writes the ne to retrieve secrets from Infisical -### Templating +## Templating The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path. @@ -40,31 +40,203 @@ If this initial attempt is unsuccessful, the agent will momentarily pauses befor Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it. It then formats these secrets using the user provided templates and writes the formatted data to configured file paths. + +### Available secret template functions + +The secret template functions is what you will use to fetch resources such as static secrets and dynamic secret leases from Infisical. Below is a list of the available secret template functions that you can use in your templates. + + + + + ```bash + secret "" "environment-slug" "" "" + ``` + ```bash example-template-usage-1 + {{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" `{"recursive": false, "expandSecretReferences": true}` }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} + ``` + ```bash example-template-usage-2 + {{- with secret "da8056c8-01e2-4d24-b39f-cb4e004b8d44" "staging" "/" `{"recursive": true, "expandSecretReferences": true}` }} + {{- range . }} + {{- if eq .SecretPath "/"}} + {{ .Key }}={{ .Value }} + {{- else}} + {{ .SecretPath }}/{{ .Key }}={{ .Value }} + {{- end}} + {{- end }} + {{- end }} + ``` + + + + **Function name**: `secret` + + **Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. + + An optional JSON argument is also available. It includes the properties `recursive`, which defaults to false, and `expandSecretReferences`, which defaults to true and expands the returned secrets. + + + **Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, SecretPath, Type, ID, and Comment` + + + + + ```bash + getSecretByName "" "" "" "" + ``` + + ```bash example-template-usage + {{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} + {{ if .Value }} + password = "{{ .Value }}" + {{ end }} + {{ end }} + ``` + + **Function name**: `getSecretByName` + + **Description**: This function can be used to render a single secret by it's name. + + **Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` + + + + + ```bash + dynamic_secret "" "" "" "" "" + ``` + + ```bash example-redis-dynamic-secret + {{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }} + {{ .DB_USERNAME }}={{ .DB_PASSWORD }} + {{- end }} + + ``` + + **Function Name**: `dynamic_secret` + + **Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date. + + **Returns**: An object with keys corresponding to the dynamic secret lease credentials. + + + Note that if you have multiple dynamic secret templates with identical configurations, only one lease will be created in Infisical for those templates, and the same lease will be written to your specified destination paths. + + + + + +## Caching + +The Infisical Agent supports clientside caching of Dynamic Secret leases. If the cache is enabled, the agent will persist the dynamic secret leases to the cache across restarts of the agent. + +### Persistent Caching + +The Agent currently only supports persistent caching. To utilize persistent caching, you must be within a Kubernetes environment. We recommend using the [Infisical Agent Injector](/integrations/platforms/kubernetes-injector) to inject the agent into pods within your Kubernetes cluster on demand. + +### Cache eviction + +Cache eviction is the process of removing cached data from the cache. The Agent will automatically evict cached data when the cache is full during a garbage collection cycle which is triggered every 10 minutes. + +The cache will also automatically evict cached data that has gone stale or is about to go stale. For dynamic resources (such as dynamic secret leases), there's a TTL (Time-to-Live) associated with each lease which is used to determine if the lease is stale or about to go stale. +If a stale dynamic secret lease is detected, it will be automatically evicted from the cache and replaced with a new up-to-date lease. + + +### Cache Configuration + +Configuring the cache is done through the agent configuration file. The following fields are available to configure the cache: + + + + + The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. + + + The path to where your persistent cache will be stored. + + + + Persistent caching is only supported within kubernetes environments at the moment. Please refer to the [Infisical Agent Injector](/integrations/platforms/kubernetes-injector) documentation for more information on how to use persistent caching within Kubernetes environments. + + + ```yaml example-agent-config-file.yaml + cache: + persistent: + type: "kubernetes" + path: "/home/infisical/cache" + service-account-token-path: "/var/run/secrets/kubernetes.io/serviceaccount/token" + ``` + + + + +## Retrying mechanism + +The agent will automatically attempt to retry failed API requests such as authentication, secrets retrieval, dynamic secret lease provisioning, etc. +By default, the agent will retry up to 3 times with a base delay of 200ms and a maximum delay of 5s. + +You can configure the retrying mechanism through the agent configuration file. The following fields are available to configure the retrying mechanism: + + + + How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. + + + The maximum delay between retries. Defaults to `5s` (5 seconds). + + + The base delay between retries. Defaults to `200ms` (200 milliseconds). + + +```yaml example-agent-config-file.yaml +infisical: + address: "https://app.infisical.com" + retry-strategy: + max-retries: 3 + max-delay: "5s" + base-delay: "200ms" + +# ... rest of the agent configuration file +``` + + + ## Agent configuration file To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below. While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional. -| Field | Description | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | -| `infisical.exit-after-auth` | Whether to exit the agent after authentication and first secret render. Default: `"false"`. | -| `infisical.revoke-credentials-on-shutdown` | Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. | -| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` | -| `auth.config.identity-id` | The file path where the machine identity id is stored

This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | -| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)

Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | -| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.

Please note that the file should be in JSON format. | -| `auth.config.client-id` | The file path where the universal-auth client id is stored. | -| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | -| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | -| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | -| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | -| `templates[].source-path` | The path to the template file that should be used to render secrets. | -| `templates[].template-content` | The inline secret template to be used for rendering the secrets. | -| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | -| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) | -| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | -| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | + + +| Field | Description | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | +| `infisical.exit-after-auth` | Whether to exit the agent after authentication and first secret render. Default: `"false"`. | +| `infisical.revoke-credentials-on-shutdown` | Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. | +| `infisical.retry-strategy.max-retries` | How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. | +| `infisical.retry-strategy.max-delay` | The maximum delay between retries. Defaults to `5s` (5 seconds). | +| `infisical.retry-strategy.base-delay` | The base delay between retries. Defaults to `200ms` (200 milliseconds). | +| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` | +| `auth.config.identity-id` | The file path where the machine identity id is stored

This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | +| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)

Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | +| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.

Please note that the file should be in JSON format. | +| `auth.config.client-id` | The file path where the universal-auth client id is stored. | +| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | +| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | +| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | +| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | +| `cache.persistent.type` | The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. | +| `cache.persistent.path` | The path to where your persistent cache will be stored. | +| `cache.persistent.service-account-token-path` | The path to the Kubernetes service account token to use for encrypting the persistent cache. Required when using `kubernetes` cache type. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token` | +| `templates[].source-path` | The path to the template file that should be used to render secrets. | +| `templates[].template-content` | The inline secret template to be used for rendering the secrets. | +| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | +| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5m` (5 minutes) (optional) | +| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | +| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | ## Authentication @@ -308,81 +480,4 @@ After defining the agent configuration file, run the command below pointing to t ```bash infisical agent --config example-agent-config-file.yaml -``` - -### Available secret template functions - - - ```bash - listSecrets "" "environment-slug" "" "" - ``` - ```bash example-template-usage-1 - {{- with listSecrets "6553ccb2b7da580d7f6e7260" "dev" "/" `{"recursive": false, "expandSecretReferences": true}` }} - {{- range . }} - {{ .Key }}={{ .Value }} - {{- end }} - {{- end }} - ``` - ```bash example-template-usage-2 -{{- with secret "da8056c8-01e2-4d24-b39f-cb4e004b8d44" "staging" "/" `{"recursive": true, "expandSecretReferences": true}` }} -{{- range . }} -{{- if eq .SecretPath "/"}} -{{ .Key }}={{ .Value }} -{{- else}} -{{ .SecretPath }}/{{ .Key }}={{ .Value }} -{{- end}} -{{- end }} -{{- end }} - ``` - - - -**Function name**: listSecrets - -**Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. - -An optional JSON argument is also available. It includes the properties `recursive`, which defaults to false, and `expandSecretReferences`, which defaults to true and expands the returned secrets. - - -**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, SecretPath, Type, ID, and Comment` - - - - - ```bash - getSecretByName "" "" "" "" - ``` - -```bash example-template-usage -{{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} -{{ if .Value }} -password = "{{ .Value }}" -{{ end }} -{{ end }} -``` - -**Function name**: getSecretByName - -**Description**: This function can be used to render a single secret by it's name. - -**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` - - - - - ```bash - dynamic_secret "" "" "" "" "" - ``` - - ```bash example-redis-dynamic-secret - {{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }} - {{ .DB_USERNAME }}={{ .DB_PASSWORD }} - {{- end }} - - **Function Name**: dynamic_secret - - **Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date. - - **Returns**: An object with keys corresponding to the dynamic secret lease credentials. - ``` - \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/integrations/platforms/kubernetes-injector.mdx b/docs/integrations/platforms/kubernetes-injector.mdx index 9903dcbc3..f51a96ab2 100644 --- a/docs/integrations/platforms/kubernetes-injector.mdx +++ b/docs/integrations/platforms/kubernetes-injector.mdx @@ -120,19 +120,83 @@ You will need to set the `nodeSelector.kubernetes.io/os` label to `windows` and The Infisical Agent Injector supports the following annotations: - - The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create. - - - The inject mode annotation is used to specify the mode to use to inject the secrets into the pod. + + + The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create. + + + The inject mode annotation is used to specify the mode to use to inject the secrets into the pod. - - `init`: The init method will create an init container for the pod that will render the secrets into a shared volume mount within the pod. The agent init container will run before any other containers in the pod runs, including other init containers. - - `sidecar`: The sidecar method will create a sidecar container for the pod that will render the secrets into a shared volume mount within the pod. The agent sidecar container will run alongside the main container in the pod. This means that the secrets rendered will always be in sync with your Infisical secrets. - - `sidecar-init`: The sidecar-init method will create the init container and the sidecar container from the other two methods. The init container will run before any other container and fetch the secrets from the start and the sidecar container will keep the secrets in sync throughout the lifecycle of the deployment. - - - The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod. - + - `init`: The init method will create an init container for the pod that will render the secrets into a shared volume mount within the pod. The agent init container will run before any other containers in the pod runs, including other init containers. + - `sidecar`: The sidecar method will create a sidecar container for the pod that will render the secrets into a shared volume mount within the pod. The agent sidecar container will run alongside the main container in the pod. This means that the secrets rendered will always be in sync with your Infisical secrets. + - `sidecar-init`: The sidecar-init method will create the init container and the sidecar container from the other two methods. The init container will run before any other container and fetch the secrets from the start and the sidecar container will keep the secrets in sync throughout the lifecycle of the deployment. + + + The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod. + + + + Whether to enable client-side caching of dynamic secret leases. Defaults to `false`. If you set this to `true`, the agent will persist any dynamic secret leases across restarts of the agent. This is especially useful when using the `sidecar-init` inject mode, to pass the dynamic secret leases created in the init container to the sidecar container. + This will ensure that no new leases are created except those initially created in the init container. The sidecar container will register the leases created in the init container and start managing them from that point onwards. + + + + Whether to revoke all managed dynamic secret leases and machine identity access tokens on shutdown. Defaults to `false`. + + If you set this to `true`, all managed dynamic secret leases and machine identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_. + + **Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked. + + + + How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + + The maximum delay between retries. Defaults to `5s` (5 seconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + + The base delay between retries. Defaults to `200ms` (200 milliseconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + + The maximum CPU limit for the agent containers. + + Linux Pods: Defaults to `500m` (500 milliCPUs). + Windows Pods: Defaults to `500m` (500 milliCPUs). + + + + The minimum CPU request for the agent containers. + + Linux Pods: Defaults to `100m` (100 milliCPUs). + Windows Pods: Defaults to `100m` (100 milliCPUs). + + + + The maximum memory limit for the agent containers. + + Linux Pods: Defaults to `128Mi` (128 megabytes). + Windows Pods: Defaults to `512Mi` (512 megabytes). + + + + The minimum memory request for the agent containers. + + Linux Pods: Defaults to `64Mi` (64 megabytes). + Windows Pods: Defaults to `256Mi` (256 megabytes). + + + + The maximum ephemeral storage limit for the agent containers. Doesn't have an explicit default value. The default value will conform to the default ephemeral storage limit for the pod. + + + + The minimum ephemeral storage request for the agent containers. Doesn't have an explicit default value. The default value will conform to the default ephemeral storage request for the pod. + + + ## ConfigMap Configuration @@ -141,18 +205,22 @@ The Infisical Agent Injector supports the following annotations: When you are configuring a pod to use the injector, you must create a config map in the same namespace as the pod you want to inject secrets into. The entire config needs to be of string format and needs to be assigned to the `config.yaml` key in the config map. You can find a full example of the config at the end of this section. + The address of your Infisical instance. This field is optional and will default to `https://app.infisical.com` if not provided. - Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. + Whether to revoke all managed dynamic secret leases and machine identity access tokens on shutdown. Default: `"false"`. - If this is set to `true`, all managed dynamic secret leases and identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_. + If this is set to `true`, all managed dynamic secret leases and machine identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_. + **Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked. - Note that this is currently unsupported on Windows-based pods, and will only work when injecting into Linux-based pods. + This is currently unsupported on Windows-based pods, and will only work when injecting into Linux-based pods. + + It's recommended to use the annotation `org.infisical.com/agent-revoke-on-shutdown: "true"` instead of configuring the revoke on shutdown on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the revoke on shutdown through annotations. @@ -162,8 +230,59 @@ The entire config needs to be of string format and needs to be assigned to the ` Please note that the pod's default service account will be used to authenticate with Infisical. + - The ID of the machine identity to use to connect to Infisical. This field is required if the `infisical.auth.type` is set to `kubernetes`. + The ID of the machine identity to use for Kubernetes or LDAP authentication. This field is required if the `infisical.auth.type` is set to `kubernetes`. + + + + The LDAP username to use for LDAP authentication. + This field is required if the `infisical.auth.type` is set to `ldap-auth`. + + + + The LDAP password to use for LDAP authentication. + This field is required if the `infisical.auth.type` is set to `ldap-auth`. + + + + How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + You can also configure the max retries through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the max retries through annotations. + + + + + The maximum delay between retries. Defaults to `5s` (5 seconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + You can also configure the max delay through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the max delay through annotations. + + + + + The base delay between retries. Defaults to `200ms` (200 milliseconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + You can also configure the base delay through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the base delay through annotations. + + + + + The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. + + + It is recommended to use the annotation `org.infisical.com/agent-cache-enabled: "true"` instead of configuring the cache on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the cache through annotations. + + + + + The path to the Kubernetes service account token to use for encrypting the persistent cache. Required when using `kubernetes` cache type. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + It is recommended to use the annotation `org.infisical.com/agent-cache-enabled: "true"` instead of configuring the cache on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the cache through annotations. + @@ -180,6 +299,7 @@ The templates hold an array of templates that will be rendered and injected into This will be rendered as a [Go Template](https://pkg.go.dev/text/template) and will have access to the following variables. It follows the templating format and supports the same functions as the [Infisical Agent](/integrations/platforms/infisical-agent#quick-start-infisical-agent) + ### Authentication @@ -271,7 +391,7 @@ The Infisical Agent Injector supports Machine Identity [Kubernetes Auth](/docume -To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above. +To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above. The config map must be in the same namespace as the pod you're injecting into. ```yaml apiVersion: v1 kind: Pod diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 8e061291d..77ea94f99 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -121,6 +121,9 @@ const Page = () => { const tableRef = useRef(null); const [isVisible, setIsVisible] = useState(false); + const [selectedDynamicSecretId, setSelectedDynamicSecretId] = useState( + routerQueryParams.dynamicSecretId || "" + ); const { isBatchMode, pendingChanges } = useBatchMode(); const { loadPendingChanges, setExistingKeys } = useBatchModeActions(); @@ -165,6 +168,28 @@ const Page = () => { if (isVisible) setIsVisible(false); }, [environment]); + useEffect(() => { + if (routerQueryParams.dynamicSecretId !== null) { + setSelectedDynamicSecretId(routerQueryParams.dynamicSecretId); + + navigate({ + search: (prev) => ({ + ...prev, + dynamicSecretId: undefined + }) + }); + + // if any of the router query params are changed, we have to clear the selected dynamic secret id to avoid re-rendering the lease modal when it suddendly becomes available + } else { + setSelectedDynamicSecretId(null); + } + }, [ + routerQueryParams.filterBy, + routerQueryParams.search, + routerQueryParams.secretPath, + routerQueryParams.tags + ]); + const canReadSecret = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, @@ -1039,6 +1064,7 @@ const Page = () => { )} {canReadDynamicSecret && Boolean(dynamicSecrets?.length) && ( { + if ( + selectedDynamicSecretId && + dynamicSecrets.find((secret) => secret.id === selectedDynamicSecretId) + ) { + handlePopUpOpen("dynamicSecretLeases", selectedDynamicSecretId); + } + }, [selectedDynamicSecretId]); + return ( <> {dynamicSecrets.map((secret) => { @@ -231,7 +244,12 @@ export const DynamicSecretListView = ({ +

Dynamic secret leases

+ {secret.name} + + } subTitle="Revoke or renew your secret leases" className="max-w-3xl" > diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx index f5796fd69..df054b6e2 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx @@ -12,6 +12,7 @@ const SecretDashboardPageQueryParamsSchema = z.object({ search: z.string().catch(""), tags: z.string().catch(""), filterBy: z.string().catch(""), + dynamicSecretId: z.string().catch(""), connectionId: z.string().optional(), connectionName: z.string().optional() }); @@ -26,7 +27,8 @@ export const Route = createFileRoute( secretPath: "/", search: "", tags: "", - filterBy: "" + filterBy: "", + dynamicSecretId: "" }) ] },