From 71524fe057f4f21f23a3b6cafe94a7de71085316 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Fri, 6 Mar 2026 23:40:45 +0200 Subject: [PATCH 1/6] Support proxy_port from registry for remote servers Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/run_flags.go | 5 +++++ pkg/api/v1/workload_service.go | 11 +++++++++++ pkg/runner/config_builder.go | 17 +++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/cmd/thv/app/run_flags.go b/cmd/thv/app/run_flags.go index 6d7b6c1bc8..1ccea72e7a 100644 --- a/cmd/thv/app/run_flags.go +++ b/cmd/thv/app/run_flags.go @@ -723,6 +723,11 @@ func configureRemoteAuth(runFlags *RunFlags, serverMetadata regtypes.ServerMetad } opts = append(opts, runner.WithRemoteAuth(remoteAuthConfig), runner.WithRemoteURL(remoteServerMetadata.URL)) + + // Use registry proxy port for remote servers if CLI flag is not set + if remoteServerMetadata.ProxyPort > 0 { + opts = append(opts, runner.WithRegistryProxyPort(remoteServerMetadata.ProxyPort)) + } } if runFlags.RemoteURL != "" { diff --git a/pkg/api/v1/workload_service.go b/pkg/api/v1/workload_service.go index f932905cc4..baf489b2fd 100644 --- a/pkg/api/v1/workload_service.go +++ b/pkg/api/v1/workload_service.go @@ -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 @@ -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 @@ -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)) diff --git a/pkg/runner/config_builder.go b/pkg/runner/config_builder.go index 289abcc64d..b1373a79bd 100644 --- a/pkg/runner/config_builder.go +++ b/pkg/runner/config_builder.go @@ -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 @@ -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 { @@ -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 From 89a5dfd86398533f7248f498e32f3f26950eb70a Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 7 Mar 2026 00:06:06 +0200 Subject: [PATCH 2/6] Bump toolhive-core Signed-off-by: Radoslav Dimitrov --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 28e46d5010..0d958c70e2 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index cb0514f627..ff4ae1bbd0 100644 --- a/go.sum +++ b/go.sum @@ -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= From 737f66c5ebc3045426f418984c7780b5b2f8d4e6 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 7 Mar 2026 00:52:54 +0200 Subject: [PATCH 3/6] Fix linting issues Signed-off-by: Radoslav Dimitrov --- cmd/thv/main.go | 2 +- pkg/auth/github_provider.go | 2 ++ pkg/auth/token.go | 2 ++ pkg/authserver/server/handlers/callback.go | 4 ++-- pkg/authserver/storage/redis.go | 10 +++++----- pkg/authz/response_filter.go | 6 +++--- pkg/container/images/registry.go | 2 +- pkg/transport/stdio.go | 2 +- pkg/updates/checker.go | 1 + pkg/vmcp/health/monitor.go | 4 ++-- pkg/workloads/statuses/file_status.go | 1 + test/e2e/oidc_mock.go | 2 +- 12 files changed, 22 insertions(+), 16 deletions(-) diff --git a/cmd/thv/main.go b/cmd/thv/main.go index 7b951b7d99..e60095800d 100644 --- a/cmd/thv/main.go +++ b/cmd/thv/main.go @@ -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") diff --git a/pkg/auth/github_provider.go b/pkg/auth/github_provider.go index bde4833ad4..a3056b37cc 100644 --- a/pkg/auth/github_provider.go +++ b/pkg/auth/github_provider.go @@ -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 @@ -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) diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 4e660bc5b8..9a67adeec7 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -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) @@ -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) diff --git a/pkg/authserver/server/handlers/callback.go b/pkg/authserver/server/handlers/callback.go index 2db4f4e56b..9f597af8c5 100644 --- a/pkg/authserver/server/handlers/callback.go +++ b/pkg/authserver/server/handlers/callback.go @@ -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, ) @@ -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, ) diff --git a/pkg/authserver/storage/redis.go b/pkg/authserver/storage/redis.go index 8ec68f83b0..01b35fd106 100644 --- a/pkg/authserver/storage/redis.go +++ b/pkg/authserver/storage/redis.go @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } diff --git a/pkg/authz/response_filter.go b/pkg/authz/response_filter.go index 32464ada96..60719e0a66 100644 --- a/pkg/authz/response_filter.go +++ b/pkg/authz/response_filter.go @@ -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 } @@ -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 } diff --git a/pkg/container/images/registry.go b/pkg/container/images/registry.go index c9e361c840..9d00965d0c 100644 --- a/pkg/container/images/registry.go +++ b/pkg/container/images/registry.go @@ -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) } diff --git a/pkg/transport/stdio.go b/pkg/transport/stdio.go index 75b5c44a3c..89289f917b 100644 --- a/pkg/transport/stdio.go +++ b/pkg/transport/stdio.go @@ -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 } diff --git a/pkg/updates/checker.go b/pkg/updates/checker.go index fae8e4ec2e..20e9bb4646 100644 --- a/pkg/updates/checker.go +++ b/pkg/updates/checker.go @@ -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) } diff --git a/pkg/vmcp/health/monitor.go b/pkg/vmcp/health/monitor.go index 02b1f2cb72..6d7588a7cb 100644 --- a/pkg/vmcp/health/monitor.go +++ b/pkg/vmcp/health/monitor.go @@ -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 @@ -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 diff --git a/pkg/workloads/statuses/file_status.go b/pkg/workloads/statuses/file_status.go index 3161b195ff..c07eccb770 100644 --- a/pkg/workloads/statuses/file_status.go +++ b/pkg/workloads/statuses/file_status.go @@ -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) } diff --git a/test/e2e/oidc_mock.go b/test/e2e/oidc_mock.go index cba8b30ef9..3e9a2aec4f 100644 --- a/test/e2e/oidc_mock.go +++ b/test/e2e/oidc_mock.go @@ -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", From 51c0028808d30ec2f580e6ce2630fa8eb458dcb6 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 7 Mar 2026 00:56:17 +0200 Subject: [PATCH 4/6] Address review feedback Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/run_flags.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/thv/app/run_flags.go b/cmd/thv/app/run_flags.go index 1ccea72e7a..f325f7f99f 100644 --- a/cmd/thv/app/run_flags.go +++ b/cmd/thv/app/run_flags.go @@ -559,6 +559,12 @@ func buildRunnerConfig( imageMetadata = md } + // Extract registry proxy port from remote server metadata + var registryProxyPort int + if remoteMd, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok && remoteMd != nil { + registryProxyPort = remoteMd.ProxyPort + } + // Build default options opts := []runner.RunConfigBuilderOption{ runner.WithRuntime(rt), @@ -603,6 +609,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...) @@ -723,11 +734,6 @@ func configureRemoteAuth(runFlags *RunFlags, serverMetadata regtypes.ServerMetad } opts = append(opts, runner.WithRemoteAuth(remoteAuthConfig), runner.WithRemoteURL(remoteServerMetadata.URL)) - - // Use registry proxy port for remote servers if CLI flag is not set - if remoteServerMetadata.ProxyPort > 0 { - opts = append(opts, runner.WithRegistryProxyPort(remoteServerMetadata.ProxyPort)) - } } if runFlags.RemoteURL != "" { From 154cbc8ff6fcb4d78ee5adcf6d87e6bd5aaa3005 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 7 Mar 2026 01:14:22 +0200 Subject: [PATCH 5/6] Address review feedback Signed-off-by: Radoslav Dimitrov --- cmd/thv/app/run_flags.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/thv/app/run_flags.go b/cmd/thv/app/run_flags.go index f325f7f99f..a64e6c7ccd 100644 --- a/cmd/thv/app/run_flags.go +++ b/cmd/thv/app/run_flags.go @@ -559,10 +559,12 @@ func buildRunnerConfig( imageMetadata = md } - // Extract registry proxy port from remote server metadata + // Extract registry proxy port from remote server metadata when CLI flag is not set var registryProxyPort int - if remoteMd, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok && remoteMd != nil { - registryProxyPort = remoteMd.ProxyPort + if runFlags.ProxyPort == 0 { + if remoteMd, ok := serverMetadata.(*regtypes.RemoteServerMetadata); ok && remoteMd != nil { + registryProxyPort = remoteMd.ProxyPort + } } // Build default options From 50443cc8ac64054553a86acbff6872dd459d9f5a Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 7 Mar 2026 15:27:53 +0200 Subject: [PATCH 6/6] Fix the keyring implementation Signed-off-by: Radoslav Dimitrov --- pkg/secrets/keyring/keyctl_linux.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/secrets/keyring/keyctl_linux.go b/pkg/secrets/keyring/keyctl_linux.go index c2a460e3ed..b3a1298cb9 100644 --- a/pkg/secrets/keyring/keyctl_linux.go +++ b/pkg/secrets/keyring/keyctl_linux.go @@ -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 {