From 0832033a7350808b39c9535bd9f3bc1ed59a3d99 Mon Sep 17 00:00:00 2001 From: Zach <3724288+zedkipp@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:44:55 -0600 Subject: [PATCH] fix: relax secrets env var denylist for model providers (#24344) Previously we reserved some env vars that may collide with AI gateway. These were incomplete and take away flexibility from the user, which we're prioritizing in the first iteration of the feature. --- coderd/usersecrets.go | 10 +- codersdk/usersecretvalidation.go | 30 +----- codersdk/usersecretvalidation_test.go | 131 +++++++++++--------------- site/src/api/typesGenerated.ts | 14 --- 4 files changed, 60 insertions(+), 125 deletions(-) diff --git a/coderd/usersecrets.go b/coderd/usersecrets.go index 16eb947824..e613d03044 100644 --- a/coderd/usersecrets.go +++ b/coderd/usersecrets.go @@ -53,10 +53,7 @@ func (api *API) postUserSecret(rw http.ResponseWriter, r *http.Request) { }) return } - envOpts := codersdk.UserSecretEnvValidationOptions{ - AIGatewayEnabled: api.DeploymentValues.AI.BridgeConfig.Enabled.Value(), - } - if err := codersdk.UserSecretEnvNameValid(req.EnvName, envOpts); err != nil { + if err := codersdk.UserSecretEnvNameValid(req.EnvName); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid environment variable name.", Detail: err.Error(), @@ -184,10 +181,7 @@ func (api *API) patchUserSecret(rw http.ResponseWriter, r *http.Request) { return } if req.EnvName != nil { - envOpts := codersdk.UserSecretEnvValidationOptions{ - AIGatewayEnabled: api.DeploymentValues.AI.BridgeConfig.Enabled.Value(), - } - if err := codersdk.UserSecretEnvNameValid(*req.EnvName, envOpts); err != nil { + if err := codersdk.UserSecretEnvNameValid(*req.EnvName); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid environment variable name.", Detail: err.Error(), diff --git a/codersdk/usersecretvalidation.go b/codersdk/usersecretvalidation.go index da7bd303ee..7702f95085 100644 --- a/codersdk/usersecretvalidation.go +++ b/codersdk/usersecretvalidation.go @@ -28,15 +28,6 @@ const ( maxFilePathLength = 4096 ) -// UserSecretEnvValidationOptions controls deployment-aware behavior -// in environment variable name validation. -type UserSecretEnvValidationOptions struct { - // AIGatewayEnabled indicates that the deployment has AI Gateway - // configured. When true, AI Gateway environment variables - // (OPENAI_API_KEY, etc.) are reserved to prevent conflicts. - AIGatewayEnabled bool -} - var ( // posixEnvNameRegex matches valid POSIX environment variable names: // must start with a letter or underscore, followed by letters, @@ -116,17 +107,6 @@ var ( "XDG_STATE_HOME": {}, } - // aiGatewayReservedEnvNames are reserved only when AI Gateway - // is enabled on the deployment. When AI Gateway is disabled, - // users may legitimately want to inject their own API keys - // via secrets. - aiGatewayReservedEnvNames = map[string]struct{}{ - "OPENAI_API_KEY": {}, - "OPENAI_BASE_URL": {}, - "ANTHROPIC_AUTH_TOKEN": {}, - "ANTHROPIC_BASE_URL": {}, - } - // reservedEnvPrefixes are namespace prefixes where every // variable in the family is reserved. Checked after the // exact-name map. The CODER / CODER_* namespace is handled @@ -154,9 +134,7 @@ var ( // UserSecretEnvNameValid validates an environment variable name for // a user secret. Empty string is allowed (means no env injection). -// The opts parameter controls deployment-aware checks such as AI -// bridge variable reservation. -func UserSecretEnvNameValid(s string, opts UserSecretEnvValidationOptions) error { +func UserSecretEnvNameValid(s string) error { if s == "" { return nil } @@ -181,12 +159,6 @@ func UserSecretEnvNameValid(s string, opts UserSecretEnvValidationOptions) error } } - if opts.AIGatewayEnabled { - if _, ok := aiGatewayReservedEnvNames[upper]; ok { - return xerrors.Errorf("%s is reserved when AI Gateway is enabled", upper) - } - } - return nil } diff --git a/codersdk/usersecretvalidation_test.go b/codersdk/usersecretvalidation_test.go index 8ccd2fac5a..07c23eda93 100644 --- a/codersdk/usersecretvalidation_test.go +++ b/codersdk/usersecretvalidation_test.go @@ -12,126 +12,109 @@ import ( func TestUserSecretEnvNameValid(t *testing.T) { t.Parallel() - // noAIGateway is the default for most tests — AI Gateway disabled. - noAIGateway := codersdk.UserSecretEnvValidationOptions{} - withAIGateway := codersdk.UserSecretEnvValidationOptions{AIGatewayEnabled: true} - tests := []struct { name string input string - opts codersdk.UserSecretEnvValidationOptions wantErr bool errMsg string }{ // Valid names. - {name: "SimpleUpper", input: "GITHUB_TOKEN", opts: noAIGateway}, - {name: "SimpleLower", input: "github_token", opts: noAIGateway}, - {name: "StartsWithUnderscore", input: "_FOO", opts: noAIGateway}, - {name: "SingleChar", input: "A", opts: noAIGateway}, - {name: "WithDigits", input: "A1B2", opts: noAIGateway}, - {name: "Empty", input: "", opts: noAIGateway}, + {name: "SimpleUpper", input: "GITHUB_TOKEN"}, + {name: "SimpleLower", input: "github_token"}, + {name: "StartsWithUnderscore", input: "_FOO"}, + {name: "SingleChar", input: "A"}, + {name: "WithDigits", input: "A1B2"}, + {name: "Empty", input: ""}, // Invalid POSIX names. - {name: "StartsWithDigit", input: "1FOO", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, - {name: "ContainsHyphen", input: "FOO-BAR", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, - {name: "ContainsDot", input: "FOO.BAR", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, - {name: "ContainsSpace", input: "FOO BAR", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, + {name: "StartsWithDigit", input: "1FOO", wantErr: true, errMsg: "must start with"}, + {name: "ContainsHyphen", input: "FOO-BAR", wantErr: true, errMsg: "must start with"}, + {name: "ContainsDot", input: "FOO.BAR", wantErr: true, errMsg: "must start with"}, + {name: "ContainsSpace", input: "FOO BAR", wantErr: true, errMsg: "must start with"}, // Reserved system names — core POSIX/login. - {name: "ReservedPATH", input: "PATH", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedHOME", input: "HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSHELL", input: "SHELL", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedUSER", input: "USER", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedLOGNAME", input: "LOGNAME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedPWD", input: "PWD", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedOLDPWD", input: "OLDPWD", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedPATH", input: "PATH", wantErr: true, errMsg: "reserved"}, + {name: "ReservedHOME", input: "HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSHELL", input: "SHELL", wantErr: true, errMsg: "reserved"}, + {name: "ReservedUSER", input: "USER", wantErr: true, errMsg: "reserved"}, + {name: "ReservedLOGNAME", input: "LOGNAME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedPWD", input: "PWD", wantErr: true, errMsg: "reserved"}, + {name: "ReservedOLDPWD", input: "OLDPWD", wantErr: true, errMsg: "reserved"}, // Reserved system names — locale/terminal. - {name: "ReservedLANG", input: "LANG", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedTERM", input: "TERM", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedLANG", input: "LANG", wantErr: true, errMsg: "reserved"}, + {name: "ReservedTERM", input: "TERM", wantErr: true, errMsg: "reserved"}, // Reserved system names — shell behavior. - {name: "ReservedIFS", input: "IFS", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedCDPATH", input: "CDPATH", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedIFS", input: "IFS", wantErr: true, errMsg: "reserved"}, + {name: "ReservedCDPATH", input: "CDPATH", wantErr: true, errMsg: "reserved"}, // Reserved system names — shell startup files. - {name: "ReservedENV", input: "ENV", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedBASH_ENV", input: "BASH_ENV", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedENV", input: "ENV", wantErr: true, errMsg: "reserved"}, + {name: "ReservedBASH_ENV", input: "BASH_ENV", wantErr: true, errMsg: "reserved"}, // Reserved system names — temp directories. - {name: "ReservedTMPDIR", input: "TMPDIR", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedTMP", input: "TMP", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedTEMP", input: "TEMP", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedTMPDIR", input: "TMPDIR", wantErr: true, errMsg: "reserved"}, + {name: "ReservedTMP", input: "TMP", wantErr: true, errMsg: "reserved"}, + {name: "ReservedTEMP", input: "TEMP", wantErr: true, errMsg: "reserved"}, // Reserved system names — host identity. - {name: "ReservedHOSTNAME", input: "HOSTNAME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedHOSTNAME", input: "HOSTNAME", wantErr: true, errMsg: "reserved"}, // Reserved system names — SSH. - {name: "ReservedSSH_AUTH_SOCK", input: "SSH_AUTH_SOCK", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSSH_CLIENT", input: "SSH_CLIENT", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSSH_CONNECTION", input: "SSH_CONNECTION", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSSH_TTY", input: "SSH_TTY", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_AUTH_SOCK", input: "SSH_AUTH_SOCK", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_CLIENT", input: "SSH_CLIENT", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_CONNECTION", input: "SSH_CONNECTION", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_TTY", input: "SSH_TTY", wantErr: true, errMsg: "reserved"}, // Reserved system names — editor/pager. - {name: "ReservedEDITOR", input: "EDITOR", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedVISUAL", input: "VISUAL", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedPAGER", input: "PAGER", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedEDITOR", input: "EDITOR", wantErr: true, errMsg: "reserved"}, + {name: "ReservedVISUAL", input: "VISUAL", wantErr: true, errMsg: "reserved"}, + {name: "ReservedPAGER", input: "PAGER", wantErr: true, errMsg: "reserved"}, // Reserved system names — IDE integration. - {name: "ReservedVSCODE_PROXY_URI", input: "VSCODE_PROXY_URI", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedCS_DISABLE", input: "CS_DISABLE_GETTING_STARTED_OVERRIDE", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedVSCODE_PROXY_URI", input: "VSCODE_PROXY_URI", wantErr: true, errMsg: "reserved"}, + {name: "ReservedCS_DISABLE", input: "CS_DISABLE_GETTING_STARTED_OVERRIDE", wantErr: true, errMsg: "reserved"}, // Reserved system names — XDG. - {name: "ReservedXDG_RUNTIME_DIR", input: "XDG_RUNTIME_DIR", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_CONFIG_HOME", input: "XDG_CONFIG_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_DATA_HOME", input: "XDG_DATA_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_CACHE_HOME", input: "XDG_CACHE_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_STATE_HOME", input: "XDG_STATE_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - - // AI Gateway vars — blocked when AI Gateway is enabled. - {name: "AIGateway/OPENAI_API_KEY/Enabled", input: "OPENAI_API_KEY", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - {name: "AIGateway/OPENAI_BASE_URL/Enabled", input: "OPENAI_BASE_URL", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - {name: "AIGateway/ANTHROPIC_AUTH_TOKEN/Enabled", input: "ANTHROPIC_AUTH_TOKEN", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - {name: "AIGateway/ANTHROPIC_BASE_URL/Enabled", input: "ANTHROPIC_BASE_URL", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - - // AI Gateway vars — allowed when AI Gateway is disabled. - {name: "AIGateway/OPENAI_API_KEY/Disabled", input: "OPENAI_API_KEY", opts: noAIGateway}, - {name: "AIGateway/OPENAI_BASE_URL/Disabled", input: "OPENAI_BASE_URL", opts: noAIGateway}, - {name: "AIGateway/ANTHROPIC_AUTH_TOKEN/Disabled", input: "ANTHROPIC_AUTH_TOKEN", opts: noAIGateway}, - {name: "AIGateway/ANTHROPIC_BASE_URL/Disabled", input: "ANTHROPIC_BASE_URL", opts: noAIGateway}, + {name: "ReservedXDG_RUNTIME_DIR", input: "XDG_RUNTIME_DIR", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_CONFIG_HOME", input: "XDG_CONFIG_HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_DATA_HOME", input: "XDG_DATA_HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_CACHE_HOME", input: "XDG_CACHE_HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_STATE_HOME", input: "XDG_STATE_HOME", wantErr: true, errMsg: "reserved"}, // Case insensitivity. - {name: "ReservedCaseInsensitive", input: "path", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedCaseInsensitive", input: "path", wantErr: true, errMsg: "reserved"}, // CODER_ prefix. - {name: "CoderExact", input: "CODER", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, - {name: "CoderPrefix", input: "CODER_WORKSPACE_NAME", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, - {name: "CoderAgentToken", input: "CODER_AGENT_TOKEN", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, - {name: "CoderLowerCase", input: "coder_foo", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, + {name: "CoderExact", input: "CODER", wantErr: true, errMsg: "CODER_"}, + {name: "CoderPrefix", input: "CODER_WORKSPACE_NAME", wantErr: true, errMsg: "CODER_"}, + {name: "CoderAgentToken", input: "CODER_AGENT_TOKEN", wantErr: true, errMsg: "CODER_"}, + {name: "CoderLowerCase", input: "coder_foo", wantErr: true, errMsg: "CODER_"}, // GIT_* prefix. - {name: "GitSSHCommand", input: "GIT_SSH_COMMAND", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, - {name: "GitAskpass", input: "GIT_ASKPASS", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, - {name: "GitAuthorName", input: "GIT_AUTHOR_NAME", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, - {name: "GitLowerCase", input: "git_editor", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, + {name: "GitSSHCommand", input: "GIT_SSH_COMMAND", wantErr: true, errMsg: "GIT_"}, + {name: "GitAskpass", input: "GIT_ASKPASS", wantErr: true, errMsg: "GIT_"}, + {name: "GitAuthorName", input: "GIT_AUTHOR_NAME", wantErr: true, errMsg: "GIT_"}, + {name: "GitLowerCase", input: "git_editor", wantErr: true, errMsg: "GIT_"}, // LC_* prefix (locale). - {name: "LcAll", input: "LC_ALL", opts: noAIGateway, wantErr: true, errMsg: "LC_"}, - {name: "LcCtype", input: "LC_CTYPE", opts: noAIGateway, wantErr: true, errMsg: "LC_"}, + {name: "LcAll", input: "LC_ALL", wantErr: true, errMsg: "LC_"}, + {name: "LcCtype", input: "LC_CTYPE", wantErr: true, errMsg: "LC_"}, // LD_* prefix (dynamic linker). - {name: "LdPreload", input: "LD_PRELOAD", opts: noAIGateway, wantErr: true, errMsg: "LD_"}, - {name: "LdLibraryPath", input: "LD_LIBRARY_PATH", opts: noAIGateway, wantErr: true, errMsg: "LD_"}, + {name: "LdPreload", input: "LD_PRELOAD", wantErr: true, errMsg: "LD_"}, + {name: "LdLibraryPath", input: "LD_LIBRARY_PATH", wantErr: true, errMsg: "LD_"}, // DYLD_* prefix (macOS dynamic linker). - {name: "DyldInsert", input: "DYLD_INSERT_LIBRARIES", opts: noAIGateway, wantErr: true, errMsg: "DYLD_"}, - {name: "DyldLibraryPath", input: "DYLD_LIBRARY_PATH", opts: noAIGateway, wantErr: true, errMsg: "DYLD_"}, + {name: "DyldInsert", input: "DYLD_INSERT_LIBRARIES", wantErr: true, errMsg: "DYLD_"}, + {name: "DyldLibraryPath", input: "DYLD_LIBRARY_PATH", wantErr: true, errMsg: "DYLD_"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := codersdk.UserSecretEnvNameValid(tt.input, tt.opts) + err := codersdk.UserSecretEnvNameValid(tt.input) if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b1dc175d4f..cb9f56561e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -8249,20 +8249,6 @@ export interface UserSecret { readonly updated_at: string; } -// From codersdk/usersecretvalidation.go -/** - * UserSecretEnvValidationOptions controls deployment-aware behavior - * in environment variable name validation. - */ -export interface UserSecretEnvValidationOptions { - /** - * AIGatewayEnabled indicates that the deployment has AI Gateway - * configured. When true, AI Gateway environment variables - * (OPENAI_API_KEY, etc.) are reserved to prevent conflicts. - */ - readonly AIGatewayEnabled: boolean; -} - // From codersdk/users.go export type UserStatus = "active" | "dormant" | "suspended";