Files
coolify/app/Traits/HasSecretManagerAutocomplete.php
T
Andras BacsaiandClaude Opus 5.5 9bdbbeef7a fix(secret-manager): use remote secret values exactly and check them before a restart
- Write remote secret values as exact dotenv literals in the service .env
  file and escape "$" where they go directly into a generated compose file
  (environment, start commands, healthchecks).
- Resolve remote secrets before a service or database restart stops the
  running resource, and show the secret manager error instead of a generic
  start failure (new RemoteSecretException).
- Cache fetched key names for the autocomplete and rate limit how often a
  user can contact the secret manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:00:41 +02:00

86 lines
2.8 KiB
PHP

<?php
namespace App\Traits;
use App\Models\SecretManagerLink;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
/**
* Exposes the resource's secret manager source to the env-var-input
* autocomplete: a boolean for the "vault" scope, and a lazy key-name fetch
* (called from the frontend only when the user types a vault reference).
* Secret values never reach the component state — key names only.
*/
trait HasSecretManagerAutocomplete
{
public function hasSecretManagerSource(): bool
{
return $this->secretManagerLinkForAutocomplete() !== null;
}
/** Seconds that fetched key names are reused for the same secret manager source. */
private const SECRET_MANAGER_KEYS_CACHE_SECONDS = 60;
/** Secret manager requests that one user can start from the autocomplete per minute. */
private const SECRET_MANAGER_KEYS_REQUESTS_PER_MINUTE = 10;
/**
* @return list<string>
*/
public function fetchSecretManagerKeys(): array
{
$this->skipRender();
$link = $this->secretManagerLinkForAutocomplete();
if (! $link) {
return [];
}
$this->authorize('view', $link->resourceable);
// Only key names are cached. Another token or other settings use another cache entry.
$cacheKey = 'secret-manager-keys:'.$link->id.':'.hash('sha256', json_encode([
$link->integration_token_id,
$link->settings,
$link->integrationToken?->updated_at?->getTimestamp(),
]));
$cachedKeys = Cache::get($cacheKey);
if (is_array($cachedKeys)) {
return $cachedKeys;
}
$rateLimitKey = 'secret-manager-keys:user:'.auth()->id();
if (RateLimiter::tooManyAttempts($rateLimitKey, self::SECRET_MANAGER_KEYS_REQUESTS_PER_MINUTE)) {
throw new \RuntimeException('Too many secret manager key requests. Try again in '.RateLimiter::availableIn($rateLimitKey).' seconds.');
}
RateLimiter::hit($rateLimitKey, 60);
try {
$keys = array_keys($link->fetchSecrets());
} catch (\Throwable) {
throw new \RuntimeException('Unable to fetch secret manager keys.');
}
sort($keys);
Cache::put($cacheKey, $keys, self::SECRET_MANAGER_KEYS_CACHE_SECONDS);
return $keys;
}
private function secretManagerLinkForAutocomplete(): ?SecretManagerLink
{
$resource = $this->secretManagerResource();
if (! $resource || ! method_exists($resource, 'secretManagerLink')) {
return null;
}
if (! $resource->relationLoaded('secretManagerLink')) {
$resource->load('secretManagerLink.integrationToken');
}
return $resource->secretManagerLink;
}
}