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, | |||
| 74 | 74 | authenticated := usernameMatch && passwordMatch | |
| 75 | 75 | if !authenticated { | |
| 76 | 76 | log.Debug(). | |
| 77 | - | Str("provided_username", username). | |
| 77 | + | Str("provided_username", redactUsername(username)). | |
| 78 | 78 | Str(zerowrap.FieldMethod, r.Method). | |
| 79 | 79 | Str(zerowrap.FieldPath, r.URL.Path). | |
| 80 | 80 | Msg("authentication failed") | |
| 81 | 81 | } else { | |
| 82 | 82 | log.Debug(). | |
| 83 | - | Str("username", username). | |
| 83 | + | Str("username", redactUsername(username)). | |
| 84 | 84 | Str(zerowrap.FieldMethod, r.Method). | |
| 85 | 85 | Str(zerowrap.FieldPath, r.URL.Path). | |
| 86 | 86 | Msg("authentication successful") | |
| @@ -89,6 +89,16 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string, | |||
| 89 | 89 | 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 | 102 | // InternalRegistryAuth holds the credentials used for loopback-only registry access. | |
| 93 | 103 | // These are generated per Gordon instance and are never exposed in config. | |
| 94 | 104 | type InternalRegistryAuth struct { | |
| @@ -191,7 +201,7 @@ func authenticatePassword(ctx context.Context, r *http.Request, authSvc in.AuthS | |||
| 191 | 201 | } | |
| 192 | 202 | ||
| 193 | 203 | log.Debug(). | |
| 194 | - | Str("provided_username", username). | |
| 204 | + | Str("provided_username", redactUsername(username)). | |
| 195 | 205 | Str(zerowrap.FieldMethod, r.Method). | |
| 196 | 206 | Str(zerowrap.FieldPath, r.URL.Path). | |
| 197 | 207 | Msg("password authentication failed") | |
| @@ -240,7 +250,7 @@ func authenticateToken(ctx context.Context, r *http.Request, authSvc in.AuthServ | |||
| 240 | 250 | if err != nil { | |
| 241 | 251 | log.Debug(). | |
| 242 | 252 | Err(err). | |
| 243 | - | Str("provided_username", username). | |
| 253 | + | Str("provided_username", redactUsername(username)). | |
| 244 | 254 | Str(zerowrap.FieldMethod, r.Method). | |
| 245 | 255 | Str(zerowrap.FieldPath, r.URL.Path). | |
| 246 | 256 | Msg("token-as-password validation failed") | |
| @@ -250,8 +260,8 @@ func authenticateToken(ctx context.Context, r *http.Request, authSvc in.AuthServ | |||
| 250 | 260 | // Verify the username matches the token subject | |
| 251 | 261 | if claims.Subject != username { | |
| 252 | 262 | 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)). | |
| 255 | 265 | Str(zerowrap.FieldMethod, r.Method). | |
| 256 | 266 | Str(zerowrap.FieldPath, r.URL.Path). | |
| 257 | 267 | Msg("username does not match token subject") | |
| @@ -74,13 +74,13 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string, | |||||
| 74 | authenticated := usernameMatch && passwordMatch | 74 | 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 authenticated | 89 | 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 subject | 260 | // 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") | ||