Skip to content

Add linux kernel keyring based credential helper #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
.PHONY: all deps osxkeychain secretservice test validate wincred pass deb
.PHONY: all deps osxkeychain secretservice test validate wincred pass keyctl deb

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

all: test

deps:
go get -u golang.org/x/lint/golint
go get -d golang.org/x/lint/golint

clean:
rm -rf bin
Expand All @@ -29,14 +29,21 @@ pass:
mkdir -p bin
go build -o bin/docker-credential-pass pass/cmd/main.go

keyctl:
set -x
mkdir -p bin
go build -o bin/docker-credential-keyctl keyctl/cmd/main.go

wincred:
mkdir -p bin
go build -o bin/docker-credential-wincred.exe wincred/cmd/main_windows.go

linuxrelease:
set -x
mkdir -p release
cd bin && tar cvfz ../release/docker-credential-pass-v$(VERSION)-amd64.tar.gz docker-credential-pass
cd bin && tar cvfz ../release/docker-credential-secretservice-v$(VERSION)-amd64.tar.gz docker-credential-secretservice
cd bin && tar cvfz ../release/docker-credential-keyctl-v$(VERSION)-amd64.tar.gz docker-credential-keyctl

osxrelease:
mkdir -p release
Expand All @@ -61,6 +68,8 @@ vet_osx:

vet_linux:
go vet ./secretservice
go vet ./keyctl
go vet ./pass

lint:
for p in `go list ./... | grep -v /vendor/`; do \
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ You can see examples of each function in the [client](https://godoc.org/github.c
2. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
3. wincred: Provides a helper to use Windows credentials manager as store.
4. pass: Provides a helper to use `pass` as credentials store.
5. keyctl: Provides a kernel keyring based helper as credential store. It is a purely non-file based credential store.

#### Note

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

`keyctl` does not need any configuration except that kernel should be compiled with CONFIG_KEYS enabled, which is default in most distro kernels.

## Development

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:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ go 1.13

require (
github.com/danieljoos/wincred v1.1.2
github.com/jsipprell/keyctl v1.0.0
github.com/pkg/errors v0.9.1
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuA
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jsipprell/keyctl v1.0.0 h1:eMMJGk3UEsCXQegACLlfIjnfPlYHV61qfGXZ9/axBn4=
github.com/jsipprell/keyctl v1.0.0/go.mod h1:64s6WpBtruURX3w8W/vhWj1/uh+nOm7vUXSJlK5+KMs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
Expand Down
10 changes: 10 additions & 0 deletions keyctl/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/keyctl"
)

func main() {
credentials.Serve(keyctl.Keyctl{})
}
250 changes: 250 additions & 0 deletions keyctl/keyctl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// Package keyctl implements a `keyctl` based credential helper. Passwords are stored
// in linux kernel keyring.
package keyctl

import (
"bytes"
"encoding/base64"
"fmt"
"os"
"os/exec"
"strconv"
"strings"

"github.com/docker/docker-credential-helpers/credentials"
"github.com/jsipprell/keyctl"
"github.com/pkg/errors"
)

// Keyctl based credential helper looks for a default keyring inside
// session keyring. It does all operations inside the default keyring

const defaultKeyringName string = "keyctlCredsStore"
const persistent int = 1

// Keyctl handles secrets using Linux Kernel keyring mechanism
type Keyctl struct{}

func (k Keyctl) createDefaultPersistentKeyring() (string, error) {
/* Create default persistent keyring. If the keyring for the user
* already exists, then it returns the id of the existing keyring
*/
var errout, out bytes.Buffer
uid := os.Getuid()
cmd := exec.Command("keyctl", "get_persistent", "@u", strconv.Itoa(uid))
cmd.Stderr = &errout
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", errors.Wrapf(err, "cannot run keyctl command to create persistent keyring %+v: %s", err, errout.String())
}
persistentKeyringID := out.String()
if err != nil {
return "", errors.Wrapf(err, "cannot create or read persistent keyring %+v", err)
}
return persistentKeyringID, nil
}

func (k Keyctl) getDefaultCredsStoreFromPersistent() (keyctl.NamedKeyring, error) {
var out, errout bytes.Buffer
persistentKeyringID, err := k.createDefaultPersistentKeyring()
if err != nil {
return nil, errors.Wrap(err, "default persistent keyring cannot be created")
}

defaultSessionKeyring, err := keyctl.SessionKeyring()
if err != nil {
return nil, errors.New("errors getting session keyring")
}

defaultKeyring, err := keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
/* if already does not exist we create */
if err != nil || defaultKeyring == nil {
cmd := exec.Command("keyctl", "newring", defaultKeyringName, strings.TrimSuffix(persistentKeyringID, "\n"))
cmd.Stdout = &out
cmd.Stderr = &errout
err := cmd.Run()
if err != nil {
return nil, errors.Wrapf(err, "cannot run keyctl command to created credstore keyring %s %s %s", cmd.String(), errout.String(), out.String())
}
}
/* Search for it again and return the default keyring*/
defaultKeyring, err = keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup default session keyring")
}

return defaultKeyring, nil
}

// getDefaultCredsStore is a helper function to get the default credsStore keyring
func (k Keyctl) getDefaultCredsStore() (keyctl.NamedKeyring, error) {
if persistent == 1 {
return k.getDefaultCredsStoreFromPersistent()
}
defaultSessionKeyring, err := keyctl.SessionKeyring()
if err != nil {
return nil, errors.Wrap(err, "error getting session keyring")
}

defaultKeyring, err := keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
if err != nil || defaultKeyring == nil {
if defaultKeyring == nil {
defaultKeyring, err = keyctl.CreateKeyring(defaultSessionKeyring, defaultKeyringName)
if err != nil {
return nil, errors.Wrap(err, "failed to create default credsStore keyring")
}
}
}

if defaultKeyring == nil {
return nil, errors.Wrap(errors.New(""), " nil credstore")
}

return defaultKeyring, nil
}

// Add adds new credentials to the keychain.
func (k Keyctl) Add(creds *credentials.Credentials) error {
defaultKeyring, err := k.getDefaultCredsStore()
if err != nil || defaultKeyring == nil {
return errors.Wrapf(err, "failed to create credsStore entry for %s", creds.ServerURL)
}

// create a child keyring under default for given url
encoded := base64.URLEncoding.EncodeToString([]byte(strings.TrimSuffix(creds.ServerURL, "\n")))
urlKeyring, err := keyctl.CreateKeyring(defaultKeyring, encoded)
if err != nil {
return errors.Wrapf(err, "failed to create keyring for %s", creds.ServerURL)
}

_, err = urlKeyring.Add(creds.Username, []byte(creds.Secret))
if err != nil {
return errors.Wrapf(err, "failed to add creds to keryring for %s with error: %+v", creds.ServerURL, err)
}
return err
}

// searchHelper function searches for an url inside the default keyring.
func (k Keyctl) searchHelper(serverURL string) (keyctl.NamedKeyring, string, error) {
defaultKeyring, err := k.getDefaultCredsStore()
if err != nil || defaultKeyring == nil {
return nil, "", fmt.Errorf("searchHelper failed: cannot read defaultCredsStore")
}

encoded := base64.URLEncoding.EncodeToString([]byte(strings.TrimSuffix(serverURL, "\n")))
urlKeyring, err := keyctl.OpenKeyring(defaultKeyring, encoded)
if err != nil {
return nil, "", fmt.Errorf("error in reading credsStore for url %s", serverURL)
}
if urlKeyring == nil {
return nil, "", fmt.Errorf("credsStore entry for suplied url %s not found", serverURL)
}

refs, err := keyctl.ListKeyring(urlKeyring)
if err != nil {
return nil, "", fmt.Errorf("key for server url not found")
}
if len(refs) < 1 {
return nil, "", fmt.Errorf("no keys in keyring %s", urlKeyring.Name())
}

obj := refs[0]
id, err := obj.Get()
if err != nil {
return nil, "", fmt.Errorf("key for server url not found")
}

info, err := id.Info()
if err != nil {
return nil, "", fmt.Errorf("cannot read info for url key")
}

return urlKeyring, info.Name, err
}

// Get returns the username and secret to use for a given registry server URL.
func (k Keyctl) Get(serverURL string) (string, string, error) {
if serverURL == "" {
return "", "", errors.New("missing server url")
}

serverURL = strings.TrimSuffix(serverURL, "\n")
urlKeyring, searchData, err := k.searchHelper(serverURL)
if err != nil {
return "", "", errors.Wrapf(err, "url not found by searchHelper: %s err: %v", serverURL, err)
}
key, err := urlKeyring.Search(searchData)
if err != nil {
return "", "", errors.Wrapf(err, "url not found in %+v", urlKeyring)
}
secret, err := key.Get()
if err != nil {
return "", "", errors.Wrapf(err, "failed to read credentials for %s:%s", serverURL, searchData)
}

return searchData, string(secret), nil
}

// Delete removes credentials from the store.
func (k Keyctl) Delete(serverURL string) error {
serverURL = strings.TrimSuffix(serverURL, "\n")
urlKeyring, searchData, err := k.searchHelper(serverURL)
if err != nil {
return errors.Wrapf(err, "cannot find server url %s", serverURL)
}

key, err := urlKeyring.Search(searchData)
if err != nil {
return err
}

err = key.Unlink()
if err != nil {
return err
}

refs, err := keyctl.ListKeyring(urlKeyring)
if err != nil {
fmt.Printf("cannot list keyring %s", urlKeyring.Name())
}
if len(refs) == 0 {
keyctl.UnlinkKeyring(urlKeyring)
} else {
return errors.Wrapf(err, "Canot remove keyring as its not empty %s", urlKeyring.Name())
}

return err
}

// List returns the stored URLs and corresponding usernames for a given credentials label
func (k Keyctl) List() (map[string]string, error) {
defaultKeyring, err := k.getDefaultCredsStore()
if err != nil || defaultKeyring == nil {
return nil, errors.Wrap(err, "List() failed: cannot read default credStore")
}

resp := map[string]string{}

refs, err := keyctl.ListKeyring(defaultKeyring)
if err != nil {
return nil, err
}

for _, r := range refs {
id, _ := r.Get()
info, _ := id.Info()
url, _ := base64.URLEncoding.DecodeString(info.Name)

key, _ := keyctl.OpenKeyring(defaultKeyring, info.Name)
innerRefs, _ := keyctl.ListKeyring(key)

if len(innerRefs) < 1 {
continue
}
k, _ := innerRefs[0].Get()
i, _ := k.Info()
resp[string(url)] = i.Name
}
return resp, nil
}
Loading