From 09b865ab2890f1af0df93ee33a7d520c14119563 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 21 Oct 2020 09:00:17 -0400 Subject: [PATCH] Add `pass` cred storage support (#731) * Add `pass` cred storage support Make it the default on non-Windows/macOS Document the token storage behavior --- CHANGELOG.md | 15 +- api/output_string.go | 11 +- go.mod | 1 + go.sum | 12 ++ internal/cmd/base/base.go | 157 +++++++++++++++--- .../cmd/commands/authenticate/password.go | 61 +++++-- internal/cmd/commands/config/token.go | 36 ++-- website/content/docs/api-clients/cli.mdx | 127 +++++++++----- 8 files changed, 326 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04682340b..40710c93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,21 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ## vNext -### What's New +### Changes/Deprecations -### Improvements +* cli: There are two changes to token storage handling: + * Specifying `none` for the `token-name` parameter has been deprecated in + favor of specifying `none` for the new `keyring-type` parameter. + * [`pass`](https://www.passwordstore.org/) is now the default keyring type on + non-Windows/non-macOS platforms. See the [CLI docs + page](https://www.boundaryproject.io/docs/api-clients/cli) for more + information. +### New and Improved + +* cli: New `keyring-type` option and `pass` keyring type for token storage + ([Issue](https://github.com/hashicorp/boundary/issues/697)) + ([PR](https://github.com/hashicorp/boundary/issues/731)) * controller: Allow API/Cluster listeners to be Unix domain sockets ([Issue](https://github.com/hashicorp/boundary/pull/699)) ([PR](https://github.com/hashicorp/boundary/pull/705)) diff --git a/api/output_string.go b/api/output_string.go index 3b3cf5d70..8ec1b573e 100644 --- a/api/output_string.go +++ b/api/output_string.go @@ -48,14 +48,15 @@ func (d *OutputStringError) parseRequest() { for k, v := range d.Request.Header { for _, h := range v { if strings.ToLower(k) == "authorization" { - env := os.Getenv("BOUNDARY_TOKEN_NAME") - switch env { - case "none": + tokenName := os.Getenv("BOUNDARY_TOKEN_NAME") + keyringType := os.Getenv("BOUNDARY_KEYRING_TYPE") + switch { + case tokenName == "none" || keyringType == "none": h = `Bearer ` - case "": + case tokenName == "" && keyringType == "": h = `Bearer $(boundary config get-token)` default: - h = fmt.Sprintf("Bearer $(boundary config get-token -token-name %s)", env) + h = fmt.Sprintf("Bearer $(boundary config get-token -keyring-type %s -token-name %s)", keyringType, tokenName) } } d.parsedCurlString = fmt.Sprintf("%s-H \"%s: %s\" ", d.parsedCurlString, k, h) diff --git a/go.mod b/go.mod index ea15add2c..2e9cb32bf 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ replace github.com/hashicorp/boundary/api => ./api replace github.com/hashicorp/boundary/sdk => ./sdk require ( + github.com/99designs/keyring v1.1.6 github.com/armon/go-metrics v0.3.4 github.com/bufbuild/buf v0.24.0 github.com/fatih/color v1.9.0 diff --git a/go.sum b/go.sum index 6948005bc..24cc74df3 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/keyring v1.1.6 h1:kVDC2uCgVwecxCk+9zoCt2uEL6dt+dfVzMvGgnVcIuM= +github.com/99designs/keyring v1.1.6/go.mod h1:16e0ds7LGQQcT59QqkTg72Hh5ShM51Byv5PEmW6uoRU= github.com/AlecAivazis/survey/v2 v2.0.5 h1:xpZp+Q55wi5C7Iaze+40onHnEkex1jSc34CltJjOoPM= github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= github.com/Azure/azure-sdk-for-go v36.2.0+incompatible h1:09cv2WoH0g6jl6m2iT+R9qcIPZKhXEL0sbmLhxP895s= @@ -252,6 +254,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b h1:HBah4D48ypg3J7Np4N+HY/ZR76fx3HEUGxDU6Uk39oQ= +github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -423,6 +427,7 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -531,6 +536,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1 github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.0-beta.5 h1:fhL1nRLxe6ppWSDwbDMhUIQVVJ8rxskTxWMBRJ8RRiI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.0-beta.5/go.mod h1:fR9dOEfEyRM7ltVH0FTpK/QA6L/5BQq8izXNRu/gyVc= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= @@ -725,6 +732,8 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -844,6 +853,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1269,6 +1280,7 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/cmd/base/base.go b/internal/cmd/base/base.go index 30a36c74d..fa89fa56e 100644 --- a/internal/cmd/base/base.go +++ b/internal/cmd/base/base.go @@ -12,17 +12,19 @@ import ( "os" "os/signal" "regexp" + "runtime" "strings" "sync" "syscall" + nkeyring "github.com/99designs/keyring" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/sdk/wrapper" "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/posener/complete" - "github.com/zalando/go-keyring" + zkeyring "github.com/zalando/go-keyring" ) const ( @@ -31,6 +33,7 @@ const ( envToken = "BOUNDARY_TOKEN" EnvTokenName = "BOUNDARY_TOKEN_NAME" + EnvKeyringType = "BOUNDARY_KEYRING_TYPE" envRecoveryConfig = "BOUNDARY_RECOVERY_CONFIG" ) @@ -61,6 +64,7 @@ type Command struct { flagFormat string FlagToken string FlagTokenName string + FlagKeyringType string FlagRecoveryConfig string flagOutputCurlString bool @@ -175,7 +179,6 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) { c.client.SetMaxRetries(0) } - tokenName := "default" switch { case c.FlagRecoveryConfig != "": wrapper, err := wrapper.GetWrapperFromPath(c.FlagRecoveryConfig, "recovery") @@ -209,15 +212,14 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) { c.client.SetToken(c.FlagToken) case c.client.Token() == "": - if c.FlagTokenName != "" { - tokenName = c.FlagTokenName + keyringType, tokenName, err := c.DiscoverKeyringTokenInfo() + if err != nil { + return nil, err } - os.Setenv(EnvTokenName, tokenName) - if tokenName != "none" { - authToken := c.ReadTokenFromKeyring(tokenName) - if authToken != nil { - c.client.SetToken(authToken.Token) - } + + authToken := c.ReadTokenFromKeyring(keyringType, tokenName) + if authToken != nil { + c.client.SetToken(authToken.Token) } } @@ -228,18 +230,121 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) { return c.client, nil } -func (c *Command) ReadTokenFromKeyring(tokenName string) *authtokens.AuthToken { - token, err := keyring.Get("HashiCorp Boundary Auth Token", tokenName) - if err != nil { - if err == keyring.ErrNotFound { - c.UI.Info("No saved credential found, continuing without") - } else { - // TODO: potentially look for dbus-launch in advance and don't issue a warning at all - c.UI.Error(fmt.Sprintf("Error reading auth token from system credential store: %s", err)) - c.UI.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -token-name=none.") - } - token = "" +func (c *Command) DiscoverKeyringTokenInfo() (string, string, error) { + tokenName := "default" + + if c.FlagTokenName != "" { + tokenName = c.FlagTokenName } + + if tokenName == "none" { + c.UI.Warn(`"-token-name=none" is deprecated, please use "-keyring-type=none"`) + c.FlagKeyringType = "none" + } + + if c.FlagKeyringType == "none" { + return "", "", nil + } + + // Set so we can look it up later when printing out curl strings + os.Setenv(EnvTokenName, tokenName) + + var foundKeyringType bool + keyringType := c.FlagKeyringType + switch runtime.GOOS { + case "windows": + switch keyringType { + case "auto", "wincred", "pass": + foundKeyringType = true + if keyringType == "auto" { + keyringType = "wincred" + } + } + case "darwin": + switch keyringType { + case "auto", "keychain", "pass": + foundKeyringType = true + if keyringType == "auto" { + keyringType = "keychain" + } + } + default: + switch keyringType { + case "auto", "secret-service", "pass": + foundKeyringType = true + if keyringType == "auto" { + keyringType = "pass" + } + } + } + + if !foundKeyringType { + return "", "", fmt.Errorf("Given keyring type %q is not valid, or not valid for this platform", c.FlagKeyringType) + } + + var available bool + switch keyringType { + case "wincred", "keychain": + available = true + case "pass", "secret-service": + avail := nkeyring.AvailableBackends() + for _, a := range avail { + if keyringType == string(a) { + available = true + } + } + } + + if !available { + return "", "", fmt.Errorf("Keyring type %q is not available on this machine. For help with setting up keyrings, see https://www.boundaryproject.io/docs/api-clients/cli.", keyringType) + } + + os.Setenv(EnvKeyringType, keyringType) + + return keyringType, tokenName, nil +} + +func (c *Command) ReadTokenFromKeyring(keyringType, tokenName string) *authtokens.AuthToken { + var token string + var err error + + switch keyringType { + case "wincred", "keychain": + token, err = zkeyring.Get("HashiCorp Boundary Auth Token", tokenName) + if err != nil { + if err == zkeyring.ErrNotFound { + c.UI.Info("No saved credential found, continuing without") + } else { + c.UI.Error(fmt.Sprintf("Error reading auth token from keyring: %s", err)) + c.UI.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.") + } + token = "" + } + + default: + krConfig := nkeyring.Config{ + LibSecretCollectionName: "login", + PassPrefix: "HashiCorp_Boundary", + AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)}, + } + + kr, err := nkeyring.Open(krConfig) + if err != nil { + c.UI.Error(fmt.Sprintf("Error opening keyring: %s", err)) + c.UI.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.") + break + } + + item, err := kr.Get(tokenName) + if err != nil { + c.UI.Error(fmt.Sprintf("Error fetching token from keyring: %s", err)) + c.UI.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.") + break + } + + token = string(item.Data) + } + if token != "" { tokenBytes, err := base64.RawStdEncoding.DecodeString(token) switch { @@ -349,7 +454,15 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { Name: "token-name", Target: &c.FlagTokenName, EnvVar: EnvTokenName, - Usage: `If specified, the given value will be used as the name when storing the token in the system credential store. This can allow switching user identities for different commands. Set to "none" to disable storing the token.`, + Usage: `If specified, the given value will be used as the name when storing the token in the system credential store. This can allow switching user identities for different commands.`, + }) + + f.StringVar(&StringVar{ + Name: "keyring-type", + Target: &c.FlagKeyringType, + Default: "auto", + EnvVar: EnvKeyringType, + Usage: `The type of keyring to use. Defaults to "auto" which will use the Windows credential manager, OSX keychain, or cross-platform password store depending on platform. Set to "none" to disable keyring functionality. Available types, depending on platform, are: "wincred", "keychain", "pass", and "secret-service".`, }) f.StringVar(&StringVar{ diff --git a/internal/cmd/commands/authenticate/password.go b/internal/cmd/commands/authenticate/password.go index 41c2d37c1..a0e86b5a5 100644 --- a/internal/cmd/commands/authenticate/password.go +++ b/internal/cmd/commands/authenticate/password.go @@ -8,6 +8,7 @@ import ( "strings" "time" + nkeyring "github.com/99designs/keyring" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authmethods" "github.com/hashicorp/boundary/api/authtokens" @@ -16,7 +17,7 @@ import ( "github.com/mitchellh/cli" "github.com/mitchellh/go-wordwrap" "github.com/posener/complete" - "github.com/zalando/go-keyring" + zkeyring "github.com/zalando/go-keyring" ) var _ cli.Command = (*PasswordCommand)(nil) @@ -158,22 +159,56 @@ func (c *PasswordCommand) Run(args []string) int { c.UI.Output(string(jsonOut)) } - tokenName := "default" - if c.Command.FlagTokenName != "" { - tokenName = c.Command.FlagTokenName - } - if tokenName != "none" { + var gotErr bool + keyringType, tokenName, err := c.DiscoverKeyringTokenInfo() + if err != nil { + c.UI.Error(fmt.Sprintf("Error fetching keyring information: %s", err)) + gotErr = true + } else if keyringType != "none" && + tokenName != "none" && + keyringType != "" && + tokenName != "" { marshaled, err := json.Marshal(token) if err != nil { - c.UI.Error(fmt.Sprintf("Error marshaling auth token to save to system credential store: %s", err)) - return 1 - } - // TODO: potentially look for dbus-launch in advance and don't issue a warning at all - if err := keyring.Set("HashiCorp Boundary Auth Token", tokenName, base64.RawStdEncoding.EncodeToString(marshaled)); err != nil { - c.UI.Error(fmt.Sprintf("Error saving auth token to system credential store: %s", err)) - c.UI.Warn("The token printed above must be manually passed in via the BOUNDARY_TOKEN env var or -token flag. Storing the token can also be disabled via -token-name=none.") + c.UI.Error(fmt.Sprintf("Error marshaling auth token to save to keyring: %s", err)) + gotErr = true + } else { + switch keyringType { + case "wincred", "keychain": + if err := zkeyring.Set("HashiCorp Boundary Auth Token", tokenName, base64.RawStdEncoding.EncodeToString(marshaled)); err != nil { + c.UI.Error(fmt.Sprintf("Error saving auth token to %q keyring: %s", keyringType, err)) + gotErr = true + } + + default: + krConfig := nkeyring.Config{ + LibSecretCollectionName: "login", + PassPrefix: "HashiCorp_Boundary", + AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)}, + } + + kr, err := nkeyring.Open(krConfig) + if err != nil { + c.UI.Error(fmt.Sprintf("Error opening %q keyring: %s", keyringType, err)) + gotErr = true + break + } + + if err := kr.Set(nkeyring.Item{ + Key: tokenName, + Data: []byte(base64.RawStdEncoding.EncodeToString(marshaled)), + }); err != nil { + c.UI.Error(fmt.Sprintf("Error storing token in %q keyring: %s", keyringType, err)) + gotErr = true + break + } + } } } + if gotErr { + c.UI.Warn("The token printed above must be manually passed in via the BOUNDARY_TOKEN env var or -token flag. Storing the token can also be disabled via -keyring-type=none.") + } + return 0 } diff --git a/internal/cmd/commands/config/token.go b/internal/cmd/commands/config/token.go index ab63000f8..cc0484558 100644 --- a/internal/cmd/commands/config/token.go +++ b/internal/cmd/commands/config/token.go @@ -61,6 +61,14 @@ func (c *TokenCommand) Flags() *base.FlagSets { Usage: `If specified, the given value will be used as the name when loading the token from the system credential store. This must correspond to a name used when authenticating.`, }) + f.StringVar(&base.StringVar{ + Name: "keyring-type", + Target: &c.FlagKeyringType, + Default: "auto", + EnvVar: base.EnvKeyringType, + Usage: `The type of keyring to use. Defaults to "auto" which will use the Windows credential manager, OSX keychain, or cross-platform password store depending on platform. Set to "none" to disable keyring functionality. Available types, depending on platform, are: "wincred", "keychain", "pass", and "secret-service".`, + }) + f.BoolVar(&base.BoolVar{ Name: "user-id", Target: &c.flagUserId, @@ -113,27 +121,31 @@ func (c *TokenCommand) Run(args []string) (ret int) { return 1 } - // Read from keyring first + // Read from client first as that will override keyring anyways var authToken *authtokens.AuthToken - tokenName := "default" - if c.FlagTokenName != "" { - tokenName = c.FlagTokenName + // Fallback to env/CLI + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 1 } - if tokenName != "none" { - at := c.ReadTokenFromKeyring(tokenName) - if at != nil { - authToken = at - } + if client.Token() != "" { + authToken = &authtokens.AuthToken{Token: client.Token()} } if authToken == nil { - // Fallback to env/CLI - client, err := c.Client() + keyringType, tokenName, err := c.DiscoverKeyringTokenInfo() if err != nil { c.UI.Error(err.Error()) return 1 } - authToken = &authtokens.AuthToken{Token: client.Token()} + + authToken = c.ReadTokenFromKeyring(keyringType, tokenName) + } + + if authToken == nil { + c.UI.Error("No token could be discovered") + return 1 } switch { diff --git a/website/content/docs/api-clients/cli.mdx b/website/content/docs/api-clients/cli.mdx index b8cddea82..e78ac9884 100644 --- a/website/content/docs/api-clients/cli.mdx +++ b/website/content/docs/api-clients/cli.mdx @@ -27,6 +27,93 @@ If you want to install it manually, for Bash, the following line in a `complete -C /path/to/boundary boundary` +## Keyring Token storage + +Boundary uses various mechanisms, depending on platform, to allow for secure +storage of authentication tokens for later use. Each platform has a +platform-specific option (which on Windows and macOS are the default); +[pass](https://www.passwordstore.org/) is also available on all platforms. On +all platforms, setting `-keyring-type` to `none` (or setting it via +`BOUNDARY_KEYRING_TYPE`) disables storage and retrieval of the token. + +Additionally, more than one token can be stored or retrieved at once via the +`-token-name` flag or `BOUNDARY_TOKEN_NAME` env var. This allows for storing +tokens used by different Boundary installations, or other needs. + +### Windows + +On Windows, the Windows credential store (`wincred`) is used. + +Available keyring types: + +* `wincred` (default) +* `pass` +* `none` + +### macOS + +On macOS, Keychain is used via `/usr/bin/security`. (Using this binary allows us +to keep the Boundary binary statically linked, which we prefer.) + +Available keyring types: + +* `keychain` (default) +* `pass` +* `none` + +### Other platforms + +On all other platforms, the default is `pass`. However, if an implementation of +the [freedesktop.org secret +service](https://specifications.freedesktop.org/secret-service/latest/) is +available (via `gnome-keyring`, `kwallet`, or others) it can be used. + +Available keyring types: + +* `pass` (default) +* `secret-service` +* `none` + +## Mapping to Collections and Sub-Types + +Generally speaking, Boundary's CLI commands map to the collections they operate +on. For instance, when operating on roles, the command will be `boundary roles +...`. + +As a result, the patterns for reading, deleting, and listing are predictable: + +* `boundary read` +* `boundary delete` +* `boundary list` + +`read` and `delete` will always operate on a particular resource identifier, so +will always take in an `-id` parameter. `list` operates on collections so will +either take a `-scope-id` parameter or, depending on type, a higher level +resource identifier, e.g. `-auth-method-id`. + +Creating and updating resources may take an extra parameter if the resource type +is abstract, that is, if the type cannot be operated on directly but must be +operated on through an implementation. As an example, a role is not an abstract +type, and does not have various implementations of it. As a result, a role can +be operated on directly: + +* `boundary roles create` +* `boundary roles update`. + +However, a target can be one of many types of targets, and a concrete +implementation of a target is a `tcp` type of target. Therefore an extra +parameter is required when creating or updating a target: + +* `boundary targets tcp create` +* `boundary targets tcp update` + +This allows the CLI to perform proper presentation and validation of parameters +and functions for the given type. + +Similar to `read`, `update` operates on an existing target so will always take +an `-id` parameter. Similar to `list`, `create` operates on a collection so will +either take a `-scope-id` parameter or a parameter defining the parent resource. + ## Parameter Handling All parameters specified on the CLI are specified as a Go-style flag with a @@ -99,43 +186,3 @@ the exact output from the controller. If using the output of the CLI in scripts or as parameters to other tools, _always_ use formatted output. The default text output is meant for human users and the formatting or the information included within that output from the original JSON may change at any time. - -## Mapping to Collections and Sub-Types - -Generally speaking, Boundary's CLI commands map to the collections they operate -on. For instance, when operating on roles, the command will be `boundary roles -...`. - -As a result, the patterns for reading, deleting, and listing are predictable: - -* `boundary read` -* `boundary delete` -* `boundary list` - -`read` and `delete` will always operate on a particular resource identifier, so -will always take in an `-id` parameter. `list` operates on collections so will -either take a `-scope-id` parameter or, depending on type, a higher level -resource identifier, e.g. `-auth-method-id`. - -Creating and updating resources may take an extra parameter if the resource type -is abstract, that is, if the type cannot be operated on directly but must be -operated on through an implementation. As an example, a role is not an abstract -type, and does not have various implementations of it. As a result, a role can -be operated on directly: - -* `boundary roles create` -* `boundary roles update`. - -However, a target can be one of many types of targets, and a concrete -implementation of a target is a `tcp` type of target. Therefore an extra -parameter is required when creating or updating a target: - -* `boundary targets tcp create` -* `boundary targets tcp update` - -This allows the CLI to perform proper presentation and validation of parameters -and functions for the given type. - -Similar to `read`, `update` operates on an existing target so will always take -an `-id` parameter. Similar to `list`, `create` operates on a collection so will -either take a `-scope-id` parameter or a parameter defining the parent resource.