Class: Aidp::Security::SecretsProxy
- Inherits:
-
Object
- Object
- Aidp::Security::SecretsProxy
- Defined in:
- lib/aidp/security/secrets_proxy.rb
Overview
Broker for credential access - agents never receive raw secrets Instead, the proxy issues short-lived, capability-scoped tokens that are exchanged for actual credentials at execution time.
Flow:
-
User registers secret: aidp security register-secret GITHUB_TOKEN
-
Agent requests credential access via proxy
-
Proxy issues short-lived token (e.g., 5 minutes)
-
At execution time, token is exchanged for actual credential
-
Actual credential is used only in isolated execution context
This design ensures:
-
Agents never see raw credentials
-
Credential access is auditable
-
Tokens are scoped and time-limited
-
Compromised agent output can’t leak credentials
Constant Summary collapse
- DEFAULT_TOKEN_TTL =
5 minutes
300- CLEANUP_INTERVAL =
How often to run automatic cleanup (every N operations)
10
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#registry ⇒ Object
readonly
Returns the value of attribute registry.
Instance Method Summary collapse
-
#active_tokens_summary ⇒ Array<Hash>
Get list of active tokens (for status display).
-
#cleanup_expired! ⇒ Integer
Clean up expired tokens.
-
#exchange_token(token) ⇒ String
Exchange a token for the actual credential value This should only be called in the isolated execution context.
-
#initialize(registry:, config: {}) ⇒ SecretsProxy
constructor
A new instance of SecretsProxy.
-
#request_token(secret_name:, scope: nil, ttl: nil) ⇒ Hash
Request a token for accessing a registered secret.
-
#reset! ⇒ Object
Reset proxy state (primarily for testing).
-
#revoke_all_for_secret(secret_name) ⇒ Integer
Revoke all tokens for a specific secret.
-
#revoke_token(token) ⇒ Boolean
Revoke a token before it expires.
-
#sanitized_environment(base_env = ENV.to_h) ⇒ Hash
Build a sanitized environment hash with registered secrets stripped.
-
#usage_log(limit: 50) ⇒ Array<Hash>
Get usage audit log.
-
#with_sanitized_environment { ... } ⇒ Object
Execute a block with a sanitized environment Registered secrets are stripped from ENV during execution.
Constructor Details
#initialize(registry:, config: {}) ⇒ SecretsProxy
Returns a new instance of SecretsProxy.
32 33 34 35 36 37 38 39 |
# File 'lib/aidp/security/secrets_proxy.rb', line 32 def initialize(registry:, config: {}) @registry = registry @config = config @active_tokens = {} @token_usage_log = [] @mutex = Mutex.new @operation_count = 0 end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
27 28 29 |
# File 'lib/aidp/security/secrets_proxy.rb', line 27 def config @config end |
#registry ⇒ Object (readonly)
Returns the value of attribute registry.
27 28 29 |
# File 'lib/aidp/security/secrets_proxy.rb', line 27 def registry @registry end |
Instance Method Details
#active_tokens_summary ⇒ Array<Hash>
Get list of active tokens (for status display)
208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/aidp/security/secrets_proxy.rb', line 208 def active_tokens_summary @mutex.synchronize do @active_tokens.values.map do |data| { secret_name: data[:secret_name], scope: data[:scope], expires_at: data[:expires_at].iso8601, created_at: data[:created_at].iso8601, used: data[:used], remaining_ttl: [(data[:expires_at] - Time.now).to_i, 0].max } end end end |
#cleanup_expired! ⇒ Integer
Clean up expired tokens
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/aidp/security/secrets_proxy.rb', line 189 def cleanup_expired! @mutex.synchronize do now = Time.now expired = @active_tokens.select { |_t, d| now > d[:expires_at] } count = expired.size expired.keys.each { |t| @active_tokens.delete(t) } if count > 0 Aidp.log_debug("security.proxy", "expired_tokens_cleaned", count: count) end count end end |
#exchange_token(token) ⇒ String
Exchange a token for the actual credential value This should only be called in the isolated execution context
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/aidp/security/secrets_proxy.rb', line 105 def exchange_token(token) @mutex.synchronize do token_data = @active_tokens[token] unless token_data Aidp.log_warn("security.proxy", "invalid_token_exchange", token_prefix: token[0..7] || "nil") raise SecretsProxyError.new( secret_name: "unknown", reason: "Invalid or unknown token" ) end if Time.now > token_data[:expires_at] @active_tokens.delete(token) raise TokenExpiredError.new( secret_name: token_data[:secret_name], expired_at: token_data[:expires_at].iso8601 ) end # Get actual value from environment env_var = token_data[:env_var] value = ENV[env_var] unless value raise SecretsProxyError.new( secret_name: token_data[:secret_name], reason: "Environment variable '#{env_var}' not set" ) end # Mark token as used and log token_data[:used] = true token_data[:used_at] = Time.now log_token_usage(token_data) Aidp.log_debug("security.proxy", "token_exchanged", secret_name: token_data[:secret_name], scope: token_data[:scope], token_prefix: token[0..7]) # Return the actual secret value # This value should ONLY be used in isolated execution context value end end |
#request_token(secret_name:, scope: nil, ttl: nil) ⇒ Hash
Request a token for accessing a registered secret
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/aidp/security/secrets_proxy.rb', line 47 def request_token(secret_name:, scope: nil, ttl: nil) @mutex.synchronize do # Periodic cleanup to prevent memory leaks maybe_cleanup_expired # Verify secret is registered registration = @registry.get(secret_name) unless registration raise UnregisteredSecretError.new(secret_name: secret_name) end # Check scope is allowed if scopes are defined allowed_scopes = registration[:scopes] || registration["scopes"] || [] if allowed_scopes.any? && scope && !allowed_scopes.include?(scope) raise SecretsProxyError.new( secret_name: secret_name, reason: "Scope '#{scope}' not allowed. Allowed scopes: #{allowed_scopes.join(", ")}" ) end # Generate token token = generate_token token_ttl = ttl || @config.fetch(:token_ttl, DEFAULT_TOKEN_TTL) expires_at = Time.now + token_ttl token_data = { token: token, secret_name: secret_name, scope: scope, expires_at: expires_at, created_at: Time.now, env_var: registration[:env_var] || registration["env_var"], used: false } @active_tokens[token] = token_data Aidp.log_debug("security.proxy", "token_issued", secret_name: secret_name, scope: scope, expires_in: token_ttl, token_prefix: token[0..7]) { token: token, expires_at: expires_at.iso8601, secret_name: secret_name, scope: scope, ttl: token_ttl } end end |
#reset! ⇒ Object
Reset proxy state (primarily for testing)
281 282 283 284 285 286 |
# File 'lib/aidp/security/secrets_proxy.rb', line 281 def reset! @mutex.synchronize do @active_tokens.clear @token_usage_log.clear end end |
#revoke_all_for_secret(secret_name) ⇒ Integer
Revoke all tokens for a specific secret
172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/aidp/security/secrets_proxy.rb', line 172 def revoke_all_for_secret(secret_name) @mutex.synchronize do tokens_to_revoke = @active_tokens.select { |_t, d| d[:secret_name] == secret_name } count = tokens_to_revoke.size tokens_to_revoke.keys.each { |t| @active_tokens.delete(t) } Aidp.log_info("security.proxy", "tokens_revoked_for_secret", secret_name: secret_name, count: count) count end end |
#revoke_token(token) ⇒ Boolean
Revoke a token before it expires
157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/aidp/security/secrets_proxy.rb', line 157 def revoke_token(token) @mutex.synchronize do if @active_tokens.delete(token) Aidp.log_info("security.proxy", "token_revoked", token_prefix: token[0..7]) true else false end end end |
#sanitized_environment(base_env = ENV.to_h) ⇒ Hash
Build a sanitized environment hash with registered secrets stripped
235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/aidp/security/secrets_proxy.rb', line 235 def sanitized_environment(base_env = ENV.to_h) env = base_env.dup vars_to_strip = @registry.env_vars_to_strip vars_to_strip.each do |var| if env.key?(var) env.delete(var) Aidp.log_debug("security.proxy", "env_var_stripped", env_var: var) end end env end |
#usage_log(limit: 50) ⇒ Array<Hash>
Get usage audit log
226 227 228 229 230 |
# File 'lib/aidp/security/secrets_proxy.rb', line 226 def usage_log(limit: 50) @mutex.synchronize do @token_usage_log.last(limit) end end |
#with_sanitized_environment { ... } ⇒ Object
Execute a block with a sanitized environment Registered secrets are stripped from ENV during execution
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/aidp/security/secrets_proxy.rb', line 254 def with_sanitized_environment original_env = {} vars_to_strip = @registry.env_vars_to_strip # Save and clear registered secrets vars_to_strip.each do |var| if ENV.key?(var) original_env[var] = ENV[var] ENV.delete(var) end end Aidp.log_debug("security.proxy", "environment_sanitized", stripped_count: original_env.size) begin yield ensure # Restore secrets original_env.each { |k, v| ENV[k] = v } Aidp.log_debug("security.proxy", "environment_restored", restored_count: original_env.size) end end |