Skip to content

Commit 574279f

Browse files
committed
Route Supabase credentials through Automic Vault
1 parent 4523bdf commit 574279f

19 files changed

Lines changed: 443 additions & 998 deletions

README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@ This fork currently adds the following behavior on top of upstream
1111

1212
- An `isotope:supabase` package recipe that builds and signs both the
1313
Bun/TypeScript `supabase` launcher and the Go `supabase-go` helper.
14-
- Direct macOS Keychain access from the signed `supabase-go` binary instead
15-
of `github.com/zalando/go-keyring` shelling out to `/usr/bin/security`, so
16-
Keychain trust is attached to the Supabase executable.
17-
- A macOS-only `automicvault` Go build tag for the secure credential backend,
18-
while default upstream builds continue to use `go-keyring`.
14+
- A macOS-only `automicvault` Go build tag that routes credential reads,
15+
writes, and deletes through the Automic Vault XPC approval service.
1916
- A hazard detector for insecure Supabase CLI installs, including the
2017
plaintext fallback token at `~/.supabase/access-token` and Keychain ACLs
2118
that allow `/usr/bin/security` to read Supabase secrets.
22-
- A hidden `supabase-go av-migrate` command used by the Automic Vault isotope
23-
migration hook to rewrite insecure Keychain items and move fallback access
24-
tokens into the signed Supabase credential backend.
19+
- Migration of existing plaintext or legacy Keychain access tokens via
20+
`av harden supabase`.
2521
- Test seams that keep the credential tests deterministic without touching the
2622
user's real Keychain.
2723

apps/cli-go/cmd/av_keyring.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/supabase/cli/internal/utils/credentials"
10+
)
11+
12+
var avKeyringCmd = &cobra.Command{
13+
Use: "av-keyring get|set|delete|delete-all",
14+
Short: "Read and write Supabase credentials through Automic Vault",
15+
Hidden: true,
16+
Args: cobra.MinimumNArgs(1),
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
switch args[0] {
19+
case "get":
20+
if len(args) != 2 {
21+
return fmt.Errorf("usage: supabase av-keyring get ACCOUNT")
22+
}
23+
value, err := credentials.StoreProvider.Get(args[1])
24+
if err != nil {
25+
return err
26+
}
27+
_, err = fmt.Fprint(cmd.OutOrStdout(), value)
28+
return err
29+
case "set":
30+
if len(args) != 2 {
31+
return fmt.Errorf("usage: supabase av-keyring set ACCOUNT")
32+
}
33+
value, err := io.ReadAll(os.Stdin)
34+
if err != nil {
35+
return err
36+
}
37+
return credentials.StoreProvider.Set(args[1], string(value))
38+
case "delete":
39+
if len(args) != 2 {
40+
return fmt.Errorf("usage: supabase av-keyring delete ACCOUNT")
41+
}
42+
return credentials.StoreProvider.Delete(args[1])
43+
case "delete-all":
44+
if len(args) != 1 {
45+
return fmt.Errorf("usage: supabase av-keyring delete-all")
46+
}
47+
return credentials.StoreProvider.DeleteAll()
48+
default:
49+
return fmt.Errorf("usage: supabase av-keyring get|set|delete|delete-all")
50+
}
51+
},
52+
}
53+
54+
func isAVKeyringCommand(cmd *cobra.Command) bool {
55+
return cmd != nil && cmd.CommandPath() == "supabase av-keyring"
56+
}
57+
58+
func init() {
59+
rootCmd.AddCommand(avKeyringCmd)
60+
}

apps/cli-go/cmd/av_migrate.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

apps/cli-go/cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ var (
101101
if err := utils.LoadProfile(ctx, fsys); err != nil {
102102
return err
103103
}
104-
if isAVMigrateCommand(cmd) {
104+
if isAVKeyringCommand(cmd) {
105105
cmd.SetContext(ctx)
106106
return nil
107107
}
@@ -189,7 +189,7 @@ func Execute() {
189189
if err != nil {
190190
panic(err)
191191
}
192-
if isAVMigrateCommand(executedCmd) {
192+
if isAVKeyringCommand(executedCmd) {
193193
return
194194
}
195195
// Check upgrade last because --version flag is initialised after execute

apps/cli-go/internal/avmigrate/avmigrate.go

Lines changed: 0 additions & 57 deletions
This file was deleted.

apps/cli-go/internal/avmigrate/avmigrate_test.go

Lines changed: 0 additions & 62 deletions
This file was deleted.

apps/cli-go/internal/utils/access_token.go

Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"os"
66
"path/filepath"
77
"regexp"
8-
"strings"
98

109
"github.com/go-errors/errors"
1110
"github.com/spf13/afero"
@@ -50,6 +49,9 @@ func loadAccessToken(fsys afero.Fs) (string, error) {
5049
fmt.Fprintln(logger, "Using access token from credentials store...")
5150
return accessToken, nil
5251
}
52+
if credentials.RequiresSecureStorage() {
53+
return "", errors.New(ErrMissingToken)
54+
}
5355
// Fallback to token file
5456
return fallbackLoadToken(fsys)
5557
}
@@ -77,6 +79,9 @@ func SaveAccessToken(accessToken string, fsys afero.Fs) error {
7779
// Save to current profile
7880
if err := credentials.StoreProvider.Set(CurrentProfile.Name, accessToken); err != nil {
7981
fmt.Fprintln(GetDebugLogger(), err)
82+
if credentials.RequiresSecureStorage() {
83+
return errors.Errorf("failed to save access token to secure storage: %w", err)
84+
}
8085
} else {
8186
return nil
8287
}
@@ -98,72 +103,6 @@ func fallbackSaveToken(accessToken string, fsys afero.Fs) error {
98103
return nil
99104
}
100105

101-
func MigrateFallbackAccessToken(fsys afero.Fs) (bool, error) {
102-
paths, err := fallbackAccessTokenMigrationPaths()
103-
if err != nil {
104-
return false, err
105-
}
106-
107-
migrated := false
108-
for _, path := range paths {
109-
didMigrate, err := migrateFallbackAccessTokenPath(path, fsys)
110-
if err != nil {
111-
return migrated, err
112-
}
113-
migrated = migrated || didMigrate
114-
}
115-
return migrated, nil
116-
}
117-
118-
func fallbackAccessTokenMigrationPaths() ([]string, error) {
119-
var paths []string
120-
supabaseHome := os.Getenv("SUPABASE_HOME")
121-
defaultPath, err := getAccessTokenPath()
122-
if err != nil {
123-
if supabaseHome == "" {
124-
return nil, err
125-
}
126-
} else {
127-
paths = append(paths, defaultPath)
128-
}
129-
if supabaseHome != "" {
130-
paths = append(paths, filepath.Join(supabaseHome, AccessTokenKey))
131-
}
132-
133-
seen := make(map[string]struct{}, len(paths))
134-
unique := paths[:0]
135-
for _, path := range paths {
136-
if _, ok := seen[path]; ok {
137-
continue
138-
}
139-
seen[path] = struct{}{}
140-
unique = append(unique, path)
141-
}
142-
return unique, nil
143-
}
144-
145-
func migrateFallbackAccessTokenPath(path string, fsys afero.Fs) (bool, error) {
146-
contents, err := afero.ReadFile(fsys, path)
147-
if errors.Is(err, os.ErrNotExist) {
148-
return false, nil
149-
}
150-
if err != nil {
151-
return false, errors.Errorf("failed to read access token file for migration: %w", err)
152-
}
153-
154-
accessToken := strings.TrimSpace(string(contents))
155-
if !AccessTokenPattern.MatchString(accessToken) {
156-
return false, errors.Errorf("refusing to migrate invalid access token file: %s", path)
157-
}
158-
if err := credentials.StoreProvider.Set(CurrentProfile.Name, accessToken); err != nil {
159-
return false, errors.Errorf("failed to migrate access token file to keychain: %w", err)
160-
}
161-
if err := fsys.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
162-
return false, errors.Errorf("failed to remove migrated access token file: %w", err)
163-
}
164-
return true, nil
165-
}
166-
167106
func DeleteAccessToken(fsys afero.Fs) error {
168107
// Always delete the fallback token file to handle legacy CLI
169108
if err := fallbackDeleteToken(fsys); err != nil && !errors.Is(err, os.ErrNotExist) {

0 commit comments

Comments
 (0)