1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Use docker-credential-helper to manage secrets (WIP)

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.
This commit is contained in:
Sam Roberts
2020-03-31 10:45:16 -07:00
parent cc8f5f76ba
commit b2982cc668
31 changed files with 484 additions and 1 deletions

164
cfg/secrets.go Normal file
View File

@@ -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, defaults 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),
}
}