fix(proxy): preserve Authorization header on proxied requests
Use httputil.ReverseProxy.Rewrite instead of Director to prevent hop-by-hop header attacks. A malicious client could send "Connection: Authorization" to strip the Authorization header when using the default Director/NewSingleHostReverseProxy. The Rewrite function processes headers after hop-by-hop removal, ensuring headers like Authorization are preserved and forwarded to backend services. This fixes external_routes proxy stripping auth headers, causing 401 errors when proxying to authenticated services like the internal registry.
- commit
- 3c39909950778ef4679aa79f16a9cbc6125c9b2a
- author
- brice · · 2026-01-15 12:02 UTC
- parent
- 99f5ba4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +39 -26
M internal/usecase/proxy/service.go +39 -26
| @@ -269,6 +269,23 @@ func (s *Service) UpdateConfig(config Config) { | |||
| 269 | 269 | ||
| 270 | 270 | // 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 | 289 | func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target *domain.ProxyTarget) { | |
| 273 | 290 | log := zerowrap.FromCtx(r.Context()) | |
| 274 | 291 | ||
| @@ -279,19 +296,17 @@ func (s *Service) proxyToTarget(w http.ResponseWriter, r *http.Request, target * | |||
| 279 | 296 | return | |
| 280 | 297 | } | |
| 281 | 298 | ||
| 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 | + | ) | |
| 295 | 310 | ||
| 296 | 311 | log.Debug(). | |
| 297 | 312 | Str("target", targetURL.String()). | |
| @@ -311,19 +326,17 @@ func (s *Service) proxyToRegistry(w http.ResponseWriter, r *http.Request) { | |||
| 311 | 326 | return | |
| 312 | 327 | } | |
| 313 | 328 | ||
| 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 | + | ) | |
| 327 | 340 | ||
| 328 | 341 | log.Debug().Str("target", targetURL.String()).Msg("proxying request to registry") | |
| 329 | 342 | ||
| @@ -269,6 +269,23 @@ func (s *Service) UpdateConfig(config Config) { | |||||
| 269 | 269 | ||||
| 270 | // Helper methods | 270 | // 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 | return | 296 | return | ||
| 280 | } | 297 | } | ||
| 281 | 298 | ||||
| 282 | - | proxy := httputil.NewSingleHostReverseProxy(targetURL) | 299 | + | proxy := newReverseProxy(targetURL, |
| 283 | - | proxy.Transport = proxyTransport | 300 | + | 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 | return | 326 | return | ||
| 312 | } | 327 | } | ||
| 313 | 328 | ||||
| 314 | - | proxy := httputil.NewSingleHostReverseProxy(targetURL) | 329 | + | proxy := newReverseProxy(targetURL, |
| 315 | - | proxy.Transport = proxyTransport | 330 | + | 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 | ||||