|
1 | 1 | package config |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "errors" |
5 | 6 | "fmt" |
6 | 7 | "os" |
@@ -93,3 +94,64 @@ func ResolveEnvAPIKey() (EnvAPIKeyResult, error) { |
93 | 94 |
|
94 | 95 | return EnvAPIKeyResult{Set: envSet, Key: envKey}, nil |
95 | 96 | } |
| 97 | + |
| 98 | +// resolveAPIKeyRef replaces an api_key that is a secret reference with the |
| 99 | +// value it points at, as a process-only override: the effective config runs |
| 100 | +// with the secret, while every save writes the reference (never the secret) |
| 101 | +// back, whatever else the save is about (see process_overrides.go). |
| 102 | +// |
| 103 | +// The reference is resolved here, once per load, and a reference that cannot be |
| 104 | +// resolved fails the load. Generating a key instead would start the proxy |
| 105 | +// under a credential the operator did not choose and save it over the |
| 106 | +// reference. |
| 107 | +// |
| 108 | +// A key from the environment (MCPPROXY_API_KEY_FILE or MCPPROXY_API_KEY) |
| 109 | +// overrides the config file's key, so with one present the reference is left |
| 110 | +// as written, unread, for EnsureAPIKey's override to replace. |
| 111 | +func resolveAPIKeyRef(c *Config) error { |
| 112 | + if !secret.IsSecretRef(c.APIKey) { |
| 113 | + return nil |
| 114 | + } |
| 115 | + envKey, err := ResolveEnvAPIKey() |
| 116 | + if err != nil { |
| 117 | + return err |
| 118 | + } |
| 119 | + if envKey.Set && envKey.Key != "" { |
| 120 | + return nil |
| 121 | + } |
| 122 | + resolved, err := secret.NewResolver().ExpandSecretRefs(context.Background(), c.APIKey) |
| 123 | + if err != nil { |
| 124 | + // The error is deliberately a fixed sentence: ExpandSecretRefs' own |
| 125 | + // text quotes the reference, and the api_key field is the one value |
| 126 | + // no error or log line may carry. The operator knows which field it is. |
| 127 | + return errors.New("api_key: could not resolve the secret reference; check that the referenced file, environment variable or keyring entry exists and is readable") |
| 128 | + } |
| 129 | + if resolved == "" { |
| 130 | + return fmt.Errorf("api_key: the reference resolved to an empty value") |
| 131 | + } |
| 132 | + // api_key is fixed for the life of the process (a reload pins the running |
| 133 | + // value), and this load may be one that never becomes the live config, such |
| 134 | + // as the disk re-read of a server restart. Once a reference has been |
| 135 | + // resolved, later loads carry the key the process runs with, not what the |
| 136 | + // file holds now: recording a rotated value as the process value would make |
| 137 | + // the running key look like an edit, and the next save would write it over |
| 138 | + // the reference. The file is still read above, so an unusable reference |
| 139 | + // fails the load; a rotated key takes effect on the next start. |
| 140 | + if running, ok := runningAPIKeyFromReference(); ok { |
| 141 | + resolved = running |
| 142 | + } |
| 143 | + OverrideForProcess(c, FieldAPIKey, OverrideSourceEnv, resolved) |
| 144 | + return nil |
| 145 | +} |
| 146 | + |
| 147 | +// runningAPIKeyFromReference returns the api_key this process runs with when it |
| 148 | +// was resolved from a secret reference. |
| 149 | +func runningAPIKeyFromReference() (string, bool) { |
| 150 | + processOverridesMu.RLock() |
| 151 | + defer processOverridesMu.RUnlock() |
| 152 | + o, ok := processOverrides[overrideKey{FieldAPIKey.Name, OverrideSourceEnv}].(typedOverride[string]) |
| 153 | + if !ok || !secret.IsSecretRef(o.loaded) { |
| 154 | + return "", false |
| 155 | + } |
| 156 | + return o.process, true |
| 157 | +} |
0 commit comments