From ac825a2c03589da5ea9925a146cb3b4bd6555d45 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sat, 5 Sep 2026 14:04:21 +0200 Subject: [PATCH] feat(api): warn at startup when the debug route has no authentication --- pkg/modules/api/api.go | 25 ++++++++++++++ pkg/modules/api/api_test.go | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/pkg/modules/api/api.go b/pkg/modules/api/api.go index 0b4506f7..5fdb3371 100644 --- a/pkg/modules/api/api.go +++ b/pkg/modules/api/api.go @@ -383,12 +383,37 @@ func (a *Api) Provision(ctx *gotenberg.Context) error { // Logger. a.logger = gotenberg.Logger(a) + a.warnInsecureDebugRoute() + // File system. a.fs = gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) return nil } +// warnInsecureDebugRoute logs a warning when the debug route is reachable +// without authentication. +// +// The route reports the resolved configuration of every module, which is +// useful to an operator and equally useful to anyone else who can reach it. +// This warns rather than refuses: an operator may sit behind a gateway that +// authenticates on Gotenberg's behalf, and failing startup would break them. +func (a *Api) warnInsecureDebugRoute() { + if !a.enableDebugRoute || a.basicAuthUsername != "" || a.oidcEnabled { + return + } + if a.logger == nil { + return + } + + a.logger.WarnContext( + context.Background(), + "--api-enable-debug-route (API_ENABLE_DEBUG_ROUTE) is enabled but no authentication is configured, so anyone who can reach Gotenberg can read its configuration. Set --api-enable-basic-auth (API_ENABLE_BASIC_AUTH) with GOTENBERG_API_BASIC_AUTH_USERNAME and GOTENBERG_API_BASIC_AUTH_PASSWORD, set --api-enable-oidc-auth (API_ENABLE_OIDC_AUTH), or disable the route.", + slog.String("flag", "--api-enable-debug-route"), + slog.String("env", "API_ENABLE_DEBUG_ROUTE"), + ) +} + // Validate validates the module properties. func (a *Api) Validate() error { var err error diff --git a/pkg/modules/api/api_test.go b/pkg/modules/api/api_test.go index d15e9269..becbe01f 100644 --- a/pkg/modules/api/api_test.go +++ b/pkg/modules/api/api_test.go @@ -1,6 +1,8 @@ package api import ( + "bytes" + "log/slog" "strings" "testing" ) @@ -65,3 +67,68 @@ func TestApi_Validate_Auth(t *testing.T) { }) } } + +func TestApi_warnInsecureDebugRoute(t *testing.T) { + for _, tc := range []struct { + scenario string + api Api + expectWarn bool + expectFields []string + }{ + { + scenario: "debug route on with no auth warns", + api: Api{enableDebugRoute: true}, + expectWarn: true, + expectFields: []string{"--api-enable-debug-route", "API_ENABLE_DEBUG_ROUTE", "--api-enable-basic-auth", "API_ENABLE_BASIC_AUTH", "--api-enable-oidc-auth", "API_ENABLE_OIDC_AUTH"}, + }, + { + scenario: "debug route off is silent", + api: Api{enableDebugRoute: false}, + expectWarn: false, + }, + { + scenario: "basic auth silences it", + api: Api{enableDebugRoute: true, basicAuthUsername: "foo"}, + expectWarn: false, + }, + { + scenario: "oidc silences it", + api: Api{enableDebugRoute: true, oidcEnabled: true}, + expectWarn: false, + }, + } { + t.Run(tc.scenario, func(t *testing.T) { + buf := new(bytes.Buffer) + tc.api.logger = slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelWarn})) + + tc.api.warnInsecureDebugRoute() + + logged := buf.String() + if !tc.expectWarn { + if logged != "" { + t.Fatalf("expected no warning, got: %s", logged) + } + return + } + + if logged == "" { + t.Fatal("expected a warning, got none") + } + // Every flag named must carry its environment variable. + for _, want := range tc.expectFields { + if !strings.Contains(logged, want) { + t.Fatalf("warning does not mention %q: %s", want, logged) + } + } + if strings.Contains(logged, "—") { + t.Fatalf("warning must not contain an em dash: %s", logged) + } + }) + } +} + +// The warning reads a.logger, which is nil until Provision assigns it. +func TestApi_warnInsecureDebugRoute_NilLoggerDoesNotPanic(t *testing.T) { + api := Api{enableDebugRoute: true} + api.warnInsecureDebugRoute() +}