detached b77f360

fix(security): require explicit opt-in for unsafe token store

commit
b77f360b73793cbc87e4300c9266294cb03f0a63
author
bnema · · 2026-01-17 20:56 UTC
parent
9cc9012
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +21 -4
M internal/adapters/out/tokenstore/store.go +1 -1
@@ -28,7 +28,7 @@ func NewStore(backend domain.SecretsBackend, dataDir string, log zerowrap.Logger
2828 if dataDir == "" {
2929 return nil, fmt.Errorf("data_dir is required for unsafe backend")
3030 }
31- return NewUnsafeStore(dataDir, log), nil
31+ return NewUnsafeStore(dataDir, log)
3232
3333 default:
3434 return nil, fmt.Errorf("unknown secrets backend: %s", backend)
@@ -28,7 +28,7 @@ func NewStore(backend domain.SecretsBackend, dataDir string, log zerowrap.Logger
28 if dataDir == "" {28 if dataDir == "" {
29 return nil, fmt.Errorf("data_dir is required for unsafe backend")29 return nil, fmt.Errorf("data_dir is required for unsafe backend")
30 }30 }
31- return NewUnsafeStore(dataDir, log), nil31+ return NewUnsafeStore(dataDir, log)
32 32
33 default:33 default:
34 return nil, fmt.Errorf("unknown secrets backend: %s", backend)34 return nil, fmt.Errorf("unknown secrets backend: %s", backend)
M internal/adapters/out/tokenstore/unsafe.go +20 -3
@@ -29,6 +29,10 @@ const (
2929 unsafeRevokedFile = "secrets/gordon/registry/revoked.json"
3030 )
3131
32+// UnsafeEnvVar is the environment variable that must be set to "true"
33+// to enable the unsafe token store.
34+const UnsafeEnvVar = "GORDON_ALLOW_UNSAFE_SECRETS"
35+
3236 // UnsafeStore implements TokenStore using plain text files.
3337 // WARNING: This store does not encrypt secrets. Only use when pass/sops are unavailable.
3438 type UnsafeStore struct {
@@ -38,7 +42,20 @@ type UnsafeStore struct {
3842
3943 // NewUnsafeStore creates a new file-based token store.
4044 // dataDir is the base directory for storing secrets (typically gordon's data_dir).
41-func NewUnsafeStore(dataDir string, log zerowrap.Logger) *UnsafeStore {
45+//
46+// SECURITY: Requires GORDON_ALLOW_UNSAFE_SECRETS=true environment variable.
47+// Returns an error if the environment variable is not set, preventing
48+// accidental use of plaintext secret storage in production.
49+func NewUnsafeStore(dataDir string, log zerowrap.Logger) (*UnsafeStore, error) {
50+ // SECURITY: Require explicit opt-in for plaintext storage
51+ if os.Getenv(UnsafeEnvVar) != "true" {
52+ return nil, fmt.Errorf(
53+ "unsafe token store is disabled by default; "+
54+ "set %s=true to enable plaintext secret storage (NOT recommended for production)",
55+ UnsafeEnvVar,
56+ )
57+ }
58+
4259 store := &UnsafeStore{
4360 dataDir: dataDir,
4461 log: log,
@@ -49,9 +66,9 @@ func NewUnsafeStore(dataDir string, log zerowrap.Logger) *UnsafeStore {
4966 Str(zerowrap.FieldAdapter, "tokenstore").
5067 Str("provider", "unsafe").
5168 Str("data_dir", dataDir).
52- Msg("using unsafe secrets backend - secrets are stored in plain text")
69+ Msg("SECURITY WARNING: using unsafe secrets backend - secrets are stored in plain text")
5370
54- return store
71+ return store, nil
5572 }
5673
5774 // unsafeTokenData holds both JWT and metadata in a single file.
@@ -29,6 +29,10 @@ const (
29 unsafeRevokedFile = "secrets/gordon/registry/revoked.json"29 unsafeRevokedFile = "secrets/gordon/registry/revoked.json"
30 )30 )
31 31
32+// UnsafeEnvVar is the environment variable that must be set to "true"
33+// to enable the unsafe token store.
34+const UnsafeEnvVar = "GORDON_ALLOW_UNSAFE_SECRETS"
35+
32 // UnsafeStore implements TokenStore using plain text files.36 // UnsafeStore implements TokenStore using plain text files.
33 // WARNING: This store does not encrypt secrets. Only use when pass/sops are unavailable.37 // WARNING: This store does not encrypt secrets. Only use when pass/sops are unavailable.
34 type UnsafeStore struct {38 type UnsafeStore struct {
@@ -38,7 +42,20 @@ type UnsafeStore struct {
38 42
39 // NewUnsafeStore creates a new file-based token store.43 // NewUnsafeStore creates a new file-based token store.
40 // dataDir is the base directory for storing secrets (typically gordon's data_dir).44 // dataDir is the base directory for storing secrets (typically gordon's data_dir).
41-func NewUnsafeStore(dataDir string, log zerowrap.Logger) *UnsafeStore {45+//
46+// SECURITY: Requires GORDON_ALLOW_UNSAFE_SECRETS=true environment variable.
47+// Returns an error if the environment variable is not set, preventing
48+// accidental use of plaintext secret storage in production.
49+func NewUnsafeStore(dataDir string, log zerowrap.Logger) (*UnsafeStore, error) {
50+ // SECURITY: Require explicit opt-in for plaintext storage
51+ if os.Getenv(UnsafeEnvVar) != "true" {
52+ return nil, fmt.Errorf(
53+ "unsafe token store is disabled by default; "+
54+ "set %s=true to enable plaintext secret storage (NOT recommended for production)",
55+ UnsafeEnvVar,
56+ )
57+ }
58+
42 store := &UnsafeStore{59 store := &UnsafeStore{
43 dataDir: dataDir,60 dataDir: dataDir,
44 log: log,61 log: log,
@@ -49,9 +66,9 @@ func NewUnsafeStore(dataDir string, log zerowrap.Logger) *UnsafeStore {
49 Str(zerowrap.FieldAdapter, "tokenstore").66 Str(zerowrap.FieldAdapter, "tokenstore").
50 Str("provider", "unsafe").67 Str("provider", "unsafe").
51 Str("data_dir", dataDir).68 Str("data_dir", dataDir).
52- Msg("using unsafe secrets backend - secrets are stored in plain text")69+ Msg("SECURITY WARNING: using unsafe secrets backend - secrets are stored in plain text")
53 70
54- return store71+ return store, nil
55 }72 }
56 73
57 // unsafeTokenData holds both JWT and metadata in a single file.74 // unsafeTokenData holds both JWT and metadata in a single file.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close