detached 552aaca

refactor: deduplicate CIDR allowlist code and clean up

- Extract shared cidrAllowlist helper replacing duplicate
  RegistryCIDRAllowlist/ProxyCIDRAllowlist implementations
- Extract parseCIDRAllowlist/denyAllHandler replacing duplicate
  buildRegistryCIDRAllowlistMiddleware/buildProxyCIDRAllowlistMiddleware
- Remove unused trustedNets param from ProxyCIDRAllowlist
- Remove redundant units import alias in runtime.go
- Remove redundant TrimSpace in normalizeIP
- Clean up unnecessary "what" comments in security headers middleware
- Consolidate verbose registry proxy ModifyResponse comments
commit
552aaca0c26fcc16b2e2cac24564a4245dfc4b05
author
bnema · · 2026-03-27 22:24 UTC
parent
66e4d1b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +75 -123
M internal/adapters/in/http/middleware/cidr.go +24 -48
@@ -13,18 +13,17 @@ import (
1313 // localhostNets contains IPv4 and IPv6 loopback ranges that are always allowed.
1414 var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
1515
16-// RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.
17-// Localhost is always allowed so Gordon can pull from its own registry.
18-// An empty allowedNets slice is a no-op (all traffic passes through).
19-func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
16+// cidrAllowlist is the shared implementation for CIDR-based access control middleware.
17+// ipExtractor determines how the client IP is obtained from the request.
18+// logLabel is used in the deny log message (e.g. "registry", "proxy origin").
19+func cidrAllowlist(allowedNets []*net.IPNet, ipExtractor func(*http.Request) string, logLabel string, log zerowrap.Logger) func(http.Handler) http.Handler {
2020 return func(next http.Handler) http.Handler {
2121 if len(allowedNets) == 0 {
2222 return next
2323 }
2424 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25- clientIP := GetClientIP(r, trustedNets)
25+ clientIP := ipExtractor(r)
2626
27- // Always allow localhost — Gordon must reach its own registry.
2827 if IsTrustedProxy(clientIP, localhostNets) {
2928 next.ServeHTTP(w, r)
3029 return
@@ -39,9 +38,8 @@ func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.L
3938 Str(zerowrap.FieldLayer, "adapter").
4039 Str(zerowrap.FieldAdapter, "http").
4140 Str(zerowrap.FieldMethod, r.Method).
42- Str(zerowrap.FieldPath, r.URL.Path).
4341 Str(zerowrap.FieldClientIP, clientIP).
44- Msg("registry access denied by CIDR allowlist")
42+ Msgf("%s access denied by CIDR allowlist", logLabel)
4543
4644 w.Header().Set("Content-Type", "application/json")
4745 w.WriteHeader(http.StatusForbidden)
@@ -50,47 +48,25 @@ func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.L
5048 }
5149 }
5250
51+// RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.
52+// Localhost is always allowed so Gordon can pull from its own registry.
53+// An empty allowedNets slice is a no-op (all traffic passes through).
54+func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
55+ return cidrAllowlist(allowedNets, func(r *http.Request) string {
56+ return GetClientIP(r, trustedNets)
57+ }, "registry", log)
58+}
59+
5360 // ProxyCIDRAllowlist returns middleware that restricts proxy access to the given CIDR ranges.
54-// This is used to ensure only trusted sources (e.g. Cloudflare edge IPs) can reach the
55-// proxy server, preventing direct-to-origin attacks that bypass CDN protections.
61+// This validates the direct network peer (RemoteAddr), not forwarded IPs, to ensure only
62+// trusted sources (e.g. Cloudflare edge IPs) can reach the proxy server.
5663 // An empty allowedNets slice is a no-op (all traffic passes through).
57-// Note: the second parameter is accepted for signature consistency with RegistryCIDRAllowlist
58-// but is intentionally unused — this middleware validates the direct network peer, not forwarded IPs.
59-func ProxyCIDRAllowlist(allowedNets, _ []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
60- return func(next http.Handler) http.Handler {
61- if len(allowedNets) == 0 {
62- return next
64+func ProxyCIDRAllowlist(allowedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
65+ return cidrAllowlist(allowedNets, func(r *http.Request) string {
66+ remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
67+ if err != nil {
68+ return r.RemoteAddr
6369 }
64- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65- // For proxy allowlisting we check the direct connection IP (RemoteAddr),
66- // not the client IP from headers, because we're validating the network
67- // peer (e.g. Cloudflare edge), not the end-user.
68- remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
69- if err != nil {
70- remoteIP = r.RemoteAddr
71- }
72-
73- if IsTrustedProxy(remoteIP, localhostNets) {
74- next.ServeHTTP(w, r)
75- return
76- }
77-
78- if IsTrustedProxy(remoteIP, allowedNets) {
79- next.ServeHTTP(w, r)
80- return
81- }
82-
83- log.Warn().
84- Str(zerowrap.FieldLayer, "adapter").
85- Str(zerowrap.FieldAdapter, "http").
86- Str(zerowrap.FieldMethod, r.Method).
87- Str("host", r.Host).
88- Str(zerowrap.FieldClientIP, remoteIP).
89- Msg("proxy access denied by origin IP allowlist")
90-
91- w.Header().Set("Content-Type", "application/json")
92- w.WriteHeader(http.StatusForbidden)
93- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
94- })
95- }
70+ return remoteIP
71+ }, "proxy origin", log)
9672 }
@@ -13,18 +13,17 @@ import (
13 // localhostNets contains IPv4 and IPv6 loopback ranges that are always allowed.13 // localhostNets contains IPv4 and IPv6 loopback ranges that are always allowed.
14 var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})14 var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
15 15
16-// RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.16+// cidrAllowlist is the shared implementation for CIDR-based access control middleware.
17-// Localhost is always allowed so Gordon can pull from its own registry.17+// ipExtractor determines how the client IP is obtained from the request.
18-// An empty allowedNets slice is a no-op (all traffic passes through).18+// logLabel is used in the deny log message (e.g. "registry", "proxy origin").
19-func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {19+func cidrAllowlist(allowedNets []*net.IPNet, ipExtractor func(*http.Request) string, logLabel string, log zerowrap.Logger) func(http.Handler) http.Handler {
20 return func(next http.Handler) http.Handler {20 return func(next http.Handler) http.Handler {
21 if len(allowedNets) == 0 {21 if len(allowedNets) == 0 {
22 return next22 return next
23 }23 }
24 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {24 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25- clientIP := GetClientIP(r, trustedNets)25+ clientIP := ipExtractor(r)
26 26
27- // Always allow localhost — Gordon must reach its own registry.
28 if IsTrustedProxy(clientIP, localhostNets) {27 if IsTrustedProxy(clientIP, localhostNets) {
29 next.ServeHTTP(w, r)28 next.ServeHTTP(w, r)
30 return29 return
@@ -39,9 +38,8 @@ func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.L
39 Str(zerowrap.FieldLayer, "adapter").38 Str(zerowrap.FieldLayer, "adapter").
40 Str(zerowrap.FieldAdapter, "http").39 Str(zerowrap.FieldAdapter, "http").
41 Str(zerowrap.FieldMethod, r.Method).40 Str(zerowrap.FieldMethod, r.Method).
42- Str(zerowrap.FieldPath, r.URL.Path).
43 Str(zerowrap.FieldClientIP, clientIP).41 Str(zerowrap.FieldClientIP, clientIP).
44- Msg("registry access denied by CIDR allowlist")42+ Msgf("%s access denied by CIDR allowlist", logLabel)
45 43
46 w.Header().Set("Content-Type", "application/json")44 w.Header().Set("Content-Type", "application/json")
47 w.WriteHeader(http.StatusForbidden)45 w.WriteHeader(http.StatusForbidden)
@@ -50,47 +48,25 @@ func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.L
50 }48 }
51 }49 }
52 50
51+// RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.
52+// Localhost is always allowed so Gordon can pull from its own registry.
53+// An empty allowedNets slice is a no-op (all traffic passes through).
54+func RegistryCIDRAllowlist(allowedNets, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
55+ return cidrAllowlist(allowedNets, func(r *http.Request) string {
56+ return GetClientIP(r, trustedNets)
57+ }, "registry", log)
58+}
59+
53 // ProxyCIDRAllowlist returns middleware that restricts proxy access to the given CIDR ranges.60 // ProxyCIDRAllowlist returns middleware that restricts proxy access to the given CIDR ranges.
54-// This is used to ensure only trusted sources (e.g. Cloudflare edge IPs) can reach the61+// This validates the direct network peer (RemoteAddr), not forwarded IPs, to ensure only
55-// proxy server, preventing direct-to-origin attacks that bypass CDN protections.62+// trusted sources (e.g. Cloudflare edge IPs) can reach the proxy server.
56 // An empty allowedNets slice is a no-op (all traffic passes through).63 // An empty allowedNets slice is a no-op (all traffic passes through).
57-// Note: the second parameter is accepted for signature consistency with RegistryCIDRAllowlist64+func ProxyCIDRAllowlist(allowedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
58-// but is intentionally unused — this middleware validates the direct network peer, not forwarded IPs.65+ return cidrAllowlist(allowedNets, func(r *http.Request) string {
59-func ProxyCIDRAllowlist(allowedNets, _ []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {66+ remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
60- return func(next http.Handler) http.Handler {67+ if err != nil {
61- if len(allowedNets) == 0 {68+ return r.RemoteAddr
62- return next
63 }69 }
64- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {70+ return remoteIP
65- // For proxy allowlisting we check the direct connection IP (RemoteAddr),71+ }, "proxy origin", log)
66- // not the client IP from headers, because we're validating the network
67- // peer (e.g. Cloudflare edge), not the end-user.
68- remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
69- if err != nil {
70- remoteIP = r.RemoteAddr
71- }
72-
73- if IsTrustedProxy(remoteIP, localhostNets) {
74- next.ServeHTTP(w, r)
75- return
76- }
77-
78- if IsTrustedProxy(remoteIP, allowedNets) {
79- next.ServeHTTP(w, r)
80- return
81- }
82-
83- log.Warn().
84- Str(zerowrap.FieldLayer, "adapter").
85- Str(zerowrap.FieldAdapter, "http").
86- Str(zerowrap.FieldMethod, r.Method).
87- Str("host", r.Host).
88- Str(zerowrap.FieldClientIP, remoteIP).
89- Msg("proxy access denied by origin IP allowlist")
90-
91- w.Header().Set("Content-Type", "application/json")
92- w.WriteHeader(http.StatusForbidden)
93- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
94- })
95- }
96 }72 }
M internal/adapters/in/http/middleware/clientip.go +1 -1
@@ -16,7 +16,7 @@ func normalizeIP(raw string) string {
1616 ip = host
1717 }
1818
19- parsed := net.ParseIP(strings.TrimSpace(ip))
19+ parsed := net.ParseIP(ip)
2020 if parsed == nil {
2121 return ""
2222 }
@@ -16,7 +16,7 @@ func normalizeIP(raw string) string {
16 ip = host16 ip = host
17 }17 }
18 18
19- parsed := net.ParseIP(strings.TrimSpace(ip))19+ parsed := net.ParseIP(ip)
20 if parsed == nil {20 if parsed == nil {
21 return ""21 return ""
22 }22 }
M internal/adapters/in/http/middleware/security.go +5 -17
@@ -5,7 +5,6 @@ import (
55 )
66
77 // SecurityHeaders middleware adds standard security headers to HTTP responses.
8-// This provides defense-in-depth against various web attacks.
98 func SecurityHeaders(next http.Handler) http.Handler {
109 return SecurityHeadersWithOptions(false)(next)
1110 }
@@ -17,32 +16,21 @@ func SecurityHeaders(next http.Handler) http.Handler {
1716 func SecurityHeadersWithOptions(forceHSTS bool) func(http.Handler) http.Handler {
1817 return func(next http.Handler) http.Handler {
1918 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20- // Prevent MIME type sniffing
2119 w.Header().Set("X-Content-Type-Options", "nosniff")
22-
23- // Prevent clickjacking
2420 w.Header().Set("X-Frame-Options", "DENY")
25-
26- // XSS protection (legacy, but still useful for older browsers)
2721 w.Header().Set("X-XSS-Protection", "1; mode=block")
28-
29- // Control referrer information
3022 w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
31-
32- // Restrict browser features
3323 w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
3424
35- // HSTS: Enforce HTTPS-only for browsers.
36- // Set when the request arrived over TLS, or when force_hsts is enabled
37- // (for deployments behind a TLS-terminating proxy like Cloudflare).
25+ // Only set when TLS is active or force_hsts is configured,
26+ // since HSTS over plain HTTP would lock out clients.
3827 if r.TLS != nil || forceHSTS {
3928 w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
4029 }
4130
42- // NOTE: CSP is intentionally NOT set here. Gordon proxies arbitrary web
43- // apps, so a blanket "default-src 'none'" would break all proxied sites.
44- // Instead, CSP is set directly on proxy-generated error responses in the
45- // proxy error handlers (service.go).
31+ // CSP is intentionally NOT set here — Gordon proxies arbitrary web apps,
32+ // so a blanket policy would break proxied sites. CSP is set only on
33+ // Gordon-generated error responses (see proxy error handlers).
4634
4735 next.ServeHTTP(w, r)
4836 })
@@ -5,7 +5,6 @@ import (
5 )5 )
6 6
7 // SecurityHeaders middleware adds standard security headers to HTTP responses.7 // SecurityHeaders middleware adds standard security headers to HTTP responses.
8-// This provides defense-in-depth against various web attacks.
9 func SecurityHeaders(next http.Handler) http.Handler {8 func SecurityHeaders(next http.Handler) http.Handler {
10 return SecurityHeadersWithOptions(false)(next)9 return SecurityHeadersWithOptions(false)(next)
11 }10 }
@@ -17,32 +16,21 @@ func SecurityHeaders(next http.Handler) http.Handler {
17 func SecurityHeadersWithOptions(forceHSTS bool) func(http.Handler) http.Handler {16 func SecurityHeadersWithOptions(forceHSTS bool) func(http.Handler) http.Handler {
18 return func(next http.Handler) http.Handler {17 return func(next http.Handler) http.Handler {
19 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {18 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20- // Prevent MIME type sniffing
21 w.Header().Set("X-Content-Type-Options", "nosniff")19 w.Header().Set("X-Content-Type-Options", "nosniff")
22-
23- // Prevent clickjacking
24 w.Header().Set("X-Frame-Options", "DENY")20 w.Header().Set("X-Frame-Options", "DENY")
25-
26- // XSS protection (legacy, but still useful for older browsers)
27 w.Header().Set("X-XSS-Protection", "1; mode=block")21 w.Header().Set("X-XSS-Protection", "1; mode=block")
28-
29- // Control referrer information
30 w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")22 w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
31-
32- // Restrict browser features
33 w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")23 w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
34 24
35- // HSTS: Enforce HTTPS-only for browsers.25+ // Only set when TLS is active or force_hsts is configured,
36- // Set when the request arrived over TLS, or when force_hsts is enabled26+ // since HSTS over plain HTTP would lock out clients.
37- // (for deployments behind a TLS-terminating proxy like Cloudflare).
38 if r.TLS != nil || forceHSTS {27 if r.TLS != nil || forceHSTS {
39 w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")28 w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
40 }29 }
41 30
42- // NOTE: CSP is intentionally NOT set here. Gordon proxies arbitrary web31+ // CSP is intentionally NOT set here — Gordon proxies arbitrary web apps,
43- // apps, so a blanket "default-src 'none'" would break all proxied sites.32+ // so a blanket policy would break proxied sites. CSP is set only on
44- // Instead, CSP is set directly on proxy-generated error responses in the33+ // Gordon-generated error responses (see proxy error handlers).
45- // proxy error handlers (service.go).
46 34
47 next.ServeHTTP(w, r)35 next.ServeHTTP(w, r)
48 })36 })
M internal/adapters/in/http/proxy/forwarder.go +2 -9
@@ -156,16 +156,9 @@ func (h *Handler) forwardToRegistry(w http.ResponseWriter, r *http.Request, regi
156156 log.Error().Err(proxyErr).Int("registry_port", registryPort).Msg("registry proxy error")
157157 proxyError(w, "Registry Unavailable", http.StatusServiceUnavailable)
158158 },
159- // ModifyResponse adjusts registry responses for Docker client consumption.
160- // Registry API responses are consumed by Docker CLI/daemon, not browsers.
161- // Upstream registry servers may inject browser-oriented security headers
162- // (e.g., HSTS, CSP, X-Frame-Options) that are irrelevant for API clients
163- // and could conflict with Gordon's own security headers on browser-facing
164- // routes. We strip them here to ensure clean, predictable registry responses.
159+ // Strip browser-oriented security headers from upstream registry responses
160+ // to avoid conflicts with Gordon's own headers on browser-facing routes.
165161 ModifyResponse: func(resp *http.Response) error {
166- // Remove browser-oriented security headers injected by the upstream
167- // registry. These are unnecessary for Docker client API traffic and
168- // would conflict with headers set on browser-facing proxy routes.
169162 resp.Header.Del("X-Content-Type-Options")
170163 resp.Header.Del("X-Frame-Options")
171164 resp.Header.Del("X-XSS-Protection")
@@ -156,16 +156,9 @@ func (h *Handler) forwardToRegistry(w http.ResponseWriter, r *http.Request, regi
156 log.Error().Err(proxyErr).Int("registry_port", registryPort).Msg("registry proxy error")156 log.Error().Err(proxyErr).Int("registry_port", registryPort).Msg("registry proxy error")
157 proxyError(w, "Registry Unavailable", http.StatusServiceUnavailable)157 proxyError(w, "Registry Unavailable", http.StatusServiceUnavailable)
158 },158 },
159- // ModifyResponse adjusts registry responses for Docker client consumption.159+ // Strip browser-oriented security headers from upstream registry responses
160- // Registry API responses are consumed by Docker CLI/daemon, not browsers.160+ // to avoid conflicts with Gordon's own headers on browser-facing routes.
161- // Upstream registry servers may inject browser-oriented security headers
162- // (e.g., HSTS, CSP, X-Frame-Options) that are irrelevant for API clients
163- // and could conflict with Gordon's own security headers on browser-facing
164- // routes. We strip them here to ensure clean, predictable registry responses.
165 ModifyResponse: func(resp *http.Response) error {161 ModifyResponse: func(resp *http.Response) error {
166- // Remove browser-oriented security headers injected by the upstream
167- // registry. These are unnecessary for Docker client API traffic and
168- // would conflict with headers set on browser-facing proxy routes.
169 resp.Header.Del("X-Content-Type-Options")162 resp.Header.Del("X-Content-Type-Options")
170 resp.Header.Del("X-Frame-Options")163 resp.Header.Del("X-Frame-Options")
171 resp.Header.Del("X-XSS-Protection")164 resp.Header.Del("X-XSS-Protection")
M internal/adapters/out/docker/runtime.go +1 -1
@@ -30,7 +30,7 @@ import (
3030 "github.com/docker/docker/client"
3131 "github.com/docker/docker/pkg/stdcopy"
3232 "github.com/docker/go-connections/nat"
33- units "github.com/docker/go-units"
33+ "github.com/docker/go-units"
3434
3535 "github.com/bnema/gordon/internal/boundaries/out"
3636 "github.com/bnema/gordon/internal/domain"
@@ -30,7 +30,7 @@ import (
30 "github.com/docker/docker/client"30 "github.com/docker/docker/client"
31 "github.com/docker/docker/pkg/stdcopy"31 "github.com/docker/docker/pkg/stdcopy"
32 "github.com/docker/go-connections/nat"32 "github.com/docker/go-connections/nat"
33- units "github.com/docker/go-units"33+ "github.com/docker/go-units"
34 34
35 "github.com/bnema/gordon/internal/boundaries/out"35 "github.com/bnema/gordon/internal/boundaries/out"
36 "github.com/bnema/gordon/internal/domain"36 "github.com/bnema/gordon/internal/domain"
M internal/app/run.go +42 -47
@@ -1747,76 +1747,71 @@ func buildRegistryHandlerWithMiddleware(
17471747 return registryWithOtel, cidrAllowlistMiddleware, rateLimitMiddleware
17481748 }
17491749
1750-func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1751- if len(cfg.Server.RegistryAllowedIPs) == 0 {
1752- return nil
1750+// parseCIDRAllowlist parses a list of IPs/CIDRs, logs warnings for invalid entries,
1751+// and returns the parsed nets. label is used in log messages (e.g. "registry_allowed_ips").
1752+func parseCIDRAllowlist(ips []string, label string, log zerowrap.Logger) ([]*net.IPNet, bool) {
1753+ if len(ips) == 0 {
1754+ return nil, false
17531755 }
17541756
1755- allowedNets := middleware.ParseTrustedProxies(cfg.Server.RegistryAllowedIPs)
1756- if len(allowedNets) != len(cfg.Server.RegistryAllowedIPs) {
1757- for _, entry := range cfg.Server.RegistryAllowedIPs {
1757+ allowedNets := middleware.ParseTrustedProxies(ips)
1758+ if len(allowedNets) != len(ips) {
1759+ for _, entry := range ips {
17581760 if nets := middleware.ParseTrustedProxies([]string{entry}); len(nets) == 0 {
1759- log.Warn().Str("entry", entry).Msg("ignoring invalid registry_allowed_ips entry")
1761+ log.Warn().Str("entry", entry).Msgf("ignoring invalid %s entry", label)
17601762 }
17611763 }
17621764 }
17631765
17641766 if len(allowedNets) == 0 {
17651767 log.Error().
1766- Strs("registry_allowed_ips", cfg.Server.RegistryAllowedIPs).
1767- Msg("registry_allowed_ips is set but no valid entries were parsed; registry will deny all traffic (fail-closed)")
1768- return func(next http.Handler) http.Handler {
1769- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1770- log.Warn().
1771- Str(zerowrap.FieldPath, r.URL.Path).
1772- Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1773- Msg("registry access denied due to invalid registry_allowed_ips configuration")
1774- w.Header().Set("Content-Type", "application/json")
1775- w.WriteHeader(http.StatusForbidden)
1776- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1777- })
1778- }
1768+ Strs(label, ips).
1769+ Msgf("%s is set but no valid entries were parsed; will deny all traffic (fail-closed)", label)
1770+ return nil, true // allInvalid
17791771 }
17801772
1781- return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
1773+ return allowedNets, false
17821774 }
17831775
1784-func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1785- if len(cfg.Server.ProxyAllowedIPs) == 0 {
1786- return nil
1776+// denyAllHandler returns a middleware that rejects every request with 403 Forbidden.
1777+func denyAllHandler(label string, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1778+ return func(next http.Handler) http.Handler {
1779+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1780+ log.Warn().
1781+ Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1782+ Msgf("access denied due to invalid %s configuration", label)
1783+ w.Header().Set("Content-Type", "application/json")
1784+ w.WriteHeader(http.StatusForbidden)
1785+ _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1786+ })
17871787 }
1788+}
17881789
1789- allowedNets := middleware.ParseTrustedProxies(cfg.Server.ProxyAllowedIPs)
1790- if len(allowedNets) != len(cfg.Server.ProxyAllowedIPs) {
1791- for _, entry := range cfg.Server.ProxyAllowedIPs {
1792- if nets := middleware.ParseTrustedProxies([]string{entry}); len(nets) == 0 {
1793- log.Warn().Str("entry", entry).Msg("ignoring invalid proxy_allowed_ips entry")
1794- }
1795- }
1790+func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1791+ allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.RegistryAllowedIPs, "registry_allowed_ips", log)
1792+ if allInvalid {
1793+ return denyAllHandler("registry_allowed_ips", trustedNets, log)
1794+ }
1795+ if allowedNets == nil {
1796+ return nil
17961797 }
1798+ return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
1799+}
17971800
1798- if len(allowedNets) == 0 {
1799- log.Error().
1800- Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).
1801- Msg("proxy_allowed_ips is set but no valid entries were parsed; proxy will deny all traffic (fail-closed)")
1802- return func(next http.Handler) http.Handler {
1803- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1804- log.Warn().
1805- Str("host", r.Host).
1806- Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1807- Msg("proxy access denied due to invalid proxy_allowed_ips configuration")
1808- w.Header().Set("Content-Type", "application/json")
1809- w.WriteHeader(http.StatusForbidden)
1810- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1811- })
1812- }
1801+func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1802+ allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.ProxyAllowedIPs, "proxy_allowed_ips", log)
1803+ if allInvalid {
1804+ return denyAllHandler("proxy_allowed_ips", trustedNets, log)
1805+ }
1806+ if allowedNets == nil {
1807+ return nil
18131808 }
18141809
18151810 log.Info().
18161811 Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).
18171812 Msg("proxy origin IP allowlist enabled")
18181813
1819- return middleware.ProxyCIDRAllowlist(allowedNets, trustedNets, log)
1814+ return middleware.ProxyCIDRAllowlist(allowedNets, log)
18201815 }
18211816
18221817 func buildRegistryRateLimitMiddleware(cfg Config, log zerowrap.Logger) func(http.Handler) http.Handler {
@@ -1747,76 +1747,71 @@ func buildRegistryHandlerWithMiddleware(
1747 return registryWithOtel, cidrAllowlistMiddleware, rateLimitMiddleware1747 return registryWithOtel, cidrAllowlistMiddleware, rateLimitMiddleware
1748 }1748 }
1749 1749
1750-func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {1750+// parseCIDRAllowlist parses a list of IPs/CIDRs, logs warnings for invalid entries,
1751- if len(cfg.Server.RegistryAllowedIPs) == 0 {1751+// and returns the parsed nets. label is used in log messages (e.g. "registry_allowed_ips").
1752- return nil1752+func parseCIDRAllowlist(ips []string, label string, log zerowrap.Logger) ([]*net.IPNet, bool) {
1753+ if len(ips) == 0 {
1754+ return nil, false
1753 }1755 }
1754 1756
1755- allowedNets := middleware.ParseTrustedProxies(cfg.Server.RegistryAllowedIPs)1757+ allowedNets := middleware.ParseTrustedProxies(ips)
1756- if len(allowedNets) != len(cfg.Server.RegistryAllowedIPs) {1758+ if len(allowedNets) != len(ips) {
1757- for _, entry := range cfg.Server.RegistryAllowedIPs {1759+ for _, entry := range ips {
1758 if nets := middleware.ParseTrustedProxies([]string{entry}); len(nets) == 0 {1760 if nets := middleware.ParseTrustedProxies([]string{entry}); len(nets) == 0 {
1759- log.Warn().Str("entry", entry).Msg("ignoring invalid registry_allowed_ips entry")1761+ log.Warn().Str("entry", entry).Msgf("ignoring invalid %s entry", label)
1760 }1762 }
1761 }1763 }
1762 }1764 }
1763 1765
1764 if len(allowedNets) == 0 {1766 if len(allowedNets) == 0 {
1765 log.Error().1767 log.Error().
1766- Strs("registry_allowed_ips", cfg.Server.RegistryAllowedIPs).1768+ Strs(label, ips).
1767- Msg("registry_allowed_ips is set but no valid entries were parsed; registry will deny all traffic (fail-closed)")1769+ Msgf("%s is set but no valid entries were parsed; will deny all traffic (fail-closed)", label)
1768- return func(next http.Handler) http.Handler {1770+ return nil, true // allInvalid
1769- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1770- log.Warn().
1771- Str(zerowrap.FieldPath, r.URL.Path).
1772- Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1773- Msg("registry access denied due to invalid registry_allowed_ips configuration")
1774- w.Header().Set("Content-Type", "application/json")
1775- w.WriteHeader(http.StatusForbidden)
1776- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1777- })
1778- }
1779 }1771 }
1780 1772
1781- return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)1773+ return allowedNets, false
1782 }1774 }
1783 1775
1784-func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {1776+// denyAllHandler returns a middleware that rejects every request with 403 Forbidden.
1785- if len(cfg.Server.ProxyAllowedIPs) == 0 {1777+func denyAllHandler(label string, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1786- return nil1778+ return func(next http.Handler) http.Handler {
1779+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1780+ log.Warn().
1781+ Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1782+ Msgf("access denied due to invalid %s configuration", label)
1783+ w.Header().Set("Content-Type", "application/json")
1784+ w.WriteHeader(http.StatusForbidden)
1785+ _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1786+ })
1787 }1787 }
1788+}
1788 1789
1789- allowedNets := middleware.ParseTrustedProxies(cfg.Server.ProxyAllowedIPs)1790+func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1790- if len(allowedNets) != len(cfg.Server.ProxyAllowedIPs) {1791+ allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.RegistryAllowedIPs, "registry_allowed_ips", log)
1791- for _, entry := range cfg.Server.ProxyAllowedIPs {1792+ if allInvalid {
1792- if nets := middleware.ParseTrustedProxies([]string{entry}); len(nets) == 0 {1793+ return denyAllHandler("registry_allowed_ips", trustedNets, log)
1793- log.Warn().Str("entry", entry).Msg("ignoring invalid proxy_allowed_ips entry")1794+ }
1794- }1795+ if allowedNets == nil {
1795- }1796+ return nil
1796 }1797 }
1798+ return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
1799+}
1797 1800
1798- if len(allowedNets) == 0 {1801+func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1799- log.Error().1802+ allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.ProxyAllowedIPs, "proxy_allowed_ips", log)
1800- Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).1803+ if allInvalid {
1801- Msg("proxy_allowed_ips is set but no valid entries were parsed; proxy will deny all traffic (fail-closed)")1804+ return denyAllHandler("proxy_allowed_ips", trustedNets, log)
1802- return func(next http.Handler) http.Handler {1805+ }
1803- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {1806+ if allowedNets == nil {
1804- log.Warn().1807+ return nil
1805- Str("host", r.Host).
1806- Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1807- Msg("proxy access denied due to invalid proxy_allowed_ips configuration")
1808- w.Header().Set("Content-Type", "application/json")
1809- w.WriteHeader(http.StatusForbidden)
1810- _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1811- })
1812- }
1813 }1808 }
1814 1809
1815 log.Info().1810 log.Info().
1816 Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).1811 Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).
1817 Msg("proxy origin IP allowlist enabled")1812 Msg("proxy origin IP allowlist enabled")
1818 1813
1819- return middleware.ProxyCIDRAllowlist(allowedNets, trustedNets, log)1814+ return middleware.ProxyCIDRAllowlist(allowedNets, log)
1820 }1815 }
1821 1816
1822 func buildRegistryRateLimitMiddleware(cfg Config, log zerowrap.Logger) func(http.Handler) http.Handler {1817 func buildRegistryRateLimitMiddleware(cfg Config, log zerowrap.Logger) func(http.Handler) http.Handler {

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