Read the standard token sink, and tell a 403 from an outage
NewFromEnv now falls back to ~/.vault-token, the file `bao login` writes and the bao and vault CLIs already read. An operator who has logged in once should not have to re-export a secret, and a token that never has to be typed is a token that never lands in shell history. Preflight distinguishes the two things a 403 means. An expired token and a token missing a policy look identical in the error, and the check that separates them -- whether `bao token lookup` also fails -- is worth naming where it is read rather than left to be rediscovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa Assistant: claude-code Assistant-Model: opus Assistant-Process: 1361245@bnt-lap001 Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
This commit is contained in:
parent
7347bd6302
commit
de7a7bdd2a
2 changed files with 28 additions and 3 deletions
|
|
@ -76,7 +76,13 @@ func cmdPreflight(args []string) error {
|
|||
appFields, found, err := store.Get(ctx, secrets.KeyOperatorApp)
|
||||
switch {
|
||||
case err != nil:
|
||||
bad("openbao unreachable or token rejected: %v", err)
|
||||
if strings.Contains(err.Error(), "403") {
|
||||
bad("openbao rejected the token (403). If `bao token lookup` also fails,\n"+
|
||||
" the token has expired -- run `bao login`. If lookup succeeds,\n"+
|
||||
" the token lacks a policy for %s", store.Ref(secrets.KeyOperatorApp))
|
||||
} else {
|
||||
bad("openbao unreachable: %v", err)
|
||||
}
|
||||
case !found:
|
||||
bad("no app credentials at %s\n write them with:\n"+
|
||||
" bao kv put %s api_id=<n> api_hash=<hash>\n"+
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -37,7 +38,10 @@ type Store struct {
|
|||
// NewFromEnv builds a store from the ambient OpenBao configuration.
|
||||
func NewFromEnv(campaign string) (*Store, error) {
|
||||
addr := firstNonEmpty(os.Getenv("BAO_ADDR"), os.Getenv("VAULT_ADDR"))
|
||||
token := firstNonEmpty(os.Getenv("BAO_TOKEN"), os.Getenv("VAULT_TOKEN"))
|
||||
// Fall back to the token sink the bao and vault CLIs already use, so an
|
||||
// operator who has logged in once does not have to re-export a secret --
|
||||
// and so a token never has to be typed into a shell that records history.
|
||||
token := firstNonEmpty(os.Getenv("BAO_TOKEN"), os.Getenv("VAULT_TOKEN"), tokenFromDisk())
|
||||
// Name the variable that is actually missing. "set both" sends someone
|
||||
// checking the one they already set.
|
||||
switch {
|
||||
|
|
@ -47,7 +51,8 @@ func NewFromEnv(campaign string) (*Store, error) {
|
|||
case addr == "":
|
||||
return nil, fmt.Errorf("BAO_ADDR is not set (BAO_TOKEN is)")
|
||||
case token == "":
|
||||
return nil, fmt.Errorf("BAO_TOKEN is not set (BAO_ADDR is %s)", addr)
|
||||
return nil, fmt.Errorf("no OpenBao token: set BAO_TOKEN, or run `bao login` "+
|
||||
"to write ~/.vault-token (BAO_ADDR is %s)", addr)
|
||||
}
|
||||
mount := firstNonEmpty(os.Getenv("BAO_MOUNT"), "secret")
|
||||
return &Store{
|
||||
|
|
@ -142,6 +147,20 @@ func (s *Store) CreateIfAbsent(ctx context.Context, key string, fields map[strin
|
|||
return true, s.Put(ctx, key, fields)
|
||||
}
|
||||
|
||||
// tokenFromDisk reads ~/.vault-token, the file `bao login` writes. Absence is
|
||||
// not an error: it is one of several ways a token may be supplied.
|
||||
func tokenFromDisk() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(home, ".vault-token"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(raw))
|
||||
}
|
||||
|
||||
func firstNonEmpty(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if v != "" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue