detached 1dae7a9

fix(app,cli): fail closed on invalid allowlist and log close errors

Remove unused internal auth setup config arg, deny registry traffic when registry_allowed_ips contains only invalid entries, and surface local control-plane cleanup errors. Also simplify remote control-plane interface test by removing self-helper usage.
commit
1dae7a96126445cb25f3a208a63f8df71906dc1b
author
brice · · 2026-02-09 06:18 UTC
parent
25e14b0
signature
unsigned
4 files · +45 -5
M internal/adapters/in/cli/controlplane_remote_test.go +0 -2
@@ -9,8 +9,6 @@ import (
99 var _ ControlPlane = (*remoteControlPlane)(nil)
1010
1111 func TestRemoteControlPlane_ImplementsInterface(t *testing.T) {
12- t.Helper()
13-
1412 client := remote.NewClient("https://gordon.example.com")
1513 if NewRemoteControlPlane(client) == nil {
1614 t.Fatal("expected non-nil remote control-plane")
@@ -9,8 +9,6 @@ import (
9 var _ ControlPlane = (*remoteControlPlane)(nil)9 var _ ControlPlane = (*remoteControlPlane)(nil)
10 10
11 func TestRemoteControlPlane_ImplementsInterface(t *testing.T) {11 func TestRemoteControlPlane_ImplementsInterface(t *testing.T) {
12- t.Helper()
13-
14 client := remote.NewClient("https://gordon.example.com")12 client := remote.NewClient("https://gordon.example.com")
15 if NewRemoteControlPlane(client) == nil {13 if NewRemoteControlPlane(client) == nil {
16 t.Fatal("expected non-nil remote control-plane")14 t.Fatal("expected non-nil remote control-plane")
M internal/adapters/in/cli/controlplane_resolver.go +6 -1
@@ -3,6 +3,8 @@ package cli
33 import (
44 "fmt"
55
6+ "github.com/bnema/zerowrap"
7+
68 "github.com/bnema/gordon/internal/app"
79 )
810
@@ -16,7 +18,10 @@ func (h *controlPlaneHandle) close() {
1618 if h == nil || h.closeFn == nil {
1719 return
1820 }
19- _ = h.closeFn()
21+ if err := h.closeFn(); err != nil {
22+ log := zerowrap.Default()
23+ log.Warn().Err(err).Msg("failed to close control-plane resources")
24+ }
2025 }
2126
2227 func resolveControlPlane(configPath string) (*controlPlaneHandle, error) {
@@ -3,6 +3,8 @@ package cli
3 import (3 import (
4 "fmt"4 "fmt"
5 5
6+ "github.com/bnema/zerowrap"
7+
6 "github.com/bnema/gordon/internal/app"8 "github.com/bnema/gordon/internal/app"
7 )9 )
8 10
@@ -16,7 +18,10 @@ func (h *controlPlaneHandle) close() {
16 if h == nil || h.closeFn == nil {18 if h == nil || h.closeFn == nil {
17 return19 return
18 }20 }
19- _ = h.closeFn()21+ if err := h.closeFn(); err != nil {
22+ log := zerowrap.Default()
23+ log.Warn().Err(err).Msg("failed to close control-plane resources")
24+ }
20 }25 }
21 26
22 func resolveControlPlane(configPath string) (*controlPlaneHandle, error) {27 func resolveControlPlane(configPath string) (*controlPlaneHandle, error) {
M internal/app/run.go +13 -2
@@ -434,7 +434,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
434434 return nil, err
435435 }
436436
437- if err := setupInternalRegistryAuth(cfg, svc, log); err != nil {
437+ if err := setupInternalRegistryAuth(svc, log); err != nil {
438438 return nil, err
439439 }
440440
@@ -516,7 +516,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
516516 return svc, nil
517517 }
518518
519-func setupInternalRegistryAuth(_ Config, svc *services, log zerowrap.Logger) error {
519+func setupInternalRegistryAuth(svc *services, log zerowrap.Logger) error {
520520 var err error
521521 svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth()
522522 if err != nil {
@@ -1381,6 +1381,17 @@ func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet,
13811381 log.Error().
13821382 Strs("registry_allowed_ips", cfg.Server.RegistryAllowedIPs).
13831383 Msg("registry_allowed_ips is set but no valid entries were parsed; registry will allow all traffic")
1384+ return func(next http.Handler) http.Handler {
1385+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1386+ log.Warn().
1387+ Str(zerowrap.FieldPath, r.URL.Path).
1388+ Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1389+ Msg("registry access denied due to invalid registry_allowed_ips configuration")
1390+ w.Header().Set("Content-Type", "application/json")
1391+ w.WriteHeader(http.StatusForbidden)
1392+ _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1393+ })
1394+ }
13841395 }
13851396
13861397 return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
@@ -434,7 +434,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
434 return nil, err434 return nil, err
435 }435 }
436 436
437- if err := setupInternalRegistryAuth(cfg, svc, log); err != nil {437+ if err := setupInternalRegistryAuth(svc, log); err != nil {
438 return nil, err438 return nil, err
439 }439 }
440 440
@@ -516,7 +516,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
516 return svc, nil516 return svc, nil
517 }517 }
518 518
519-func setupInternalRegistryAuth(_ Config, svc *services, log zerowrap.Logger) error {519+func setupInternalRegistryAuth(svc *services, log zerowrap.Logger) error {
520 var err error520 var err error
521 svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth()521 svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth()
522 if err != nil {522 if err != nil {
@@ -1381,6 +1381,17 @@ func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet,
1381 log.Error().1381 log.Error().
1382 Strs("registry_allowed_ips", cfg.Server.RegistryAllowedIPs).1382 Strs("registry_allowed_ips", cfg.Server.RegistryAllowedIPs).
1383 Msg("registry_allowed_ips is set but no valid entries were parsed; registry will allow all traffic")1383 Msg("registry_allowed_ips is set but no valid entries were parsed; registry will allow all traffic")
1384+ return func(next http.Handler) http.Handler {
1385+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1386+ log.Warn().
1387+ Str(zerowrap.FieldPath, r.URL.Path).
1388+ Str(zerowrap.FieldClientIP, middleware.GetClientIP(r, trustedNets)).
1389+ Msg("registry access denied due to invalid registry_allowed_ips configuration")
1390+ w.Header().Set("Content-Type", "application/json")
1391+ w.WriteHeader(http.StatusForbidden)
1392+ _ = json.NewEncoder(w).Encode(dto.ErrorResponse{Error: "Forbidden"})
1393+ })
1394+ }
1384 }1395 }
1385 1396
1386 return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)1397 return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
M internal/app/run_http_handlers_test.go +26 -0
@@ -86,3 +86,29 @@ func TestCreateHTTPHandlers_AuthEnabled_ExposesAdminOnProxy(t *testing.T) {
8686 t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
8787 }
8888 }
89+
90+func TestBuildRegistryCIDRAllowlistMiddleware_InvalidEntries_DenyAll(t *testing.T) {
91+ t.Parallel()
92+
93+ cfg := Config{}
94+ cfg.Server.RegistryAllowedIPs = []string{"not-a-cidr"}
95+
96+ mw := buildRegistryCIDRAllowlistMiddleware(cfg, nil, zerowrap.Default())
97+ if mw == nil {
98+ t.Fatal("expected non-nil allowlist middleware")
99+ }
100+
101+ handler := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
102+ w.WriteHeader(http.StatusOK)
103+ }))
104+
105+ req := httptest.NewRequest(http.MethodGet, "/v2/test/manifests/latest", nil)
106+ req.RemoteAddr = "127.0.0.1:12345"
107+
108+ rec := httptest.NewRecorder()
109+ handler.ServeHTTP(rec, req)
110+
111+ if rec.Code != http.StatusForbidden {
112+ t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
113+ }
114+}
@@ -86,3 +86,29 @@ func TestCreateHTTPHandlers_AuthEnabled_ExposesAdminOnProxy(t *testing.T) {
86 t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)86 t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
87 }87 }
88 }88 }
89+
90+func TestBuildRegistryCIDRAllowlistMiddleware_InvalidEntries_DenyAll(t *testing.T) {
91+ t.Parallel()
92+
93+ cfg := Config{}
94+ cfg.Server.RegistryAllowedIPs = []string{"not-a-cidr"}
95+
96+ mw := buildRegistryCIDRAllowlistMiddleware(cfg, nil, zerowrap.Default())
97+ if mw == nil {
98+ t.Fatal("expected non-nil allowlist middleware")
99+ }
100+
101+ handler := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
102+ w.WriteHeader(http.StatusOK)
103+ }))
104+
105+ req := httptest.NewRequest(http.MethodGet, "/v2/test/manifests/latest", nil)
106+ req.RemoteAddr = "127.0.0.1:12345"
107+
108+ rec := httptest.NewRecorder()
109+ handler.ServeHTTP(rec, req)
110+
111+ if rec.Code != http.StatusForbidden {
112+ t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
113+ }
114+}

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