diff --git a/packages/cmd/agent.go b/packages/cmd/agent.go index f2781057..75a240e7 100644 --- a/packages/cmd/agent.go +++ b/packages/cmd/agent.go @@ -49,7 +49,7 @@ const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com" const CACHE_TYPE_KUBERNETES = "kubernetes" -const DYNAMIC_SECRET_LEASE_TEMPLATE = "dynamic-secret-lease-%s-%s-%s-%s-%s" +const DYNAMIC_SECRET_LEASE_TEMPLATE = "dynamic-secret-lease-%s-%s-%s-%s-%s-%s" // duration to reduce from expiry of dynamic leases so that it gets triggered before expiry const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15 @@ -273,6 +273,7 @@ type DynamicSecretLeaseWithTTL struct { Data map[string]interface{} TemplateIDs []int RequestedLeaseTTL string + Principals string } func (c *CacheManager) WriteToCache(key string, value interface{}, ttl *time.Duration) error { @@ -399,6 +400,7 @@ func (d *DynamicSecretLeaseManager) WriteLeaseToCache(lease *DynamicSecretLeaseW lease.SecretPath, lease.Slug, requestedLeaseTTL, + lease.Principals, ) ttl := time.Until(lease.ExpireAt) @@ -412,13 +414,13 @@ func (d *DynamicSecretLeaseManager) WriteLeaseToCache(lease *DynamicSecretLeaseW } } -func (d *DynamicSecretLeaseManager) ReadLeaseFromCache(projectSlug, environment, secretPath, slug string, requestedLeaseTTL string) *DynamicSecretLeaseWithTTL { +func (d *DynamicSecretLeaseManager) ReadLeaseFromCache(projectSlug, environment, secretPath, slug, requestedLeaseTTL, principals string) *DynamicSecretLeaseWithTTL { if d.cacheManager == nil || !d.cacheManager.IsEnabled { return nil } - cacheKey := fmt.Sprintf(DYNAMIC_SECRET_LEASE_TEMPLATE, projectSlug, environment, secretPath, slug, requestedLeaseTTL) + cacheKey := fmt.Sprintf(DYNAMIC_SECRET_LEASE_TEMPLATE, projectSlug, environment, secretPath, slug, requestedLeaseTTL, principals) var lease *DynamicSecretLeaseWithTTL err := d.cacheManager.ReadFromCache(cacheKey, &lease) if err != nil { @@ -431,12 +433,12 @@ func (d *DynamicSecretLeaseManager) ReadLeaseFromCache(projectSlug, environment, return lease } -func (d *DynamicSecretLeaseManager) DeleteLeaseFromCache(projectSlug, environment, secretPath, slug, requestedLeaseTTL string) error { +func (d *DynamicSecretLeaseManager) DeleteLeaseFromCache(projectSlug, environment, secretPath, slug, requestedLeaseTTL, principals string) error { if d.cacheManager == nil || !d.cacheManager.IsEnabled { return nil } - cacheKey := fmt.Sprintf(DYNAMIC_SECRET_LEASE_TEMPLATE, projectSlug, environment, secretPath, slug, requestedLeaseTTL) + cacheKey := fmt.Sprintf(DYNAMIC_SECRET_LEASE_TEMPLATE, projectSlug, environment, secretPath, slug, requestedLeaseTTL, principals) err := d.cacheManager.DeleteFromCache(cacheKey) if err != nil { return fmt.Errorf("unable to delete lease from cache: %v", err) @@ -490,11 +492,12 @@ func (d *DynamicSecretLeaseManager) DeleteUnusedLeasesFromCache() error { // now we need to check if any of the cached leases are not in the d.leases list. If they are not, we need to delete them from the cache. for _, cachedLease := range cachedLeases { log.Debug().Msgf( - "[cache]: checking cached lease: [project=%s], [env=%s], [path=%s], [slug=%s]", + "[cache]: checking cached lease: [project=%s], [env=%s], [path=%s], [slug=%s], [principals=%s]", cachedLease.ProjectSlug, cachedLease.Environment, cachedLease.SecretPath, cachedLease.Slug, + cachedLease.Principals, ) // check if a lease with the same configuration exists (not comparing LeaseID since that changes on refresh) @@ -503,14 +506,16 @@ func (d *DynamicSecretLeaseManager) DeleteUnusedLeasesFromCache() error { s.Environment == cachedLease.Environment && s.SecretPath == cachedLease.SecretPath && s.Slug == cachedLease.Slug && - s.RequestedLeaseTTL == cachedLease.RequestedLeaseTTL + s.RequestedLeaseTTL == cachedLease.RequestedLeaseTTL && + s.Principals == cachedLease.Principals if match { - log.Debug().Msgf("[cache]: found matching active lease: [project=%s], [env=%s], [path=%s], [slug=%s]", + log.Debug().Msgf("[cache]: found matching active lease: [project=%s], [env=%s], [path=%s], [slug=%s], [principals=%s]", s.ProjectSlug, s.Environment, s.SecretPath, s.Slug, + s.Principals, ) } return match @@ -518,12 +523,13 @@ func (d *DynamicSecretLeaseManager) DeleteUnusedLeasesFromCache() error { if !found { log.Info().Msgf( - "[cache]: no matching active lease found, deleting cached lease: [lease-id=%s], [project=%s], [env=%s], [path=%s], [slug=%s]", + "[cache]: no matching active lease found, deleting cached lease: [lease-id=%s], [project=%s], [env=%s], [path=%s], [slug=%s], [principals=%s]", cachedLease.LeaseID, cachedLease.ProjectSlug, cachedLease.Environment, cachedLease.SecretPath, cachedLease.Slug, + cachedLease.Principals, ) if err := d.DeleteLeaseFromCache( @@ -532,6 +538,7 @@ func (d *DynamicSecretLeaseManager) DeleteUnusedLeasesFromCache() error { cachedLease.SecretPath, cachedLease.Slug, cachedLease.RequestedLeaseTTL, + cachedLease.Principals, ); err != nil { log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err) } @@ -551,7 +558,7 @@ func (d *DynamicSecretLeaseManager) Prune() { shouldDelete := time.Now().After(s.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)) if shouldDelete { - if err := d.DeleteLeaseFromCache(s.ProjectSlug, s.Environment, s.SecretPath, s.Slug, s.RequestedLeaseTTL); err != nil { + if err := d.DeleteLeaseFromCache(s.ProjectSlug, s.Environment, s.SecretPath, s.Slug, s.RequestedLeaseTTL, s.Principals); err != nil { log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err) } } @@ -563,9 +570,9 @@ func (d *DynamicSecretLeaseManager) Prune() { func (d *DynamicSecretLeaseManager) AppendUnsafe(lease DynamicSecretLeaseWithTTL) { index := slices.IndexFunc(d.leases, func(s DynamicSecretLeaseWithTTL) bool { - // match by configuration (project, env, path, slug, TTL) and same lease ID + // match by configuration (project, env, path, slug, TTL, principals) and same lease ID // this allows merging template IDs when the same lease is added multiple times - if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug && lease.LeaseID == s.LeaseID && lease.RequestedLeaseTTL == s.RequestedLeaseTTL { + if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug && lease.LeaseID == s.LeaseID && lease.RequestedLeaseTTL == s.RequestedLeaseTTL && lease.Principals == s.Principals { return true } return false @@ -588,12 +595,12 @@ func (d *DynamicSecretLeaseManager) AppendUnsafe(lease DynamicSecretLeaseWithTTL } // Expects a lock to be held before invocation -func (d *DynamicSecretLeaseManager) RegisterTemplateUnsafe(projectSlug, environment, secretPath, slug string, templateId int, requestedLeaseTTL string) { +func (d *DynamicSecretLeaseManager) RegisterTemplateUnsafe(projectSlug, environment, secretPath, slug string, templateId int, requestedLeaseTTL, principals string) { index := slices.IndexFunc(d.leases, func(lease DynamicSecretLeaseWithTTL) bool { // find lease by configuration, not by template ID // this allows us to register new template IDs to existing leases - return lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug && lease.RequestedLeaseTTL == requestedLeaseTTL + return lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug && lease.RequestedLeaseTTL == requestedLeaseTTL && lease.Principals == principals }) log.Debug().Msgf("\n[cache]: registering template [template-id=%d] for lease [project=%s], [env=%s], [path=%s], [slug=%s]\nIndex: %d", templateId, projectSlug, environment, secretPath, slug, index) @@ -616,21 +623,21 @@ func (d *DynamicSecretLeaseManager) RegisterTemplateUnsafe(projectSlug, environm } // Expects a lock to be held before invocation -func (d *DynamicSecretLeaseManager) GetLeaseUnsafe(accessToken, projectSlug, environment, secretPath, slug string, templateId int, requestedLeaseTTL string) *DynamicSecretLeaseWithTTL { +func (d *DynamicSecretLeaseManager) GetLeaseUnsafe(accessToken, projectSlug, environment, secretPath, slug string, templateId int, requestedLeaseTTL, principals string) *DynamicSecretLeaseWithTTL { // first try to get from in-memory storage - // find lease by configuration (project, env, path, slug, TTL) regardless of template IDs + // find lease by configuration (project, env, path, slug, TTL, principals) regardless of template IDs // this allows multiple templates to share the same lease for i := range d.leases { lease := &d.leases[i] - if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug && lease.RequestedLeaseTTL == requestedLeaseTTL { + if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug && lease.RequestedLeaseTTL == requestedLeaseTTL && lease.Principals == principals { log.Debug().Msgf("[cache]: lease found in in-memory storage: [project=%s], [env=%s], [path=%s], [slug=%s]", projectSlug, environment, secretPath, slug) return lease } } // if no lease is found in in-memory storage, try to get from cache - leaseFromCache := d.ReadLeaseFromCache(projectSlug, environment, secretPath, slug, requestedLeaseTTL) + leaseFromCache := d.ReadLeaseFromCache(projectSlug, environment, secretPath, slug, requestedLeaseTTL, principals) if leaseFromCache == nil { log.Info().Msgf("[cache]: cache miss, no lease found [template-id=%d]", templateId) @@ -651,7 +658,7 @@ func (d *DynamicSecretLeaseManager) GetLeaseUnsafe(accessToken, projectSlug, env // lease not found in API, delete it from cache and return nil if errors.Is(err, api.ErrNotFound) { log.Warn().Msgf("dynamic secret lease does not exist, deleting from cache: [lease-id=%s]", leaseFromCache.LeaseID) - if err := d.DeleteLeaseFromCache(leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.Slug, leaseFromCache.RequestedLeaseTTL); err != nil { + if err := d.DeleteLeaseFromCache(leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.Slug, leaseFromCache.RequestedLeaseTTL, leaseFromCache.Principals); err != nil { log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err) } @@ -661,7 +668,7 @@ func (d *DynamicSecretLeaseManager) GetLeaseUnsafe(accessToken, projectSlug, env // lease is found in cache but not in the the API, and the API returned a non 404-error. We should attempt to revoke it // at this point we know that we should be able to reach the API because we've done authentication successfully log.Warn().Msgf("unable to get dynamic secret lease from API. Revoking lease from cache: [lease-id=%s]", leaseFromCache.LeaseID) - if err := d.DeleteLeaseFromCache(leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.Slug, leaseFromCache.RequestedLeaseTTL); err != nil { + if err := d.DeleteLeaseFromCache(leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.Slug, leaseFromCache.RequestedLeaseTTL, leaseFromCache.Principals); err != nil { log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err) } @@ -676,7 +683,7 @@ func (d *DynamicSecretLeaseManager) GetLeaseUnsafe(accessToken, projectSlug, env // lease is expired or about to expire, delete from cache and attempt to revoke it if dynamicSecretLease.Lease.ExpireAt.Before(time.Now().Add(CACHE_LEASE_EXPIRE_BUFFER)) { log.Warn().Msgf("dynamic secret lease is expired or about to expire, deleting from cache: [lease-id=%s]", leaseFromCache.LeaseID) - if err := d.DeleteLeaseFromCache(leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.Slug, leaseFromCache.RequestedLeaseTTL); err != nil { + if err := d.DeleteLeaseFromCache(leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.Slug, leaseFromCache.RequestedLeaseTTL, leaseFromCache.Principals); err != nil { log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err) } @@ -942,22 +949,25 @@ func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *Dyn defer dynamicSecretManager.mutex.Unlock() argLength := len(args) - if argLength != 4 && argLength != 5 { + if argLength < 4 || argLength > 6 { return nil, fmt.Errorf("invalid arguments found for dynamic-secret function. Check template %d", templateId) } - projectSlug, envSlug, secretPath, slug, ttl := args[0], args[1], args[2], args[3], "" - if argLength == 5 { + projectSlug, envSlug, secretPath, slug, ttl, principals := args[0], args[1], args[2], args[3], "", "" + if argLength >= 5 { ttl = args[4] } + if argLength == 6 { + principals = args[5] + } - dynamicSecretData := dynamicSecretManager.GetLeaseUnsafe(accessToken, projectSlug, envSlug, secretPath, slug, templateId, ttl) + dynamicSecretData := dynamicSecretManager.GetLeaseUnsafe(accessToken, projectSlug, envSlug, secretPath, slug, templateId, ttl, principals) // if a lease is found (either in memory or in cache), we register the template and return the data if dynamicSecretData != nil { - dynamicSecretManager.RegisterTemplateUnsafe(projectSlug, envSlug, secretPath, slug, templateId, ttl) + dynamicSecretManager.RegisterTemplateUnsafe(projectSlug, envSlug, secretPath, slug, templateId, ttl, principals) - etagData := fmt.Sprintf("%s-%s-%s-%s-%s", projectSlug, envSlug, secretPath, slug, ttl) + etagData := fmt.Sprintf("%s-%s-%s-%s-%s-%s", projectSlug, envSlug, secretPath, slug, ttl, principals) dynamicSecretDataBytes, err := json.Marshal(dynamicSecretData.Data) if err != nil { return nil, err @@ -980,19 +990,28 @@ func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *Dyn // if there's no lease (either in memory or in cache), we create a new lease + leaseConfig := map[string]any{} + if principals != "" { + parsedPrincipals := util.ParsePrincipals(principals) + if len(parsedPrincipals) > 0 { + leaseConfig["principals"] = parsedPrincipals + } + } + leaseData, _, res, err := temporaryInfisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{ DynamicSecretName: slug, ProjectSlug: projectSlug, EnvironmentSlug: envSlug, SecretPath: secretPath, TTL: ttl, + Config: leaseConfig, }) if err != nil { return nil, err } - dynamicSecretManager.AppendUnsafe(DynamicSecretLeaseWithTTL{LeaseID: res.Id, ExpireAt: res.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: leaseData, TemplateIDs: []int{templateId}, RequestedLeaseTTL: ttl}) + dynamicSecretManager.AppendUnsafe(DynamicSecretLeaseWithTTL{LeaseID: res.Id, ExpireAt: res.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: leaseData, TemplateIDs: []int{templateId}, RequestedLeaseTTL: ttl, Principals: principals}) return leaseData, nil } diff --git a/packages/cmd/dynamic_secrets.go b/packages/cmd/dynamic_secrets.go index 542f98f3..bc5c8b19 100644 --- a/packages/cmd/dynamic_secrets.go +++ b/packages/cmd/dynamic_secrets.go @@ -271,10 +271,21 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } + principalsStr, err := cmd.Flags().GetString("principals") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + config := map[string]any{} if kubernetesNamespace != "" { config["namespace"] = kubernetesNamespace } + if principalsStr != "" { + principals := util.ParsePrincipals(principalsStr) + if len(principals) > 0 { + config["principals"] = principals + } + } leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{ DynamicSecretName: dynamicSecretRootCredential.Name, @@ -716,6 +727,9 @@ func init() { // Kubernetes specific flags dynamicSecretLeaseCreateCmd.Flags().String("kubernetes-namespace", "", "The namespace to create the lease in. Only used for Kubernetes dynamic secrets.") + // SSH specific flags + dynamicSecretLeaseCreateCmd.Flags().String("principals", "", "Comma-separated list of principals for SSH dynamic secret leases") + dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd) dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") diff --git a/packages/util/helper.go b/packages/util/helper.go index cdaef77c..ced0dbcc 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -679,3 +679,14 @@ func ParseTimeDurationString(pollingInterval string, allowLessThanOneSecond bool return 0, fmt.Errorf("invalid time unit") } } + +func ParsePrincipals(s string) []string { + var principals []string + for _, p := range strings.Split(s, ",") { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + principals = append(principals, trimmed) + } + } + return principals +}