Skip to content

Commit 8d087d0

Browse files
alakeshthaJeztah
authored andcommitted
Add linux kernel keyring based credential helper
Implement kernel kerying based credential helper for storing and retrieving secrets. Signed-off-by: Alakesh Haloi <[email protected]> Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent 6b9df3e commit 8d087d0

31 files changed

+2128
-2
lines changed

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ RUN --mount=type=bind,target=. \
109109
make build-pass build-secretservice PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
110110
xx-verify /out/docker-credential-pass
111111
xx-verify /out/docker-credential-secretservice
112+
113+
# keyctl credential helper
114+
xx-go build -ldflags "$(cat /tmp/.ldflags)" -o /out/docker-credential-keyctl ./keyctl/cmd/
115+
xx-verify /out/docker-credential-keyctl
112116
EOT
113117

114118
FROM base AS build-darwin

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ build-%: # build, can be one of build-osxkeychain build-pass build-secretservice
2727
go build -trimpath -ldflags="$(GO_LDFLAGS) -X ${GO_PKG}/credentials.Name=docker-credential-$*" -o "$(DESTDIR)/docker-credential-$*" ./$*/cmd/
2828

2929
# aliases for build-* targets
30-
.PHONY: osxkeychain secretservice pass wincred
30+
.PHONY: osxkeychain secretservice pass wincred keyctl
3131
osxkeychain: build-osxkeychain
3232
secretservice: build-secretservice
3333
pass: build-pass
3434
wincred: build-wincred
35+
keyctl: build-keyctl
3536

3637
.PHONY: cross
3738
cross: # cross build all supported credential helpers

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@ You can see examples of each function in the [client](https://godoc.org/github.c
8484
2. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
8585
3. wincred: Provides a helper to use Windows credentials manager as store.
8686
4. pass: Provides a helper to use `pass` as credentials store.
87+
5. keyctl: Provides a kernel keyring based helper as credential store. It is a purely non-file based credential store.
8788

8889
#### Note
8990

9091
`pass` needs to be configured for `docker-credential-pass` to work properly.
9192
It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`.
9293

94+
`keyctl` does not need any configuration except that kernel should be compiled with CONFIG_KEYS enabled, which is default in most distro kernels.
95+
9396
## Development
9497

9598
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are four valid values:

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ module github.com/docker/docker-credential-helpers
22

33
go 1.19
44

5-
require github.com/danieljoos/wincred v1.2.1
5+
require (
6+
github.com/danieljoos/wincred v1.2.1
7+
github.com/jsipprell/keyctl v1.0.0
8+
github.com/pkg/errors v0.9.1
9+
)
610

711
require golang.org/x/sys v0.15.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
22
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
33
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/jsipprell/keyctl v1.0.0 h1:eMMJGk3UEsCXQegACLlfIjnfPlYHV61qfGXZ9/axBn4=
5+
github.com/jsipprell/keyctl v1.0.0/go.mod h1:64s6WpBtruURX3w8W/vhWj1/uh+nOm7vUXSJlK5+KMs=
6+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
7+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
48
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
59
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
610
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=

keyctl/cmd/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package main
2+
3+
import (
4+
"github.com/docker/docker-credential-helpers/credentials"
5+
"github.com/docker/docker-credential-helpers/keyctl"
6+
)
7+
8+
func main() {
9+
credentials.Serve(keyctl.Keyctl{})
10+
}

keyctl/keyctl.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Package keyctl implements a `keyctl` based credential helper. Passwords are stored
2+
// in linux kernel keyring.
3+
package keyctl
4+
5+
import (
6+
"bytes"
7+
"encoding/base64"
8+
"fmt"
9+
"os"
10+
"os/exec"
11+
"strconv"
12+
"strings"
13+
14+
"github.com/docker/docker-credential-helpers/credentials"
15+
"github.com/jsipprell/keyctl"
16+
"github.com/pkg/errors"
17+
)
18+
19+
// Keyctl based credential helper looks for a default keyring inside
20+
// session keyring. It does all operations inside the default keyring
21+
22+
const defaultKeyringName string = "keyctlCredsStore"
23+
const persistent int = 1
24+
25+
// Keyctl handles secrets using Linux Kernel keyring mechanism
26+
type Keyctl struct{}
27+
28+
func (k Keyctl) createDefaultPersistentKeyring() (string, error) {
29+
/* Create default persistent keyring. If the keyring for the user
30+
* already exists, then it returns the id of the existing keyring
31+
*/
32+
var errout, out bytes.Buffer
33+
uid := os.Getuid()
34+
cmd := exec.Command("keyctl", "get_persistent", "@u", strconv.Itoa(uid))
35+
cmd.Stderr = &errout
36+
cmd.Stdout = &out
37+
err := cmd.Run()
38+
if err != nil {
39+
return "", errors.Wrapf(err, "cannot run keyctl command to create persistent keyring %+v: %s", err, errout.String())
40+
}
41+
persistentKeyringID := out.String()
42+
if err != nil {
43+
return "", errors.Wrapf(err, "cannot create or read persistent keyring %+v", err)
44+
}
45+
return persistentKeyringID, nil
46+
}
47+
48+
func (k Keyctl) getDefaultCredsStoreFromPersistent() (keyctl.NamedKeyring, error) {
49+
var out, errout bytes.Buffer
50+
persistentKeyringID, err := k.createDefaultPersistentKeyring()
51+
if err != nil {
52+
return nil, errors.Wrap(err, "default persistent keyring cannot be created")
53+
}
54+
55+
defaultSessionKeyring, err := keyctl.SessionKeyring()
56+
if err != nil {
57+
return nil, errors.New("errors getting session keyring")
58+
}
59+
60+
defaultKeyring, err := keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
61+
/* if already does not exist we create */
62+
if err != nil || defaultKeyring == nil {
63+
cmd := exec.Command("keyctl", "newring", defaultKeyringName, strings.TrimSuffix(persistentKeyringID, "\n"))
64+
cmd.Stdout = &out
65+
cmd.Stderr = &errout
66+
err := cmd.Run()
67+
if err != nil {
68+
return nil, errors.Wrapf(err, "cannot run keyctl command to created credstore keyring %s %s %s", cmd.String(), errout.String(), out.String())
69+
}
70+
}
71+
/* Search for it again and return the default keyring*/
72+
defaultKeyring, err = keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
73+
if err != nil {
74+
return nil, errors.Wrap(err, "failed to lookup default session keyring")
75+
}
76+
77+
return defaultKeyring, nil
78+
}
79+
80+
// getDefaultCredsStore is a helper function to get the default credsStore keyring
81+
func (k Keyctl) getDefaultCredsStore() (keyctl.NamedKeyring, error) {
82+
if persistent == 1 {
83+
return k.getDefaultCredsStoreFromPersistent()
84+
}
85+
defaultSessionKeyring, err := keyctl.SessionKeyring()
86+
if err != nil {
87+
return nil, errors.Wrap(err, "error getting session keyring")
88+
}
89+
90+
defaultKeyring, err := keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
91+
if err != nil || defaultKeyring == nil {
92+
if defaultKeyring == nil {
93+
defaultKeyring, err = keyctl.CreateKeyring(defaultSessionKeyring, defaultKeyringName)
94+
if err != nil {
95+
return nil, errors.Wrap(err, "failed to create default credsStore keyring")
96+
}
97+
}
98+
}
99+
100+
if defaultKeyring == nil {
101+
return nil, errors.Wrap(errors.New(""), " nil credstore")
102+
}
103+
104+
return defaultKeyring, nil
105+
}
106+
107+
// Add adds new credentials to the keychain.
108+
func (k Keyctl) Add(creds *credentials.Credentials) error {
109+
defaultKeyring, err := k.getDefaultCredsStore()
110+
if err != nil || defaultKeyring == nil {
111+
return errors.Wrapf(err, "failed to create credsStore entry for %s", creds.ServerURL)
112+
}
113+
114+
// create a child keyring under default for given url
115+
encoded := base64.URLEncoding.EncodeToString([]byte(strings.TrimSuffix(creds.ServerURL, "\n")))
116+
urlKeyring, err := keyctl.CreateKeyring(defaultKeyring, encoded)
117+
if err != nil {
118+
return errors.Wrapf(err, "failed to create keyring for %s", creds.ServerURL)
119+
}
120+
121+
_, err = urlKeyring.Add(creds.Username, []byte(creds.Secret))
122+
if err != nil {
123+
return errors.Wrapf(err, "failed to add creds to keryring for %s with error: %+v", creds.ServerURL, err)
124+
}
125+
return err
126+
}
127+
128+
// searchHelper function searches for an url inside the default keyring.
129+
func (k Keyctl) searchHelper(serverURL string) (keyctl.NamedKeyring, string, error) {
130+
defaultKeyring, err := k.getDefaultCredsStore()
131+
if err != nil || defaultKeyring == nil {
132+
return nil, "", fmt.Errorf("searchHelper failed: cannot read defaultCredsStore")
133+
}
134+
135+
encoded := base64.URLEncoding.EncodeToString([]byte(strings.TrimSuffix(serverURL, "\n")))
136+
urlKeyring, err := keyctl.OpenKeyring(defaultKeyring, encoded)
137+
if err != nil {
138+
return nil, "", fmt.Errorf("error in reading credsStore for url %s", serverURL)
139+
}
140+
if urlKeyring == nil {
141+
return nil, "", fmt.Errorf("credsStore entry for suplied url %s not found", serverURL)
142+
}
143+
144+
refs, err := keyctl.ListKeyring(urlKeyring)
145+
if err != nil {
146+
return nil, "", fmt.Errorf("key for server url not found")
147+
}
148+
if len(refs) < 1 {
149+
return nil, "", fmt.Errorf("no keys in keyring %s", urlKeyring.Name())
150+
}
151+
152+
obj := refs[0]
153+
id, err := obj.Get()
154+
if err != nil {
155+
return nil, "", fmt.Errorf("key for server url not found")
156+
}
157+
158+
info, err := id.Info()
159+
if err != nil {
160+
return nil, "", fmt.Errorf("cannot read info for url key")
161+
}
162+
163+
return urlKeyring, info.Name, err
164+
}
165+
166+
// Get returns the username and secret to use for a given registry server URL.
167+
func (k Keyctl) Get(serverURL string) (string, string, error) {
168+
if serverURL == "" {
169+
return "", "", errors.New("missing server url")
170+
}
171+
172+
serverURL = strings.TrimSuffix(serverURL, "\n")
173+
urlKeyring, searchData, err := k.searchHelper(serverURL)
174+
if err != nil {
175+
return "", "", errors.Wrapf(err, "url not found by searchHelper: %s err: %v", serverURL, err)
176+
}
177+
key, err := urlKeyring.Search(searchData)
178+
if err != nil {
179+
return "", "", errors.Wrapf(err, "url not found in %+v", urlKeyring)
180+
}
181+
secret, err := key.Get()
182+
if err != nil {
183+
return "", "", errors.Wrapf(err, "failed to read credentials for %s:%s", serverURL, searchData)
184+
}
185+
186+
return searchData, string(secret), nil
187+
}
188+
189+
// Delete removes credentials from the store.
190+
func (k Keyctl) Delete(serverURL string) error {
191+
serverURL = strings.TrimSuffix(serverURL, "\n")
192+
urlKeyring, searchData, err := k.searchHelper(serverURL)
193+
if err != nil {
194+
return errors.Wrapf(err, "cannot find server url %s", serverURL)
195+
}
196+
197+
key, err := urlKeyring.Search(searchData)
198+
if err != nil {
199+
return err
200+
}
201+
202+
err = key.Unlink()
203+
if err != nil {
204+
return err
205+
}
206+
207+
refs, err := keyctl.ListKeyring(urlKeyring)
208+
if err != nil {
209+
fmt.Printf("cannot list keyring %s", urlKeyring.Name())
210+
}
211+
if len(refs) == 0 {
212+
keyctl.UnlinkKeyring(urlKeyring)
213+
} else {
214+
return errors.Wrapf(err, "Canot remove keyring as its not empty %s", urlKeyring.Name())
215+
}
216+
217+
return err
218+
}
219+
220+
// List returns the stored URLs and corresponding usernames for a given credentials label
221+
func (k Keyctl) List() (map[string]string, error) {
222+
defaultKeyring, err := k.getDefaultCredsStore()
223+
if err != nil || defaultKeyring == nil {
224+
return nil, errors.Wrap(err, "List() failed: cannot read default credStore")
225+
}
226+
227+
resp := map[string]string{}
228+
229+
refs, err := keyctl.ListKeyring(defaultKeyring)
230+
if err != nil {
231+
return nil, err
232+
}
233+
234+
for _, r := range refs {
235+
id, _ := r.Get()
236+
info, _ := id.Info()
237+
url, _ := base64.URLEncoding.DecodeString(info.Name)
238+
239+
key, _ := keyctl.OpenKeyring(defaultKeyring, info.Name)
240+
innerRefs, _ := keyctl.ListKeyring(key)
241+
242+
if len(innerRefs) < 1 {
243+
continue
244+
}
245+
k, _ := innerRefs[0].Get()
246+
i, _ := k.Info()
247+
resp[string(url)] = i.Name
248+
}
249+
return resp, nil
250+
}

0 commit comments

Comments
 (0)