Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/thv/app/run_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,14 @@ func buildRunnerConfig(
imageMetadata = md
}

// Extract registry proxy port from remote server metadata when CLI flag is not set
var registryProxyPort int
if runFlags.ProxyPort == 0 {
if remoteMd, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok && remoteMd != nil {
registryProxyPort = remoteMd.ProxyPort
}
}

// Build default options
opts := []runner.RunConfigBuilderOption{
runner.WithRuntime(rt),
Expand Down Expand Up @@ -603,6 +611,11 @@ func buildRunnerConfig(
}
opts = append(opts, remoteHeaderOpts...)

// Use registry proxy port for remote servers if CLI flag is not set
if registryProxyPort > 0 {
opts = append(opts, runner.WithRegistryProxyPort(registryProxyPort))
}

// Configure runtime options
runtimeOpts := configureRuntimeOptions(runFlags)
opts = append(opts, runtimeOpts...)
Expand Down
2 changes: 1 addition & 1 deletion cmd/thv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func setupSignalHandler() context.Context {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)

ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // G118 - cancel called in signal handler goroutine
go func() {
<-sigCh
slog.Debug("received signal, cleaning up lock files")
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ require (
github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/viper v1.21.0
github.com/stacklok/toolhive-catalog v0.20260223.0
github.com/stacklok/toolhive-core v0.0.10
github.com/stacklok/toolhive-core v0.0.11
github.com/stretchr/testify v1.11.1
github.com/swaggo/swag/v2 v2.0.0-rc5
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -800,8 +800,8 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stacklok/toolhive-catalog v0.20260223.0 h1:KIabB8gLNBkNOcCbYEukhoJeSlNEN5CvsE2oSKCKeME=
github.com/stacklok/toolhive-catalog v0.20260223.0/go.mod h1:pRmjVHQU2pIqKctbsFErmFmADAXfeIjIlQUOgZMVUiw=
github.com/stacklok/toolhive-core v0.0.10 h1:krQhmcFpZkjIy1i/n+XinOYBHRnuOPO5kJE0ZyK6EgU=
github.com/stacklok/toolhive-core v0.0.10/go.mod h1:XnAsVL81S2T9010NfoeByxY1RUlBGbFAO3E+e0FENQo=
github.com/stacklok/toolhive-core v0.0.11 h1:tFLwSHE/AUikLYu6x7N9iTfMUR9eJS9JAVzvoPU+yrI=
github.com/stacklok/toolhive-core v0.0.11/go.mod h1:XnAsVL81S2T9010NfoeByxY1RUlBGbFAO3E+e0FENQo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
Expand Down
11 changes: 11 additions & 0 deletions pkg/api/v1/workload_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (s *WorkloadService) BuildFullRunConfig(
var imageURL string
var imageMetadata *regtypes.ImageMetadata
var serverMetadata regtypes.ServerMetadata
var registryProxyPort int

if req.URL != "" {
// Configure remote authentication if OAuth config is provided
Expand Down Expand Up @@ -193,6 +194,11 @@ func (s *WorkloadService) BuildFullRunConfig(
}

if remoteServerMetadata, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok && remoteServerMetadata != nil {
// Use registry proxy port if not set by request
if req.ProxyPort == 0 && remoteServerMetadata.ProxyPort > 0 {
registryProxyPort = remoteServerMetadata.ProxyPort
}

if remoteServerMetadata.OAuthConfig != nil {
// Default resource: user-provided > registry metadata > derived from remote URL
resource := req.OAuthConfig.Resource
Expand Down Expand Up @@ -285,6 +291,11 @@ func (s *WorkloadService) BuildFullRunConfig(
}
}

// Use registry proxy port for remote servers if not set by request
if registryProxyPort > 0 {
options = append(options, runner.WithRegistryProxyPort(registryProxyPort))
}

// Add existing port if provided (for update operations)
if existingPort > 0 {
options = append(options, runner.WithExistingPort(existingPort))
Expand Down
2 changes: 2 additions & 0 deletions pkg/auth/github_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func (*GitHubProvider) CanHandle(introspectURL string) bool {
// IntrospectToken introspects a GitHub OAuth token and returns JWT claims
// This calls GitHub's token validation API to verify the token and extract user information
func (g *GitHubProvider) IntrospectToken(ctx context.Context, token string) (jwt.MapClaims, error) {
//nolint:gosec // G706 - baseURL is a configured GitHub API endpoint
slog.Debug("using GitHub token validation provider", "url", g.baseURL)

// Apply rate limiting to prevent DoS and respect GitHub API limits
Expand All @@ -142,6 +143,7 @@ func (g *GitHubProvider) IntrospectToken(ctx context.Context, token string) (jwt
}

// Create POST request
//nolint:gosec // G704 - URL is configured GitHub API endpoint
req, err := http.NewRequestWithContext(ctx, "POST", g.baseURL, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create GitHub validation request: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func (g *GoogleProvider) IntrospectToken(ctx context.Context, token string) (jwt
u.RawQuery = query.Encode()

// Create the GET request
//nolint:gosec // G704 - URL from trusted OIDC discovery config
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create Google tokeninfo request: %w", err)
Expand Down Expand Up @@ -290,6 +291,7 @@ func (r *RFC7662Provider) IntrospectToken(ctx context.Context, token string) (jw
formData.Set("token_type_hint", "access_token")

// Create POST request with form data
//nolint:gosec // G704 - URL is configured introspection endpoint
req, err := http.NewRequestWithContext(ctx, "POST", r.url, strings.NewReader(formData.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to create introspection request: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions pkg/authserver/server/handlers/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func (h *Handler) writeAuthorizationResponse(
authorizeRequest.RequestedScope = append(authorizeRequest.RequestedScope, scope)
authorizeRequest.GrantedScope = append(authorizeRequest.GrantedScope, scope)
} else {
slog.Warn("filtered unregistered scope from authorization",
slog.Warn("filtered unregistered scope from authorization", //nolint:gosec // G706 - scope from server-side storage
"scope", scope,
"client_id", pending.ClientID,
)
Expand Down Expand Up @@ -242,7 +242,7 @@ func (h *Handler) buildAuthorizeRequesterFromPending(
// so failure indicates storage corruption
redirectURI, err := url.Parse(pending.RedirectURI)
if err != nil {
slog.Error("stored redirect URI is invalid",
slog.Error("stored redirect URI is invalid", //nolint:gosec // G706 - redirect URI from server-side storage
"redirect_uri", pending.RedirectURI,
"error", err,
)
Expand Down
10 changes: 5 additions & 5 deletions pkg/authserver/storage/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (s *RedisStorage) RegisterClient(ctx context.Context, client fosite.Client)
Public: client.IsPublic(),
}

data, err := json.Marshal(stored)
data, err := json.Marshal(stored) //nolint:gosec // G117 - internal Redis storage serialization, not exposed to users
if err != nil {
return fmt.Errorf("failed to marshal client: %w", err)
}
Expand Down Expand Up @@ -760,7 +760,7 @@ func marshalUpstreamTokensWithTTL(tokens *UpstreamTokens) ([]byte, time.Duration
ClientID: tokens.ClientID,
}

data, err := json.Marshal(stored)
data, err := json.Marshal(stored) //nolint:gosec // G117 - internal Redis storage serialization, not exposed to users
if err != nil {
return nil, 0, fmt.Errorf("failed to marshal upstream tokens: %w", err)
}
Expand Down Expand Up @@ -932,7 +932,7 @@ func (s *RedisStorage) StorePendingAuthorization(ctx context.Context, state stri
CreatedAt: pending.CreatedAt.Unix(),
}

data, err := json.Marshal(stored)
data, err := json.Marshal(stored) //nolint:gosec // G117 - internal Redis storage serialization, not exposed to users
if err != nil {
return fmt.Errorf("failed to marshal pending authorization: %w", err)
}
Expand Down Expand Up @@ -1021,7 +1021,7 @@ func (s *RedisStorage) CreateUser(ctx context.Context, user *User) error {
UpdatedAt: user.UpdatedAt.Unix(),
}

data, err := json.Marshal(stored)
data, err := json.Marshal(stored) //nolint:gosec // G117 - internal Redis storage serialization, not exposed to users
if err != nil {
return fmt.Errorf("failed to marshal user: %w", err)
}
Expand Down Expand Up @@ -1153,7 +1153,7 @@ func (s *RedisStorage) CreateProviderIdentity(ctx context.Context, identity *Pro
LastUsedAt: identity.LastUsedAt.Unix(),
}

data, err := json.Marshal(stored)
data, err := json.Marshal(stored) //nolint:gosec // G117 - internal Redis storage serialization, not exposed to users
if err != nil {
return fmt.Errorf("failed to marshal identity: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/authz/response_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ func (rfw *ResponseFilteringWriter) FlushAndFilter() error {
// If it's not a successful response, just pass it through
if rfw.statusCode != http.StatusOK && rfw.statusCode != http.StatusAccepted {
rfw.ResponseWriter.WriteHeader(rfw.statusCode)
_, err := rfw.ResponseWriter.Write(rfw.buffer.Bytes())
_, err := rfw.ResponseWriter.Write(rfw.buffer.Bytes()) //nolint:gosec // G705 - JSON-RPC response, not rendered as HTML
return err
}

// Check if this is a list operation that needs filtering
if !isListOperation(rfw.method) {
rfw.ResponseWriter.WriteHeader(rfw.statusCode)
_, err := rfw.ResponseWriter.Write(rfw.buffer.Bytes())
_, err := rfw.ResponseWriter.Write(rfw.buffer.Bytes()) //nolint:gosec // G705 - JSON-RPC response, not rendered as HTML
return err
}

Expand All @@ -77,7 +77,7 @@ func (rfw *ResponseFilteringWriter) FlushAndFilter() error {
// Skip filtering for empty responses (common in SSE scenarios where actual data comes via SSE stream)
if len(rawResponse) == 0 {
rfw.ResponseWriter.WriteHeader(rfw.statusCode)
_, err := rfw.ResponseWriter.Write(rawResponse)
_, err := rfw.ResponseWriter.Write(rawResponse) //nolint:gosec // G705 - JSON-RPC response, not rendered as HTML
return err
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/container/images/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func createTarFromDir(srcDir string, writer io.Writer) error {
// If it's a regular file, write the contents
if !info.IsDir() {
// #nosec G304 - This is safe because we're only opening files within the specified context directory
file, err := os.Open(path)
file, err := os.Open(path) //nolint:gosec // G122 - path from filepath.Walk within validated source directory
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
Expand Down
17 changes: 17 additions & 0 deletions pkg/runner/config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type runConfigBuilder struct {
// Store ports separately for proper validation
port int
targetPort int
// registryProxyPort is the proxy port from the registry metadata (remote servers).
// Used as a fallback when port is 0 (not set by CLI).
registryProxyPort int
// Store network mode to apply to permission profile after it's loaded
networkMode string
// Build context determines which validation and features are enabled
Expand Down Expand Up @@ -95,6 +98,15 @@ func WithRemoteURL(remoteURL string) RunConfigBuilderOption {
}
}

// WithRegistryProxyPort sets the proxy port from registry metadata.
// This is used as a fallback when the CLI --proxy-port flag is not set.
func WithRegistryProxyPort(port int) RunConfigBuilderOption {
return func(b *runConfigBuilder) error {
b.registryProxyPort = port
return nil
}
}

// WithRemoteAuth sets the remote authentication configuration
func WithRemoteAuth(config *remote.Config) RunConfigBuilderOption {
return func(b *runConfigBuilder) error {
Expand Down Expand Up @@ -866,6 +878,11 @@ func (b *runConfigBuilder) validateConfig(imageMetadata *regtypes.ImageMetadata)
}
}
}
// Use registry proxy port from remote server metadata if not set by CLI
if proxyPort == 0 && b.registryProxyPort > 0 {
slog.Debug("Using remote server registry proxy port", "port", b.registryProxyPort)
proxyPort = b.registryProxyPort
}
// Configure ports and target host
if _, err = c.WithPorts(proxyPort, targetPort); err != nil {
return err
Expand Down
8 changes: 6 additions & 2 deletions pkg/secrets/keyring/keyctl_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,14 @@ func (k *keyctlProvider) deleteKeyUnlocked(service, key string) error {
return nil
}

_, err = unix.KeyctlInt(unix.KEYCTL_REVOKE, keyID, 0, 0, 0)
// Unlink the key from the keyring first so it's no longer searchable,
// then revoke it to invalidate any remaining references.
_, err = unix.KeyctlInt(unix.KEYCTL_UNLINK, keyID, k.ringID, 0, 0)
if err != nil {
return fmt.Errorf("failed to delete key '%s': %w", keyName, err)
return fmt.Errorf("failed to unlink key '%s': %w", keyName, err)
}
// Best-effort revoke — key is already removed from keyring
_, _ = unix.KeyctlInt(unix.KEYCTL_REVOKE, keyID, 0, 0, 0)

// Remove from tracking
if serviceKeys, exists := k.keys[service]; exists {
Expand Down
2 changes: 1 addition & 1 deletion pkg/transport/stdio.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func (t *StdioTransport) Start(ctx context.Context) error {
}

// Start a goroutine to handle container exit
go t.handleContainerExit(ctx)
go t.handleContainerExit(ctx) //nolint:gosec // G118 - background goroutine manages container lifecycle, outlives request

return nil
}
Expand Down
1 change: 1 addition & 0 deletions pkg/updates/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func (d *defaultUpdateChecker) CheckLatestVersion() error {
}
defer lockfile.ReleaseTrackedLock(lockPath, lockFile)

//nolint:gosec // G703 - path from trusted app config directory
if err := os.WriteFile(d.updateFilePath, updatedData, 0600); err != nil {
return fmt.Errorf("failed to write updated file: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/vmcp/health/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func (m *Monitor) Start(ctx context.Context) error {
for i := range m.backends {
backend := &m.backends[i] // Capture backend pointer for this iteration

backendCtx, cancel := context.WithCancel(m.ctx)
backendCtx, cancel := context.WithCancel(m.ctx) //nolint:gosec // G118 - cancel stored in m.activeChecks, called during Stop
m.activeChecks[backend.ID] = cancel
m.wg.Add(1)
m.initialCheckWg.Add(1) // Track initial health check
Expand Down Expand Up @@ -331,7 +331,7 @@ func (m *Monitor) UpdateBackends(newBackends []vmcp.Backend) {

// Circuit breaker will be lazily initialized on first health check

backendCtx, cancel := context.WithCancel(m.ctx)
backendCtx, cancel := context.WithCancel(m.ctx) //nolint:gosec // G118 - cancel stored in m.activeChecks, called during Stop
m.activeChecks[id] = cancel
m.wg.Add(1)
// Clear the "removed" flag if this backend was previously removed
Expand Down
1 change: 1 addition & 0 deletions pkg/workloads/statuses/file_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ func (f *fileStatusManager) readStatusFile(statusFilePath string) (*workloadStat
if recoveryErr != nil {
// Recovery failed - back up the corrupted file
backupPath := statusFilePath + ".corrupted"
//nolint:gosec // G703 - path derived from trusted status file with fixed suffix
if backupErr := os.WriteFile(backupPath, data, 0o600); backupErr == nil {
slog.Warn("backed up corrupted status file", "path", backupPath)
}
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/oidc_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func (m *OIDCMockServer) handleToken(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()

// Check for test auth code from auto-complete flow
if r.FormValue("code") == "test-auth-code" {
if r.FormValue("code") == "test-auth-code" { //nolint:gosec // G120 - test-only mock server
// Return a test token directly for auto-complete flow
tokenResponse := map[string]interface{}{
"access_token": "test-access-token",
Expand Down
Loading