diff --git a/backend/src/server/routes/v1/dynamic-secret-lease-router.ts b/backend/src/server/routes/v1/dynamic-secret-lease-router.ts index bd8decab76..3c59742dc4 100644 --- a/backend/src/server/routes/v1/dynamic-secret-lease-router.ts +++ b/backend/src/server/routes/v1/dynamic-secret-lease-router.ts @@ -34,20 +34,21 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide response: { 200: z.object({ lease: DynamicSecretLeasesSchema, + dynamicSecret: SanitizedDynamicSecretSchema, data: z.unknown() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { data, lease } = await server.services.dynamicSecretLease.create({ + const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); - return { lease, data }; + return { lease, data, dynamicSecret }; } }); diff --git a/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 69c6686fd3..e1e9b7514c 100644 --- a/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -13,7 +13,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { const doc = await (tx || db)(TableName.DynamicSecretLease) - .where({ id }) + .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .first() .join( TableName.DynamicSecret, diff --git a/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index fe5d75a83a..baabce7ecc 100644 --- a/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -31,6 +31,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ { dynamicSecretCfgId }, { jobId: dynamicSecretCfgId, + backoff: { + type: "exponential", + delay: 3000 + }, removeOnFail: { count: 3 }, @@ -46,6 +50,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ { leaseId }, { jobId: leaseId, + backoff: { + type: "exponential", + delay: 3000 + }, delay: expiry, removeOnFail: { count: 3 @@ -64,12 +72,11 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if (job.name === QueueJobs.DynamicSecretRevocation) { const { leaseId } = job.data as { leaseId: string }; logger.info("Dynamic secret lease revocation started: ", leaseId, job.id); + const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); - const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretLease.dynamicSecretId); - if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" }); - + const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( infisicalSymmetricDecrypt({ diff --git a/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 5e1ccd60b6..6c214b9fd3 100644 --- a/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -101,7 +101,7 @@ export const dynamicSecretLeaseServiceFactory = ({ externalEntityId: entityId }); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date())); - return { lease: dynamicSecretLease, data }; + return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; }; const renewLease = async ({ diff --git a/backend/src/services/dynamic-secret/providers/sql-database.ts b/backend/src/services/dynamic-secret/providers/sql-database.ts index 7576e4f56e..4cf84e77a9 100644 --- a/backend/src/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/services/dynamic-secret/providers/sql-database.ts @@ -14,7 +14,7 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; const generatePassword = (size?: number) => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*'$#"; - return customAlphabet(charset, 20)(size); + return customAlphabet(charset, 32)(size); }; export const SqlDatabaseProvider = (): TDynamicProviderFns => { @@ -50,7 +50,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); const db = await getClient(providerInputs); - const username = alphaNumericNanoId(16); + const username = alphaNumericNanoId(21); const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); diff --git a/cli/.gitignore b/cli/.gitignore index dcc148f21e..5fa3e39c55 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -1,2 +1,3 @@ .infisical.json dist/ +agent-config.test.yaml diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index dfb0cf7bcc..00462f2ff1 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -535,3 +535,23 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques return getRawSecretsV3Response, nil } + +func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDynamicSecretLeaseV1Request) (CreateDynamicSecretLeaseV1Response, error) { + var createDynamicSecretLeaseResponse CreateDynamicSecretLeaseV1Response + response, err := httpClient. + R(). + SetResult(&createDynamicSecretLeaseResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/dynamic-secrets/leases", config.INFISICAL_URL)) + + if err != nil { + return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return createDynamicSecretLeaseResponse, nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index f80c17a92b..9d450d38ef 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -501,6 +501,28 @@ type UniversalAuthRefreshResponse struct { AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` } +type CreateDynamicSecretLeaseV1Request struct { + Environment string `json:"environment"` + ProjectSlug string `json:"projectSlug"` + SecretPath string `json:"secretPath,omitempty"` + Slug string `json:"slug"` + TTL string `json:"ttl,omitempty"` +} + +type CreateDynamicSecretLeaseV1Response struct { + Lease struct { + Id string `json:"id"` + ExpireAt string `json:"expireAt"` + } `json:"lease"` + DynamicSecret struct { + Id string `json:"id"` + DefaultTTL string `json:"defaultTTL"` + MaxTTL string `json:"maxTTL"` + Type string `json:"type"` + } `json:"dynamicSecret"` + Data map[string]interface{} `json:"data"` +} + type GetRawSecretsV3Request struct { Environment string `json:"environment"` WorkspaceId string `json:"workspaceId"` diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index db7c812254..37de930acb 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -84,6 +84,30 @@ type Template struct { } `yaml:"config"` } +type DynamicSecretLease struct { + LeaseID string + ExpireAt time.Time + Environment string + SecretPath string + Slug string + ProjectSlug string + Data map[string]interface{} +} + +type DynamicSecretLeaseManger []DynamicSecretLease + +func (d DynamicSecretLeaseManger) FindLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease { + for _, lease := range d { + // presentTime := time.Now() + // isExpired := presentTime.Before(lease.ExpireAt.Add(-15 * time.Second)) + if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug { + return &lease + } + } + return nil +} + + func ReadFile(filePath string) ([]byte, error) { return ioutil.ReadFile(filePath) } @@ -234,11 +258,24 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag } } +func dynamicSecretTemplateFunction(accessToken string) func(string, string, string, string) (map[string]interface{}, error) { + return func(projectSlug, envSlug, secretPath, slug string) (map[string]interface{}, error) { + res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug) + if err != nil { + return nil, err + } + + return res.Data, nil + } +} + func ProcessTemplate(templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { // custom template function to fetch secrets from Infisical secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) + dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken) funcs := template.FuncMap{ - "secret": secretFunction, + "secret": secretFunction, + "dynamic_secret": dynamicSecretFunction, } templateName := path.Base(templatePath) @@ -266,8 +303,10 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken templateString := string(decoded) secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this + dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken) funcs := template.FuncMap{ - "secret": secretFunction, + "secret": secretFunction, + "dynamic_secret": dynamicSecretFunction, } templateName := "base64Template" @@ -285,7 +324,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken return &buf, nil } -type TokenManager struct { +type AgentManager struct { accessToken string accessTokenTTL time.Duration accessTokenMaxTTL time.Duration @@ -294,6 +333,7 @@ type TokenManager struct { mutex sync.Mutex filePaths []Sink // Store file paths if needed templates []Template + dynamicSecretLeases []DynamicSecretLeaseManger clientIdPath string clientSecretPath string newAccessTokenNotificationChan chan bool @@ -302,8 +342,8 @@ type TokenManager struct { exitAfterAuth bool } -func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *TokenManager { - return &TokenManager{ +func NewAgentManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *AgentManager { + return &AgentManager{ filePaths: fileDeposits, templates: templates, clientIdPath: clientIdPath, @@ -315,7 +355,7 @@ func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath str } -func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { +func (tm *AgentManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { tm.mutex.Lock() defer tm.mutex.Unlock() @@ -326,7 +366,7 @@ func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, acc tm.newAccessTokenNotificationChan <- true } -func (tm *TokenManager) GetToken() string { +func (tm *AgentManager) GetToken() string { tm.mutex.Lock() defer tm.mutex.Unlock() @@ -334,7 +374,7 @@ func (tm *TokenManager) GetToken() string { } // Fetches a new access token using client credentials -func (tm *TokenManager) FetchNewAccessToken() error { +func (tm *AgentManager) FetchNewAccessToken() error { clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID") if clientID == "" { clientIDAsByte, err := ReadFile(tm.clientIdPath) @@ -384,7 +424,7 @@ func (tm *TokenManager) FetchNewAccessToken() error { } // Refreshes the existing access token -func (tm *TokenManager) RefreshAccessToken() error { +func (tm *AgentManager) RefreshAccessToken() error { httpClient := resty.New() httpClient.SetRetryCount(10000). SetRetryMaxWaitTime(20 * time.Second). @@ -405,7 +445,7 @@ func (tm *TokenManager) RefreshAccessToken() error { return nil } -func (tm *TokenManager) ManageTokenLifecycle() { +func (tm *AgentManager) ManageTokenLifecycle() { for { accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second)) accessTokenRefreshedTime := tm.accessTokenRefreshedTime @@ -473,7 +513,7 @@ func (tm *TokenManager) ManageTokenLifecycle() { } } -func (tm *TokenManager) WriteTokenToFiles() { +func (tm *AgentManager) WriteTokenToFiles() { token := tm.GetToken() for _, sinkFile := range tm.filePaths { if sinkFile.Type == "file" { @@ -490,7 +530,7 @@ func (tm *TokenManager) WriteTokenToFiles() { } } -func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) { +func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) { if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil { log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err) return @@ -498,7 +538,7 @@ func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Templ log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath) } -func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan chan os.Signal) { +func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, sigChan chan os.Signal) { pollingInterval := time.Duration(5 * time.Minute) @@ -645,7 +685,7 @@ var agentCmd = &cobra.Command{ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) filePaths := agentConfig.Sinks - tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth) + tm := NewAgentManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth) go tm.ManageTokenLifecycle() diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 576e749092..4242c832b4 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -40,6 +40,23 @@ type PlaintextSecretResult struct { Etag string } +type DynamicSecret struct { + Id string `json:"id"` + DefaultTTL string `json:"defaultTTL"` + MaxTTL string `json:"maxTTL"` + Type string `json:"type"` +} + +type DynamicSecretLease struct { + Lease struct { + Id string `json:"id"` + ExpireAt string `json:"expireAt"` + } `json:"lease"` + DynamicSecret DynamicSecret `json:"dynamicSecret"` + // this is a varying dict based on provider + Data map[string]interface{} `json:"data"` +} + type SingleFolder struct { ID string `json:"_id"` Name string `json:"name"` diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 8c142a4ecd..d6c8d9f4d1 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -195,6 +195,29 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin }, nil } +func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string) (models.DynamicSecretLease, error) { + httpClient := resty.New() + httpClient.SetAuthToken(accessToken). + SetHeader("Accept", "application/json") + + dynamicSecretRequest := api.CreateDynamicSecretLeaseV1Request{ + ProjectSlug: projectSlug, + Environment: environmentName, + Slug: slug, + } + + dynamicSecret, err := api.CallCreateDynamicSecretLeaseV1(httpClient, dynamicSecretRequest) + if err != nil { + return models.DynamicSecretLease{}, err + } + + return models.DynamicSecretLease{ + Lease: dynamicSecret.Lease, + Data: dynamicSecret.Data, + DynamicSecret: dynamicSecret.DynamicSecret, + }, nil +} + func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) { if importedSecrets == nil { return secrets, nil