Skip to content

Commit 8aa8e4d

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 ebd9dc6 commit 8aa8e4d

31 files changed

+2135
-1
lines changed

Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,18 @@ RUN --mount=type=bind,target=. \
102102
--mount=type=bind,from=version,source=/tmp/.ldflags,target=/tmp/.ldflags <<EOT
103103
set -ex
104104
mkdir /out
105+
106+
# pass credential helper
105107
xx-go build -ldflags "$(cat /tmp/.ldflags)" -o /out/docker-credential-pass-${TARGETOS}-${TARGETARCH}${TARGETVARIANT} ./pass/cmd/
106108
xx-verify /out/docker-credential-pass-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}
109+
110+
# secretservice credential helper
107111
xx-go build -ldflags "$(cat /tmp/.ldflags)" -o /out/docker-credential-secretservice-${TARGETOS}-${TARGETARCH}${TARGETVARIANT} ./secretservice/cmd/
108112
xx-verify /out/docker-credential-secretservice-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}
113+
114+
# keyctl credential helper
115+
xx-go build -ldflags "$(cat /tmp/.ldflags)" -o /out/docker-credential-keyctl-${TARGETOS}-${TARGETARCH}${TARGETVARIANT} ./keyctl/cmd/
116+
xx-verify /out/docker-credential-keyctl-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}
109117
EOT
110118

111119
FROM base AS build-darwin

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all osxkeychain secretservice test lint validate-vendor fmt validate wincred pass deb vendor
1+
.PHONY: all osxkeychain secretservice test lint validate-vendor fmt validate wincred pass keyctl deb vendor
22

33
VERSION := $(shell grep 'const Version' credentials/version.go | awk -F'"' '{ print $$2 }')
44

@@ -25,14 +25,21 @@ pass:
2525
mkdir -p bin
2626
go build -o bin/docker-credential-pass pass/cmd/
2727

28+
keyctl:
29+
set -x
30+
mkdir -p bin
31+
go build -o bin/docker-credential-keyctl keyctl/cmd/
32+
2833
wincred:
2934
mkdir -p bin
3035
go build -o bin/docker-credential-wincred.exe wincred/cmd/
3136

3237
linuxrelease:
38+
set -x
3339
mkdir -p release
3440
cd bin && tar cvfz ../release/docker-credential-pass-v$(VERSION)-amd64.tar.gz docker-credential-pass
3541
cd bin && tar cvfz ../release/docker-credential-secretservice-v$(VERSION)-amd64.tar.gz docker-credential-secretservice
42+
cd bin && tar cvfz ../release/docker-credential-keyctl-v$(VERSION)-amd64.tar.gz docker-credential-keyctl
3643

3744
osxrelease:
3845
mkdir -p release

README.md

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

8586
#### Note
8687

8788
`pass` needs to be configured for `docker-credential-pass` to work properly.
8889
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`.
8990

91+
`keyctl` does not need any configuration except that kernel should be compiled with CONFIG_KEYS enabled, which is default in most distro kernels.
92+
9093
## Development
9194

9295
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ go 1.18
44

55
require (
66
github.com/danieljoos/wincred v1.1.2
7+
github.com/jsipprell/keyctl v1.0.0
8+
github.com/pkg/errors v0.9.1
79
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c
810
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuA
22
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
33
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
44
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/jsipprell/keyctl v1.0.0 h1:eMMJGk3UEsCXQegACLlfIjnfPlYHV61qfGXZ9/axBn4=
6+
github.com/jsipprell/keyctl v1.0.0/go.mod h1:64s6WpBtruURX3w8W/vhWj1/uh+nOm7vUXSJlK5+KMs=
7+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
8+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
59
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
610
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
711
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=

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)