Skip to content

Commit df76baa

Browse files
authored
Merge branch 'main' into barbican-support
2 parents b55ca4a + aca8fe9 commit df76baa

23 files changed

Lines changed: 400 additions & 335 deletions

.github/workflows/cli.yml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
matrix:
2020
os: [linux, darwin, windows]
2121
arch: [amd64, arm64]
22-
go-version: ['1.24', '1.25']
22+
go-version: ['1.24', '1.25', '1.26']
2323
exclude:
2424
- os: windows
2525
arch: arm64
@@ -28,18 +28,19 @@ jobs:
2828
VAULT_TOKEN: "root"
2929
VAULT_ADDR: "http://127.0.0.1:8200"
3030
steps:
31+
- name: Check out code into the Go module directory
32+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
33+
with:
34+
persist-credentials: false
35+
3136
- name: Set up Go ${{ matrix.go-version }}
3237
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
3338
with:
3439
go-version: ${{ matrix.go-version }}
40+
cache: false
3541
id: go
3642

37-
- name: Check out code into the Go module directory
38-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
39-
with:
40-
persist-credentials: false
41-
42-
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
43+
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
4344
with:
4445
path: ~/go/pkg/mod
4546
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
@@ -85,14 +86,14 @@ jobs:
8586
needs: [build]
8687
strategy:
8788
matrix:
88-
go-version: ['1.25']
89+
go-version: ['1.26']
8990
env:
9091
VAULT_VERSION: "1.14.0"
9192
VAULT_TOKEN: "root"
9293
VAULT_ADDR: "http://127.0.0.1:8200"
9394
steps:
9495
- name: Check out code
95-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
96+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
9697
with:
9798
persist-credentials: false
9899

.github/workflows/codeql.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ jobs:
2929

3030
steps:
3131
- name: Checkout code
32-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3333
with:
3434
persist-credentials: false
3535

3636
# Initializes the CodeQL tools for scanning.
3737
- name: Initialize CodeQL
38-
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
38+
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
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@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
55+
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
5656
with:
5757
category: "/language:go"

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323

2424
steps:
2525
- name: Checkout code
26-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
26+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2727
with:
2828
persist-credentials: false
2929

.github/workflows/linters.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
runs-on: ubuntu-latest
2222
steps:
2323
- name: Check out code
24-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
24+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2525
with:
2626
persist-credentials: false
2727

.github/workflows/release.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,25 @@ jobs:
2525

2626
steps:
2727
- name: Checkout
28-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2929
with:
3030
fetch-depth: 0
3131
persist-credentials: false
3232

3333
- name: Setup Go
3434
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
3535
with:
36-
go-version: 1.25
36+
go-version: 1.26
3737
cache: false
3838

3939
- name: Setup Syft
40-
uses: anchore/sbom-action/download-syft@0b82b0b1a22399a1c542d4d656f70cd903571b5c # v0.21.1
40+
uses: anchore/sbom-action/download-syft@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2
4141

4242
- name: Setup Cosign
4343
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
44+
with:
45+
# TODO: update cosign and go-releaser, and adjust go-releaser config
46+
cosign-release: 'v2.6.2'
4447

4548
- name: Setup QEMU
4649
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
@@ -49,22 +52,22 @@ jobs:
4952
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
5053

5154
- name: Login to GitHub Container Registry
52-
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
55+
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
5356
with:
5457
registry: ghcr.io
5558
username: ${{ github.actor }}
5659
password: ${{ secrets.GITHUB_TOKEN }}
5760

5861
- name: Login to Quay.io
59-
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
62+
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
6063
with:
6164
registry: quay.io
6265
username: ${{ secrets.QUAY_BOT_USERNAME }}
6366
password: ${{ secrets.QUAY_BOT_TOKEN }}
6467

6568
- name: Run GoReleaser
6669
id: goreleaser
67-
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
70+
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
6871
with:
6972
# Note that the following is the version of goreleaser, and NOT a Go version!
7073
# When bumping it, make sure to check out goreleaser's changelog first!

CHANGELOG.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
11
# Changelog
22

3+
## 3.12.1
4+
5+
This is a re-release of 3.12.0 with no code changes.
6+
7+
Due to a failure during the 3.12.0 release, and the commit for the 3.12.0
8+
release already being cached by the Go infrastructure, we need to bump
9+
the version to properly get a release out.
10+
(We did learn this from a similar incident with the 3.10.0 release.)
11+
12+
## 3.12.0
13+
14+
Features:
15+
16+
* Add support for HuaweiCloud KMS ([#2001](https://github.com/getsops/sops/pull/2001)).
17+
* GCP KMS: Add `SOPS_GCP_KMS_CLIENT_TYPE` environment variable support to select
18+
between gRPC and REST clients ([#1973](https://github.com/getsops/sops/pull/1973)).
19+
* Age: support hybrid post-quantum identities ([#2033](https://github.com/getsops/sops/pull/2033)).
20+
* Age: pass `SOPS_AGE_RECIPIENT` environment variable to `SOPS_AGE_KEY_CMD` ([#2045](https://github.com/getsops/sops/pull/2045)).
21+
* Age: add `SOPS_AGE_SSH_PRIVATE_KEY_CMD` environment variable ([#2070](https://github.com/getsops/sops/pull/2070)).
22+
23+
Improvements:
24+
25+
* Dependency updates ([#1967](https://github.com/getsops/sops/pull/1967),
26+
[#1971](https://github.com/getsops/sops/pull/1971), [#1978](https://github.com/getsops/sops/pull/1978),
27+
[#1986](https://github.com/getsops/sops/pull/1986), [#1988](https://github.com/getsops/sops/pull/1988),
28+
[#1991](https://github.com/getsops/sops/pull/1991), [#1993](https://github.com/getsops/sops/pull/1993),
29+
[#2002](https://github.com/getsops/sops/pull/2002), [#2004](https://github.com/getsops/sops/pull/2004),
30+
[#2007](https://github.com/getsops/sops/pull/2007), [#2012](https://github.com/getsops/sops/pull/2012),
31+
[#2018](https://github.com/getsops/sops/pull/2018), [#2024](https://github.com/getsops/sops/pull/2024),
32+
[#2029](https://github.com/getsops/sops/pull/2029), [#2037](https://github.com/getsops/sops/pull/2037),
33+
[#2043](https://github.com/getsops/sops/pull/2043), [#2047](https://github.com/getsops/sops/pull/2047),
34+
[#2050](https://github.com/getsops/sops/pull/2050), [#2059](https://github.com/getsops/sops/pull/2059),
35+
[#2074](https://github.com/getsops/sops/pull/2074), [#2078](https://github.com/getsops/sops/pull/2078)).
36+
* Fix mistakes in `--help` output ([#1975](https://github.com/getsops/sops/pull/1975),
37+
[#1963](https://github.com/getsops/sops/pull/1963)).
38+
* Improve documentation ([#1997](https://github.com/getsops/sops/pull/1997)).
39+
* Unset user's `GNUPGHOME` environment variable for tests ([#2052](https://github.com/getsops/sops/pull/2052)).
40+
* Use age's `plugin.NewTerminalUI()` instead of vendoring the code ([#2034](https://github.com/getsops/sops/pull/2034)).
41+
* Remove dead code during YAML loading ([#2072](https://github.com/getsops/sops/pull/2072)).
42+
* Build release with Go 1.26 ([#2071](https://github.com/getsops/sops/pull/2071)).
43+
44+
Bugfixes:
45+
46+
* Add `--decryption-order` flag to `exec-env`, `exec-file`, and `publish` commands.
47+
The subcommand code was using the flags, but it wasn't declared ([#1965](https://github.com/getsops/sops/pull/1965)).
48+
* Fix AWS KMS encryption context not being passed when config is pre-loaded ([#2021](https://github.com/getsops/sops/pull/2021)).
49+
* Fix recursive publish ([#2019](https://github.com/getsops/sops/pull/2019)).
50+
* Set quota project to API project in GCP KMS ([#1697](https://github.com/getsops/sops/pull/1697)).
51+
* DotEnv store now properly reports missing metadata ([#2055](https://github.com/getsops/sops/pull/2055)).
52+
* AWS KMS: allow role splitting without hard-coded `aws` partition ([#2042](https://github.com/getsops/sops/pull/2042)).
53+
54+
Project changes:
55+
56+
* Add Go 1.26 to CI ([#2071](https://github.com/getsops/sops/pull/2071)).
57+
* CI dependency updates ([#1961](https://github.com/getsops/sops/pull/1961),
58+
[#1966](https://github.com/getsops/sops/pull/1966), [#1970](https://github.com/getsops/sops/pull/1970),
59+
[#1979](https://github.com/getsops/sops/pull/1979), [#1985](https://github.com/getsops/sops/pull/1985),
60+
[#1989](https://github.com/getsops/sops/pull/1989), [#1992](https://github.com/getsops/sops/pull/1992),
61+
[#2003](https://github.com/getsops/sops/pull/2003), [#2006](https://github.com/getsops/sops/pull/2006),
62+
[#2010](https://github.com/getsops/sops/pull/2010), [#2011](https://github.com/getsops/sops/pull/2011),
63+
[#2017](https://github.com/getsops/sops/pull/2017), [#2023](https://github.com/getsops/sops/pull/2023),
64+
[#2028](https://github.com/getsops/sops/pull/2028), [#2038](https://github.com/getsops/sops/pull/2038),
65+
[#2044](https://github.com/getsops/sops/pull/2044), [#2046](https://github.com/getsops/sops/pull/2046),
66+
[#2049](https://github.com/getsops/sops/pull/2049), [#2058](https://github.com/getsops/sops/pull/2058),
67+
[#2075](https://github.com/getsops/sops/pull/2075)).
68+
* Rust dependency updates for functional tests ([#1962](https://github.com/getsops/sops/pull/1962),
69+
[#2027](https://github.com/getsops/sops/pull/2027), [#2035](https://github.com/getsops/sops/pull/2035),
70+
[#2073](https://github.com/getsops/sops/pull/2073)).
71+
372
## 3.11.0
473

574
Security fixes:

README.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,17 +265,25 @@ You can override the default lookup by:
265265

266266
- setting the environment variable **SOPS_AGE_KEY_FILE**;
267267
- setting the **SOPS_AGE_KEY** environment variable;
268-
- providing a command to output the age keys by setting the **SOPS_AGE_KEY_CMD** environment variable..
268+
- providing a command to output the age keys by setting the **SOPS_AGE_KEY_CMD** environment variable.
269+
This command can read the age recipient for which to return the private key from the **SOPS_AGE_RECIPIENT** environment variable.
269270

270271
The contents of this key file should be a list of age X25519 identities, one
271272
per line. Lines beginning with ``#`` are considered comments and ignored. Each
272273
identity will be tried in sequence until one is able to decrypt the data.
273274

274275
Encrypting with SSH keys via age is also supported by SOPS. You can use SSH public keys
275276
("ssh-ed25519 AAAA...", "ssh-rsa AAAA...") as age recipients when encrypting a file.
276-
When decrypting a file, SOPS will look for ``~/.ssh/id_ed25519`` and falls back to
277-
``~/.ssh/id_rsa``. You can specify the location of the private key manually by setting
278-
the environment variable **SOPS_AGE_SSH_PRIVATE_KEY_FILE**.
277+
278+
When decrypting a file, SOPS will attempt to source the SSH private key as follows:
279+
280+
- From the path specified in environment variable **SOPS_AGE_SSH_PRIVATE_KEY_FILE**.
281+
- From the output of the command specified in environment variable **SOPS_AGE_SSH_PRIVATE_KEY_CMD**.
282+
283+
.. note:: The output of this command must provide a key that is not password protected.
284+
285+
- From ``~/.ssh/id_ed25519``.
286+
- From ``~/.ssh/id_rsa``.
279287

280288
Note that only ``ssh-rsa`` and ``ssh-ed25519`` are supported.
281289

age/encrypted_keys.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,7 @@ func unwrapIdentities(location string, reader io.Reader) (ParsedIdentities, erro
145145
Passphrase: func() (string, error) {
146146
conn, err := gpgagent.NewConn()
147147
if err != nil {
148-
passphrase, err := readSecret(fmt.Sprintf("Enter passphrase for identity '%s':", location))
149-
if err != nil {
150-
return "", err
151-
}
152-
return string(passphrase), nil
148+
return pluginTerminalUI.RequestValue("", fmt.Sprintf("Enter passphrase for identity '%s':", location), true)
153149
}
154150
defer func(conn *gpgagent.Conn) {
155151
if err := conn.Close(); err != nil {

age/keysource.go

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"filippo.io/age/armor"
1818
"filippo.io/age/plugin"
1919
"github.com/sirupsen/logrus"
20+
"golang.org/x/crypto/ssh"
2021

2122
"github.com/getsops/sops/v3/logging"
2223
"github.com/google/shlex"
@@ -32,6 +33,13 @@ const (
3233
// SopsAgeKeyCmdEnv can be set as an environment variable with a command
3334
// to execute that returns the age keys.
3435
SopsAgeKeyCmdEnv = "SOPS_AGE_KEY_CMD"
36+
// SopsAgeRecipientEnv is passed as an environment variable to the command
37+
// set in SopsAgeKeyCmdEnv and contains the Bech32-encoded age public key
38+
// for which the private key should be returned.
39+
SopsAgeRecipientEnv = "SOPS_AGE_RECIPIENT"
40+
// SopsAgeSshPrivateKeyCmdEnv can be set as an environment variable with a command
41+
// to execute that returns the private SSH key.
42+
SopsAgeSshPrivateKeyCmdEnv = "SOPS_AGE_SSH_PRIVATE_KEY_CMD"
3543
// SopsAgeSshPrivateKeyFileEnv can be set as an environment variable pointing to
3644
// a private SSH key file.
3745
SopsAgeSshPrivateKeyFileEnv = "SOPS_AGE_SSH_PRIVATE_KEY_FILE"
@@ -286,11 +294,35 @@ func (key *MasterKey) TypeToIdentifier() string {
286294
return KeyTypeIdentifier
287295
}
288296

289-
// loadAgeSSHIdentity attempts to load the age SSH identity based on an SSH
290-
// private key from the SopsAgeSshPrivateKeyFileEnv environment variable. If the
291-
// environment variable is not present, it will fall back to `~/.ssh/id_ed25519`
292-
// or `~/.ssh/id_rsa`. If no age SSH identity is found, it will return nil.
293-
func loadAgeSSHIdentities() ([]age.Identity, []string, errSet) {
297+
// getOutputFromCmd executes a shell command provided in param 'cmdString',
298+
// optionally adding env vars provided in param 'envVars',
299+
// and returns the command's output and error
300+
func getOutputFromCmd(cmdString string, envVars []string) ([]byte, error) {
301+
var out []byte
302+
303+
args, err := shlex.Split(cmdString)
304+
if err != nil {
305+
return nil, fmt.Errorf("failed to parse command %s: %w", cmdString, err)
306+
}
307+
cmd := exec.Command(args[0], args[1:]...)
308+
if envVars != nil {
309+
cmd.Env = append(os.Environ(), envVars[0:]...)
310+
}
311+
out, err = cmd.Output()
312+
if err != nil {
313+
return nil, fmt.Errorf("failed to execute command %s: %w", cmdString, err)
314+
}
315+
316+
return out, nil
317+
}
318+
319+
// loadAgeSSHIdentity attempts to load age SSH identities in this order:
320+
// 1. An SSH private key from the SopsAgeSshPrivateKeyFileEnv environment variable.
321+
// 2. An SSH private key returned by executing the command from the
322+
// SopsAgeSshPrivateKeyCmdEnv environment variable
323+
// 3. `~/.ssh/id_ed25519` or `~/.ssh/id_rsa`.
324+
// If no age SSH identity is found, it will return nil.
325+
func (key *MasterKey) loadAgeSSHIdentities() ([]age.Identity, []string, errSet) {
294326
var identities []age.Identity
295327
var unusedLocations []string
296328
var errs errSet
@@ -307,6 +339,23 @@ func loadAgeSSHIdentities() ([]age.Identity, []string, errSet) {
307339
unusedLocations = append(unusedLocations, SopsAgeSshPrivateKeyFileEnv)
308340
}
309341

342+
sshKeyCmd, ok := os.LookupEnv(SopsAgeSshPrivateKeyCmdEnv)
343+
if ok {
344+
out, err := getOutputFromCmd(sshKeyCmd, []string{fmt.Sprintf("%s=%s", SopsAgeRecipientEnv, key.Recipient)})
345+
if err != nil {
346+
errs = append(errs, err)
347+
} else {
348+
identity, err := parseSSHIdentityFromPrivateKeyCmdOutput(out)
349+
if err != nil {
350+
errs = append(errs, err)
351+
} else {
352+
identities = append(identities, identity)
353+
}
354+
}
355+
} else {
356+
unusedLocations = append(unusedLocations, SopsAgeSshPrivateKeyCmdEnv)
357+
}
358+
310359
userHomeDir, err := os.UserHomeDir()
311360
if err != nil {
312361
errs = append(errs, err)
@@ -355,7 +404,7 @@ func getUserConfigDir() (string, error) {
355404
// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all
356405
// found references, and expects at least one configuration to be present.
357406
func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
358-
identities, unusedLocations, errs := loadAgeSSHIdentities()
407+
identities, unusedLocations, errs := key.loadAgeSSHIdentities()
359408

360409
var readers = make(map[string]io.Reader, 0)
361410

@@ -378,16 +427,11 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
378427
}
379428

380429
if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok {
381-
args, err := shlex.Split(ageKeyCmd)
430+
out, err := getOutputFromCmd(ageKeyCmd, []string{fmt.Sprintf("%s=%s", SopsAgeRecipientEnv, key.Recipient)})
382431
if err != nil {
383-
errs = append(errs, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err))
432+
errs = append(errs, err)
384433
} else {
385-
out, err := exec.Command(args[0], args[1:]...).Output()
386-
if err != nil {
387-
errs = append(errs, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err))
388-
} else {
389-
readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out)
390-
}
434+
readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out)
391435
}
392436
} else {
393437
unusedLocations = append(unusedLocations, SopsAgeKeyCmdEnv)
@@ -496,3 +540,16 @@ func parseIdentity(s string) (age.Identity, error) {
496540
return nil, fmt.Errorf("unknown identity type")
497541
}
498542
}
543+
544+
// parseSSHIdentityFromPrivateKeyCmdOutput returns an age.Identity from the given
545+
// private key. Note that encrypted private keys are not supported.
546+
func parseSSHIdentityFromPrivateKeyCmdOutput(key []byte) (age.Identity, error) {
547+
id, err := agessh.ParseIdentity(key)
548+
if sshErr, ok := err.(*ssh.PassphraseMissingError); ok {
549+
return nil, fmt.Errorf("the SSH key returned by running SOPS_AGE_SSH_PRIVATE_KEY_CMD is password protected, which is unsupported. (%q)", sshErr)
550+
}
551+
if err != nil {
552+
return nil, fmt.Errorf("malformed SSH identity returned by running SOPS_AGE_SSH_PRIVATE_KEY_CMD: %q", err)
553+
}
554+
return id, nil
555+
}

0 commit comments

Comments
 (0)