detached 0749bab

fix(security): redact usernames in debug logs

commit
0749bab43cd1abd0fd748de538d8f361df40de4d
author
bnema · · 2026-01-17 20:58 UTC
parent
125bab8
signature
unsigned
1 file · +16 -6
M internal/adapters/in/http/middleware/auth.go +16 -6
@@ -74,13 +74,13 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string,
7474 authenticated := usernameMatch && passwordMatch
7575 if !authenticated {
7676 log.Debug().
77- Str("provided_username", username).
77+ Str("provided_username", redactUsername(username)).
7878 Str(zerowrap.FieldMethod, r.Method).
7979 Str(zerowrap.FieldPath, r.URL.Path).
8080 Msg("authentication failed")
8181 } else {
8282 log.Debug().
83- Str("username", username).
83+ Str("username", redactUsername(username)).
8484 Str(zerowrap.FieldMethod, r.Method).
8585 Str(zerowrap.FieldPath, r.URL.Path).
8686 Msg("authentication successful")
@@ -89,6 +89,16 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string,
8989 return authenticated
9090 }
9191
92+// redactUsername partially redacts a username for logging.
93+// Shows first and last character with asterisks in between.
94+// SECURITY: Prevents full username exposure in logs.
95+func redactUsername(username string) string {
96+ if len(username) <= 2 {
97+ return "***"
98+ }
99+ return username[:1] + "***" + username[len(username)-1:]
100+}
101+
92102 // InternalRegistryAuth holds the credentials used for loopback-only registry access.
93103 // These are generated per Gordon instance and are never exposed in config.
94104 type InternalRegistryAuth struct {
@@ -191,7 +201,7 @@ func authenticatePassword(ctx context.Context, r *http.Request, authSvc in.AuthS
191201 }
192202
193203 log.Debug().
194- Str("provided_username", username).
204+ Str("provided_username", redactUsername(username)).
195205 Str(zerowrap.FieldMethod, r.Method).
196206 Str(zerowrap.FieldPath, r.URL.Path).
197207 Msg("password authentication failed")
@@ -240,7 +250,7 @@ func authenticateToken(ctx context.Context, r *http.Request, authSvc in.AuthServ
240250 if err != nil {
241251 log.Debug().
242252 Err(err).
243- Str("provided_username", username).
253+ Str("provided_username", redactUsername(username)).
244254 Str(zerowrap.FieldMethod, r.Method).
245255 Str(zerowrap.FieldPath, r.URL.Path).
246256 Msg("token-as-password validation failed")
@@ -250,8 +260,8 @@ func authenticateToken(ctx context.Context, r *http.Request, authSvc in.AuthServ
250260 // Verify the username matches the token subject
251261 if claims.Subject != username {
252262 log.Debug().
253- Str("provided_username", username).
254- Str("token_subject", claims.Subject).
263+ Str("provided_username", redactUsername(username)).
264+ Str("token_subject", redactUsername(claims.Subject)).
255265 Str(zerowrap.FieldMethod, r.Method).
256266 Str(zerowrap.FieldPath, r.URL.Path).
257267 Msg("username does not match token subject")
@@ -74,13 +74,13 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string,
74 authenticated := usernameMatch && passwordMatch74 authenticated := usernameMatch && passwordMatch
75 if !authenticated {75 if !authenticated {
76 log.Debug().76 log.Debug().
77- Str("provided_username", username).77+ Str("provided_username", redactUsername(username)).
78 Str(zerowrap.FieldMethod, r.Method).78 Str(zerowrap.FieldMethod, r.Method).
79 Str(zerowrap.FieldPath, r.URL.Path).79 Str(zerowrap.FieldPath, r.URL.Path).
80 Msg("authentication failed")80 Msg("authentication failed")
81 } else {81 } else {
82 log.Debug().82 log.Debug().
83- Str("username", username).83+ Str("username", redactUsername(username)).
84 Str(zerowrap.FieldMethod, r.Method).84 Str(zerowrap.FieldMethod, r.Method).
85 Str(zerowrap.FieldPath, r.URL.Path).85 Str(zerowrap.FieldPath, r.URL.Path).
86 Msg("authentication successful")86 Msg("authentication successful")
@@ -89,6 +89,16 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string,
89 return authenticated89 return authenticated
90 }90 }
91 91
92+// redactUsername partially redacts a username for logging.
93+// Shows first and last character with asterisks in between.
94+// SECURITY: Prevents full username exposure in logs.
95+func redactUsername(username string) string {
96+ if len(username) <= 2 {
97+ return "***"
98+ }
99+ return username[:1] + "***" + username[len(username)-1:]
100+}
101+
92 // InternalRegistryAuth holds the credentials used for loopback-only registry access.102 // InternalRegistryAuth holds the credentials used for loopback-only registry access.
93 // These are generated per Gordon instance and are never exposed in config.103 // These are generated per Gordon instance and are never exposed in config.
94 type InternalRegistryAuth struct {104 type InternalRegistryAuth struct {
@@ -191,7 +201,7 @@ func authenticatePassword(ctx context.Context, r *http.Request, authSvc in.AuthS
191 }201 }
192 202
193 log.Debug().203 log.Debug().
194- Str("provided_username", username).204+ Str("provided_username", redactUsername(username)).
195 Str(zerowrap.FieldMethod, r.Method).205 Str(zerowrap.FieldMethod, r.Method).
196 Str(zerowrap.FieldPath, r.URL.Path).206 Str(zerowrap.FieldPath, r.URL.Path).
197 Msg("password authentication failed")207 Msg("password authentication failed")
@@ -240,7 +250,7 @@ func authenticateToken(ctx context.Context, r *http.Request, authSvc in.AuthServ
240 if err != nil {250 if err != nil {
241 log.Debug().251 log.Debug().
242 Err(err).252 Err(err).
243- Str("provided_username", username).253+ Str("provided_username", redactUsername(username)).
244 Str(zerowrap.FieldMethod, r.Method).254 Str(zerowrap.FieldMethod, r.Method).
245 Str(zerowrap.FieldPath, r.URL.Path).255 Str(zerowrap.FieldPath, r.URL.Path).
246 Msg("token-as-password validation failed")256 Msg("token-as-password validation failed")
@@ -250,8 +260,8 @@ func authenticateToken(ctx context.Context, r *http.Request, authSvc in.AuthServ
250 // Verify the username matches the token subject260 // Verify the username matches the token subject
251 if claims.Subject != username {261 if claims.Subject != username {
252 log.Debug().262 log.Debug().
253- Str("provided_username", username).263+ Str("provided_username", redactUsername(username)).
254- Str("token_subject", claims.Subject).264+ Str("token_subject", redactUsername(claims.Subject)).
255 Str(zerowrap.FieldMethod, r.Method).265 Str(zerowrap.FieldMethod, r.Method).
256 Str(zerowrap.FieldPath, r.URL.Path).266 Str(zerowrap.FieldPath, r.URL.Path).
257 Msg("username does not match token subject")267 Msg("username does not match token subject")

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