Skip to content

Commit

Permalink
Use docker-credential-helper to manage secrets (WIP)
Browse files Browse the repository at this point in the history
Store service credentials securely in the stores supported by docker:
- https://github.com/docker/docker-credential-helpers#available-programs

Introduces a top-level config property, "secretStore" and additional
command line arguments to manage the stored secrets.

The value of secretStore is used to find a helper command,
`docker-credential-<secretStore>`.

The docker project currently provides 4 store helpers:
- "osxkeychain" (OS X only)
- "secretservice" (Linux only)
- "wincred" (Windows only)
- "pass" (any OS supporting pass, which uses gpg2)

Docker-for-desktop installs the credential helpers above, as well as
"desktop" (docker-credential-desktop).

Generic installation instructions for the helpers:
- https://github.com/docker/docker-credential-helpers#installation

Users could provide additional helpers, the only requirement is that the
helper implements the credential store protocol:
- https://github.com/docker/docker-credential-helpers#development

The credential protocol is open, and new credential stores can be
implemented by any CLI satisfying the protocol:
- https://github.com/docker/docker-credential-helpers#development

The modifications to existing modules is not tested due to lack
of API keys, but demonstrates the unobtrusive changes required to
use the secret store.
  • Loading branch information
sam-github committed Apr 2, 2020
1 parent a7d615b commit a9fdf77
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 8 deletions.
164 changes: 164 additions & 0 deletions cfg/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package cfg

import (
"errors"
"fmt"
"runtime"

"github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/olebedev/config"
"github.com/wtfutil/wtf/logger"
)

type Secret struct {
Service string
Secret string
Username string
Store string
}

// Configure the secret for a service.
//
// Does not overwrite explicitly configured values, so is safe to call
// if username and secret were explicitly set in module config.
//
// Input:
// * service: URL or identifier for service if configured by user. Not all
// modules support or need this. Optional, defatuls to serviceDefault.
// * serviceDefault: Default URL or identifier for service. Must be unique,
// using the API URL is customary, but using the module name is reasonable.
// Required, secrets cannot be stored unless associated with a service.
//
// Output:
// * username: If a user/subdomain/identifier specific to the service is
// configurable, it can be saved as a "username". Optional.
// * secret: The secret for service. Optional.
func ConfigureSecret(
globalConfig *config.Config,
service string,
serviceDefault string,
username *string,
secret *string, // unfortunate order dependency...
) {
notWanted := func(out *string) bool {
return out == nil && *out != ""
}

// Don't try to fetch from cred store if nothing is wanted.
if notWanted(secret) && notWanted(username) {
return
}

if service == "" {
service = serviceDefault
}

if service == "" {
return
}

cred, err := FetchSecret(globalConfig, service)

if err != nil {
logger.Log(fmt.Sprintf("Loading secret failed: %s", err.Error()))
return
}

if cred == nil {
// No secret store configued.
return
}

if username != nil && *username == "" {
*username = cred.Username
}

if secret != nil && *secret == "" {
*secret = cred.Secret
}
}

// Fetch secret for `service`. Service is customarily a URL, but can be any
// identifier uniquely used by wtf to identify the service, such as the name
// of the module. nil is returned if the secretStore global property is not
// present or the secret is not found in that store.
func FetchSecret(globalConfig *config.Config, service string) (*Secret, error) {
prog := newProgram(globalConfig)

if prog == nil {
// No secret store configured.
return nil, nil
}

cred, err := client.Get(prog.runner, service)

if err != nil {
return nil, fmt.Errorf("get %v from %v: %w", service, prog.store, err)
}

return &Secret{
Service: cred.ServerURL,
Secret: cred.Secret,
Username: cred.Username,
Store: prog.store,
}, nil
}

func StoreSecret(globalConfig *config.Config, secret *Secret) error {
prog := newProgram(globalConfig)

if prog == nil {
return errors.New("Cannot store secrets: wtf.secretStore is not configured")
}

cred := &credentials.Credentials{
ServerURL: secret.Service,
Username: secret.Username,
Secret: secret.Secret,
}

// docker-credential requires a username, but it isn't necessary for
// all services. Use a default if a username was not set.
if cred.Username == "" {
cred.Username = "default"
}

err := client.Store(prog.runner, cred)

if err != nil {
return fmt.Errorf("store %v: %w", prog.store, err)
}

return nil
}

type program struct {
store string
runner client.ProgramFunc
}

func newProgram(globalConfig *config.Config) *program {
secretStore := globalConfig.UString("wtf.secretStore", "(none)")

if secretStore == "(none)" {
return nil
}

if secretStore == "" {
switch runtime.GOOS {
case "windows":
secretStore = "winrt"
case "darwin":
secretStore = "osxkeychain"
default:
secretStore = "secretservice"
}

}

return &program{
secretStore,
client.NewShellProgramFunc("docker-credential-" + secretStore),
}
}
63 changes: 63 additions & 0 deletions flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,28 @@ type Flags struct {
Module string `short:"m" long:"module" optional:"yes" description:"Display info about a specific module, i.e.: 'wtfutil -m=todo'"`
Profile bool `short:"p" long:"profile" optional:"yes" description:"Profile application memory usage"`
Version bool `short:"v" long:"version" description:"Show version info"`
// Work-around go-flags misfeatures. If any sub-command is defined
// then `wtf` (no sub-commands, the common usage), is warned about.
Opt struct {
Cmd string `positional-arg-name:"command"`
Args []string `positional-arg-name:"args"`
} `positional-args:"yes"`

hasCustom bool
}

var EXTRA = `
Commands:
save-secret <service> <secret> [username]
service Service URL or name for secret.
secret Secret to be saved for the service.
username Username to associate with the service.
Save a secret into the secret store. Requires wtf.secretStore
to be configured. See individual modules for information on what
service, secret, and username means for their configuration. Not
all modules use secrets, and not all secrets require a username.
`

// NewFlags creates an instance of Flags
func NewFlags() *Flags {
flags := Flags{}
Expand All @@ -46,6 +64,50 @@ func (flags *Flags) RenderIf(version, date string, config *config.Config) {
fmt.Println(fmt.Sprintf("%s (%s)", version, date))
os.Exit(0)
}

if flags.Opt.Cmd == "" {
return
}

switch cmd := flags.Opt.Cmd; cmd {
case "save-secret":
var service, secret, username string
args := flags.Opt.Args

if len(flags.Opt.Args) < 2 || args[0] == "" {
fmt.Fprintf(os.Stderr, "save-secret: service required, see `%s --help`\n", os.Args[0])
os.Exit(1)
}

if len(flags.Opt.Args) < 2 || args[1] == "" {
fmt.Fprintf(os.Stderr, "save-secret: secret required, see `%s --help`\n", os.Args[0])
os.Exit(1)
}

service = args[0]
secret = args[1]

if len(args) > 2 {
username = args[2]
}

err := cfg.StoreSecret(config, &cfg.Secret{
Service: service,
Secret: secret,
Username: username,
})

if err != nil {
fmt.Fprintf(os.Stderr, "Saving secret for service %v: %s\n", service, err.Error())
os.Exit(1)
}

fmt.Printf("Saved secret for service %v\n", service)
os.Exit(0)
default:
fmt.Fprintf(os.Stderr, "Command `%s` is not supported, try `%s --help`\n", cmd, os.Args[0])
os.Exit(1)
}
}

// HasCustomConfig returns TRUE if a config path was passed in, FALSE if one was not
Expand All @@ -68,6 +130,7 @@ func (flags *Flags) Parse() {
parser := goFlags.NewParser(flags, goFlags.Default)
if _, err := parser.Parse(); err != nil {
if flagsErr, ok := err.(*goFlags.Error); ok && flagsErr.Type == goFlags.ErrHelp {
fmt.Println(EXTRA)
os.Exit(0)
}
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/digitalocean/godo v1.33.1
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/docker-credential-helpers v0.6.3
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0
Expand All @@ -30,7 +31,6 @@ require (
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/google/go-github/v26 v26.1.3
github.com/gophercloud/gophercloud v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hekmon/cunits v2.0.1+incompatible // indirect
github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd
github.com/imdario/mergo v0.3.8 // indirect
Expand Down
9 changes: 3 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/digitalocean/godo v1.32.0 h1:ljfhYi/IqDYiZcBYV7nUPzw1Q7NlmPSFDtI69UeRThk=
github.com/digitalocean/godo v1.32.0/go.mod h1:iJnN9rVu6K5LioLxLimlq0uRI+y/eAQjROUmeU/r0hY=
github.com/digitalocean/godo v1.33.1 h1:W5e7EgW8EVOM+ycZ6z+LU/WTZrxohIkgxbVwLOU/Q6s=
github.com/digitalocean/godo v1.33.1/go.mod h1:gfLm3JSupWD9V/ibQygXWW3IVz7hranzckH5UimhZsI=
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
Expand All @@ -143,6 +141,8 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
Expand Down Expand Up @@ -297,6 +297,7 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
github.com/hashicorp/go-cleanhttp v0.0.0-20160407174126-ad28ea4487f0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
Expand Down Expand Up @@ -469,8 +470,6 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/ncw/swift v0.0.0-20171019114456-c95c6e5c2d1a h1:SAjW6pL/9NssyKM1Qvyy5/V4kR3z76qlTbaqJLixhP4=
github.com/ncw/swift v0.0.0-20171019114456-c95c6e5c2d1a/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/nicklaw5/helix v0.5.7 h1:DvNyoKkuLYrqZv5/yugL18Ud99UeQoXzzAsg4OwU8uY=
github.com/nicklaw5/helix v0.5.7/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
github.com/nicklaw5/helix v0.5.8 h1:RG1vV/XDI6Kc0V/KvoUzRb3Q/7rmAQvVuisfxxYg1ZY=
github.com/nicklaw5/helix v0.5.8/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
Expand Down Expand Up @@ -647,8 +646,6 @@ github.com/wtfutil/spotigopher v0.0.0-20191127141047-7d8168fe103a/go.mod h1:AlO4
github.com/wtfutil/todoist v0.0.2-0.20191216004217-0ec29ceda61a h1:nD8ALd4TSo+zPHK5MqQWFj01G8fMMHFfC3rWvoq/9JA=
github.com/wtfutil/todoist v0.0.2-0.20191216004217-0ec29ceda61a/go.mod h1:YuuGLJSsTK6DGBD5Zaf3J8LSMfpEC2WtzYPey3XVOdI=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xanzy/go-gitlab v0.28.0 h1:nsyjDVvBrP4KRXEN4b1m1ewiqmTNL4BOWW041nKGV7k=
github.com/xanzy/go-gitlab v0.28.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
github.com/xanzy/go-gitlab v0.29.0 h1:9tMvAkG746eIlzcdpnRgpcKPA1woUDmldMIjR/E5OWM=
github.com/xanzy/go-gitlab v0.29.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
Expand Down
8 changes: 8 additions & 0 deletions modules/azuredevops/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,13 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co
projectName: ymlConfig.UString("projectName", os.Getenv("WTF_AZURE_DEVOPS_PROJECT_NAME")),
}

cfg.ConfigureSecret(
globalConfig,
settings.orgURL,
"",
&settings.projectName,
&settings.apiToken,
)

return &settings
}
8 changes: 8 additions & 0 deletions modules/bamboohr/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,13 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co
subdomain: ymlConfig.UString("subdomain", os.Getenv("WTF_BAMBOO_HR_SUBDOMAIN")),
}

cfg.ConfigureSecret(
globalConfig,
"",
name,
&settings.subdomain,
&settings.apiKey,
)

return &settings
}
11 changes: 10 additions & 1 deletion modules/buildkite/settings.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package buildkite

import (
"os"

"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/utils"
"os"
)

const (
Expand Down Expand Up @@ -35,6 +36,14 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co
pipelines: buildPipelineSettings(ymlConfig),
}

cfg.ConfigureSecret(
globalConfig,
"",
name,
&settings.orgSlug,
&settings.apiKey,
)

return &settings
}

Expand Down
8 changes: 8 additions & 0 deletions modules/circleci/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,13 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co
apiKey: ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_CIRCLE_API_KEY"))),
}

cfg.ConfigureSecret(
globalConfig,
"",
name,
nil,
&settings.apiKey,
)

return &settings
}
Loading

0 comments on commit a9fdf77

Please sign in to comment.