Skip to content

Commit 5c0f96c

Browse files
Merge pull request #54 from Infisical/daniel/agent-manage-leases
feat(agent): revoke credentials on shutdown
2 parents 38f0fed + b4c5ec0 commit 5c0f96c

File tree

1 file changed

+211
-20
lines changed

1 file changed

+211
-20
lines changed

packages/cmd/agent.go

Lines changed: 211 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"path"
1717
"runtime"
1818
"slices"
19+
"strings"
1920
"sync"
2021
"syscall"
2122
"text/template"
@@ -44,9 +45,15 @@ type Config struct {
4445
Templates []Template `yaml:"templates"`
4546
}
4647

48+
type TemplateWithID struct {
49+
ID int
50+
Template Template
51+
}
52+
4753
type InfisicalConfig struct {
48-
Address string `yaml:"address"`
49-
ExitAfterAuth bool `yaml:"exit-after-auth"`
54+
Address string `yaml:"address"`
55+
ExitAfterAuth bool `yaml:"exit-after-auth"`
56+
RevokeCredentialsOnShutdown bool `yaml:"revoke-credentials-on-shutdown"`
5057
}
5158

5259
type AuthConfig struct {
@@ -143,7 +150,7 @@ func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) {
143150
defer d.mutex.Unlock()
144151

145152
index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool {
146-
if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug {
153+
if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug && lease.LeaseID == s.LeaseID {
147154
return true
148155
}
149156
return false
@@ -161,7 +168,7 @@ func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, s
161168
defer d.mutex.Unlock()
162169

163170
index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool {
164-
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
171+
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug && slices.Contains(lease.TemplateIDs, templateId) {
165172
return true
166173
}
167174
return false
@@ -172,12 +179,12 @@ func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, s
172179
}
173180
}
174181

175-
func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease {
182+
func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string, templateId int) *DynamicSecretLease {
176183
d.mutex.Lock()
177184
defer d.mutex.Unlock()
178185

179186
for _, lease := range d.leases {
180-
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
187+
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug && slices.Contains(lease.TemplateIDs, templateId) {
181188
return &lease
182189
}
183190
}
@@ -384,7 +391,7 @@ func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *Dyn
384391
if argLength == 5 {
385392
ttl = args[4]
386393
}
387-
dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug)
394+
dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug, templateId)
388395
if dynamicSecretData != nil {
389396
dynamicSecretManager.RegisterTemplate(projectSlug, envSlug, secretPath, slug, templateId)
390397
return dynamicSecretData.Data, nil
@@ -498,16 +505,18 @@ type AgentManager struct {
498505
accessTokenRefreshedTime time.Time
499506
mutex sync.Mutex
500507
filePaths []Sink // Store file paths if needed
501-
templates []Template
508+
templates []TemplateWithID
502509
dynamicSecretLeases *DynamicSecretLeaseManager
503510

504511
authConfigBytes []byte
505512
authStrategy util.AuthStrategyType
506513

507-
newAccessTokenNotificationChan chan bool
508-
removeUniversalAuthClientSecretOnRead bool
509-
cachedUniversalAuthClientSecret string
510-
exitAfterAuth bool
514+
newAccessTokenNotificationChan chan bool
515+
cachedUniversalAuthClientSecret string
516+
exitAfterAuth bool
517+
revokeCredentialsOnShutdown bool
518+
519+
isShuttingDown bool
511520

512521
infisicalClient infisicalSdk.InfisicalClientInterface
513522
}
@@ -521,22 +530,30 @@ type NewAgentMangerOptions struct {
521530

522531
NewAccessTokenNotificationChan chan bool
523532
ExitAfterAuth bool
533+
RevokeCredentialsOnShutdown bool
524534
}
525535

526536
func NewAgentManager(options NewAgentMangerOptions) *AgentManager {
527537
customHeaders, err := util.GetInfisicalCustomHeadersMap()
528538
if err != nil {
529539
util.HandleError(err, "Unable to get custom headers")
530540
}
541+
542+
templates := make([]TemplateWithID, len(options.Templates))
543+
for i, template := range options.Templates {
544+
templates[i] = TemplateWithID{ID: i + 1, Template: template}
545+
}
546+
531547
return &AgentManager{
532548
filePaths: options.FileDeposits,
533-
templates: options.Templates,
549+
templates: templates,
534550

535551
authConfigBytes: options.AuthConfigBytes,
536552
authStrategy: options.AuthStrategy,
537553

538554
newAccessTokenNotificationChan: options.NewAccessTokenNotificationChan,
539555
exitAfterAuth: options.ExitAfterAuth,
556+
revokeCredentialsOnShutdown: options.RevokeCredentialsOnShutdown,
540557

541558
infisicalClient: infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
542559
SiteUrl: config.INFISICAL_URL,
@@ -755,6 +772,145 @@ func (tm *AgentManager) FetchNewAccessToken() error {
755772
return nil
756773
}
757774

775+
func (tm *AgentManager) RevokeCredentials() error {
776+
var token string
777+
778+
log.Info().Msg("revoking credentials...")
779+
780+
token = tm.GetToken()
781+
782+
if token == "" {
783+
return fmt.Errorf("no access token found")
784+
}
785+
// lock the dynamic secret leases to prevent renewals during the revoke process
786+
tm.dynamicSecretLeases.mutex.Lock()
787+
defer tm.dynamicSecretLeases.mutex.Unlock()
788+
789+
dynamicSecretLeases := tm.dynamicSecretLeases.leases
790+
791+
customHeaders, err := util.GetInfisicalCustomHeadersMap()
792+
if err != nil {
793+
return fmt.Errorf("unable to get custom headers: %v", err)
794+
}
795+
796+
for _, lease := range dynamicSecretLeases {
797+
798+
temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
799+
SiteUrl: config.INFISICAL_URL,
800+
UserAgent: api.USER_AGENT,
801+
AutoTokenRefresh: false,
802+
CustomHeaders: customHeaders,
803+
})
804+
805+
temporaryInfisicalClient.Auth().SetAccessToken(token)
806+
807+
_, err = temporaryInfisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
808+
LeaseId: lease.LeaseID,
809+
ProjectSlug: lease.ProjectSlug,
810+
SecretPath: lease.SecretPath,
811+
EnvironmentSlug: lease.Environment,
812+
})
813+
814+
if err != nil {
815+
816+
if strings.Contains(err.Error(), "status-code=404") {
817+
log.Info().Msgf("dynamic secret lease %s not found, skipping", lease.LeaseID)
818+
continue
819+
}
820+
821+
log.Error().Msgf("unable to revoke dynamic secret lease %s: %v", lease.LeaseID, err)
822+
continue
823+
}
824+
825+
// write to the lease file, and make it an empty file
826+
827+
// find the template that this lease is associated with
828+
templateIndex := slices.IndexFunc(tm.templates, func(t TemplateWithID) bool {
829+
for _, templateID := range lease.TemplateIDs {
830+
if t.ID == templateID {
831+
return true
832+
}
833+
}
834+
return false
835+
})
836+
837+
if templateIndex != -1 {
838+
template := tm.templates[templateIndex]
839+
if _, err := os.Stat(template.Template.DestinationPath); !os.IsNotExist(err) {
840+
if err := os.WriteFile(template.Template.DestinationPath, []byte(""), 0644); err != nil {
841+
log.Warn().Msgf("unable to erase lease from file '%s' because %v", template.Template.DestinationPath, err)
842+
}
843+
}
844+
}
845+
846+
log.Info().Msgf("successfully revoked dynamic secret lease [id=%s] [project-slug=%s]", lease.LeaseID, lease.ProjectSlug)
847+
}
848+
849+
var deletedTokens []string
850+
851+
for _, sink := range tm.filePaths {
852+
if sink.Type == "file" {
853+
tokenBytes, err := os.ReadFile(sink.Config.Path)
854+
if err != nil {
855+
log.Error().Msgf("unable to read token from file '%s' because %v", sink.Config.Path, err)
856+
continue
857+
}
858+
859+
token := string(tokenBytes)
860+
if token != "" {
861+
log.Info().Msgf("revoking token from file '%s'", sink.Config.Path)
862+
863+
temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
864+
SiteUrl: config.INFISICAL_URL,
865+
UserAgent: api.USER_AGENT,
866+
AutoTokenRefresh: false,
867+
CustomHeaders: customHeaders,
868+
})
869+
870+
temporaryInfisicalClient.Auth().SetAccessToken(token)
871+
err := temporaryInfisicalClient.Auth().RevokeAccessToken()
872+
if err != nil {
873+
log.Error().Msgf("unable to revoke access token from file '%s' because %v", sink.Config.Path, err)
874+
continue
875+
}
876+
877+
if _, err := os.Stat(sink.Config.Path); !os.IsNotExist(err) {
878+
if err := os.WriteFile(sink.Config.Path, []byte(""), 0644); err != nil {
879+
log.Warn().Msgf("unable to erase access token from file '%s' because %v", sink.Config.Path, err)
880+
continue
881+
}
882+
}
883+
884+
log.Info().Msgf("successfully revoked access token from file '%s'", sink.Config.Path)
885+
886+
deletedTokens = append(deletedTokens, token)
887+
}
888+
}
889+
}
890+
891+
// check to see if the active token was already deleted, if not, delete it
892+
if !slices.Contains(deletedTokens, token) {
893+
temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
894+
SiteUrl: config.INFISICAL_URL,
895+
UserAgent: api.USER_AGENT,
896+
AutoTokenRefresh: false,
897+
CustomHeaders: customHeaders,
898+
})
899+
temporaryInfisicalClient.Auth().SetAccessToken(token)
900+
err := temporaryInfisicalClient.Auth().RevokeAccessToken()
901+
if err != nil {
902+
log.Error().Msgf("unable to revoke token because %v", err)
903+
}
904+
905+
log.Info().Msgf("successfully revoked active access token")
906+
deletedTokens = append(deletedTokens, token)
907+
}
908+
909+
log.Info().Msgf("successfully revoked %d access tokens", len(deletedTokens))
910+
911+
return nil
912+
}
913+
758914
// Refreshes the existing access token
759915
func (tm *AgentManager) RefreshAccessToken(accessToken string) error {
760916
httpClient, err := util.GetRestyClientWithCustomHeaders()
@@ -782,6 +938,11 @@ func (tm *AgentManager) RefreshAccessToken(accessToken string) error {
782938

783939
func (tm *AgentManager) ManageTokenLifecycle() {
784940
for {
941+
942+
if tm.isShuttingDown {
943+
return
944+
}
945+
785946
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
786947
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
787948

@@ -945,6 +1106,10 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId
9451106
return
9461107
default:
9471108
{
1109+
if tm.isShuttingDown {
1110+
return
1111+
}
1112+
9481113
tm.dynamicSecretLeases.Prune()
9491114
token := tm.GetToken()
9501115
if token != "" {
@@ -1068,7 +1233,7 @@ var agentCmd = &cobra.Command{
10681233

10691234
tokenRefreshNotifier := make(chan bool)
10701235
sigChan := make(chan os.Signal, 1)
1071-
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
1236+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
10721237

10731238
filePaths := agentConfig.Sinks
10741239

@@ -1085,25 +1250,51 @@ var agentCmd = &cobra.Command{
10851250
NewAccessTokenNotificationChan: tokenRefreshNotifier,
10861251
ExitAfterAuth: agentConfig.Infisical.ExitAfterAuth,
10871252
AuthStrategy: authStrategy,
1253+
RevokeCredentialsOnShutdown: agentConfig.Infisical.RevokeCredentialsOnShutdown,
10881254
})
10891255

10901256
tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
10911257

10921258
go tm.ManageTokenLifecycle()
10931259

1094-
for i, template := range agentConfig.Templates {
1095-
log.Info().Msgf("template engine started for template %v...", i+1)
1096-
go tm.MonitorSecretChanges(template, i, sigChan)
1260+
for _, template := range tm.templates {
1261+
log.Info().Msgf("template engine started for template %v...", template.ID)
1262+
go tm.MonitorSecretChanges(template.Template, template.ID, sigChan)
10971263
}
10981264

10991265
for {
11001266
select {
11011267
case <-tokenRefreshNotifier:
11021268
go tm.WriteTokenToFiles()
11031269
case <-sigChan:
1104-
log.Info().Msg("agent is gracefully shutting...")
1105-
// TODO: check if we are in the middle of writing files to disk
1106-
os.Exit(1)
1270+
tm.isShuttingDown = true
1271+
log.Info().Msg("agent is gracefully shutting down...")
1272+
1273+
exitCode := 0
1274+
1275+
if !tm.exitAfterAuth && tm.revokeCredentialsOnShutdown {
1276+
1277+
done := make(chan error, 1)
1278+
1279+
go func() {
1280+
done <- tm.RevokeCredentials()
1281+
}()
1282+
1283+
select {
1284+
case err := <-done:
1285+
if err != nil {
1286+
log.Error().Msgf("unable to revoke credentials [err=%v]", err)
1287+
exitCode = 1
1288+
}
1289+
// 5 minute timeout to prevent any hanging edge cases
1290+
case <-time.After(5 * time.Minute):
1291+
log.Warn().Msg("credential revocation timed out after 5 minutes, forcing exit")
1292+
exitCode = 1
1293+
}
1294+
1295+
}
1296+
1297+
os.Exit(exitCode)
11071298
}
11081299
}
11091300

0 commit comments

Comments
 (0)