Skip to content

Commit ae60459

Browse files
authored
Merge branch 'main' into add-completion
2 parents b7a3618 + d5a890c commit ae60459

File tree

13 files changed

+373
-185
lines changed

13 files changed

+373
-185
lines changed

.github/workflows/codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535

3636
# Initializes the CodeQL tools for scanning.
3737
- name: Initialize CodeQL
38-
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
38+
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
3939
with:
4040
languages: go
4141
# xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
@@ -52,6 +52,6 @@ jobs:
5252
make install
5353
5454
- name: Perform CodeQL Analysis
55-
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
55+
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
5656
with:
5757
category: "/language:go"

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ jobs:
3737
cache: false
3838

3939
- name: Setup Syft
40-
uses: anchore/sbom-action/download-syft@cee1b8e05ae5b2593a75e197229729eabaa9f8ec # v0.20.2
40+
uses: anchore/sbom-action/download-syft@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # v0.20.4
4141

4242
- name: Setup Cosign
43-
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
43+
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
4444

4545
- name: Setup QEMU
4646
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0

README.rst

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,17 @@ configuration directory.
225225

226226
- **Linux**
227227

228-
- Looks for `keys.txt` in `$XDG_CONFIG_HOME/sops/age/keys.txt`;
229-
- Falls back to `$HOME/.config/sops/age/keys.txt` if `$XDG_CONFIG_HOME` isn’t set.
228+
- Looks for ``keys.txt`` in ``$XDG_CONFIG_HOME/sops/age/keys.txt``;
229+
- Falls back to ``$HOME/.config/sops/age/keys.txt`` if ``$XDG_CONFIG_HOME`` isn’t set.
230230

231231
- **macOS**
232232

233-
- Looks for `keys.txt` in `$HOME/Library/Application Support/sops/age/keys.txt`.
233+
- Looks for ``keys.txt`` in ``$XDG_CONFIG_HOME/sops/age/keys.txt``;
234+
- Falls back to ``$HOME/Library/Application Support/sops/age/keys.txt``.
234235

235236
- **Windows**
236237

237-
- Looks for `keys.txt` in `%AppData%\\sops\\age\\keys.txt`.
238+
- Looks for ``keys.txt`` in `%AppData%\\sops\\age\\keys.txt``.
238239

239240
You can override the default lookup by:
240241

@@ -1476,7 +1477,7 @@ original file after encrypting or decrypting it.
14761477
Encrypting binary files
14771478
~~~~~~~~~~~~~~~~~~~~~~~
14781479
1479-
SOPS primary use case is encrypting YAML and JSON configuration files, but it
1480+
SOPS primary use case is encrypting YAML, JSON, ENV, and INI configuration files, but it
14801481
also has the ability to manage binary files. When encrypting a binary, SOPS will
14811482
read the data as bytes, encrypt it, store the encrypted base64 under
14821483
``tree['data']`` and write the result as JSON.
@@ -1559,6 +1560,17 @@ The value must be formatted as json.
15591560
15601561
$ sops set ~/git/svc/sops/example.yaml '["an_array"][1]' '{"uid1":null,"uid2":1000,"uid3":["bob"]}'
15611562
1563+
You can also provide the value from a file or stdin:
1564+
1565+
.. code:: sh
1566+
1567+
# Provide the value from a file
1568+
$ echo '{"uid1":null,"uid2":1000,"uid3":["bob"]}' > /tmp/example-value
1569+
$ sops set ~/git/svc/sops/example.yaml --value-file '["an_array"][1]' /tmp/example-value
1570+
1571+
# Provide the value from stdin
1572+
$ echo '{"uid1":null,"uid2":1000,"uid3":["bob"]}' | sops set ~/git/svc/sops/example.yaml --value-stdin '["an_array"][1]'
1573+
15621574
Unset a sub-part in a document tree
15631575
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15641576
@@ -1610,9 +1622,9 @@ git client interfaces, because they call git diff under the hood!
16101622
Encrypting only parts of a file
16111623
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16121624
1613-
Note: this only works on YAML and JSON files, not on BINARY files.
1625+
Note: this only works on YAML, JSON, ENV, and INI files, not on BINARY files.
16141626
1615-
By default, SOPS encrypts all the values of a YAML or JSON file and leaves the
1627+
By default, SOPS encrypts all the values of a YAML, JSON, ENV, or INI file and leaves the
16161628
keys in cleartext. In some instances, you may want to exclude some values from
16171629
being encrypted. This can be accomplished by adding the suffix **_unencrypted**
16181630
to any key of a file. When set, all values underneath the key that set the
@@ -1823,9 +1835,9 @@ automation, we found this to be a hard problem with a number of prerequisites:
18231835
git repo, jenkins and S3) and only be decrypted on the target
18241836
systems
18251837
1826-
SOPS can be used to encrypt YAML, JSON and BINARY files. In BINARY mode, the
1838+
SOPS can be used to encrypt YAML, JSON, ENV, INI, and BINARY files. In BINARY mode, the
18271839
content of the file is treated as a blob, the same way PGP would encrypt an
1828-
entire file. In YAML and JSON modes, however, the content of the file is
1840+
entire file. In YAML, JSON, ENV, and INI modes, however, the content of the file is
18291841
manipulated as a tree where keys are stored in cleartext, and values are
18301842
encrypted. hiera-eyaml does something similar, and over the years we learned
18311843
to appreciate its benefits, namely:

age/keysource.go

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ func MasterKeysFromRecipients(commaSeparatedRecipients string) ([]*MasterKey, er
9494
return keys, nil
9595
}
9696

97+
// errSet is a collection of captured errors.
98+
type errSet []error
99+
100+
// Error joins the errors into a "; " separated string.
101+
func (e errSet) Error() string {
102+
str := make([]string, len(e))
103+
for i, err := range e {
104+
str[i] = err.Error()
105+
}
106+
return strings.Join(str, "; ")
107+
}
108+
97109
// MasterKeyFromRecipient takes a Bech32-encoded age public key, parses it, and
98110
// returns a new MasterKey.
99111
func MasterKeyFromRecipient(recipient string) (*MasterKey, error) {
@@ -197,11 +209,13 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
197209
// Decrypt decrypts the EncryptedKey with the parsed or loaded identities, and
198210
// returns the result.
199211
func (key *MasterKey) Decrypt() ([]byte, error) {
212+
var errs errSet
200213
if len(key.parsedIdentities) == 0 {
201-
ids, err := key.loadIdentities()
202-
if err != nil {
214+
var ids ParsedIdentities
215+
ids, errs = key.loadIdentities()
216+
if len(ids) == 0 {
203217
log.Info("Decryption failed")
204-
return nil, fmt.Errorf("failed to load age identities: %w", err)
218+
return nil, fmt.Errorf("failed to load age identities: %w", errs)
205219
}
206220
ids.ApplyToMasterKey(key)
207221
}
@@ -211,7 +225,11 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
211225
r, err := age.Decrypt(ar, key.parsedIdentities...)
212226
if err != nil {
213227
log.Info("Decryption failed")
214-
return nil, fmt.Errorf("failed to create reader for decrypting sops data key with age: %w", err)
228+
var loadErrors string
229+
if len(errs) > 0 {
230+
loadErrors = fmt.Sprintf(". Errors while loading age identities: %s", errs.Error())
231+
}
232+
return nil, fmt.Errorf("failed to create reader for decrypting sops data key with age: %w%s", err, loadErrors)
215233
}
216234

217235
var b bytes.Buffer
@@ -289,14 +307,15 @@ func getUserConfigDir() (string, error) {
289307
// environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv,
290308
// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all
291309
// found references, and expects at least one configuration to be present.
292-
func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
310+
func (key *MasterKey) loadIdentities() (ParsedIdentities, errSet) {
293311
var identities ParsedIdentities
294312

313+
var errs errSet
314+
295315
sshIdentity, err := loadAgeSSHIdentity()
296316
if err != nil {
297-
return nil, fmt.Errorf("failed to get SSH identity: %w", err)
298-
}
299-
if sshIdentity != nil {
317+
errs = append(errs, fmt.Errorf("failed to get SSH identity: %w", err))
318+
} else if sshIdentity != nil {
300319
identities = append(identities, sshIdentity)
301320
}
302321

@@ -309,39 +328,39 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
309328
if ageKeyFile, ok := os.LookupEnv(SopsAgeKeyFileEnv); ok {
310329
f, err := os.Open(ageKeyFile)
311330
if err != nil {
312-
return nil, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err)
331+
errs = append(errs, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err))
332+
} else {
333+
defer f.Close()
334+
readers[SopsAgeKeyFileEnv] = f
313335
}
314-
defer f.Close()
315-
readers[SopsAgeKeyFileEnv] = f
316336
}
317337

318338
if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok {
319339
args, err := shlex.Split(ageKeyCmd)
320340
if err != nil {
321-
return nil, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err)
341+
errs = append(errs, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err))
342+
} else {
343+
out, err := exec.Command(args[0], args[1:]...).Output()
344+
if err != nil {
345+
errs = append(errs, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err))
346+
} else {
347+
readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out)
348+
}
322349
}
323-
out, err := exec.Command(args[0], args[1:]...).Output()
324-
if err != nil {
325-
return nil, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err)
326-
}
327-
readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out)
328350
}
329351

330352
userConfigDir, err := getUserConfigDir()
331353
if err != nil && len(readers) == 0 && len(identities) == 0 {
332-
return nil, fmt.Errorf("user config directory could not be determined: %w", err)
333-
}
334-
if userConfigDir != "" {
354+
errs = append(errs, fmt.Errorf("user config directory could not be determined: %w", err))
355+
} else if userConfigDir != "" {
335356
ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath))
336357
f, err := os.Open(ageKeyFilePath)
337358
if err != nil && !errors.Is(err, os.ErrNotExist) {
338-
return nil, fmt.Errorf("failed to open file: %w", err)
339-
}
340-
if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
359+
errs = append(errs, fmt.Errorf("failed to open file: %w", err))
360+
} else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
341361
// If we have no other readers, presence of the file is required.
342-
return nil, fmt.Errorf("failed to open file: %w", err)
343-
}
344-
if err == nil {
362+
errs = append(errs, fmt.Errorf("failed to open file: %w", err))
363+
} else if err == nil {
345364
defer f.Close()
346365
readers[ageKeyFilePath] = f
347366
}
@@ -350,11 +369,12 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
350369
for n, r := range readers {
351370
ids, err := unwrapIdentities(n, r)
352371
if err != nil {
353-
return nil, err
372+
errs = append(errs, err)
373+
} else {
374+
identities = append(identities, ids...)
354375
}
355-
identities = append(identities, ids...)
356376
}
357-
return identities, nil
377+
return identities, errs
358378
}
359379

360380
// parseRecipient attempts to parse a string containing an encoded age public

age/keysource_test.go

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
369369
t.Setenv(SopsAgeKeyEnv, mockIdentity)
370370

371371
key := &MasterKey{}
372-
got, err := key.loadIdentities()
373-
assert.NoError(t, err)
372+
got, errs := key.loadIdentities()
373+
assert.Len(t, errs, 0)
374374
assert.Len(t, got, 1)
375375
})
376376

@@ -382,8 +382,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
382382
t.Setenv(SopsAgeKeyEnv, mockIdentity+"\n"+mockOtherIdentity)
383383

384384
key := &MasterKey{}
385-
got, err := key.loadIdentities()
386-
assert.NoError(t, err)
385+
got, errs := key.loadIdentities()
386+
assert.Len(t, errs, 0)
387387
assert.Len(t, got, 2)
388388
})
389389

@@ -398,8 +398,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
398398
t.Setenv(SopsAgeKeyFileEnv, keyPath)
399399

400400
key := &MasterKey{}
401-
got, err := key.loadIdentities()
402-
assert.NoError(t, err)
401+
got, errs := key.loadIdentities()
402+
assert.Len(t, errs, 0)
403403
assert.Len(t, got, 1)
404404
})
405405

@@ -416,8 +416,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
416416
assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700))
417417
assert.NoError(t, os.WriteFile(keyPath, []byte(mockIdentity), 0o644))
418418

419-
got, err := (&MasterKey{}).loadIdentities()
420-
assert.NoError(t, err)
419+
got, errs := (&MasterKey{}).loadIdentities()
420+
assert.Len(t, errs, 0)
421421
assert.Len(t, got, 1)
422422
})
423423

@@ -435,18 +435,19 @@ func TestMasterKey_loadIdentities(t *testing.T) {
435435
t.Setenv(SopsAgeSshPrivateKeyFileEnv, keyPath)
436436

437437
key := &MasterKey{}
438-
got, err := key.loadIdentities()
439-
assert.NoError(t, err)
438+
got, errs := key.loadIdentities()
439+
assert.Len(t, errs, 0)
440440
assert.Len(t, got, 1)
441441
})
442442

443443
t.Run("no identity", func(t *testing.T) {
444444
tmpDir := t.TempDir()
445445
overwriteUserConfigDir(t, tmpDir)
446446

447-
got, err := (&MasterKey{}).loadIdentities()
448-
assert.Error(t, err)
449-
assert.ErrorContains(t, err, "failed to open file")
447+
got, errs := (&MasterKey{}).loadIdentities()
448+
assert.Len(t, errs, 1)
449+
assert.Error(t, errs[0])
450+
assert.ErrorContains(t, errs[0], "failed to open file")
450451
assert.Nil(t, got)
451452
})
452453

@@ -467,8 +468,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
467468
assert.NoError(t, os.WriteFile(keyPath2, []byte(mockOtherIdentity), 0o644))
468469
t.Setenv(SopsAgeKeyFileEnv, keyPath2)
469470

470-
got, err := (&MasterKey{}).loadIdentities()
471-
assert.NoError(t, err)
471+
got, errs := (&MasterKey{}).loadIdentities()
472+
assert.Len(t, errs, 0)
472473
assert.Len(t, got, 2)
473474
})
474475

@@ -480,9 +481,10 @@ func TestMasterKey_loadIdentities(t *testing.T) {
480481
t.Setenv(SopsAgeKeyEnv, "invalid")
481482

482483
key := &MasterKey{}
483-
got, err := key.loadIdentities()
484-
assert.Error(t, err)
485-
assert.ErrorContains(t, err, fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv))
484+
got, errs := key.loadIdentities()
485+
assert.Len(t, errs, 1)
486+
assert.Error(t, errs[0])
487+
assert.ErrorContains(t, errs[0], fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv))
486488
assert.Nil(t, got)
487489
})
488490

@@ -494,8 +496,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
494496
t.Setenv(SopsAgeKeyCmdEnv, "echo '"+mockIdentity+"'")
495497

496498
key := &MasterKey{}
497-
got, err := key.loadIdentities()
498-
assert.NoError(t, err)
499+
got, errs := key.loadIdentities()
500+
assert.Len(t, errs, 0)
499501
assert.Len(t, got, 1)
500502
})
501503

@@ -507,9 +509,10 @@ func TestMasterKey_loadIdentities(t *testing.T) {
507509
t.Setenv(SopsAgeKeyCmdEnv, "meow")
508510

509511
key := &MasterKey{}
510-
got, err := key.loadIdentities()
511-
assert.Error(t, err)
512-
assert.ErrorContains(t, err, "failed to execute command meow")
512+
got, errs := key.loadIdentities()
513+
assert.Len(t, errs, 2)
514+
assert.Error(t, errs[0])
515+
assert.ErrorContains(t, errs[0], "failed to execute command meow")
513516
assert.Nil(t, got)
514517
})
515518
}

cmd/sops/edit.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) {
109109
}
110110
// Ensure that in any case, the temporary file is always closed.
111111
defer tmpfile.Close()
112+
// Ensure that the file is read+write for owner only.
113+
if err = tmpfile.Chmod(0600); err != nil {
114+
return nil, common.NewExitError(fmt.Sprintf("Could not change permissions of temporary file to read-write for owner only: %s", err), codes.CouldNotWriteOutputFile)
115+
}
112116

113117
tmpfileName := tmpfile.Name()
114118

0 commit comments

Comments
 (0)