Merge pull request #182 from copenhagentruckwash/fix-edge-broker-default-shared-secret-issue
Remove insecure default edge broker shared secret and stop exposing port 4300
This commit is contained in:
@@ -119,7 +119,11 @@ services:
|
||||
environment:
|
||||
AUTO_COMPOSER_INSTALL: "true"
|
||||
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
|
||||
<<<<<<< HEAD
|
||||
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-}
|
||||
=======
|
||||
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:?set EDGE_BROKER_SHARED_SECRET in .env}
|
||||
>>>>>>> origin/master
|
||||
volumes:
|
||||
- ./services/nginx/app:/var/www/html
|
||||
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
|
||||
@@ -140,7 +144,11 @@ services:
|
||||
environment:
|
||||
AUTO_COMPOSER_INSTALL: "false"
|
||||
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
|
||||
<<<<<<< HEAD
|
||||
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-}
|
||||
=======
|
||||
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:?set EDGE_BROKER_SHARED_SECRET in .env}
|
||||
>>>>>>> origin/master
|
||||
volumes:
|
||||
- ./services/nginx/app:/var/www/html
|
||||
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
|
||||
|
||||
@@ -7,6 +7,11 @@ import { fileURLToPath } from "node:url";
|
||||
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(testDirectory, "../../..");
|
||||
|
||||
<<<<<<< HEAD
|
||||
test("traefik defines a dedicated edge broker entrypoint on port 4300", () => {
|
||||
assert.match(traefikSource, /edge-broker:\s*\n\s*address:\s*":4300"/);
|
||||
assert.match(traefikProdSource, /edge-broker:\s*\n\s*address:\s*":4300"/);
|
||||
=======
|
||||
function readRequiredSource(...pathSegments) {
|
||||
const sourcePath = path.resolve(repoRoot, ...pathSegments);
|
||||
assert.equal(existsSync(sourcePath), true, `Expected config fixture to exist: ${sourcePath}`);
|
||||
@@ -34,6 +39,7 @@ function readComposeServiceBlock(composeSource, serviceName) {
|
||||
|
||||
test("traefik does not expose a dedicated public edge broker port", () => {
|
||||
assert.doesNotMatch(traefikSource, /edge-broker:\s*\n\s*address:\s*":4300"/);
|
||||
>>>>>>> origin/master
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
@@ -42,6 +48,13 @@ test("base docker compose binds edge broker port 4300 to localhost only", () =>
|
||||
assert.match(baseComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "127.0.0.1:4300:4300"/);
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
test("php services receive broker url defaults and require explicit broker shared secret", () => {
|
||||
assert.match(composeSource, /php1:[\s\S]*EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/);
|
||||
assert.match(composeSource, /php1:[\s\S]*EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:-\}/);
|
||||
assert.match(composeSource, /php-staging:[\s\S]*EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/);
|
||||
assert.match(composeSource, /php-cron:[\s\S]*EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:-\}/);
|
||||
=======
|
||||
test("example docker compose binds edge broker port 4300 to localhost only", () => {
|
||||
assert.match(exampleComposeSource, /\bedge-broker:\b/);
|
||||
assert.match(exampleComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "127.0.0.1:4300:4300"/);
|
||||
@@ -101,4 +114,5 @@ test("base docker compose wires the broker into each php worker", () => {
|
||||
assert.match(serviceBlock, /EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/);
|
||||
assert.match(serviceBlock, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:\?set EDGE_BROKER_SHARED_SECRET in \.env\}/);
|
||||
}
|
||||
>>>>>>> origin/master
|
||||
});
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
use Exception;
|
||||
|
||||
class edge_broker_transport_exception extends Exception
|
||||
{
|
||||
public function __construct(string $message, private readonly int $curlErrno = 0, int $code = 0, ?Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function curlErrno(): int
|
||||
{
|
||||
return $this->curlErrno;
|
||||
}
|
||||
}
|
||||
|
||||
class edge_broker_http_exception extends Exception
|
||||
{
|
||||
public function __construct(string $message, private readonly int $statusCode, int $code = 0, ?Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function statusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
class edge_broker_client
|
||||
{
|
||||
private const DEFAULT_BROKER_URL = 'http://edge-broker:4300';
|
||||
|
||||
public function __construct(
|
||||
private readonly ?string $baseUrl = null,
|
||||
private readonly ?string $sharedSecret = null,
|
||||
private readonly int $timeoutSeconds = 10
|
||||
) {
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return trim((string)$this->resolveBaseUrl()) !== '';
|
||||
}
|
||||
|
||||
public function dispatchCommand(int $gatewayId, string $commandType, array $payload): array
|
||||
{
|
||||
$url = rtrim($this->resolveBaseUrl(), '/') . '/api/gateways/' . $gatewayId . '/commands';
|
||||
$response = $this->request('POST', $url, [
|
||||
'commandType' => $commandType,
|
||||
'payload' => $payload,
|
||||
]);
|
||||
|
||||
return is_array($response) ? $response : ['ok' => false, 'response' => $response];
|
||||
}
|
||||
|
||||
public function validateAgent(int $gatewayId, string $agentToken): array
|
||||
{
|
||||
$url = rtrim($this->resolveBaseUrl(), '/') . '/api/internal/agent/auth';
|
||||
$response = $this->request('POST', $url, [
|
||||
'gatewayId' => $gatewayId,
|
||||
'agentToken' => $agentToken,
|
||||
]);
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
public function validateShellSession(string $sessionToken): array
|
||||
{
|
||||
$url = rtrim($this->resolveBaseUrl(), '/') . '/api/internal/shell/auth';
|
||||
$response = $this->request('POST', $url, [
|
||||
'sessionToken' => $sessionToken,
|
||||
]);
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
public function closeShellSession(int $sessionId, string $sessionToken, string $transcript, string $closedReason): array
|
||||
{
|
||||
$url = rtrim($this->resolveBaseUrl(), '/') . '/api/internal/shell-sessions/' . $sessionId . '/close';
|
||||
$response = $this->request('POST', $url, [
|
||||
'sessionToken' => $sessionToken,
|
||||
'transcript' => $transcript,
|
||||
'closedReason' => $closedReason,
|
||||
]);
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
private function resolveBaseUrl(): string
|
||||
{
|
||||
return trim((string)($this->baseUrl ?? getenv('EDGE_BROKER_URL') ?: self::DEFAULT_BROKER_URL));
|
||||
}
|
||||
|
||||
private function resolveSharedSecret(): string
|
||||
{
|
||||
return trim((string)($this->sharedSecret
|
||||
?? getenv('EDGE_BROKER_SHARED_SECRET')
|
||||
?: getenv('EDGE_INTERNAL_SECRET')
|
||||
?: ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function request(string $method, string $url, array $payload): array|object|null
|
||||
{
|
||||
if (trim($url) === '') {
|
||||
throw new Exception('Edge broker URL is not configured');
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeoutSeconds);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array_values(array_filter([
|
||||
'Content-Type: application/json',
|
||||
$this->resolveSharedSecret() !== '' ? 'X-Edge-Broker-Secret: ' . $this->resolveSharedSecret() : null,
|
||||
])));
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$rawResponse = curl_exec($ch);
|
||||
$statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlErrno = curl_errno($ch);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($rawResponse === false) {
|
||||
throw new edge_broker_transport_exception('Edge broker request failed: ' . $curlError, $curlErrno);
|
||||
}
|
||||
|
||||
$decoded = json_decode((string)$rawResponse, true);
|
||||
if ($statusCode >= 400) {
|
||||
$message = is_array($decoded)
|
||||
? (string)($decoded['error'] ?? $decoded['message'] ?? 'Edge broker request failed')
|
||||
: 'Edge broker request failed';
|
||||
throw new edge_broker_http_exception($message, $statusCode);
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
it('uses default broker url but requires explicit shared secret configuration', function (): void {
|
||||
$source = file_get_contents(app_path('classes/edge_broker_client.php'));
|
||||
|
||||
expect($source)->not->toBeFalse();
|
||||
expect($source)->toContain("private const DEFAULT_BROKER_URL = 'http://edge-broker:4300';");
|
||||
expect($source)->toContain('class edge_broker_transport_exception extends Exception');
|
||||
expect($source)->toContain('class edge_broker_http_exception extends Exception');
|
||||
expect($source)->toContain("getenv('EDGE_BROKER_SHARED_SECRET')");
|
||||
expect($source)->toContain("getenv('EDGE_INTERNAL_SECRET')");
|
||||
expect($source)->not->toContain('DEFAULT_SHARED_SECRET');
|
||||
});
|
||||
Reference in New Issue
Block a user