From 5490cbe7a67adece3b5336b98615b24f20138957 Mon Sep 17 00:00:00 2001 From: Stephane Bouchet Date: Thu, 22 Oct 2026 17:36:56 +0200 Subject: [PATCH] Require an explicit authentication mode (-use-bearer-token or -no-auth) Previously che-machine-exec started with authentication disabled by default (UseBearerToken defaulted to false), so an operator could silently run it unauthenticated. Make the choice explicit and fail-safe: - Add a -no-auth flag (and NO_AUTH env var) that explicitly disables auth. - At startup, require exactly one of -use-bearer-token or -no-auth. If neither (or both) is set, refuse to start with a fatal error. There is no longer a silent unauthenticated default. - When -no-auth is used, warn about the exposure: without an allowed-origins list the API is open to any client that can reach the port (loopback/trusted network only); with an allowed-origins list, note that the origin check only blocks browsers and does not stop a direct client that omits the Origin header. This is a breaking change for deployments that relied on the implicit no-auth default: they must now pass -no-auth (e.g. che-code, which binds to loopback) or -use-bearer-token. WTO already sets USE_BEARER_TOKEN=true and is unaffected. The fatal check is factored into checkAuthMode so it can be unit-tested without the exec manager, which requires an in-cluster service account to initialize. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Stephane Bouchet --- cfg/cfg.go | 53 ++++++++++++++++++++++++++++++++++++++++ cfg/cfg_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 cfg/cfg_test.go diff --git a/cfg/cfg.go b/cfg/cfg.go index f2244131..55a31494 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -13,6 +13,7 @@ package cfg import ( + "errors" "flag" "fmt" "os" @@ -31,6 +32,13 @@ var ( UseBearerToken bool // AuthenticatedUserID is a user's ID who is authenticated to use API. Is ignored if useBearerToken is disabled AuthenticatedUserID string + // NoAuth explicitly disables authentication. It is mutually exclusive with + // UseBearerToken. One of the two must be chosen explicitly, otherwise the + // server refuses to start (see Parse), so there is no silent unauthenticated + // default. Running with NoAuth and no AllowedOrigins leaves the API open to + // any client that can reach the port, so it must only be used on a loopback + // or otherwise trusted network (as che-code does over 127.0.0.1). + NoAuth bool // IdleTimeout is a inactivity period after which workspace should be stopped // Default value is 30 minutes @@ -107,6 +115,18 @@ func init() { } flag.BoolVar(&UseBearerToken, "use-bearer-token", defaultUseTokenValue, "to avoid users impersonation while accessing to k8s API. When enabled - authenticated user id must be configured") + defaultNoAuthValue := false + noAuthEnv := "NO_AUTH" + noAuthEnvValue, isFound := os.LookupEnv(noAuthEnv) + if isFound && len(noAuthEnvValue) > 0 { + if v, err := strconv.ParseBool(noAuthEnvValue); err == nil { + defaultNoAuthValue = v + } else { + logrus.Errorf("Invalid value '%s' for env variable key '%s'. Value should be boolean", noAuthEnvValue, noAuthEnv) + } + } + flag.BoolVar(&NoAuth, "no-auth", defaultNoAuthValue, "Explicitly disable authentication. Mutually exclusive with -use-bearer-token; one of the two must be set. Without an allowed-origins list the API is open to any client that can reach the port, so -no-auth must only be used on a loopback or otherwise trusted network.") + defaultAuthenticatedUserID := "" authenticatedUserID, isFound := os.LookupEnv("AUTHENTICATED_USER_ID") if isFound { @@ -187,6 +207,8 @@ func Parse() { AllowedOrigins = allowedOriginsFlag + validateAuthMode() + if PodSelector == "" { logrus.Fatal("pod selector is required. Configure custom pod selector or che workspace/devworkspace id env var to activate defaults") } @@ -196,6 +218,36 @@ func Parse() { } } +// validateAuthMode enforces that an authentication mode is chosen explicitly, so +// che-machine-exec never starts in a silently unauthenticated state. Exactly one +// of -use-bearer-token or -no-auth must be set, otherwise the server refuses to +// start. When authentication is disabled it warns about the exposure risk. +func validateAuthMode() { + if err := checkAuthMode(NoAuth, UseBearerToken); err != nil { + logrus.Fatal(err) + } + + if NoAuth { + if len(AllowedOrigins) > 0 { + logrus.Warnf("Authentication is DISABLED (-no-auth). Access is restricted to the configured origins (%s), which only blocks browsers; a direct client that omits the Origin header is not stopped. Do not rely on this alone on an untrusted network.", strings.Join(AllowedOrigins, ", ")) + } else { + logrus.Warn("Authentication is DISABLED (-no-auth) and no allowed origins are configured. The API is open to any client that can reach the port. Only use this on a loopback or otherwise trusted network - do NOT expose this port, especially in production.") + } + } +} + +// checkAuthMode returns an error when the authentication mode is not chosen +// explicitly. Exactly one of noAuth or useBearer must be set. +func checkAuthMode(noAuth, useBearer bool) error { + switch { + case noAuth && useBearer: + return errors.New("-no-auth and -use-bearer-token are mutually exclusive: choose exactly one authentication mode") + case !noAuth && !useBearer: + return errors.New("no authentication mode selected: start che-machine-exec with -use-bearer-token (recommended) or, only on a loopback/trusted network, with -no-auth") + } + return nil +} + // Print configuration information func Print() { logrus.Info("Exec containers configuration:") @@ -203,6 +255,7 @@ func Print() { logrus.Infof("==> Debug level %s", logrus.GetLevel().String()) logrus.Infof("==> Application url %s", URL) logrus.Infof("==> Use bearer token: %t", UseBearerToken) + logrus.Infof("==> Authentication disabled (no-auth): %t", NoAuth) logrus.Infof("==> Pod selector: %s", PodSelector) if len(AllowedOrigins) > 0 { logrus.Infof("==> Allowed WebSocket origins: %s", strings.Join(AllowedOrigins, ", ")) diff --git a/cfg/cfg_test.go b/cfg/cfg_test.go new file mode 100644 index 00000000..8c0fdbe5 --- /dev/null +++ b/cfg/cfg_test.go @@ -0,0 +1,65 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package cfg + +import "testing" + +func TestCheckAuthMode(t *testing.T) { + tests := []struct { + name string + noAuth bool + useBearer bool + wantErr bool + }{ + {name: "bearer only is valid", noAuth: false, useBearer: true, wantErr: false}, + {name: "no-auth only is valid", noAuth: true, useBearer: false, wantErr: false}, + {name: "neither selected is rejected", noAuth: false, useBearer: false, wantErr: true}, + {name: "both selected is rejected", noAuth: true, useBearer: true, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := checkAuthMode(tt.noAuth, tt.useBearer) + if (err != nil) != tt.wantErr { + t.Errorf("checkAuthMode(noAuth=%v, useBearer=%v) error = %v, wantErr %v", + tt.noAuth, tt.useBearer, err, tt.wantErr) + } + }) + } +} + +func TestParseAllowedOrigins(t *testing.T) { + tests := []struct { + name string + input string + want []string + }{ + {name: "empty string yields no origins", input: "", want: nil}, + {name: "single origin", input: "https://a.example", want: []string{"https://a.example"}}, + {name: "trims whitespace and drops blanks", input: " https://a.example , , https://b.example ", want: []string{"https://a.example", "https://b.example"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseAllowedOrigins(tt.input) + if len(got) != len(tt.want) { + t.Fatalf("parseAllowedOrigins(%q) = %v, want %v", tt.input, got, tt.want) + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("parseAllowedOrigins(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i]) + } + } + }) + } +}