-
Notifications
You must be signed in to change notification settings - Fork 66
Expand file tree
/
Copy pathagent.go
More file actions
3476 lines (2829 loc) · 114 KB
/
Copy pathagent.go
File metadata and controls
3476 lines (2829 loc) · 114 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"os/signal"
"path"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"text/template"
"time"
"github.com/awnumar/memguard"
"github.com/dgraph-io/badger/v3"
"github.com/go-resty/resty/v2"
infisicalSdk "github.com/infisical/go-sdk"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/templates"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/util/cache"
"github.com/spf13/cobra"
)
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"
// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry
const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15
// duration remove leases from the cache before they expire when the agent is first started with existing leases in the cache.
// if a lease is expired, or expires in 30 seconds or less, it will be deleted from the cache and a new lease will be created.
var CACHE_LEASE_EXPIRE_BUFFER = 30 * time.Second
const EXTERNAL_CA_INITIAL_POLLING_INTERVAL = 10 * time.Second
const EXTERNAL_CA_MAX_POLLING_INTERVAL = 1 * time.Hour
const DEFAULT_MONITORING_INTERVAL = 10 * time.Second
type PersistentCacheConfig struct {
Type string `yaml:"type"` // file or kubernetes
ServiceAccountTokenPath string `yaml:"service-account-token-path"` // relevant if type is kubernetes
Path string `yaml:"path"` // where to store the cache
}
type CacheConfig struct {
Persistent *PersistentCacheConfig `yaml:"persistent,omitempty"`
}
type DecryptedCache struct {
Type string `json:"type"` // currently only "access_token" is supported
AccessToken string `json:"access_token"`
}
type CacheManager struct {
cacheConfig *CacheConfig
cacheStorage *cache.EncryptedStorage
IsEnabled bool
DecryptedCache DecryptedCache
}
type RetryConfig struct {
MaxRetries int `yaml:"max-retries"`
BaseDelay string `yaml:"base-delay"`
MaxDelay string `yaml:"max-delay"`
}
type Config struct {
Version string `yaml:"version,omitempty"`
Infisical InfisicalConfig `yaml:"infisical"`
Auth AuthConfig `yaml:"auth"`
Sinks []Sink `yaml:"sinks"`
Cache CacheConfig `yaml:"cache,omitempty"`
Templates []Template `yaml:"templates"`
Certificates []AgentCertificateConfig `yaml:"certificates,omitempty"`
}
type TemplateWithID struct {
ID int
Template Template
}
type CertificateWithID struct {
ID int
Certificate AgentCertificateConfig
}
type CertificateState struct {
CertificateID string `json:"certificate_id"`
CertificateRequestID string `json:"certificate_request_id,omitempty"`
SerialNumber string `json:"serial_number"`
CommonName string `json:"common_name"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
NextRenewalCheck time.Time `json:"next_renewal_check"`
Status string `json:"status"`
LastError string `json:"last_error,omitempty"`
RetryCount int `json:"retry_count"`
LastRetry time.Time `json:"last_retry,omitempty"`
}
type InfisicalConfig struct {
Address string `yaml:"address"`
ExitAfterAuth bool `yaml:"exit-after-auth"`
RevokeCredentialsOnShutdown bool `yaml:"revoke-credentials-on-shutdown"`
RetryConfig *RetryConfig `yaml:"retry-strategy,omitempty"`
}
type AuthConfig struct {
Type string `yaml:"type"`
Config interface{} `yaml:"config"`
}
type UniversalAuth struct {
ClientIDPath string `yaml:"client-id"`
ClientSecretPath string `yaml:"client-secret"`
RemoveClientSecretOnRead bool `yaml:"remove_client_secret_on_read"`
}
type KubernetesAuth struct {
IdentityID string `yaml:"identity-id"`
ServiceAccountToken string `yaml:"service-account-token"`
}
type AzureAuth struct {
IdentityID string `yaml:"identity-id"`
}
type GcpIdTokenAuth struct {
IdentityID string `yaml:"identity-id"`
}
type GcpIamAuth struct {
IdentityID string `yaml:"identity-id"`
ServiceAccountKey string `yaml:"service-account-key"`
}
type AwsIamAuth struct {
IdentityID string `yaml:"identity-id"`
}
type LdapAuth struct {
IdentityID string `yaml:"identity-id"`
LdapUsername string `yaml:"username"`
LdapPassword string `yaml:"password"`
RemovePasswordOnRead bool `yaml:"remove-password-on-read"`
}
type Sink struct {
Type string `yaml:"type"`
Config SinkDetails `yaml:"config"`
}
type SinkDetails struct {
Path string `yaml:"path"`
}
type Template struct {
SourcePath string `yaml:"source-path"`
Base64TemplateContent string `yaml:"base64-template-content"`
DestinationPath string `yaml:"destination-path"`
TemplateContent string `yaml:"template-content"`
Config struct { // Configurations for the template
PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret
Execute struct {
Command string `yaml:"command"` // Command to execute once the template has been rendered
Timeout int64 `yaml:"timeout"` // Timeout for the command
} `yaml:"execute"` // Command to execute once the template has been rendered
} `yaml:"config"`
}
type CertificateLifecycleConfig struct {
RenewBeforeExpiry string `yaml:"renew-before-expiry"`
StatusCheckInterval string `yaml:"status-check-interval"`
FailureRetryInterval string `yaml:"failure-retry-interval,omitempty"`
MaxFailureRetries int `yaml:"max-failure-retries,omitempty"`
}
type CertificateAttributes struct {
CommonName string `yaml:"common-name,omitempty"`
AltNames []string `yaml:"alt-names,omitempty"`
KeyAlgorithm string `yaml:"key-algorithm,omitempty"`
SignatureAlgorithm string `yaml:"signature-algorithm,omitempty"`
KeyUsages []string `yaml:"key-usages,omitempty"`
ExtendedKeyUsages []string `yaml:"extended-key-usages,omitempty"`
NotBefore string `yaml:"not-before,omitempty"`
NotAfter string `yaml:"not-after,omitempty"`
RemoveRootsFromChain bool `yaml:"remove-roots-from-chain"`
TTL string `yaml:"ttl"`
}
type AgentCertificateConfig struct {
ProjectName string `yaml:"project-slug"`
ProfileName string `yaml:"profile-name"`
ProfileID string `yaml:"-"`
DestinationPath string `yaml:"destination-path"`
CSR string `yaml:"csr,omitempty"`
CSRPath string `yaml:"csr-path,omitempty"`
Attributes *CertificateAttributes `yaml:"attributes,omitempty"`
// Certificate lifecycle and monitoring configuration
Lifecycle CertificateLifecycleConfig `yaml:"lifecycle"`
PostHooks struct {
OnIssuance struct {
Command string `yaml:"command,omitempty"`
Timeout int64 `yaml:"timeout,omitempty"`
} `yaml:"on-issuance,omitempty"`
OnRenewal struct {
Command string `yaml:"command,omitempty"`
Timeout int64 `yaml:"timeout,omitempty"`
} `yaml:"on-renewal,omitempty"`
OnFailure struct {
Command string `yaml:"command,omitempty"`
Timeout int64 `yaml:"timeout,omitempty"`
} `yaml:"on-failure,omitempty"`
} `yaml:"post-hooks,omitempty"`
FileConfig struct {
PrivateKey struct {
Path string `yaml:"path,omitempty"`
Permission string `yaml:"permission,omitempty"`
} `yaml:"private-key,omitempty"`
Certificate struct {
Path string `yaml:"path,omitempty"`
Permission string `yaml:"permission,omitempty"`
} `yaml:"certificate,omitempty"`
Chain struct {
Path string `yaml:"path,omitempty"`
Permission string `yaml:"permission,omitempty"`
OmitRoot *bool `yaml:"omit-root,omitempty"`
} `yaml:"chain,omitempty"`
} `yaml:"file-output,omitempty"`
}
type DynamicSecretLeaseWithTTL struct {
LeaseID string
ExpireAt time.Time
Environment string
SecretPath string
Slug string
ProjectSlug string
Data map[string]interface{}
TemplateIDs []int
RequestedLeaseTTL string
}
func (c *CacheManager) WriteToCache(key string, value interface{}, ttl *time.Duration) error {
if !c.IsEnabled {
return nil
}
var err error
if ttl != nil {
if *ttl <= 0 {
return fmt.Errorf("ttl must be greater than 0")
}
err = c.cacheStorage.SetWithTTL(key, value, *ttl)
} else {
err = c.cacheStorage.Set(key, value)
}
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
return fmt.Errorf("unable to write to cache: %v", err)
}
return nil
}
func (c *CacheManager) GetAllCacheEntries() (map[string]interface{}, error) {
if c.cacheStorage == nil || !c.IsEnabled {
return nil, nil
}
response, err := c.cacheStorage.GetAll()
if err != nil {
return nil, fmt.Errorf("unable to get all cache keys: %v", err)
}
return response, nil
}
func (c *CacheManager) ReadFromCache(key string, destination interface{}) error {
err := c.cacheStorage.Get(key, destination)
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
return fmt.Errorf("unable to read from cache: %v", err)
}
return nil
}
func (c *CacheManager) DeleteFromCache(key string) error {
if !c.IsEnabled {
return nil
}
err := c.cacheStorage.Delete(key)
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
return fmt.Errorf("unable to delete from cache: %v", err)
}
return nil
}
func NewCacheManager(ctx context.Context, cacheConfig *CacheConfig) (*CacheManager, error) {
if cacheConfig == nil || cacheConfig.Persistent == nil {
log.Info().Msg("caching is disabled, continuing without caching.")
return &CacheManager{
IsEnabled: false,
DecryptedCache: DecryptedCache{},
cacheConfig: cacheConfig,
}, nil
}
if cacheConfig.Persistent.Type != CACHE_TYPE_KUBERNETES {
return &CacheManager{}, fmt.Errorf("unsupported cache type: %s", cacheConfig.Persistent.Type)
}
// try to read the service account token file
serviceAccountToken, err := ReadFile(cacheConfig.Persistent.ServiceAccountTokenPath)
if err != nil || len(serviceAccountToken) == 0 {
return &CacheManager{}, fmt.Errorf("unable to read service account token: %v. Please ensure the file exists and is not empty", err)
}
hash := sha256.Sum256(serviceAccountToken)
encryptionKey := memguard.NewBufferFromBytes(hash[:]) // the hash (source) is wiped after copied to the secure buffer
defer encryptionKey.Destroy()
cacheStorage, err := cache.NewEncryptedStorage(cache.EncryptedStorageOptions{
DBPath: cacheConfig.Persistent.Path,
EncryptionKey: encryptionKey,
InMemory: false,
})
go cacheStorage.StartPeriodicGarbageCollection(ctx)
if err != nil {
return nil, fmt.Errorf("unable to create cache storage: %v", err)
}
return &CacheManager{
IsEnabled: true,
cacheConfig: cacheConfig,
cacheStorage: cacheStorage,
}, nil
}
type DynamicSecretLeaseManager struct {
leases []DynamicSecretLeaseWithTTL
mutex sync.Mutex
cacheManager *CacheManager
retryConfig *infisicalSdk.RetryRequestsConfig
}
func (d *DynamicSecretLeaseManager) WriteLeaseToCache(lease *DynamicSecretLeaseWithTTL, requestedLeaseTTL string) {
if d.cacheManager == nil || !d.cacheManager.IsEnabled {
return
}
if lease == nil {
return
}
cacheKey := fmt.Sprintf(
DYNAMIC_SECRET_LEASE_TEMPLATE,
lease.ProjectSlug,
lease.Environment,
lease.SecretPath,
lease.Slug,
requestedLeaseTTL,
)
ttl := time.Until(lease.ExpireAt)
log.Info().Msgf("[cache]: writing dynamic secret lease to cache: [cache-key=%s] [entry-ttl=%s]", cacheKey, ttl.String())
if err := d.cacheManager.WriteToCache(cacheKey, lease, &ttl); err != nil {
log.Error().Msgf("[cache]: unable to write dynamic secret lease to cache because %v", err)
} else {
log.Info().Msgf("[cache]: dynamic secret lease written to cache: %s", cacheKey)
}
}
func (d *DynamicSecretLeaseManager) ReadLeaseFromCache(projectSlug, environment, secretPath, slug string, requestedLeaseTTL string) *DynamicSecretLeaseWithTTL {
if d.cacheManager == nil || !d.cacheManager.IsEnabled {
return nil
}
cacheKey := fmt.Sprintf(DYNAMIC_SECRET_LEASE_TEMPLATE, projectSlug, environment, secretPath, slug, requestedLeaseTTL)
var lease *DynamicSecretLeaseWithTTL
err := d.cacheManager.ReadFromCache(cacheKey, &lease)
if err != nil {
if errors.Is(err, badger.ErrKeyNotFound) {
return nil
}
log.Error().Msgf("[cache]: unable to read dynamic secret lease from cache because %v", err)
return nil
}
return lease
}
func (d *DynamicSecretLeaseManager) DeleteLeaseFromCache(projectSlug, environment, secretPath, slug, requestedLeaseTTL string) error {
if d.cacheManager == nil || !d.cacheManager.IsEnabled {
return nil
}
cacheKey := fmt.Sprintf(DYNAMIC_SECRET_LEASE_TEMPLATE, projectSlug, environment, secretPath, slug, requestedLeaseTTL)
err := d.cacheManager.DeleteFromCache(cacheKey)
if err != nil {
return fmt.Errorf("unable to delete lease from cache: %v", err)
}
return nil
}
func (d *DynamicSecretLeaseManager) DeleteUnusedLeasesFromCache() error {
if d.cacheManager.IsEnabled {
log.Info().Msgf("[cache]: deleting unused dynamic secret leases from cache")
}
d.mutex.Lock()
defer d.mutex.Unlock()
allCacheKeys, err := d.cacheManager.GetAllCacheEntries()
if err != nil {
return fmt.Errorf("unable to get all cache entries: %v", err)
}
if allCacheKeys == nil {
log.Debug().Msgf("[cache]: no cache entries found")
return nil
}
var cachedLeases []DynamicSecretLeaseWithTTL
for cacheKey, leaseData := range allCacheKeys {
if strings.HasPrefix(cacheKey, "dynamic-secret-lease-") {
// Marshal back to JSON and unmarshal into the correct type
jsonData, err := json.Marshal(leaseData)
if err != nil {
log.Warn().Msgf("[cache]: failed to marshal cached lease data for key %s: %v", cacheKey, err)
continue
}
var lease DynamicSecretLeaseWithTTL
if err := json.Unmarshal(jsonData, &lease); err != nil {
log.Warn().Msgf("[cache]: failed to unmarshal cached lease data for key %s: %v", cacheKey, err)
continue
}
cachedLeases = append(cachedLeases, lease)
}
}
log.Debug().Msgf("[cache]: found %d cached leases", len(cachedLeases))
log.Debug().Msgf("[cache]: current active leases count: %d", len(d.leases))
// 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]",
cachedLease.ProjectSlug,
cachedLease.Environment,
cachedLease.SecretPath,
cachedLease.Slug,
)
// check if a lease with the same configuration exists (not comparing LeaseID since that changes on refresh)
found := slices.ContainsFunc(d.leases, func(s DynamicSecretLeaseWithTTL) bool {
match := s.ProjectSlug == cachedLease.ProjectSlug &&
s.Environment == cachedLease.Environment &&
s.SecretPath == cachedLease.SecretPath &&
s.Slug == cachedLease.Slug &&
s.RequestedLeaseTTL == cachedLease.RequestedLeaseTTL
if match {
log.Debug().Msgf("[cache]: found matching active lease: [project=%s], [env=%s], [path=%s], [slug=%s]",
s.ProjectSlug,
s.Environment,
s.SecretPath,
s.Slug,
)
}
return match
})
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]",
cachedLease.LeaseID,
cachedLease.ProjectSlug,
cachedLease.Environment,
cachedLease.SecretPath,
cachedLease.Slug,
)
if err := d.DeleteLeaseFromCache(
cachedLease.ProjectSlug,
cachedLease.Environment,
cachedLease.SecretPath,
cachedLease.Slug,
cachedLease.RequestedLeaseTTL,
); err != nil {
log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err)
}
}
}
return nil
}
func (d *DynamicSecretLeaseManager) Prune() {
d.mutex.Lock()
defer d.mutex.Unlock()
d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLeaseWithTTL) bool {
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 {
log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err)
}
}
return shouldDelete
})
}
// AppendUnsafe can be used if you already hold the lock
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
// 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 {
return true
}
return false
})
if index != -1 {
// merge template IDs, avoiding duplicates
for _, newTemplateID := range lease.TemplateIDs {
if !slices.Contains(d.leases[index].TemplateIDs, newTemplateID) {
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, newTemplateID)
}
}
return
}
d.leases = append(d.leases, lease)
d.WriteLeaseToCache(&lease, lease.RequestedLeaseTTL)
}
// Expects a lock to be held before invocation
func (d *DynamicSecretLeaseManager) RegisterTemplateUnsafe(projectSlug, environment, secretPath, slug string, templateId int, requestedLeaseTTL 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
})
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)
if index != -1 {
log.Debug().Msgf("Lease: %+v", d.leases[index])
} else {
log.Debug().Msgf("No lease found for the given configuration")
}
if index != -1 {
// only add template ID if it's not already present
if !slices.Contains(d.leases[index].TemplateIDs, templateId) {
log.Debug().Msgf("Adding template ID %d to lease", templateId)
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, templateId)
d.WriteLeaseToCache(&d.leases[index], d.leases[index].RequestedLeaseTTL)
} else {
log.Debug().Msgf("Template ID %d already exists for lease, skipping", templateId)
}
}
}
// Expects a lock to be held before invocation
func (d *DynamicSecretLeaseManager) GetLeaseUnsafe(accessToken, projectSlug, environment, secretPath, slug string, templateId int, requestedLeaseTTL string) *DynamicSecretLeaseWithTTL {
// first try to get from in-memory storage
// find lease by configuration (project, env, path, slug, TTL) 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 {
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)
if leaseFromCache == nil {
log.Info().Msgf("[cache]: cache miss, no lease found [template-id=%d]", templateId)
} else {
log.Debug().Msgf("[cache]: cache hit, lease found [template-id=%d]", templateId)
}
log.Debug().Msgf("[cache]: lease from cache: %+v", leaseFromCache)
if leaseFromCache != nil {
// try to get the lease from the API
dynamicSecretLease, err := util.GetDynamicSecretLease(accessToken, leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.LeaseID)
if err != nil {
log.Warn().Msgf("[cache]: error: %+v", err)
// 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 {
log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err)
}
return nil
}
// 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 {
log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err)
}
if err := revokeDynamicSecretLease(accessToken, leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.LeaseID, d.retryConfig); err != nil {
log.Warn().Msgf("unable to revoke dynamic secret lease %s: %v", leaseFromCache.LeaseID, err)
return nil
}
return nil
}
// 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 {
log.Warn().Msgf("[cache]: unable to delete lease from cache: %v", err)
}
if err := revokeDynamicSecretLease(accessToken, leaseFromCache.ProjectSlug, leaseFromCache.Environment, leaseFromCache.SecretPath, leaseFromCache.LeaseID, d.retryConfig); err != nil {
log.Warn().Msgf("unable to revoke expired dynamic secret lease %s: %v. Non-critical, the lease is already expired or will expire automatically within the next 2 minutes.", leaseFromCache.LeaseID, err)
return nil
}
return nil
}
// we call appendUnsafe because we already hold the lock, and if we call Append directly we'll get a deadlock
d.AppendUnsafe(*leaseFromCache)
return leaseFromCache
}
return nil
}
// for a given template find the first expiring lease
// The bool indicates whether it contains valid expiry list
func (d *DynamicSecretLeaseManager) GetFirstExpiringLeaseTime() (time.Time, bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
if len(d.leases) == 0 {
return time.Time{}, false
}
var firstExpiry time.Time
for i, el := range d.leases {
if i == 0 {
firstExpiry = el.ExpireAt
}
newLeaseTime := el.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)
if newLeaseTime.Before(firstExpiry) {
firstExpiry = newLeaseTime
}
}
return firstExpiry, true
}
func NewDynamicSecretLeaseManager(cacheManager *CacheManager, retryConfig *infisicalSdk.RetryRequestsConfig) *DynamicSecretLeaseManager {
manager := &DynamicSecretLeaseManager{
cacheManager: cacheManager,
retryConfig: retryConfig,
}
return manager
}
func ReadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
func ExecuteCommandWithTimeout(command string, timeout int64) error {
shell := [2]string{"sh", "-c"}
if runtime.GOOS == "windows" {
shell = [2]string{"cmd", "/C"}
} else {
currentShell := os.Getenv("SHELL")
if currentShell != "" {
shell[0] = currentShell
}
}
ctx := context.Background()
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
}
cmd := exec.CommandContext(ctx, shell[0], shell[1], command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok { // type assertion
if exitError.ProcessState.ExitCode() == -1 {
return fmt.Errorf("command timed out")
}
}
return err
} else {
return nil
}
}
func FileExists(filepath string) bool {
info, err := os.Stat(filepath)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// WriteToFile writes data to the specified file path.
func WriteBytesToFile(data *bytes.Buffer, outputPath string) error {
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outputFile.Close()
_, err = outputFile.Write(data.Bytes())
return err
}
func ParseAuthConfig(authConfigFile []byte, destination interface{}) error {
if err := yaml.Unmarshal(authConfigFile, destination); err != nil {
return err
}
return nil
}
func validateAgentConfigVersionCompatibility(config *Config) error {
return validateAgentConfigVersionCompatibilityWithMode(config, false)
}
func validateAgentConfigVersionCompatibilityWithMode(config *Config, isCertManagerMode bool) error {
if config.Version == "" {
if len(config.Certificates) > 0 {
return fmt.Errorf("certificates are configured but 'version' field is not specified. Add 'version: v1' to your config")
}
return nil
}
switch config.Version {
case "v1":
if isCertManagerMode {
return validateCertificateManagementV1ForCertManager(config)
} else {
return validateCertificateManagementV1(config)
}
default:
return fmt.Errorf("unsupported version: %s. Supported versions: v1", config.Version)
}
}
func validateCertificateManagementV1(config *Config) error {
return fmt.Errorf("version: v1 is for certificate management. Please use 'infisical cert-manager agent' for certificate configurations")
}
func validateCertificateManagementV1ForCertManager(config *Config) error {
if len(config.Certificates) == 0 {
return fmt.Errorf("certificate management requires at least one certificate to be configured")
}
return nil
}
func ParseAgentConfig(configFile []byte) (*Config, error) {
return parseAgentConfigWithMode(configFile, false)
}
func ParseAgentConfigForCertManager(configFile []byte) (*Config, error) {
return parseAgentConfigWithMode(configFile, true)
}
func parseAgentConfigWithMode(configFile []byte, isCertManagerMode bool) (*Config, error) {
var rawConfig Config
if err := yaml.Unmarshal(configFile, &rawConfig); err != nil {
return nil, err
}
// Set defaults
if rawConfig.Infisical.Address == "" {
rawConfig.Infisical.Address = DEFAULT_INFISICAL_CLOUD_URL
}
if rawConfig.Cache.Persistent != nil && rawConfig.Cache.Persistent.Type == CACHE_TYPE_KUBERNETES {
if rawConfig.Cache.Persistent.ServiceAccountTokenPath == "" {
rawConfig.Cache.Persistent.ServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
}
config.INFISICAL_URL = util.AppendAPIEndpoint(rawConfig.Infisical.Address)
log.Info().Msgf("Infisical instance address set to %s", rawConfig.Infisical.Address)
if err := validateAgentConfigVersionCompatibilityWithMode(&rawConfig, isCertManagerMode); err != nil {
return nil, err
}
return &rawConfig, nil
}
type secretArguments struct {
IsRecursive bool `json:"recursive"`
ShouldExpandSecretReferences *bool `json:"expandSecretReferences,omitempty"`
}
func (s *secretArguments) SetDefaults() {
if s.ShouldExpandSecretReferences == nil {
var bool = true
s.ShouldExpandSecretReferences = &bool
}
}
func secretTemplateFunction(accessToken string, currentEtag *string) func(string, string, string, ...string) ([]models.SingleEnvironmentVariable, error) {
// ...string is because golang doesn't have optional arguments.
// thus we make it slice and pick it only first element
return func(projectID, envSlug, secretPath string, args ...string) ([]models.SingleEnvironmentVariable, error) {
var parsedArguments secretArguments
// to make it optional
if len(args) > 0 {
err := json.Unmarshal([]byte(args[0]), &parsedArguments)
if err != nil {
return nil, err
}
}
parsedArguments.SetDefaults()
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, true, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences)
if err != nil {
return nil, err
}
*currentEtag = res.Etag
return res.Secrets, nil
}
}
func secretTemplateByProjectSlugFunction(accessToken string, currentEtag *string) func(string, string, string, ...string) ([]models.SingleEnvironmentVariable, error) {
return func(projectSlug, envSlug, secretPath string, args ...string) ([]models.SingleEnvironmentVariable, error) {
httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
}
httpClient.SetAuthToken(accessToken)
project, err := api.CallGetProjectBySlug(httpClient, projectSlug)
if err != nil {
return nil, fmt.Errorf("failed to get project by slug: %v", err)
}
return secretTemplateFunction(accessToken, currentEtag)(project.ID, envSlug, secretPath, args...)
}
}
func getSingleSecretTemplateFunction(accessToken string, currentEtag *string) func(string, string, string, string) (models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath, secretName string) (models.SingleEnvironmentVariable, error) {
secret, etag, err := util.GetSinglePlainTextSecretByNameV3(accessToken, projectID, envSlug, secretPath, secretName)
if err != nil {
return models.SingleEnvironmentVariable{}, err
}
*currentEtag = etag
return secret, nil
}
}
func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, agentManager *AgentManager, templateId int, currentEtag *string) func(...string) (map[string]interface{}, error) {
return func(args ...string) (map[string]interface{}, error) {
dynamicSecretManager.mutex.Lock()
defer dynamicSecretManager.mutex.Unlock()
argLength := len(args)
if argLength != 4 && argLength != 5 {
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 {
ttl = args[4]
}
dynamicSecretData := dynamicSecretManager.GetLeaseUnsafe(accessToken, projectSlug, envSlug, secretPath, slug, templateId, ttl)
// 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)
etagData := fmt.Sprintf("%s-%s-%s-%s-%s", projectSlug, envSlug, secretPath, slug, ttl)
dynamicSecretDataBytes, err := json.Marshal(dynamicSecretData.Data)
if err != nil {
return nil, err
}
hexEncodedData := hex.EncodeToString(dynamicSecretDataBytes)
etag := sha256.Sum256([]byte(fmt.Sprintf("%s-%s", etagData, hexEncodedData)))
*currentEtag = hex.EncodeToString(etag[:])
return dynamicSecretData.Data, nil
}
temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
AutoTokenRefresh: false,
RetryRequestsConfig: agentManager.SdkRetryConfig(),
})
temporaryInfisicalClient.Auth().SetAccessToken(accessToken)
// if there's no lease (either in memory or in cache), we create a new lease
leaseData, _, res, err := temporaryInfisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
DynamicSecretName: slug,
ProjectSlug: projectSlug,
EnvironmentSlug: envSlug,
SecretPath: secretPath,
TTL: ttl,
})
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})
return leaseData, nil
}
}
func newTemplateFunctions(accessToken string, currentEtag *string, dynamicSecretManager *DynamicSecretLeaseManager, agentManager *AgentManager, templateId int) template.FuncMap {
secretFunction := secretTemplateFunction(accessToken, currentEtag)
secretByProjectSlugFunction := secretTemplateByProjectSlugFunction(accessToken, currentEtag)
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, agentManager, templateId, currentEtag)
getSingleSecretFunction := getSingleSecretTemplateFunction(accessToken, currentEtag)