diff --git a/cmd/provision/preflight.go b/cmd/provision/preflight.go index 6e18a1b..6083adc 100644 --- a/cmd/provision/preflight.go +++ b/cmd/provision/preflight.go @@ -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= api_hash=\n"+ diff --git a/internal/secrets/secrets.go b/internal/secrets/secrets.go index dfbae86..d8414e3 100644 --- a/internal/secrets/secrets.go +++ b/internal/secrets/secrets.go @@ -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 != "" {