Add secure Bird gateway for Pleno Control Plane (#332)
Add the Bird Control Plane gateway, signed webhook ingestion, policy-gated writes, fail-closed production auto-activation, and RSA-OAEP bootstrap credential flow.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "This command is CLI-only.\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$command = $argv[1] ?? 'check';
|
||||
if (!in_array($command, ['check', 'apply', 'webhooks-check', 'webhooks-apply'], true)) {
|
||||
fwrite(
|
||||
STDERR,
|
||||
"Usage: scripts/bird-control-plane-activate.php check|apply|webhooks-check|webhooks-apply [--yes]\n"
|
||||
);
|
||||
exit(2);
|
||||
}
|
||||
if (in_array($command, ['apply', 'webhooks-apply'], true) && ($argv[2] ?? '') !== '--yes') {
|
||||
fwrite(STDERR, "Refusing Bird activation without: apply --yes\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$appDirectory = __DIR__ . '/../services/nginx/app';
|
||||
if (!is_file($appDirectory . '/config.php')) {
|
||||
$appDirectory = dirname(__DIR__);
|
||||
}
|
||||
define('WD', $appDirectory);
|
||||
require_once WD . '/vendor/autoload.php';
|
||||
require_once WD . '/config.php';
|
||||
require_once WD . '/classes/db.php';
|
||||
require_once WD . '/modules/bird/classes/bird_control_plane_activator.php';
|
||||
require_once WD . '/modules/bird/classes/bird_webhook_subscription_reconciler.php';
|
||||
|
||||
try {
|
||||
$pdo = \classes\db::getPDO();
|
||||
if (str_starts_with($command, 'webhooks-')) {
|
||||
$reconciler = new \bird\classes\bird_webhook_subscription_reconciler($pdo);
|
||||
$organizationId = trim((string)(getenv('BIRD_ORGANIZATION_ID') ?: ''));
|
||||
$status = $command === 'webhooks-apply'
|
||||
? $reconciler->apply($organizationId)
|
||||
: $reconciler->check($organizationId);
|
||||
} else {
|
||||
$activator = new \bird\classes\bird_control_plane_activator($pdo);
|
||||
$status = $command === 'apply' ? $activator->apply([
|
||||
'controlPlaneToken' => trim((string)(getenv('BIRD_CONTROL_PLANE_TOKEN') ?: '')),
|
||||
'webhookSigningKey' => trim((string)(getenv('BIRD_WEBHOOK_SIGNING_KEY') ?: '')),
|
||||
'participantId' => trim((string)(getenv('BIRD_PARTICIPANT_ID') ?: '')),
|
||||
]) : $activator->check();
|
||||
}
|
||||
fwrite(STDOUT, json_encode($status, JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
exit(($status['ready'] ?? false) === true ? 0 : 1);
|
||||
} catch (Throwable $throwable) {
|
||||
error_log('[bird-control-plane-activate] Failed: ' . get_class($throwable));
|
||||
fwrite(STDOUT, json_encode([
|
||||
'ready' => false,
|
||||
'errorCode' => 'bird_activation_failed',
|
||||
], JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$appDirectory = __DIR__ . '/../services/nginx/app';
|
||||
if (!is_file($appDirectory . '/config.php')) {
|
||||
$appDirectory = dirname(__DIR__);
|
||||
}
|
||||
define('WD', $appDirectory);
|
||||
require_once WD . '/vendor/autoload.php';
|
||||
require_once WD . '/config.php';
|
||||
require_once WD . '/classes/db.php';
|
||||
require_once WD . '/modules/bird/classes/bird_control_plane_auto_activation.php';
|
||||
|
||||
try {
|
||||
$status = (new \bird\classes\bird_control_plane_auto_activation(
|
||||
\classes\db::getPDO()
|
||||
))->run();
|
||||
fwrite(STDOUT, json_encode($status, JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
exit(($status['ready'] ?? false) === true ? 0 : 1);
|
||||
} catch (Throwable $throwable) {
|
||||
error_log('[bird-control-plane-auto-activate] Failed: ' . get_class($throwable));
|
||||
fwrite(STDOUT, '{"ready":false,"errorCode":"bird_auto_activation_failed"}' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
bootstrap_url='https://api.truckwash.io:4433/bird/control-plane/v1/bootstrap'
|
||||
status_url='https://api.truckwash.io:4433/bird/control-plane/v1/status'
|
||||
expected_algorithm='RSA-OAEP-256'
|
||||
expected_fingerprint='6dc63c6ffe33b8de0b1396d7f529f56aea0a685ef98168016161cf721ddc8c21'
|
||||
private_key='/home/jeppe/.openclaw/credentials/bird.bootstrap-private.pem'
|
||||
credential_dir='/home/jeppe/.openclaw/credentials'
|
||||
destination="$credential_dir/bird.gateway-token"
|
||||
|
||||
umask 077
|
||||
mkdir -p "$credential_dir"
|
||||
envelope_file="$(mktemp "$credential_dir/.bird-bootstrap-envelope.XXXXXX")"
|
||||
candidate_file="$(mktemp "$credential_dir/.bird-gateway-token.XXXXXX")"
|
||||
payload_file="$(mktemp "$credential_dir/.bird-bootstrap-payload.XXXXXX")"
|
||||
status_file="$(mktemp "$credential_dir/.bird-bootstrap-status.XXXXXX")"
|
||||
cleanup() {
|
||||
rm -f "$envelope_file" "$candidate_file" "$payload_file" "$status_file"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
test -r "$private_key"
|
||||
test "$(stat -c '%a' "$private_key")" = '600'
|
||||
|
||||
curl --proto '=https' --tlsv1.2 --fail --silent --show-error \
|
||||
--max-time 30 "$bootstrap_url" > "$envelope_file"
|
||||
|
||||
test "$(jq -r '.success // false' "$envelope_file")" = 'true'
|
||||
test "$(jq -r '.data.algorithm // empty' "$envelope_file")" = "$expected_algorithm"
|
||||
test "$(jq -r '.data.keyFingerprint // empty' "$envelope_file")" = "$expected_fingerprint"
|
||||
jq -e '.data | keys == ["algorithm","ciphertext","keyFingerprint","tokenVersion","updatedAt"]' \
|
||||
"$envelope_file" >/dev/null
|
||||
jq -e '.data.tokenVersion | type == "number" and . >= 1 and floor == .' \
|
||||
"$envelope_file" >/dev/null
|
||||
jq -e '.data.updatedAt | type == "string" and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")' \
|
||||
"$envelope_file" >/dev/null
|
||||
jq -e '.data.ciphertext | type == "string" and length == 512 and test("^[A-Za-z0-9+/]{512}$")' \
|
||||
"$envelope_file" >/dev/null
|
||||
|
||||
jq -r '.data.ciphertext' "$envelope_file" \
|
||||
| base64 -d \
|
||||
| openssl pkeyutl -decrypt -inkey "$private_key" \
|
||||
-pkeyopt rsa_padding_mode:oaep \
|
||||
-pkeyopt rsa_oaep_md:sha256 \
|
||||
-pkeyopt rsa_mgf1_md:sha256 > "$payload_file"
|
||||
|
||||
jq -e '. | keys == ["algorithm","keyFingerprint","token","tokenVersion","updatedAt"]' \
|
||||
"$payload_file" >/dev/null
|
||||
test "$(jq -r '.algorithm // empty' "$payload_file")" = "$expected_algorithm"
|
||||
test "$(jq -r '.keyFingerprint // empty' "$payload_file")" = "$expected_fingerprint"
|
||||
test "$(jq -r '.tokenVersion // empty' "$payload_file")" = \
|
||||
"$(jq -r '.data.tokenVersion' "$envelope_file")"
|
||||
test "$(jq -r '.updatedAt // empty' "$payload_file")" = \
|
||||
"$(jq -r '.data.updatedAt' "$envelope_file")"
|
||||
jq -j '.token' "$payload_file" > "$candidate_file"
|
||||
|
||||
test "$(wc -c < "$candidate_file")" = '64'
|
||||
grep -Eq '^[A-Za-z0-9_-]{64}$' "$candidate_file"
|
||||
chmod 600 "$candidate_file"
|
||||
|
||||
token="$(cat "$candidate_file")"
|
||||
{
|
||||
printf 'url = "%s"\n' "$status_url"
|
||||
printf 'proto = "=https"\n'
|
||||
printf 'tlsv1.2\n'
|
||||
printf 'fail\nsilent\nshow-error\n'
|
||||
printf 'max-time = 30\n'
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$token"
|
||||
} | curl --config - > "$status_file"
|
||||
unset token
|
||||
|
||||
jq -e '.success == true and .data.enabled == true and .data.webhookConfigured == true' \
|
||||
"$status_file" >/dev/null
|
||||
mv -f "$candidate_file" "$destination"
|
||||
chmod 600 "$destination"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -f "$envelope_file" "$payload_file" "$status_file"
|
||||
printf 'Bird gateway credential bootstrapped and authenticated.\n'
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "This command is CLI-only.\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
const WD = __DIR__ . '/../services/nginx/app';
|
||||
require_once WD . '/vendor/autoload.php';
|
||||
require_once WD . '/config.php';
|
||||
require_once WD . '/classes/db.php';
|
||||
require_once WD . '/modules/bird/classes/bird_control_plane_schema_bootstrap.php';
|
||||
|
||||
$command = $argv[1] ?? 'check';
|
||||
if (!in_array($command, ['check', 'apply'], true)) {
|
||||
fwrite(STDERR, "Usage: scripts/bird-control-plane-schema.php check|apply --yes\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$pdo = \classes\db::getPDO();
|
||||
if ($command === 'apply') {
|
||||
if (($argv[2] ?? '') !== '--yes') {
|
||||
fwrite(STDERR, "Refusing schema mutation without: apply --yes\n");
|
||||
exit(2);
|
||||
}
|
||||
\bird\classes\bird_control_plane_schema_bootstrap::apply($pdo);
|
||||
}
|
||||
|
||||
$status = \bird\classes\bird_control_plane_schema_bootstrap::check($pdo);
|
||||
fwrite(STDOUT, json_encode($status, JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
exit($status['ready'] ? 0 : 1);
|
||||
@@ -149,6 +149,9 @@ tar \
|
||||
docker-compose.yml \
|
||||
docker-compose.example.yml \
|
||||
docker-compose.prod.standalone.yml \
|
||||
scripts/bird-control-plane-auto-activate.php \
|
||||
scripts/bird-control-plane-bootstrap-local.sh \
|
||||
services/coolify/api/start.sh \
|
||||
services/php/Dockerfile \
|
||||
services/php/php-fpm-pool.conf \
|
||||
| docker compose $compose_files exec -T php1 tar --no-same-owner -C /var/www/repo-root -xf -
|
||||
|
||||
Reference in New Issue
Block a user