Read the DNS and monitoring tokens from the vault, not config
Two of the Zugangsdaten page's three entries were decorative: whatever an operator typed was encrypted and stored, but HttpHetznerDnsClient and HttpMonitoringClient still read config() directly, so the stored value never reached an outgoing request. Both now resolve through SecretVault::get() at the point of use, matching how the Stripe client already worked.feat/granted-plans
parent
49a3ca2e33
commit
a457bea87e
|
|
@ -2,19 +2,26 @@
|
|||
|
||||
namespace App\Services\Dns;
|
||||
|
||||
use App\Services\Secrets\SecretVault;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Hetzner DNS API client. Zone + token from config/provisioning.php.
|
||||
* Not unit-tested (live I/O).
|
||||
* Hetzner DNS API client. Zone from config/provisioning.php; the token comes
|
||||
* from the vault (console-stored, falling back to HETZNER_DNS_TOKEN).
|
||||
* Not unit-tested against the real service (live I/O).
|
||||
*/
|
||||
class HttpHetznerDnsClient implements HetznerDnsClient
|
||||
{
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
return Http::withHeaders(['Auth-API-Token' => (string) config('provisioning.dns.token')])
|
||||
// Read HERE, at the point of use, rather than in a constructor or
|
||||
// overlaid onto config at boot — see SecretVault's own docblock (rule
|
||||
// 3): an overlay would add a query to every request, and a
|
||||
// long-running queue worker would keep whatever token was true when
|
||||
// it started.
|
||||
return Http::withHeaders(['Auth-API-Token' => (string) app(SecretVault::class)->get('dns.token')])
|
||||
->baseUrl('https://dns.hetzner.com/api/v1')
|
||||
->timeout(15);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
namespace App\Services\Monitoring;
|
||||
|
||||
use App\Services\Secrets\SecretVault;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Registers uptime targets with the configured monitoring service. If none is
|
||||
* configured, returns a deterministic id (no-op) so provisioning still records a
|
||||
* stable breadcrumb. Not unit-tested (live I/O).
|
||||
*
|
||||
* The bridge token comes from the vault (console-stored, falling back to
|
||||
* MONITORING_API_TOKEN) — read here, at the point of use, never overlaid onto
|
||||
* config at boot. See SecretVault's docblock (rule 3).
|
||||
*/
|
||||
class HttpMonitoringClient implements MonitoringClient
|
||||
{
|
||||
|
|
@ -19,7 +24,7 @@ class HttpMonitoringClient implements MonitoringClient
|
|||
}
|
||||
|
||||
$base = rtrim($endpoint, '/');
|
||||
$token = (string) config('services.monitoring.token');
|
||||
$token = (string) app(SecretVault::class)->get('monitoring.token');
|
||||
|
||||
// Idempotent: reuse an existing monitor for this URL rather than creating
|
||||
// a duplicate when a crash retries after a prior successful POST.
|
||||
|
|
@ -51,7 +56,7 @@ class HttpMonitoringClient implements MonitoringClient
|
|||
}
|
||||
|
||||
try {
|
||||
$monitor = Http::withToken((string) config('services.monitoring.token'))
|
||||
$monitor = Http::withToken((string) app(SecretVault::class)->get('monitoring.token'))
|
||||
->get(rtrim($endpoint, '/').'/monitors/'.$externalId)->throw()->json('monitor', []);
|
||||
} catch (\Throwable) {
|
||||
return null; // the monitor is unreachable, which says nothing about the instance
|
||||
|
|
@ -64,7 +69,7 @@ class HttpMonitoringClient implements MonitoringClient
|
|||
{
|
||||
$endpoint = (string) config('services.monitoring.url');
|
||||
if (filled($endpoint)) {
|
||||
Http::withToken((string) config('services.monitoring.token'))
|
||||
Http::withToken((string) app(SecretVault::class)->get('monitoring.token'))
|
||||
->delete(rtrim($endpoint, '/').'/monitors/'.$externalId)->throw();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Operator;
|
||||
use App\Services\Dns\HttpHetznerDnsClient;
|
||||
use App\Services\Monitoring\HttpMonitoringClient;
|
||||
use App\Services\Secrets\SecretVault;
|
||||
use App\Services\Stripe\HttpStripeClient;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -97,6 +99,39 @@ it('hands the Stripe client the stored key rather than the environment one', fun
|
|||
);
|
||||
});
|
||||
|
||||
it('hands the Hetzner DNS client the stored token rather than the environment one', function () {
|
||||
// Mutation boundary (Part A): before this, HttpHetznerDnsClient read
|
||||
// config('provisioning.dns.token') directly — a value stored here was
|
||||
// encrypted, kept, and never once reached the outgoing request.
|
||||
config()->set('provisioning.dns.token', 'env-token-should-not-be-used');
|
||||
app(SecretVault::class)->put('dns.token', 'dns-token-from-console', Operator::factory()->create());
|
||||
|
||||
Http::fake([
|
||||
'dns.hetzner.com/api/v1/zones*' => Http::response(['zones' => [['id' => 'zone1', 'name' => config('provisioning.dns.zone')]]]),
|
||||
'dns.hetzner.com/api/v1/records*' => Http::response(['record' => ['id' => 'rec1']]),
|
||||
]);
|
||||
|
||||
app(HttpHetznerDnsClient::class)->upsertRecord('fsn-01.'.config('provisioning.dns.zone'), 'A', '10.0.0.1');
|
||||
|
||||
Http::assertSent(fn ($request) => $request->hasHeader('Auth-API-Token', 'dns-token-from-console'));
|
||||
Http::assertNotSent(fn ($request) => $request->hasHeader('Auth-API-Token', 'env-token-should-not-be-used'));
|
||||
});
|
||||
|
||||
it('hands the monitoring client the stored token rather than the environment one', function () {
|
||||
config()->set('services.monitoring.url', 'https://kuma-bridge.test');
|
||||
config()->set('services.monitoring.token', 'env-token-should-not-be-used');
|
||||
app(SecretVault::class)->put('monitoring.token', 'mon-token-from-console', Operator::factory()->create());
|
||||
|
||||
Http::fake([
|
||||
'kuma-bridge.test/monitors*' => Http::response(['monitors' => [], 'monitor' => ['id' => 5]]),
|
||||
]);
|
||||
|
||||
app(HttpMonitoringClient::class)->registerTarget('probe', 'https://probe.example');
|
||||
|
||||
Http::assertSent(fn ($request) => $request->hasHeader('Authorization', 'Bearer mon-token-from-console'));
|
||||
Http::assertNotSent(fn ($request) => $request->hasHeader('Authorization', 'Bearer env-token-should-not-be-used'));
|
||||
});
|
||||
|
||||
it('accepts the key in the format the documentation tells you to generate', function () {
|
||||
// `head -c 32 /dev/urandom | base64` produces raw base64 with no prefix.
|
||||
// Requiring the prefix made every read and write fail on exactly the
|
||||
|
|
|
|||
Loading…
Reference in New Issue