@envsync-cloud/deploy-cli 0.8.13 → 0.8.15

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 +436 -60
  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";
@@ -4400,7 +4400,6 @@ function buildRuntimeEnv2(config, generated) {
4400
4400
  const oss = isOssConfig(config);
4401
4401
  const enterprise = !oss;
4402
4402
  const license = config.license ?? {};
4403
- const accessProxy = config.access_proxy;
4404
4403
  return {
4405
4404
  NODE_ENV: "production",
4406
4405
  ENVSYNC_EDITION: oss ? "oss" : "enterprise",
@@ -4429,13 +4428,6 @@ function buildRuntimeEnv2(config, generated) {
4429
4428
  POSTGRES_USER: "postgres",
4430
4429
  POSTGRES_PASSWORD: "envsync-postgres",
4431
4430
  POSTGRES_DB: "envsync",
4432
- ENVSYNC_ACCESS_PROXY_ENABLED: `${accessProxy?.enabled ?? false}`,
4433
- ENVSYNC_ACCESS_PROXY_PROVIDER: accessProxy?.provider ?? "",
4434
- ENVSYNC_ACCESS_PROXY_DOMAIN: accessProxy?.domain ?? "",
4435
- ENVSYNC_ACCESS_PROXY_SERVICE_SCOPE: accessProxy?.service_scope ?? "all",
4436
- ENVSYNC_ACCESS_PROXY_SERVICES: Array.isArray(accessProxy?.services) ? accessProxy.services.join(",") : "",
4437
- ENVSYNC_ACCESS_PROXY_ADVERTISE_ROUTES: `${accessProxy?.advertise_routes ?? false}`,
4438
- ENVSYNC_ACCESS_PROXY_INCLUDE_ADMIN_SERVICES: `${accessProxy?.include_admin_services ?? false}`,
4439
4431
  S3_BUCKET: bucketName,
4440
4432
  S3_REGION: "us-east-1",
4441
4433
  S3_ACCESS_KEY: "envsync-rustfs",
@@ -4745,16 +4737,11 @@ function renderFrontendRuntimeConfig(config, generated) {
4745
4737
  const otelEndpoint = publicHttpsUrl(config, hosts.obs);
4746
4738
  const managementApiEnabled = !isOssConfig(config);
4747
4739
  const activeReleaseVersion = generated.deployment.slots[generated.deployment.active_slot].release_version || config.release.version;
4748
- const accessProxy = config.access_proxy;
4749
4740
  const managementApiUrl = managementApiEnabled ? publicHttpsUrl(config, hosts.manage_api) : "";
4750
4741
  return `window.__ENVSYNC_RUNTIME_CONFIG__ = ${JSON.stringify({
4751
4742
  apiBaseUrl: publicHttpsUrl(config, hosts.api),
4752
4743
  appBaseUrl: publicHttpsUrl(config, hosts.app),
4753
4744
  authBaseUrl: publicHttpsUrl(config, hosts.auth),
4754
- accessProxyEnabled: accessProxy?.enabled ?? false,
4755
- accessProxyDomain: accessProxy?.domain ?? "",
4756
- accessProxyServiceScope: accessProxy?.service_scope ?? "all",
4757
- accessProxyServices: accessProxy?.services ?? [],
4758
4745
  managementApiUrl,
4759
4746
  keycloakRealm: config.auth.keycloak_realm,
4760
4747
  webClientId: config.auth.web_client_id,
@@ -4769,6 +4756,39 @@ function renderFrontendRuntimeConfig(config, generated) {
4769
4756
  }, null, 2)};
4770
4757
  `;
4771
4758
  }
4759
+ function renderNetutilsService(config) {
4760
+ const netutils = config.netutils;
4761
+ if (!netutils?.enabled) return "";
4762
+ const forwards = Array.isArray(netutils.forwards) ? netutils.forwards : [];
4763
+ if (forwards.length === 0) return "";
4764
+ const commands = forwards.map((forward) => {
4765
+ const protocol = forward.protocol ?? "tcp";
4766
+ if (protocol === "udp") {
4767
+ return `socat UDP-LISTEN:${forward.host_port},fork,reuseaddr UDP:${forward.service}:${forward.service_port} &`;
4768
+ }
4769
+ return `socat TCP-LISTEN:${forward.host_port},fork,reuseaddr TCP:${forward.service}:${forward.service_port} &`;
4770
+ }).join("\n ");
4771
+ const ports = forwards.map((forward) => {
4772
+ const protocol = forward.protocol ?? "tcp";
4773
+ return ` - target: ${forward.host_port}
4774
+ published: ${forward.host_port}
4775
+ protocol: ${protocol}
4776
+ mode: ingress`;
4777
+ }).join("\n");
4778
+ return `
4779
+ netutils:
4780
+ image: alpine/socat
4781
+ command:
4782
+ - sh
4783
+ - -c
4784
+ - |
4785
+ set -eu
4786
+ ${commands}
4787
+ wait
4788
+ ports:
4789
+ ${ports}
4790
+ networks: [envsync]`;
4791
+ }
4772
4792
  function renderOtelAgentConfig(config) {
4773
4793
  return [
4774
4794
  "receivers:",
@@ -4825,6 +4845,7 @@ function renderStack(config, runtimeEnv, generated, mode, paths) {
4825
4845
  const s3ServiceName = `${stackName}-s3-service`;
4826
4846
  const s3ConsoleRouterName = `${stackName}-s3-console-router`;
4827
4847
  const s3ConsoleServiceName = `${stackName}-s3-console-service`;
4848
+ const netutilsService = renderNetutilsService(config);
4828
4849
  const apiEnvironment = {
4829
4850
  ...runtimeEnv,
4830
4851
  OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
@@ -5092,7 +5113,7 @@ ${renderEnvList({
5092
5113
  ${apiLicenseVolume}
5093
5114
  networks: [envsync]
5094
5115
  deploy:
5095
- replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}
5116
+ replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}${netutilsService}
5096
5117
 
5097
5118
  networks:
5098
5119
  envsync:
@@ -5169,6 +5190,7 @@ var BACKUPS_ROOT = path3.join(HOST_ROOT, "backups");
5169
5190
  var REPO_ROOT = process.env.ENVSYNC_REPO_ROOT ?? path3.join(HOST_ROOT, "repo");
5170
5191
  var DEPLOY_ENV = path3.join(ETC_ROOT, "deploy.env");
5171
5192
  var DEPLOY_YAML = path3.join(ETC_ROOT, "deploy.yaml");
5193
+ var WIREGUARD_ROOT = path3.join(ETC_ROOT, "wireguard");
5172
5194
  var LICENSE_ROOT = path3.join(ETC_ROOT, "license");
5173
5195
  var LICENSE_BUNDLE_FILE = path3.join(LICENSE_ROOT, "enterprise-license-bundle.json");
5174
5196
  var LICENSE_CERT_FILE = path3.join(LICENSE_ROOT, "enterprise-cert.pem");
@@ -5189,6 +5211,10 @@ var OTEL_AGENT_CONF = path3.join(DEPLOY_ROOT, "otel-agent.yaml");
5189
5211
  var CLICKSTACK_CLICKHOUSE_CONF = path3.join(DEPLOY_ROOT, "clickhouse-listen.xml");
5190
5212
  var INTERNAL_CONFIG_JSON = path3.join(DEPLOY_ROOT, "config.json");
5191
5213
  var UPGRADE_BACKUPS_ROOT = path3.join(BACKUPS_ROOT, "upgrade");
5214
+ var WIREGUARD_STATE_FILE = path3.join(WIREGUARD_ROOT, "state.json");
5215
+ var WIREGUARD_PRIVATE_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-private.key");
5216
+ var WIREGUARD_PUBLIC_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-public.key");
5217
+ var WIREGUARD_CONFIG_FILE = path3.join(WIREGUARD_ROOT, "envsync-wg.conf");
5192
5218
  var DEPLOY_RENDER_PATHS = {
5193
5219
  traefikStateRoot: TRAEFIK_STATE_ROOT,
5194
5220
  deployRoot: DEPLOY_ROOT,
@@ -5215,6 +5241,11 @@ var REMOVE_TARGETS = [
5215
5241
  LICENSE_ROOT_CA_FILE,
5216
5242
  INTERNAL_CONFIG_JSON,
5217
5243
  VERSIONS_LOCK,
5244
+ WIREGUARD_ROOT,
5245
+ WIREGUARD_STATE_FILE,
5246
+ WIREGUARD_PRIVATE_KEY_FILE,
5247
+ WIREGUARD_PUBLIC_KEY_FILE,
5248
+ WIREGUARD_CONFIG_FILE,
5218
5249
  STACK_FILE,
5219
5250
  BOOTSTRAP_BASE_STACK_FILE,
5220
5251
  BOOTSTRAP_STACK_FILE,
@@ -5340,6 +5371,28 @@ ${stderr}` : ""}`);
5340
5371
  }
5341
5372
  return result.stdout?.toString() ?? "";
5342
5373
  }
5374
+ function runCapture(cmd, args, opts = {}) {
5375
+ const result = spawnSync(cmd, args, {
5376
+ cwd: opts.cwd,
5377
+ env: { ...process.env, ...opts.env },
5378
+ input: opts.input,
5379
+ stdio: "pipe",
5380
+ encoding: "utf8"
5381
+ });
5382
+ if (result.status !== 0) {
5383
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
5384
+ throw new Error(`Command failed: ${cmd} ${args.join(" ")}${stderr ? `
5385
+ ${stderr}` : ""}`);
5386
+ }
5387
+ return (result.stdout ?? "").toString().trim();
5388
+ }
5389
+ function parseJsonOrDefault(value, fallback) {
5390
+ try {
5391
+ return JSON.parse(value);
5392
+ } catch {
5393
+ return fallback;
5394
+ }
5395
+ }
5343
5396
  function tryRun(cmd, args, opts = {}) {
5344
5397
  try {
5345
5398
  return run(cmd, args, opts);
@@ -5426,6 +5479,178 @@ function deterministicInstallFingerprint(rootDomain, stackName) {
5426
5479
  function randomStrongPassword() {
5427
5480
  return `EnvSync!${randomBytes(8).toString("hex")}Aa1`;
5428
5481
  }
5482
+ function randomWireguardPort() {
5483
+ return randomInt(5e4, 65535);
5484
+ }
5485
+ function parsePort(value, label) {
5486
+ const numericValue = typeof value === "string" ? Number(value.trim()) : Number(value);
5487
+ if (!Number.isInteger(numericValue) || numericValue < 1 || numericValue > 65535) {
5488
+ throw new Error(`Invalid ${label}: expected an integer from 1 to 65535.`);
5489
+ }
5490
+ return numericValue;
5491
+ }
5492
+ function parseNetutilsForwardsFromInput(raw, stackName) {
5493
+ const chunks = asStringList(raw);
5494
+ if (chunks.length === 0) {
5495
+ return [];
5496
+ }
5497
+ const candidates = chunks.map((chunk) => {
5498
+ const parts = chunk.split(":");
5499
+ if (parts.length < 3 || parts.length > 4) {
5500
+ throw new Error(
5501
+ `Invalid netutils forward '${chunk}'. Expected format service:service_port:host_port[:protocol].`
5502
+ );
5503
+ }
5504
+ return {
5505
+ service: parts[0].trim(),
5506
+ service_port: parts[1].trim(),
5507
+ host_port: parts[2].trim(),
5508
+ protocol: parts[3] ?? "tcp"
5509
+ };
5510
+ });
5511
+ return normalizeNetutilsForwards(candidates, stackName, true).forwards;
5512
+ }
5513
+ function normalizeNetutilsProtocol(raw) {
5514
+ if (typeof raw === "string") {
5515
+ const protocol = raw.toLowerCase();
5516
+ if (protocol === "tcp" || protocol === "udp") return protocol;
5517
+ throw new Error(`Invalid protocol '${raw}'. Supported values are tcp or udp.`);
5518
+ }
5519
+ return "tcp";
5520
+ }
5521
+ function normalizeNetutilsForwards(raw, stackName, enabled) {
5522
+ if (!Array.isArray(raw)) {
5523
+ return {
5524
+ enabled,
5525
+ forwards: []
5526
+ };
5527
+ }
5528
+ const usedHostPorts = /* @__PURE__ */ new Set();
5529
+ const unique = /* @__PURE__ */ new Set();
5530
+ const forwards = [];
5531
+ for (const entry of raw) {
5532
+ const candidate = entry ?? {};
5533
+ const rawService = typeof candidate.service === "string" ? candidate.service.trim() : "";
5534
+ if (!rawService) {
5535
+ throw new Error("Invalid netutils forward: missing service");
5536
+ }
5537
+ const service = rawService.includes(".") || rawService.includes(":") || rawService.startsWith(`${stackName}_`) ? rawService : rawService;
5538
+ const servicePort = parsePort(candidate.service_port, "service_port");
5539
+ const hostPort = parsePort(candidate.host_port, "host_port");
5540
+ const protocol = normalizeNetutilsProtocol(candidate.protocol);
5541
+ const mapKey = `${service}:${servicePort}:${hostPort}:${protocol}`;
5542
+ if (unique.has(mapKey)) {
5543
+ continue;
5544
+ }
5545
+ const hostKey = `${protocol}:${hostPort}`;
5546
+ if (usedHostPorts.has(hostKey)) {
5547
+ throw new Error(
5548
+ `Duplicate netutils host port mapping detected for ${protocol.toUpperCase()} ${hostPort}. Use unique host ports per protocol.`
5549
+ );
5550
+ }
5551
+ unique.add(mapKey);
5552
+ usedHostPorts.add(hostKey);
5553
+ forwards.push({
5554
+ service,
5555
+ service_port: servicePort,
5556
+ host_port: hostPort,
5557
+ protocol
5558
+ });
5559
+ }
5560
+ forwards.sort((a, b) => {
5561
+ if (a.protocol !== b.protocol) return a.protocol.localeCompare(b.protocol);
5562
+ if (a.host_port !== b.host_port) return a.host_port - b.host_port;
5563
+ return a.service.localeCompare(b.service);
5564
+ });
5565
+ return {
5566
+ enabled,
5567
+ forwards
5568
+ };
5569
+ }
5570
+ function loadWireguardState() {
5571
+ if (!exists2(WIREGUARD_STATE_FILE)) return null;
5572
+ const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
5573
+ return {
5574
+ private_key: raw.private_key,
5575
+ public_key: raw.public_key,
5576
+ listen_port: raw.listen_port,
5577
+ network: raw.network,
5578
+ server_ip: raw.server_ip,
5579
+ peers: Array.isArray(raw.peers) ? raw.peers : []
5580
+ };
5581
+ }
5582
+ function saveWireguardState(state) {
5583
+ if (currentOptions.dryRun) {
5584
+ logDryRun(`Would save WireGuard state to ${WIREGUARD_STATE_FILE}`);
5585
+ return;
5586
+ }
5587
+ writeFile(WIREGUARD_STATE_FILE, JSON.stringify(state, null, 2), 384);
5588
+ }
5589
+ function ensureWireguardState() {
5590
+ ensureDir(WIREGUARD_ROOT);
5591
+ let state = loadWireguardState();
5592
+ if (state) {
5593
+ state.private_key = state.private_key?.trim();
5594
+ state.public_key = state.public_key?.trim();
5595
+ state.network = state.network || "10.66.0.0/24";
5596
+ state.server_ip = state.server_ip || "10.66.0.1";
5597
+ state.listen_port = state.listen_port || randomWireguardPort();
5598
+ state.peers = Array.isArray(state.peers) ? state.peers : [];
5599
+ if (state.private_key && !state.public_key) {
5600
+ state.public_key = runCapture("wg", ["pubkey"], { input: `${state.private_key}
5601
+ ` });
5602
+ }
5603
+ if (!state.private_key || !state.public_key) {
5604
+ state = null;
5605
+ }
5606
+ }
5607
+ if (!state) {
5608
+ const privateKey = runCapture("wg", ["genkey"]);
5609
+ const publicKey = runCapture("wg", ["pubkey"], { input: `${privateKey}
5610
+ ` });
5611
+ state = {
5612
+ private_key: privateKey,
5613
+ public_key: publicKey,
5614
+ listen_port: randomWireguardPort(),
5615
+ network: "10.66.0.0/24",
5616
+ server_ip: "10.66.0.1",
5617
+ peers: []
5618
+ };
5619
+ saveWireguardState(state);
5620
+ }
5621
+ writeFileMaybe(WIREGUARD_PRIVATE_KEY_FILE, `${state.private_key}
5622
+ `, 384);
5623
+ writeFileMaybe(WIREGUARD_PUBLIC_KEY_FILE, `${state.public_key}
5624
+ `);
5625
+ return state;
5626
+ }
5627
+ function wireguardPeerAllowedIp(state) {
5628
+ const usedIps = new Set(state.peers.map((peer) => peer.allowed_ip));
5629
+ for (let i = 2; i <= 254; i++) {
5630
+ const candidate = `${state.server_ip.split(".").slice(0, 3).join(".")}.${i}/32`;
5631
+ if (!usedIps.has(candidate)) {
5632
+ return candidate;
5633
+ }
5634
+ }
5635
+ throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
5636
+ }
5637
+ function writeWireguardConfig(state) {
5638
+ const peersSection = state.peers.map((peer) => `
5639
+ [Peer]
5640
+ PublicKey = ${peer.public_key}
5641
+ AllowedIPs = ${peer.allowed_ip}`).join("\n");
5642
+ const content = `[Interface]
5643
+ Address = ${state.server_ip}/24
5644
+ ListenPort = ${state.listen_port}
5645
+ PrivateKey = ${state.private_key}
5646
+ PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
5647
+ PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
5648
+ PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
5649
+ PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
5650
+ ${peersSection}
5651
+ `;
5652
+ writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
5653
+ }
5429
5654
  function emptyApiSlotState() {
5430
5655
  return {
5431
5656
  api_image: "",
@@ -5475,9 +5700,6 @@ function parseEnvFile(content) {
5475
5700
  }
5476
5701
  return out;
5477
5702
  }
5478
- function asBool(value) {
5479
- return value.trim().toLowerCase() === "true" || value.trim().toLowerCase() === "y" || value.trim().toLowerCase() === "yes";
5480
- }
5481
5703
  function asStringList(value) {
5482
5704
  return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
5483
5705
  }
@@ -5567,6 +5789,9 @@ function renderHelpBlock() {
5567
5789
  " validate-topology Validate Enterprise edition topology rules",
5568
5790
  " upgrade [version] Pin a target release and deploy it",
5569
5791
  " upgrade-deps Refresh dependency images and redeploy",
5792
+ " vpn pubkey Generate or print WireGuard server public key",
5793
+ " vpn whitelist <key> Add a client public key to allowed peers",
5794
+ " vpn status Show WireGuard server/peer status",
5570
5795
  " license issue-cert Issue and install an Enterprise certificate bundle",
5571
5796
  " license renew-cert Renew and install the Enterprise certificate bundle",
5572
5797
  " license validate-cert Validate the installed Enterprise certificate bundle files",
@@ -5812,6 +6037,7 @@ function normalizeConfig(raw) {
5812
6037
  db_snapshot_on_api_upgrade: raw.upgrade?.db_snapshot_on_api_upgrade ?? true,
5813
6038
  keep_failed_upgrade_db_snapshot: raw.upgrade?.keep_failed_upgrade_db_snapshot ?? true
5814
6039
  },
6040
+ netutils: normalizeNetutilsForwards(raw.netutils?.forwards, stackName, raw.netutils?.enabled ?? false),
5815
6041
  license: raw.license ? {
5816
6042
  server_url: raw.license.server_url,
5817
6043
  key: raw.license.key,
@@ -5822,30 +6048,6 @@ function normalizeConfig(raw) {
5822
6048
  } : raw.edition === "oss" ? void 0 : {
5823
6049
  install_fingerprint: deterministicInstallFingerprint(rootDomain, stackName),
5824
6050
  certificate_validity_days: 1095
5825
- },
5826
- access_proxy: {
5827
- enabled: raw.access_proxy?.enabled ?? false,
5828
- provider: "tsdproxy",
5829
- auth_token: raw.access_proxy?.auth_token ?? "",
5830
- domain: raw.access_proxy?.domain ?? `tsdproxy.${rootDomain}`,
5831
- service_scope: raw.access_proxy?.service_scope === "selected" ? "selected" : "all",
5832
- services: Array.isArray(raw.access_proxy?.services) ? raw.access_proxy.services.filter((s) => typeof s === "string" && s.trim().length > 0) : [
5833
- "api",
5834
- "web",
5835
- "landing",
5836
- "keycloak",
5837
- "openfga",
5838
- "postgre*",
5839
- "redis",
5840
- "rustfs",
5841
- "clickstack",
5842
- "obs",
5843
- "envsync_api_*",
5844
- "envsync_management-api",
5845
- "envsync-api"
5846
- ],
5847
- advertise_routes: raw.access_proxy?.advertise_routes ?? false,
5848
- include_admin_services: raw.access_proxy?.include_admin_services ?? false
5849
6051
  }
5850
6052
  };
5851
6053
  }
@@ -7222,7 +7424,19 @@ async function cmdPreinstall() {
7222
7424
  ensureDir(TRAEFIK_STATE_ROOT);
7223
7425
  run("bash", ["-lc", "command -v apt-get >/dev/null"]);
7224
7426
  run("sudo", ["apt-get", "update"]);
7225
- run("sudo", ["apt-get", "install", "-y", "docker.io", "docker-compose-v2", "git", "curl", "jq", "openssl", "tar"]);
7427
+ run("sudo", [
7428
+ "apt-get",
7429
+ "install",
7430
+ "-y",
7431
+ "docker.io",
7432
+ "docker-compose-v2",
7433
+ "git",
7434
+ "curl",
7435
+ "jq",
7436
+ "openssl",
7437
+ "tar",
7438
+ "wireguard-tools"
7439
+ ]);
7226
7440
  run("sudo", ["systemctl", "enable", "--now", "docker"]);
7227
7441
  try {
7228
7442
  run("docker", ["swarm", "init"]);
@@ -7251,18 +7465,18 @@ async function cmdSetup() {
7251
7465
  const publicAuth = await ask("Expose auth.<domain> publicly (true/false)", "true") === "true";
7252
7466
  const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
7253
7467
  const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
7468
+ const netutilsEnabled = await ask("Enable WireGuard jump host access (true/false)", "false") === "true";
7469
+ const netutilsForwards = netutilsEnabled ? parseNetutilsForwardsFromInput(
7470
+ await ask(
7471
+ "Netutils forwards (format service:service_port:host_port[:protocol], comma-separated)",
7472
+ "postgres:5432:15432:tcp,keycloak:8080:18080:tcp"
7473
+ ),
7474
+ "envsync"
7475
+ ) : [];
7254
7476
  const licenseServerUrl = await ask("Enterprise license server URL", DEFAULT_ENTERPRISE_LICENSE_SERVER_URL);
7255
7477
  const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
7256
7478
  const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
7257
7479
  const installFingerprint = deterministicInstallFingerprint(rootDomain, "envsync");
7258
- const enableAccessProxy = asBool(await ask("Enable private access proxy for container services (y/N)", "n"));
7259
- const accessProxyProvider = enableAccessProxy ? await ask("Private access proxy provider", "tsdproxy") : "tsdproxy";
7260
- const accessProxyAuthToken = enableAccessProxy ? await ask("Proxy auth token or API key", "") : "";
7261
- const accessProxyDomain = enableAccessProxy ? await ask("Private access domain", `tsdproxy.${rootDomain}`) : `tsdproxy.${rootDomain}`;
7262
- const accessProxyExposeAll = enableAccessProxy ? asBool(await ask("Expose all services through proxy (Y/n)", "y")) : true;
7263
- const accessProxyServices = !accessProxyExposeAll ? asStringList(await ask("Service hostnames to expose (comma-separated)", "postgres,api,web,landing,keycloak,minikms,openfga,rustfs,clickstack,obs,manage-api")) : [];
7264
- const includeAdminServices = enableAccessProxy ? asBool(await ask("Expose admin/maintenance services (y/N)", "n")) : false;
7265
- const accessProxyAdvertiseRoutes = enableAccessProxy ? asBool(await ask("Advertise overlay subnet routes through the proxy provider (y/N)", "n")) : false;
7266
7480
  const config = {
7267
7481
  edition: "enterprise",
7268
7482
  source: defaultSourceConfig(releaseVersion),
@@ -7336,15 +7550,9 @@ async function cmdSetup() {
7336
7550
  certificate_bundle_file: certificateBundleFile || void 0,
7337
7551
  certificate_validity_days: 1095
7338
7552
  },
7339
- access_proxy: {
7340
- enabled: enableAccessProxy,
7341
- provider: accessProxyProvider,
7342
- auth_token: accessProxyAuthToken,
7343
- domain: accessProxyDomain,
7344
- service_scope: accessProxyExposeAll ? "all" : "selected",
7345
- services: accessProxyServices,
7346
- advertise_routes: accessProxyAdvertiseRoutes,
7347
- include_admin_services: includeAdminServices
7553
+ netutils: {
7554
+ enabled: netutilsEnabled,
7555
+ forwards: netutilsForwards
7348
7556
  }
7349
7557
  };
7350
7558
  saveDesiredConfig(config);
@@ -8226,6 +8434,171 @@ async function cmdRestore(archivePath, autoDeploy = false) {
8226
8434
  }
8227
8435
  logInfo("Next: envsync-deploy deploy");
8228
8436
  }
8437
+ async function cmdVpn(args) {
8438
+ const action = args[0];
8439
+ switch (action) {
8440
+ case "pubkey":
8441
+ await cmdVpnPubkey();
8442
+ break;
8443
+ case "whitelist":
8444
+ await cmdVpnWhitelist(args[1]);
8445
+ break;
8446
+ case "status":
8447
+ await cmdVpnStatus();
8448
+ break;
8449
+ default:
8450
+ throw new Error("Usage: envsync-deploy vpn <pubkey|whitelist|status>");
8451
+ }
8452
+ }
8453
+ async function cmdVpnPubkey() {
8454
+ logSection("WireGuard VPN");
8455
+ const state = ensureWireguardState();
8456
+ writeWireguardConfig(state);
8457
+ logInfo(`WireGuard interface: envsync-wg`);
8458
+ logInfo(`Server config: ${WIREGUARD_CONFIG_FILE}`);
8459
+ logInfo(`Private key: ${WIREGUARD_PRIVATE_KEY_FILE}`);
8460
+ logInfo(`Public key: ${WIREGUARD_PUBLIC_KEY_FILE}`);
8461
+ logInfo(`Server listen port: ${state.listen_port}`);
8462
+ logInfo(`Network: ${state.network}`);
8463
+ console.log("");
8464
+ logSuccess(`Server public key: ${state.public_key}`);
8465
+ }
8466
+ async function cmdVpnWhitelist(clientPublicKeyArg) {
8467
+ if (!clientPublicKeyArg) {
8468
+ throw new Error("usage: envsync-deploy vpn whitelist <client_public_key>");
8469
+ }
8470
+ const clientPublicKey = clientPublicKeyArg.trim();
8471
+ if (!clientPublicKey) {
8472
+ throw new Error("client public key cannot be empty");
8473
+ }
8474
+ const state = ensureWireguardState();
8475
+ const existing = state.peers.find((peer) => peer.public_key === clientPublicKey);
8476
+ if (existing) {
8477
+ logWarn(`Client key already whitelisted for ${existing.allowed_ip}`);
8478
+ return;
8479
+ }
8480
+ const allowedIp = wireguardPeerAllowedIp(state);
8481
+ state.peers.push({
8482
+ public_key: clientPublicKey,
8483
+ allowed_ip: allowedIp,
8484
+ added_at: (/* @__PURE__ */ new Date()).toISOString()
8485
+ });
8486
+ state.peers.sort((a, b) => a.public_key.localeCompare(b.public_key));
8487
+ saveWireguardState(state);
8488
+ writeWireguardConfig(state);
8489
+ logSuccess(`Whitelisted client ${clientPublicKey}`);
8490
+ console.log(`AllowedIP: ${allowedIp}`);
8491
+ console.log(`Append to client config:`);
8492
+ console.log(`[Peer]`);
8493
+ console.log(`PublicKey = ${clientPublicKey}`);
8494
+ console.log(`AllowedIPs = ${allowedIp}`);
8495
+ }
8496
+ function getVpnStackName() {
8497
+ try {
8498
+ return loadConfig().services.stack_name;
8499
+ } catch {
8500
+ return "envsync";
8501
+ }
8502
+ }
8503
+ function listStackServiceNames(stackName) {
8504
+ const raw = tryRun("docker", ["service", "ls", "--filter", `label=com.docker.stack.namespace=${stackName}`, "--format", "{{.Name}}"], {
8505
+ quiet: true
8506
+ });
8507
+ if (!raw.trim()) return [];
8508
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8509
+ }
8510
+ function listServicePublishedPorts(serviceName) {
8511
+ const raw = tryRun("docker", ["service", "inspect", "--format", "{{json .Endpoint.Ports}}", serviceName], { quiet: true });
8512
+ const ports = parseJsonOrDefault(raw, []);
8513
+ const list = Array.isArray(ports) ? ports : [];
8514
+ if (list.length === 0) return [];
8515
+ return list.map((port) => {
8516
+ const typed = port;
8517
+ if (!typed || typeof typed !== "object") return "";
8518
+ if (!typed.PublishedPort || !typed.TargetPort || !typed.Protocol) return "";
8519
+ const mode = typed.PublishMode || "ingress";
8520
+ return `${typed.PublishedPort}/${typed.Protocol} -> ${typed.TargetPort}/${typed.Protocol} (${mode})`;
8521
+ }).filter(Boolean);
8522
+ }
8523
+ function listRunningContainerEndpoints(serviceName) {
8524
+ const raw = tryRun(
8525
+ "docker",
8526
+ ["service", "ps", serviceName, "--filter", "desired-state=running", "--no-trunc", "--format", "{{.ID}}"],
8527
+ { quiet: true }
8528
+ );
8529
+ const taskIds = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8530
+ if (taskIds.length === 0) return [];
8531
+ const results = [];
8532
+ for (const taskId of taskIds) {
8533
+ const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
8534
+ if (!taskRaw) continue;
8535
+ const parsedTaskInspect = parseJsonOrDefault(taskRaw, null);
8536
+ const inspect = Array.isArray(parsedTaskInspect) ? parsedTaskInspect[0] : parsedTaskInspect;
8537
+ if (!inspect || typeof inspect !== "object") continue;
8538
+ const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
8539
+ const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
8540
+ const networkName = attachment.Network?.Spec?.Name ?? "overlay";
8541
+ const ips = Array.isArray(attachment.Addresses) ? attachment.Addresses : [];
8542
+ return ips.map((ip) => {
8543
+ const trimmed = typeof ip === "string" ? ip.trim() : "";
8544
+ return trimmed ? `${networkName}:${trimmed}` : "";
8545
+ }).filter(Boolean);
8546
+ }) : [];
8547
+ const line = `task ${taskId.slice(0, 12)} container=${containerId.slice(0, 12) || "unknown"} ${addresses.length ? `ips=${addresses.join(", ")}` : "ips=none"}`;
8548
+ results.push(line);
8549
+ }
8550
+ return results;
8551
+ }
8552
+ async function cmdVpnStatus() {
8553
+ logSection("WireGuard VPN");
8554
+ const state = loadWireguardState();
8555
+ if (!state) {
8556
+ logWarn("WireGuard state not initialized.");
8557
+ logInfo("Run: envsync-deploy vpn pubkey");
8558
+ return;
8559
+ }
8560
+ logInfo(`Config file: ${WIREGUARD_CONFIG_FILE}`);
8561
+ logInfo(`State file: ${WIREGUARD_STATE_FILE}`);
8562
+ logInfo(`Server endpoint: 0.0.0.0:${state.listen_port}`);
8563
+ logInfo(`Network: ${state.network}`);
8564
+ logInfo(`Server IP: ${state.server_ip}`);
8565
+ if (state.peers.length === 0) {
8566
+ logInfo("Peers: none");
8567
+ } else {
8568
+ logInfo("Peers:");
8569
+ for (const peer of state.peers) {
8570
+ console.log(` ${peer.public_key} -> ${peer.allowed_ip}`);
8571
+ }
8572
+ }
8573
+ console.log("");
8574
+ logInfo("Container endpoints:");
8575
+ const stackName = getVpnStackName();
8576
+ const services = listStackServiceNames(stackName);
8577
+ if (services.length === 0) {
8578
+ logWarn(`No running services found for stack ${stackName}`);
8579
+ } else {
8580
+ for (const service of services) {
8581
+ const ports = listServicePublishedPorts(service);
8582
+ const endpoints = listRunningContainerEndpoints(service);
8583
+ console.log(` ${service}`);
8584
+ console.log(` Published ports: ${ports.length ? ports.join(", ") : "none"}`);
8585
+ if (endpoints.length === 0) {
8586
+ console.log(" Containers: none running");
8587
+ } else {
8588
+ for (const endpoint of endpoints) {
8589
+ console.log(` ${endpoint}`);
8590
+ }
8591
+ }
8592
+ }
8593
+ }
8594
+ try {
8595
+ const ifaceStatus = runCapture("wg", ["show", "envsync-wg"]);
8596
+ console.log("Runtime:");
8597
+ console.log(ifaceStatus);
8598
+ } catch {
8599
+ logWarn("WireGuard interface is not running in kernel yet. Run wg-quick up on the generated config to activate.");
8600
+ }
8601
+ }
8229
8602
  async function main() {
8230
8603
  const argv = process.argv.slice(2);
8231
8604
  const command = argv[0];
@@ -8273,6 +8646,9 @@ async function main() {
8273
8646
  case "upgrade-deps":
8274
8647
  await cmdUpgradeDeps();
8275
8648
  break;
8649
+ case "vpn":
8650
+ await cmdVpn(positionals);
8651
+ break;
8276
8652
  case "license":
8277
8653
  await cmdLicense(positionals[0]);
8278
8654
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.8.13",
3
+ "version": "0.8.15",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {