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.
This commit is contained in:
Zach
2026-04-14 12:44:55 -06:00
committed by GitHub
parent 7e68d18e04
commit 0832033a73
4 changed files with 60 additions and 125 deletions
+1 -29
View File
@@ -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
}
+57 -74
View File
@@ -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 != "" {