@envsync-cloud/deploy-cli 0.8.12 → 0.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +304 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/index.ts
9
- import { createHash, randomBytes } from "crypto";
9
+ import { createHash, randomBytes, randomInt } from "crypto";
10
10
  import { spawnSync } from "child_process";
11
11
  import fs3 from "fs";
12
12
  import path3 from "path";
@@ -5156,6 +5156,7 @@ var BACKUPS_ROOT = path3.join(HOST_ROOT, "backups");
5156
5156
  var REPO_ROOT = process.env.ENVSYNC_REPO_ROOT ?? path3.join(HOST_ROOT, "repo");
5157
5157
  var DEPLOY_ENV = path3.join(ETC_ROOT, "deploy.env");
5158
5158
  var DEPLOY_YAML = path3.join(ETC_ROOT, "deploy.yaml");
5159
+ var WIREGUARD_ROOT = path3.join(ETC_ROOT, "wireguard");
5159
5160
  var LICENSE_ROOT = path3.join(ETC_ROOT, "license");
5160
5161
  var LICENSE_BUNDLE_FILE = path3.join(LICENSE_ROOT, "enterprise-license-bundle.json");
5161
5162
  var LICENSE_CERT_FILE = path3.join(LICENSE_ROOT, "enterprise-cert.pem");
@@ -5176,6 +5177,10 @@ var OTEL_AGENT_CONF = path3.join(DEPLOY_ROOT, "otel-agent.yaml");
5176
5177
  var CLICKSTACK_CLICKHOUSE_CONF = path3.join(DEPLOY_ROOT, "clickhouse-listen.xml");
5177
5178
  var INTERNAL_CONFIG_JSON = path3.join(DEPLOY_ROOT, "config.json");
5178
5179
  var UPGRADE_BACKUPS_ROOT = path3.join(BACKUPS_ROOT, "upgrade");
5180
+ var WIREGUARD_STATE_FILE = path3.join(WIREGUARD_ROOT, "state.json");
5181
+ var WIREGUARD_PRIVATE_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-private.key");
5182
+ var WIREGUARD_PUBLIC_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-public.key");
5183
+ var WIREGUARD_CONFIG_FILE = path3.join(WIREGUARD_ROOT, "envsync-wg.conf");
5179
5184
  var DEPLOY_RENDER_PATHS = {
5180
5185
  traefikStateRoot: TRAEFIK_STATE_ROOT,
5181
5186
  deployRoot: DEPLOY_ROOT,
@@ -5202,6 +5207,11 @@ var REMOVE_TARGETS = [
5202
5207
  LICENSE_ROOT_CA_FILE,
5203
5208
  INTERNAL_CONFIG_JSON,
5204
5209
  VERSIONS_LOCK,
5210
+ WIREGUARD_ROOT,
5211
+ WIREGUARD_STATE_FILE,
5212
+ WIREGUARD_PRIVATE_KEY_FILE,
5213
+ WIREGUARD_PUBLIC_KEY_FILE,
5214
+ WIREGUARD_CONFIG_FILE,
5205
5215
  STACK_FILE,
5206
5216
  BOOTSTRAP_BASE_STACK_FILE,
5207
5217
  BOOTSTRAP_STACK_FILE,
@@ -5327,6 +5337,28 @@ ${stderr}` : ""}`);
5327
5337
  }
5328
5338
  return result.stdout?.toString() ?? "";
5329
5339
  }
5340
+ function runCapture(cmd, args, opts = {}) {
5341
+ const result = spawnSync(cmd, args, {
5342
+ cwd: opts.cwd,
5343
+ env: { ...process.env, ...opts.env },
5344
+ input: opts.input,
5345
+ stdio: "pipe",
5346
+ encoding: "utf8"
5347
+ });
5348
+ if (result.status !== 0) {
5349
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
5350
+ throw new Error(`Command failed: ${cmd} ${args.join(" ")}${stderr ? `
5351
+ ${stderr}` : ""}`);
5352
+ }
5353
+ return (result.stdout ?? "").toString().trim();
5354
+ }
5355
+ function parseJsonOrDefault(value, fallback) {
5356
+ try {
5357
+ return JSON.parse(value);
5358
+ } catch {
5359
+ return fallback;
5360
+ }
5361
+ }
5330
5362
  function tryRun(cmd, args, opts = {}) {
5331
5363
  try {
5332
5364
  return run(cmd, args, opts);
@@ -5413,6 +5445,93 @@ function deterministicInstallFingerprint(rootDomain, stackName) {
5413
5445
  function randomStrongPassword() {
5414
5446
  return `EnvSync!${randomBytes(8).toString("hex")}Aa1`;
5415
5447
  }
5448
+ function randomWireguardPort() {
5449
+ return randomInt(5e4, 65535);
5450
+ }
5451
+ function loadWireguardState() {
5452
+ if (!exists2(WIREGUARD_STATE_FILE)) return null;
5453
+ const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
5454
+ return {
5455
+ private_key: raw.private_key,
5456
+ public_key: raw.public_key,
5457
+ listen_port: raw.listen_port,
5458
+ network: raw.network,
5459
+ server_ip: raw.server_ip,
5460
+ peers: Array.isArray(raw.peers) ? raw.peers : []
5461
+ };
5462
+ }
5463
+ function saveWireguardState(state) {
5464
+ if (currentOptions.dryRun) {
5465
+ logDryRun(`Would save WireGuard state to ${WIREGUARD_STATE_FILE}`);
5466
+ return;
5467
+ }
5468
+ writeFile(WIREGUARD_STATE_FILE, JSON.stringify(state, null, 2), 384);
5469
+ }
5470
+ function ensureWireguardState() {
5471
+ ensureDir(WIREGUARD_ROOT);
5472
+ let state = loadWireguardState();
5473
+ if (state) {
5474
+ state.private_key = state.private_key?.trim();
5475
+ state.public_key = state.public_key?.trim();
5476
+ state.network = state.network || "10.66.0.0/24";
5477
+ state.server_ip = state.server_ip || "10.66.0.1";
5478
+ state.listen_port = state.listen_port || randomWireguardPort();
5479
+ state.peers = Array.isArray(state.peers) ? state.peers : [];
5480
+ if (state.private_key && !state.public_key) {
5481
+ state.public_key = runCapture("wg", ["pubkey"], { input: `${state.private_key}
5482
+ ` });
5483
+ }
5484
+ if (!state.private_key || !state.public_key) {
5485
+ state = null;
5486
+ }
5487
+ }
5488
+ if (!state) {
5489
+ const privateKey = runCapture("wg", ["genkey"]);
5490
+ const publicKey = runCapture("wg", ["pubkey"], { input: `${privateKey}
5491
+ ` });
5492
+ state = {
5493
+ private_key: privateKey,
5494
+ public_key: publicKey,
5495
+ listen_port: randomWireguardPort(),
5496
+ network: "10.66.0.0/24",
5497
+ server_ip: "10.66.0.1",
5498
+ peers: []
5499
+ };
5500
+ saveWireguardState(state);
5501
+ }
5502
+ writeFileMaybe(WIREGUARD_PRIVATE_KEY_FILE, `${state.private_key}
5503
+ `, 384);
5504
+ writeFileMaybe(WIREGUARD_PUBLIC_KEY_FILE, `${state.public_key}
5505
+ `);
5506
+ return state;
5507
+ }
5508
+ function wireguardPeerAllowedIp(state) {
5509
+ const usedIps = new Set(state.peers.map((peer) => peer.allowed_ip));
5510
+ for (let i = 2; i <= 254; i++) {
5511
+ const candidate = `${state.server_ip.split(".").slice(0, 3).join(".")}.${i}/32`;
5512
+ if (!usedIps.has(candidate)) {
5513
+ return candidate;
5514
+ }
5515
+ }
5516
+ throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
5517
+ }
5518
+ function writeWireguardConfig(state) {
5519
+ const peersSection = state.peers.map((peer) => `
5520
+ [Peer]
5521
+ PublicKey = ${peer.public_key}
5522
+ AllowedIPs = ${peer.allowed_ip}`).join("\n");
5523
+ const content = `[Interface]
5524
+ Address = ${state.server_ip}/24
5525
+ ListenPort = ${state.listen_port}
5526
+ PrivateKey = ${state.private_key}
5527
+ PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
5528
+ PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
5529
+ PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
5530
+ PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
5531
+ ${peersSection}
5532
+ `;
5533
+ writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
5534
+ }
5416
5535
  function emptyApiSlotState() {
5417
5536
  return {
5418
5537
  api_image: "",
@@ -5548,6 +5667,9 @@ function renderHelpBlock() {
5548
5667
  " validate-topology Validate Enterprise edition topology rules",
5549
5668
  " upgrade [version] Pin a target release and deploy it",
5550
5669
  " upgrade-deps Refresh dependency images and redeploy",
5670
+ " vpn pubkey Generate or print WireGuard server public key",
5671
+ " vpn whitelist <key> Add a client public key to allowed peers",
5672
+ " vpn status Show WireGuard server/peer status",
5551
5673
  " license issue-cert Issue and install an Enterprise certificate bundle",
5552
5674
  " license renew-cert Renew and install the Enterprise certificate bundle",
5553
5675
  " license validate-cert Validate the installed Enterprise certificate bundle files",
@@ -7179,7 +7301,19 @@ async function cmdPreinstall() {
7179
7301
  ensureDir(TRAEFIK_STATE_ROOT);
7180
7302
  run("bash", ["-lc", "command -v apt-get >/dev/null"]);
7181
7303
  run("sudo", ["apt-get", "update"]);
7182
- run("sudo", ["apt-get", "install", "-y", "docker.io", "docker-compose-v2", "git", "curl", "jq", "openssl", "tar"]);
7304
+ run("sudo", [
7305
+ "apt-get",
7306
+ "install",
7307
+ "-y",
7308
+ "docker.io",
7309
+ "docker-compose-v2",
7310
+ "git",
7311
+ "curl",
7312
+ "jq",
7313
+ "openssl",
7314
+ "tar",
7315
+ "wireguard-tools"
7316
+ ]);
7183
7317
  run("sudo", ["systemctl", "enable", "--now", "docker"]);
7184
7318
  try {
7185
7319
  run("docker", ["swarm", "init"]);
@@ -8165,6 +8299,171 @@ async function cmdRestore(archivePath, autoDeploy = false) {
8165
8299
  }
8166
8300
  logInfo("Next: envsync-deploy deploy");
8167
8301
  }
8302
+ async function cmdVpn(args) {
8303
+ const action = args[0];
8304
+ switch (action) {
8305
+ case "pubkey":
8306
+ await cmdVpnPubkey();
8307
+ break;
8308
+ case "whitelist":
8309
+ await cmdVpnWhitelist(args[1]);
8310
+ break;
8311
+ case "status":
8312
+ await cmdVpnStatus();
8313
+ break;
8314
+ default:
8315
+ throw new Error("Usage: envsync-deploy vpn <pubkey|whitelist|status>");
8316
+ }
8317
+ }
8318
+ async function cmdVpnPubkey() {
8319
+ logSection("WireGuard VPN");
8320
+ const state = ensureWireguardState();
8321
+ writeWireguardConfig(state);
8322
+ logInfo(`WireGuard interface: envsync-wg`);
8323
+ logInfo(`Server config: ${WIREGUARD_CONFIG_FILE}`);
8324
+ logInfo(`Private key: ${WIREGUARD_PRIVATE_KEY_FILE}`);
8325
+ logInfo(`Public key: ${WIREGUARD_PUBLIC_KEY_FILE}`);
8326
+ logInfo(`Server listen port: ${state.listen_port}`);
8327
+ logInfo(`Network: ${state.network}`);
8328
+ console.log("");
8329
+ logSuccess(`Server public key: ${state.public_key}`);
8330
+ }
8331
+ async function cmdVpnWhitelist(clientPublicKeyArg) {
8332
+ if (!clientPublicKeyArg) {
8333
+ throw new Error("usage: envsync-deploy vpn whitelist <client_public_key>");
8334
+ }
8335
+ const clientPublicKey = clientPublicKeyArg.trim();
8336
+ if (!clientPublicKey) {
8337
+ throw new Error("client public key cannot be empty");
8338
+ }
8339
+ const state = ensureWireguardState();
8340
+ const existing = state.peers.find((peer) => peer.public_key === clientPublicKey);
8341
+ if (existing) {
8342
+ logWarn(`Client key already whitelisted for ${existing.allowed_ip}`);
8343
+ return;
8344
+ }
8345
+ const allowedIp = wireguardPeerAllowedIp(state);
8346
+ state.peers.push({
8347
+ public_key: clientPublicKey,
8348
+ allowed_ip: allowedIp,
8349
+ added_at: (/* @__PURE__ */ new Date()).toISOString()
8350
+ });
8351
+ state.peers.sort((a, b) => a.public_key.localeCompare(b.public_key));
8352
+ saveWireguardState(state);
8353
+ writeWireguardConfig(state);
8354
+ logSuccess(`Whitelisted client ${clientPublicKey}`);
8355
+ console.log(`AllowedIP: ${allowedIp}`);
8356
+ console.log(`Append to client config:`);
8357
+ console.log(`[Peer]`);
8358
+ console.log(`PublicKey = ${clientPublicKey}`);
8359
+ console.log(`AllowedIPs = ${allowedIp}`);
8360
+ }
8361
+ function getVpnStackName() {
8362
+ try {
8363
+ return loadConfig().services.stack_name;
8364
+ } catch {
8365
+ return "envsync";
8366
+ }
8367
+ }
8368
+ function listStackServiceNames(stackName) {
8369
+ const raw = tryRun("docker", ["service", "ls", "--filter", `label=com.docker.stack.namespace=${stackName}`, "--format", "{{.Name}}"], {
8370
+ quiet: true
8371
+ });
8372
+ if (!raw.trim()) return [];
8373
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8374
+ }
8375
+ function listServicePublishedPorts(serviceName) {
8376
+ const raw = tryRun("docker", ["service", "inspect", "--format", "{{json .Endpoint.Ports}}", serviceName], { quiet: true });
8377
+ const ports = parseJsonOrDefault(raw, []);
8378
+ const list = Array.isArray(ports) ? ports : [];
8379
+ if (list.length === 0) return [];
8380
+ return list.map((port) => {
8381
+ const typed = port;
8382
+ if (!typed || typeof typed !== "object") return "";
8383
+ if (!typed.PublishedPort || !typed.TargetPort || !typed.Protocol) return "";
8384
+ const mode = typed.PublishMode || "ingress";
8385
+ return `${typed.PublishedPort}/${typed.Protocol} -> ${typed.TargetPort}/${typed.Protocol} (${mode})`;
8386
+ }).filter(Boolean);
8387
+ }
8388
+ function listRunningContainerEndpoints(serviceName) {
8389
+ const raw = tryRun(
8390
+ "docker",
8391
+ ["service", "ps", serviceName, "--filter", "desired-state=running", "--no-trunc", "--format", "{{.ID}}"],
8392
+ { quiet: true }
8393
+ );
8394
+ const taskIds = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8395
+ if (taskIds.length === 0) return [];
8396
+ const results = [];
8397
+ for (const taskId of taskIds) {
8398
+ const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
8399
+ if (!taskRaw) continue;
8400
+ const inspections = parseJsonOrDefault(taskRaw, []);
8401
+ if (!Array.isArray(inspections) || inspections.length === 0) continue;
8402
+ const inspect = inspections[0];
8403
+ const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
8404
+ const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
8405
+ const networkName = attachment.Network?.Spec?.Name ?? "overlay";
8406
+ const ips = Array.isArray(attachment.Addresses) ? attachment.Addresses : [];
8407
+ return ips.map((ip) => {
8408
+ const trimmed = typeof ip === "string" ? ip.trim() : "";
8409
+ return trimmed ? `${networkName}:${trimmed}` : "";
8410
+ }).filter(Boolean);
8411
+ }) : [];
8412
+ const line = `task ${taskId.slice(0, 12)} container=${containerId.slice(0, 12) || "unknown"} ${addresses.length ? `ips=${addresses.join(", ")}` : "ips=none"}`;
8413
+ results.push(line);
8414
+ }
8415
+ return results;
8416
+ }
8417
+ async function cmdVpnStatus() {
8418
+ logSection("WireGuard VPN");
8419
+ const state = loadWireguardState();
8420
+ if (!state) {
8421
+ logWarn("WireGuard state not initialized.");
8422
+ logInfo("Run: envsync-deploy vpn pubkey");
8423
+ return;
8424
+ }
8425
+ logInfo(`Config file: ${WIREGUARD_CONFIG_FILE}`);
8426
+ logInfo(`State file: ${WIREGUARD_STATE_FILE}`);
8427
+ logInfo(`Server endpoint: 0.0.0.0:${state.listen_port}`);
8428
+ logInfo(`Network: ${state.network}`);
8429
+ logInfo(`Server IP: ${state.server_ip}`);
8430
+ if (state.peers.length === 0) {
8431
+ logInfo("Peers: none");
8432
+ } else {
8433
+ logInfo("Peers:");
8434
+ for (const peer of state.peers) {
8435
+ console.log(` ${peer.public_key} -> ${peer.allowed_ip}`);
8436
+ }
8437
+ }
8438
+ console.log("");
8439
+ logInfo("Container endpoints:");
8440
+ const stackName = getVpnStackName();
8441
+ const services = listStackServiceNames(stackName);
8442
+ if (services.length === 0) {
8443
+ logWarn(`No running services found for stack ${stackName}`);
8444
+ } else {
8445
+ for (const service of services) {
8446
+ const ports = listServicePublishedPorts(service);
8447
+ const endpoints = listRunningContainerEndpoints(service);
8448
+ console.log(` ${service}`);
8449
+ console.log(` Published ports: ${ports.length ? ports.join(", ") : "none"}`);
8450
+ if (endpoints.length === 0) {
8451
+ console.log(" Containers: none running");
8452
+ } else {
8453
+ for (const endpoint of endpoints) {
8454
+ console.log(` ${endpoint}`);
8455
+ }
8456
+ }
8457
+ }
8458
+ }
8459
+ try {
8460
+ const ifaceStatus = runCapture("wg", ["show", "envsync-wg"]);
8461
+ console.log("Runtime:");
8462
+ console.log(ifaceStatus);
8463
+ } catch {
8464
+ logWarn("WireGuard interface is not running in kernel yet. Run wg-quick up on the generated config to activate.");
8465
+ }
8466
+ }
8168
8467
  async function main() {
8169
8468
  const argv = process.argv.slice(2);
8170
8469
  const command = argv[0];
@@ -8212,6 +8511,9 @@ async function main() {
8212
8511
  case "upgrade-deps":
8213
8512
  await cmdUpgradeDeps();
8214
8513
  break;
8514
+ case "vpn":
8515
+ await cmdVpn(positionals);
8516
+ break;
8215
8517
  case "license":
8216
8518
  await cmdLicense(positionals[0]);
8217
8519
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.8.12",
3
+ "version": "0.8.14",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {