detached dbdcf7e

Merge pull request #18 from bnema/fix/proxy-auth-header-forwarding

fix(proxy): preserve Authorization header on proxied requests
commit
dbdcf7e3ab0a74806891b03012c7ed650ea5cb29
author
bnema · · 2026-01-15 12:42 UTC
committer
GitHub · · 2026-01-15 12:42 UTC
merge of
99f5ba4 + 3c39909 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v2.2.2
1 file · +39 -26
M internal/usecase/proxy/service.go +39 -26
@@ -269,6 +269,23 @@ func (s *Service) UpdateConfig(config Config) {
269269
270270 // Helper methods
271271
272+// newReverseProxy creates a reverse proxy using Rewrite instead of Director to prevent
273+// hop-by-hop header attacks. A malicious client could send "Connection: Authorization"
274+// to strip the Authorization header when using the default Director. Rewrite processes
275+// headers after hop-by-hop removal, ensuring headers like Authorization are preserved.
276+func newReverseProxy(targetURL *url.URL, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResponse func(*http.Response) error) *httputil.ReverseProxy {
277+ return &httputil.ReverseProxy{
278+ Rewrite: func(pr *httputil.ProxyRequest) {
279+ pr.SetURL(targetURL)
280+ pr.SetXForwarded()
281+ pr.Out.Host = targetURL.Host
282+ },
283+ Transport: proxyTransport,
284+ ErrorHandler: errorHandler,
285+ ModifyResponse: modifyResponse,
286+ }
287+}
288+
272289 func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target *domain.ProxyTarget) {
273290 log := zerowrap.FromCtx(r.Context())
274291
@@ -279,19 +296,17 @@ func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target *
279296 return
280297 }
281298
282- proxy := httputil.NewSingleHostReverseProxy(targetURL)
283- proxy.Transport = proxyTransport
284-
285- proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) {
286- log.Error().Err(err).Str("target", targetURL.String()).Msg("proxy error")
287- http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
288- }
289-
290- proxy.ModifyResponse = func(resp *http.Response) error {
291- resp.Header.Set("X-Proxied-By", "Gordon")
292- resp.Header.Set("X-Container-ID", target.ContainerID)
293- return nil
294- }
299+ proxy := newReverseProxy(targetURL,
300+ func(w http.ResponseWriter, _ *http.Request, err error) {
301+ log.Error().Err(err).Str("target", targetURL.String()).Msg("proxy error")
302+ http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
303+ },
304+ func(resp *http.Response) error {
305+ resp.Header.Set("X-Proxied-By", "Gordon")
306+ resp.Header.Set("X-Container-ID", target.ContainerID)
307+ return nil
308+ },
309+ )
295310
296311 log.Debug().
297312 Str("target", targetURL.String()).
@@ -311,19 +326,17 @@ func (s *Service) proxyToRegistry(w http.ResponseWriter, r *http.Request) {
311326 return
312327 }
313328
314- proxy := httputil.NewSingleHostReverseProxy(targetURL)
315- proxy.Transport = proxyTransport
316-
317- proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) {
318- log.Error().Err(err).Int("registry_port", s.config.RegistryPort).Msg("registry proxy error")
319- http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable)
320- }
321-
322- proxy.ModifyResponse = func(resp *http.Response) error {
323- resp.Header.Set("X-Proxied-By", "Gordon")
324- resp.Header.Set("X-Registry-Backend", "gordon-registry")
325- return nil
326- }
329+ proxy := newReverseProxy(targetURL,
330+ func(w http.ResponseWriter, _ *http.Request, err error) {
331+ log.Error().Err(err).Int("registry_port", s.config.RegistryPort).Msg("registry proxy error")
332+ http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable)
333+ },
334+ func(resp *http.Response) error {
335+ resp.Header.Set("X-Proxied-By", "Gordon")
336+ resp.Header.Set("X-Registry-Backend", "gordon-registry")
337+ return nil
338+ },
339+ )
327340
328341 log.Debug().Str("target", targetURL.String()).Msg("proxying request to registry")
329342
@@ -269,6 +269,23 @@ func (s *Service) UpdateConfig(config Config) {
269 269
270 // Helper methods270 // Helper methods
271 271
272+// newReverseProxy creates a reverse proxy using Rewrite instead of Director to prevent
273+// hop-by-hop header attacks. A malicious client could send "Connection: Authorization"
274+// to strip the Authorization header when using the default Director. Rewrite processes
275+// headers after hop-by-hop removal, ensuring headers like Authorization are preserved.
276+func newReverseProxy(targetURL *url.URL, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResponse func(*http.Response) error) *httputil.ReverseProxy {
277+ return &httputil.ReverseProxy{
278+ Rewrite: func(pr *httputil.ProxyRequest) {
279+ pr.SetURL(targetURL)
280+ pr.SetXForwarded()
281+ pr.Out.Host = targetURL.Host
282+ },
283+ Transport: proxyTransport,
284+ ErrorHandler: errorHandler,
285+ ModifyResponse: modifyResponse,
286+ }
287+}
288+
272 func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target *domain.ProxyTarget) {289 func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target *domain.ProxyTarget) {
273 log := zerowrap.FromCtx(r.Context())290 log := zerowrap.FromCtx(r.Context())
274 291
@@ -279,19 +296,17 @@ func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target *
279 return296 return
280 }297 }
281 298
282- proxy := httputil.NewSingleHostReverseProxy(targetURL)299+ proxy := newReverseProxy(targetURL,
283- proxy.Transport = proxyTransport300+ func(w http.ResponseWriter, _ *http.Request, err error) {
284-301+ log.Error().Err(err).Str("target", targetURL.String()).Msg("proxy error")
285- proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) {302+ http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
286- log.Error().Err(err).Str("target", targetURL.String()).Msg("proxy error")303+ },
287- http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)304+ func(resp *http.Response) error {
288- }305+ resp.Header.Set("X-Proxied-By", "Gordon")
289-306+ resp.Header.Set("X-Container-ID", target.ContainerID)
290- proxy.ModifyResponse = func(resp *http.Response) error {307+ return nil
291- resp.Header.Set("X-Proxied-By", "Gordon")308+ },
292- resp.Header.Set("X-Container-ID", target.ContainerID)309+ )
293- return nil
294- }
295 310
296 log.Debug().311 log.Debug().
297 Str("target", targetURL.String()).312 Str("target", targetURL.String()).
@@ -311,19 +326,17 @@ func (s *Service) proxyToRegistry(w http.ResponseWriter, r *http.Request) {
311 return326 return
312 }327 }
313 328
314- proxy := httputil.NewSingleHostReverseProxy(targetURL)329+ proxy := newReverseProxy(targetURL,
315- proxy.Transport = proxyTransport330+ func(w http.ResponseWriter, _ *http.Request, err error) {
316-331+ log.Error().Err(err).Int("registry_port", s.config.RegistryPort).Msg("registry proxy error")
317- proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) {332+ http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable)
318- log.Error().Err(err).Int("registry_port", s.config.RegistryPort).Msg("registry proxy error")333+ },
319- http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable)334+ func(resp *http.Response) error {
320- }335+ resp.Header.Set("X-Proxied-By", "Gordon")
321-336+ resp.Header.Set("X-Registry-Backend", "gordon-registry")
322- proxy.ModifyResponse = func(resp *http.Response) error {337+ return nil
323- resp.Header.Set("X-Proxied-By", "Gordon")338+ },
324- resp.Header.Set("X-Registry-Backend", "gordon-registry")339+ )
325- return nil
326- }
327 340
328 log.Debug().Str("target", targetURL.String()).Msg("proxying request to registry")341 log.Debug().Str("target", targetURL.String()).Msg("proxying request to registry")
329 342

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