diff --git a/..env.swp b/..env.swp new file mode 100644 index 0000000..b1c79c4 Binary files /dev/null and b/..env.swp differ diff --git a/.env.example b/.env.example index fd428b9..9f7ab00 100644 --- a/.env.example +++ b/.env.example @@ -71,10 +71,77 @@ VITE_APP_NAME=CluPilot # when developing against a remote host; localhost for local-only. VITE_HMR_HOST=localhost -# ── Mail (dev → log) ───────────────────────────────────────────────────── +# ── Mail ───────────────────────────────────────────────────────────────── +# DEV: `log` writes mails to storage/logs (nothing is sent). +# PROD: switch to smtp and fill in the credentials below. MAIL_MAILER=log MAIL_FROM_ADDRESS=hello@clupilot.local MAIL_FROM_NAME=CluPilot +# MAIL_MAILER=smtp +# MAIL_HOST=smtp.your-provider.tld +# MAIL_PORT=587 +# MAIL_USERNAME= +# MAIL_PASSWORD= +# MAIL_SCHEME=tls # tls | smtps (465) + +# ═════════════════════════════════════════════════════════════════════════ +# CluPilot operations — fill these in to run real provisioning. +# Everything below is OPTIONAL for local dev: left blank, the services are +# no-ops/fakes and the app + test suite still run. Only real infrastructure +# work (host onboarding, customer provisioning) needs them. +# ═════════════════════════════════════════════════════════════════════════ + +# ── Stripe (payments) ──────────────────────────────────────────────────── +# WEBHOOK_SECRET is READ BY CODE today (signature check on /webhooks/stripe). +# Outside local/testing a missing secret makes the webhook fail closed. +STRIPE_WEBHOOK_SECRET= +# Not read yet — needed once the Checkout flow is built (currently the portal +# only records a purchase intent): +STRIPE_KEY= +STRIPE_SECRET= + +# ── DNS (Hetzner DNS API) ──────────────────────────────────────────────── +# Creates . A-records for each customer instance. +HETZNER_DNS_TOKEN= +CLUPILOT_DNS_ZONE=clupilot.com + +# ── Traefik (reverse proxy + TLS) ──────────────────────────────────────── +# Directory ON THE PROXMOX HOST where CluPilot writes dynamic route files. +TRAEFIK_DYNAMIC_PATH=/etc/traefik/dynamic + +# ── WireGuard hub (this CluPilot VM) ───────────────────────────────────── +# Hosts join this hub during onboarding; CluPilot reaches them over the tunnel. +CLUPILOT_WG_SUBNET=10.66.0.0/24 +CLUPILOT_WG_HUB_IP=10.66.0.1 +CLUPILOT_WG_ENDPOINT= # public host:port peers dial, e.g. vpn.clupilot.com:51820 +CLUPILOT_WG_HUB_PUBKEY= # `wg pubkey < /etc/wireguard/privatekey` +CLUPILOT_WG_CONFIG_PATH=/etc/wireguard/wg0.conf + +# ── SSH identity for host onboarding ───────────────────────────────────── +# Deployed to each fresh server after the one-time root password login. +# PREFERRED: point at files (a multi-line PEM cannot live in .env). +# ssh-keygen -t ed25519 -N '' -C clupilot -f storage/app/ssh/clupilot +CLUPILOT_SSH_PUBLIC_KEY_PATH= +CLUPILOT_SSH_PRIVATE_KEY_PATH= +# Alternative (single-line values only): +CLUPILOT_SSH_PUBLIC_KEY= +CLUPILOT_SSH_PRIVATE_KEY= +CLUPILOT_SSH_COMMAND_TIMEOUT=2000 + +# ── Monitoring ─────────────────────────────────────────────────────────── +# The built-in client speaks a GENERIC REST API: +# GET/POST /monitors, GET/DELETE /monitors/{id} +# Uptime Kuma's own REST API is READ-ONLY (badges/status page/push) — creating +# monitors goes through Socket.IO, so Kuma needs a small REST bridge in front +# of it (or a dedicated client). Leave blank to disable monitoring entirely: +# provisioning then records a stable breadcrumb and its own health checks +# (occ status, TLS, admin user) still gate acceptance. +MONITORING_API_URL= +MONITORING_API_TOKEN= + +# ── Nextcloud blueprint template ───────────────────────────────────────── +# NOT an env var: set the Proxmox template VMID per plan in +# config/provisioning.php → plans.*.template_vmid (default 9000). # ── docker-compose knobs (host-side) ───────────────────────────────────── HOST_UID=1000 diff --git a/config/provisioning.php b/config/provisioning.php index df1fa14..5adb90d 100644 --- a/config/provisioning.php +++ b/config/provisioning.php @@ -8,6 +8,21 @@ $wgSubnet = env('CLUPILOT_WG_SUBNET', '10.66.0.0/24'); $wgBase = ip2long($wgNetwork) & (0xFFFFFFFF << (32 - (int) $wgBits)) & 0xFFFFFFFF; $wgHubDefault = long2ip($wgBase + 1); // first usable address in the CIDR +/** + * Read a secret from a FILE path if given, else from the inline env var. + * SSH keys are multi-line PEM, which a plain .env cannot hold — pointing at a + * file keeps the key out of .env entirely (and off the environment). + * Note: after `config:cache`, changing the file needs `php artisan config:clear`. + */ +$secretFrom = function (string $pathVar, string $inlineVar): string { + $path = (string) env($pathVar, ''); + if ($path !== '' && is_readable($path)) { + return (string) file_get_contents($path); + } + + return (string) env($inlineVar, ''); +}; + return [ /* | Ordered step classes per pipeline. ::class is textual — listing steps that @@ -104,8 +119,9 @@ return [ // SSH identity CluPilot deploys to each host after first password login. 'ssh' => [ - 'public_key' => env('CLUPILOT_SSH_PUBLIC_KEY', ''), - 'private_key' => env('CLUPILOT_SSH_PRIVATE_KEY', ''), + // Prefer *_PATH (a file on disk); fall back to the inline env value. + 'public_key' => $secretFrom('CLUPILOT_SSH_PUBLIC_KEY_PATH', 'CLUPILOT_SSH_PUBLIC_KEY'), + 'private_key' => $secretFrom('CLUPILOT_SSH_PRIVATE_KEY_PATH', 'CLUPILOT_SSH_PRIVATE_KEY'), // Per-command SSH timeout; below the job timeout (2100s) but far above // the phpseclib ~10s default so apt full-upgrade etc. can finish. 'command_timeout' => (int) env('CLUPILOT_SSH_COMMAND_TIMEOUT', 2000),