Skip to content

Commit 317333c

Browse files
committed
feat(039): store scanner API keys in OS keyring, not plaintext
Scanner API keys configured via the API or CLI are now stored in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager) using the existing mcpproxy secrets system. - ConfigureScanner stores values via keyring, keeps ${keyring:...} refs - Engine resolves keyring references at scan time before passing to containers - Secrets visible in `mcpproxy secrets list` alongside server secrets - Fallback to direct storage if keyring is unavailable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8ac390b commit 317333c

3 files changed

Lines changed: 82 additions & 8 deletions

File tree

internal/security/scanner/engine.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"strings"
78
"sync"
89
"time"
910

1011
"go.uber.org/zap"
1112
)
1213

14+
// SecretResolverFunc resolves a secret reference like ${keyring:name} to its value
15+
type SecretResolverFunc func(ctx context.Context, ref string) (string, error)
16+
1317
// Engine orchestrates parallel scanner execution for a server
1418
type Engine struct {
15-
docker *DockerRunner
16-
registry *Registry
17-
logger *zap.Logger
18-
dataDir string
19+
docker *DockerRunner
20+
registry *Registry
21+
logger *zap.Logger
22+
dataDir string
23+
secretResolver SecretResolverFunc
1924

2025
// Track active scans (one per server)
2126
mu sync.Mutex
@@ -265,9 +270,20 @@ func (e *Engine) runSingleScanner(ctx context.Context, s *ScannerPlugin, req Sca
265270
}
266271

267272
// Build env vars: scanner config + request env
273+
// Resolve ${keyring:...} references if a secret resolver is available
268274
env := make(map[string]string)
269275
for k, v := range s.ConfiguredEnv {
270-
env[k] = v
276+
if e.secretResolver != nil && strings.HasPrefix(v, "${keyring:") {
277+
resolved, err := e.secretResolver(ctx, v)
278+
if err != nil {
279+
e.logger.Warn("Failed to resolve secret for scanner env",
280+
zap.String("key", k), zap.Error(err))
281+
continue // Skip unresolvable secrets
282+
}
283+
env[k] = resolved
284+
} else {
285+
env[k] = v
286+
}
271287
}
272288
for k, v := range req.Env {
273289
env[k] = v

internal/security/scanner/service.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scanner
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"time"
78

89
"go.uber.org/zap"
@@ -67,6 +68,7 @@ type Service struct {
6768
emitter EventEmitter
6869
sourceResolver *SourceResolver
6970
serverInfo ServerInfoProvider
71+
secretStore SecretStore
7072
logger *zap.Logger
7173
}
7274

@@ -232,18 +234,55 @@ func (s *Service) RemoveScanner(ctx context.Context, id string) error {
232234
return nil
233235
}
234236

235-
// ConfigureScanner sets environment variables (API keys) for a scanner
237+
// SecretStore allows storing and resolving secrets via the OS keyring
238+
type SecretStore interface {
239+
StoreSecret(ctx context.Context, name, value string) error
240+
ResolveSecret(ctx context.Context, ref string) (string, error)
241+
}
242+
243+
// SetSecretStore sets the secret store for secure API key management.
244+
// Also wires secret resolution into the scan engine for resolving
245+
// ${keyring:...} references in scanner env vars at scan time.
246+
func (s *Service) SetSecretStore(store SecretStore) {
247+
s.secretStore = store
248+
if store != nil {
249+
s.engine.secretResolver = func(ctx context.Context, ref string) (string, error) {
250+
return store.ResolveSecret(ctx, ref)
251+
}
252+
}
253+
}
254+
255+
// ConfigureScanner sets environment variables (API keys) for a scanner.
256+
// Secret values are stored in the OS keyring; only references are kept in config.
236257
func (s *Service) ConfigureScanner(ctx context.Context, id string, env map[string]string) error {
237258
sc, err := s.storage.GetScanner(id)
238259
if err != nil {
239-
return fmt.Errorf("scanner not installed: %w", err)
260+
// If not in storage yet (just registered in registry), create from registry
261+
regScanner, regErr := s.registry.Get(id)
262+
if regErr != nil {
263+
return fmt.Errorf("scanner not found: %w", err)
264+
}
265+
sc = regScanner
240266
}
241267

242268
if sc.ConfiguredEnv == nil {
243269
sc.ConfiguredEnv = make(map[string]string)
244270
}
271+
272+
// Store secrets in keyring, keep references in config
245273
for k, v := range env {
246-
sc.ConfiguredEnv[k] = v
274+
if s.secretStore != nil && v != "" {
275+
keyringName := fmt.Sprintf("scanner_%s_%s", id, strings.ToLower(k))
276+
if err := s.secretStore.StoreSecret(ctx, keyringName, v); err != nil {
277+
s.logger.Warn("Failed to store secret in keyring, storing as reference",
278+
zap.String("key", k), zap.Error(err))
279+
sc.ConfiguredEnv[k] = v // Fallback: store directly
280+
} else {
281+
sc.ConfiguredEnv[k] = fmt.Sprintf("${keyring:%s}", keyringName)
282+
}
283+
} else {
284+
sc.ConfiguredEnv[k] = v
285+
}
247286
}
248287
sc.Status = ScannerStatusConfigured
249288

internal/server/server.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,7 @@ func (s *Server) startCustomHTTPServer(ctx context.Context, streamableServer *se
16591659
secDocker := scanner.NewDockerRunner(s.logger)
16601660
secService := scanner.NewService(sm, secRegistry, secDocker, dataDir, s.logger)
16611661
secService.SetServerInfoProvider(&configServerInfoProvider{cfg: cfg})
1662+
secService.SetSecretStore(&keyringSecretStore{resolver: secret.NewResolver()})
16621663
httpAPIServer.SetSecurityController(secService)
16631664
}
16641665
// Wire teams multi-user OAuth (no-op in personal edition)
@@ -2352,6 +2353,24 @@ func (s *Server) GetToolApprovalStatus(serverName, toolName string) (string, err
23522353
return string(record.Status), nil
23532354
}
23542355

2356+
// keyringSecretStore adapts secret.Resolver to scanner.SecretStore for API key management.
2357+
type keyringSecretStore struct {
2358+
resolver *secret.Resolver
2359+
}
2360+
2361+
func (k *keyringSecretStore) StoreSecret(ctx context.Context, name, value string) error {
2362+
ref := secret.Ref{Type: "keyring", Name: name}
2363+
return k.resolver.Store(ctx, ref, value)
2364+
}
2365+
2366+
func (k *keyringSecretStore) ResolveSecret(ctx context.Context, refStr string) (string, error) {
2367+
ref, err := secret.ParseSecretRef(refStr)
2368+
if err != nil {
2369+
return "", err
2370+
}
2371+
return k.resolver.Resolve(ctx, *ref)
2372+
}
2373+
23552374
// configServerInfoProvider implements scanner.ServerInfoProvider using the config.
23562375
type configServerInfoProvider struct {
23572376
cfg *config.Config

0 commit comments

Comments
 (0)