1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00
wtf/cfg/secrets.go
Sam Roberts 3c405da087 Use docker-credential-helper to manage secrets
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.
2020-05-10 19:26:32 -07:00

225 lines
5.1 KiB
Go

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 SecretLoadParams struct {
name string
globalConfig *config.Config
service string
secret *string
}
// Load module secrets.
//
// The credential helpers impose this structure:
//
// SERVICE is mapped to a SECRET and USERNAME
//
// Only SECRET is secret, SERVICE and USERNAME are not, so this
// API doesn't expose USERNAME.
//
// SERVICE was intended to be the URL of an API server, but
// for hosted services that do not have or need a configurable
// API server, its easier to just use the module name as the
// SERVICE:
//
// cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()
//
// The user will use the module name as the service, and the API key as
// the secret, for example:
//
// % wtfutil save-secret circleci
// Secret: ...
//
// If a module (such as pihole, jenkins, or github) might have multiple
// instantiations each using a different API service (with its own unique
// API key), then the module should use the API URL to lookup the secret.
// For example, for github:
//
// cfg.ModuleSecret(name, globalConfig, &settings.apiKey).
// Service(settings.baseURL).
// Load()
//
// The user will use the API URL as the service, and the API key as the
// secret, for example, with github configured as:
//
// -- config.yml
// mods:
// github:
// baseURL: "https://github.mycompany.com/api/v3"
// ...
//
// the secret must be saved as:
//
// % wtfutil save-secret https://github.mycompany.com/api/v3
// Secret: ...
//
// If baseURL is not set in the configuration it will be the modules
// default, and the SERVICE will default to the module name, "github",
// and the user must save the secret as:
//
// % wtfutil save-secret github
// Secret: ...
//
// Ideally, the individual module documentation would describe the
// SERVICE name to use to save the secret.
func ModuleSecret(name string, globalConfig *config.Config, secret *string) *SecretLoadParams {
return &SecretLoadParams{
name: name,
globalConfig: globalConfig,
secret: secret,
service: name, // Default the service to the module name
}
}
func (slp *SecretLoadParams) Service(service string) *SecretLoadParams {
if service != "" {
slp.service = service
}
return slp
}
func (slp *SecretLoadParams) Load() {
configureSecret(
slp.globalConfig,
slp.service,
slp.secret,
)
}
type Secret struct {
Service string
Secret string
Username string
Store string
}
func configureSecret(
globalConfig *config.Config,
service string,
secret *string,
) {
if service == "" {
return
}
if secret == nil {
return
}
// Don't overwrite the secret if it was configured with yaml
if *secret != "" {
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 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),
}
}