@@ -1258,9 +1258,27 @@ func sopsDecryptFile(ctx context.Context, pctx *ParsingContext, l log.Logger, pa
12581258 var result string
12591259
12601260 err := telemetry .TelemeterFromContext (ctx ).Collect (ctx , "hcl_fn_sops_decrypt_file" , attrs , func (childCtx context.Context ) error {
1261+ if len (params ) != 1 {
1262+ return errors .New (WrongNumberOfParamsError {Func : "sops_decrypt_file" , Expected : "1" , Actual : len (params )})
1263+ }
1264+
1265+ format , err := getSopsFileFormat (sourceFile )
1266+ if err != nil {
1267+ return errors .New (err )
1268+ }
1269+
1270+ path := sourceFile
1271+
1272+ if ! filepath .IsAbs (path ) {
1273+ path = filepath .Join (pctx .TerragruntOptions .WorkingDir , path )
1274+ path = filepath .Clean (path )
1275+ }
1276+
1277+ trackFileRead (pctx .FilesRead , path )
1278+
12611279 var innerErr error
12621280
1263- result , innerErr = sopsDecryptFileImpl (childCtx , pctx , l , params )
1281+ result , innerErr = sopsDecryptFileImpl (childCtx , pctx , l , path , format , decrypt . File )
12641282
12651283 return innerErr
12661284 })
@@ -1269,60 +1287,59 @@ func sopsDecryptFile(ctx context.Context, pctx *ParsingContext, l log.Logger, pa
12691287}
12701288
12711289// sopsDecryptFileImpl contains the actual implementation of sopsDecryptFile
1272- func sopsDecryptFileImpl (ctx context.Context , pctx * ParsingContext , _ log.Logger , params []string ) (string , error ) {
1273- numParams := len (params )
1274-
1275- var sourceFile string
1276-
1277- if numParams > 0 {
1278- sourceFile = params [0 ]
1279- }
1290+ func sopsDecryptFileImpl (ctx context.Context , pctx * ParsingContext , l log.Logger , path string , format string , decryptFn func (string , string ) ([]byte , error )) (string , error ) {
1291+ // Fast path: check cache before acquiring lock.
1292+ // Cache has its own sync.RWMutex, safe for concurrent reads.
1293+ if val , ok := sopsCache .Get (ctx , path ); ok {
1294+ l .Debugf ("sops decrypt: cache hit for %s (len=%d)" , path , len (val ))
12801295
1281- if numParams != 1 {
1282- return "" , errors .New (WrongNumberOfParamsError {Func : "sops_decrypt_file" , Expected : "1" , Actual : numParams })
1296+ return val , nil
12831297 }
12841298
1285- format , err := getSopsFileFormat (sourceFile )
1286- if err != nil {
1287- return "" , errors .New (err )
1288- }
1299+ // Cache miss: acquire lock for env mutation + decrypt.
1300+ // The lock serializes os.Setenv/os.Unsetenv to prevent race conditions
1301+ // when multiple units decrypt concurrently with different auth credentials.
1302+ // See https://github.com/gruntwork-io/terragrunt/issues/5515
1303+ l .Debugf ("sops decrypt: cache miss, acquiring lock for %s (format=%s)" , path , format )
12891304
1290- path := sourceFile
1305+ locks .EnvLock .Lock ()
1306+ defer locks .EnvLock .Unlock ()
12911307
1292- if ! filepath .IsAbs (path ) {
1293- path = filepath .Join (pctx .TerragruntOptions .WorkingDir , path )
1294- path = filepath .Clean (path )
1295- }
1308+ // Set env vars from opts.Env that are missing from process env.
1309+ // Auth-provider credentials (e.g., AWS_SESSION_TOKEN) may not exist
1310+ // in process env yet — SOPS needs them for KMS auth.
1311+ // Existing process env vars are preserved to avoid overriding real
1312+ // credentials with empty auth-provider values.
1313+ env := pctx .TerragruntOptions .Env
12961314
1297- // Track that this file was read during parsing
1298- trackFileRead (pctx .FilesRead , path )
1315+ var setKeys []string
12991316
1300- // Set environment variables from the TerragruntOptions.Env map.
1301- // This is especially useful for integrations with things like the `auth-provider` flag,
1302- // which can set environment variables that are used for decryption.
1303- //
1304- // Due to the fact that sops doesn't expose a way of explicitly setting authentication configurations
1305- // for decryption, we have to rely on environment variables to pass these configurations.
1306- // This can cause a race condition, so we have to be careful to avoid having anything else
1307- // running concurrently that might interfere with the environment variables.
1308- env := pctx .TerragruntOptions .Env
1309- if len (env ) > 0 {
1310- locks .EnvLock .Lock ()
1311- defer locks .EnvLock .Unlock ()
1312-
1313- for k , v := range env {
1314- if os .Getenv (k ) == "" {
1315- os .Setenv (k , v ) //nolint:errcheck
1316- defer os .Unsetenv (k ) //nolint:errcheck
1317- }
1317+ for k , v := range env {
1318+ if _ , exists := os .LookupEnv (k ); exists {
1319+ continue
13181320 }
1321+
1322+ os .Setenv (k , v ) //nolint:errcheck
1323+
1324+ setKeys = append (setKeys , k )
13191325 }
13201326
1327+ defer func () {
1328+ for _ , k := range setKeys {
1329+ os .Unsetenv (k ) //nolint:errcheck
1330+ }
1331+ }()
1332+
1333+ // Double-check: another goroutine may have populated cache while we waited for the lock.
13211334 if val , ok := sopsCache .Get (ctx , path ); ok {
1335+ l .Debugf ("sops decrypt: cache hit after lock for %s (len=%d)" , path , len (val ))
1336+
13221337 return val , nil
13231338 }
13241339
1325- rawData , err := decrypt .File (path , format )
1340+ l .Debugf ("sops decrypt: decrypting %s" , path )
1341+
1342+ rawData , err := decryptFn (path , format )
13261343 if err != nil {
13271344 return "" , errors .New (extractSopsErrors (err ))
13281345 }
@@ -1334,7 +1351,7 @@ func sopsDecryptFileImpl(ctx context.Context, pctx *ParsingContext, _ log.Logger
13341351 return value , nil
13351352 }
13361353
1337- return "" , errors .New (InvalidSopsFormatError {SourceFilePath : sourceFile })
1354+ return "" , errors .New (InvalidSopsFormatError {SourceFilePath : path })
13381355}
13391356
13401357// Mapping of SOPS format to string
0 commit comments