Services

Guacamole — remote desktop trên trình duyệt— browser remote desktop

Admin bấm một nút trên /vps/{id} là mở màn hình VPS ngay trong trình duyệt, có ghi audit. Trang này tách rõ phần đã chạy trong code, phần đã đổi so với bản thiết kế và phần vẫn chỉ là kế hoạch. One click on /vps/{id} opens the VPS desktop in the admin's browser, with an audit log. This page separates what is implemented in code, what changed from the design, and what is still only planned.

  • Cập nhậtUpdated 29/09/2026
  • NguồnSource monitor/GUACAMOLE_FEATURE_PLAN.md
  • monitor/fxconsole/docker-compose.yml (guacd, guacamole)
  • monitor/fxconsole/app/Http/Controllers/GuacamoleSessionController.php
  • monitor/fxconsole/docker/nginx-guac/README.md
Bản thiết kế đã cũ ở một điểm lớnThe design doc is stale on one big point

GUACAMOLE_FEATURE_PLAN.md (ngày 03/06/2026, "Phase 1 in progress") mô tả kết nối RDP. Code hiện tại dùng VNC: migration 2026_06_04_000001_replace_rdp_with_vnc_on_vpses xoá các cột rdp_username / rdp_password / rdp_port và thêm vnc_port / vnc_password. Lý do: RDP gắn vào console session sẽ khoá desktop khi ngắt kết nối, làm hỏng GUI automation của CmdAgent (thêm WebRequest URL cho MT5 dùng SendInput, khi bị khoá thì phím rơi vào Winlogon). VNC chỉ phản chiếu framebuffer đang chạy nên không tạo hay khoá session.

GUACAMOLE_FEATURE_PLAN.md (dated 03/06/2026, "Phase 1 in progress") describes RDP. The code now uses VNC: migration 2026_06_04_000001_replace_rdp_with_vnc_on_vpses drops rdp_username / rdp_password / rdp_port and adds vnc_port / vnc_password. Reason: attaching RDP to the Windows console session locks the desktop on disconnect, which breaks CmdAgent's GUI automation (the MT5 WebRequest URL step uses SendInput, which lands on Winlogon while locked). VNC mirrors the live framebuffer without creating or locking a session.

Đã làm và kế hoạchImplemented vs planned

Hạng mụcItemTrạng tháiStatusGhi chúNotes
Service guacd + guacamole trong composeguacd + guacamole compose servicesIMPLEMENTEDKhông có container nginx-guac như bản kế hoạch; TLS do reverse proxy trên host (NPM hoặc aaPanel) đảm nhận.No nginx-guac container as the plan had; TLS is terminated by the host reverse proxy (NPM or aaPanel).
Cầu xác thực JSON Auth (token ký + mã hoá, hạn 60 s)JSON Auth bridge (signed + encrypted token, 60 s expiry)IMPLEMENTEDGuacamoleSessionController::open
Giao thức kết nốiConnection protocolCHANGEDRDP → VNC (TightVNC do prepare-vm.ps1 cài, cổng 5900)RDP → VNC (TightVNC installed by prepare-vm.ps1, port 5900)
Credentials trên bảng vpsesCredentials on vpsesCHANGEDvnc_password (mã hoá theo APP_KEY, sinh ngẫu nhiên 8 ký tự phía server) + vnc_port (mặc định 5900); host lấy từ lan_ip agent báo qua heartbeatvnc_password (encrypted with APP_KEY, 8 random chars generated server-side) + vnc_port (default 5900); the host is the lan_ip the agent reports in its heartbeat
Bảng audit vps_remote_sessionsAudit table vps_remote_sessionsIMPLEMENTEDGhi dòng audit trước khi dựng tokenThe audit row is written before the token is built
Nút Connect trên /vps/{id} + trang /admin/vps/{id}/remote-sessionsConnect button on /vps/{id} + /admin/vps/{id}/remote-sessions pageIMPLEMENTEDChỉ adminAdmin only
Dọn session treo mỗi giờHourly stale-session GCIMPLEMENTEDfxconsole:close-stale-remote-sessions
Setting guac_json_secret_key, guac_public_urlSettings guac_json_secret_key, guac_public_urlIMPLEMENTEDHiện trên /admin/settingsShown on /admin/settings
Hướng dẫn reverse proxy (NPM, aaPanel)Reverse-proxy guide (NPM, aaPanel)IMPLEMENTEDdocker/nginx-guac/
DNS + chứng chỉ cho subdomain Guacamole, smoke test end-to-endDNS + certificate for the Guacamole subdomain, end-to-end smoke testUNVERIFIEDLà việc còn lại của Phase 1 trong bản kế hoạch; repo không cho biết đã xong chưaListed as remaining Phase 1 work in the plan; the repo doesn't show whether it's done
Webhook ngắt kết nối (điền closed_at)Disconnect webhook (fills closed_at)PLANNEDPhase 2
Kiểm tra TCP trước khi mởPre-flight TCP probePLANNEDPhase 2
Chế độ nhúng iframeInline iframe modePLANNEDPhase 2
Ghi hình session, credentials riêng từng admin, MFA, dọn audit sau 1 nămSession recording, per-admin credentials, MFA, 1-year audit prunePLANNEDPhase 3

Kiến trúc thực tếActual architecture

flowchart LR
  ADM(["Trình duyệt admin"]) -- "HTTPS" --> FX["fxconsole /vps/{id}"]
  FX -- "ghi audit" --> MY[("MySQL vps_remote_sessions")]
  FX -- "trả URL ?data=token (60 s)" --> ADM
  ADM -- "HTTPS + WebSocket (tab mới)" --> RP["Reverse proxy NPM / aaPanel (TLS)"]
  RP -- "HTTP" --> GC["guacamole :8080 (host :8282)"]
  GC -- "guac_internal :4822" --> GD["guacd"]
  GD -- "VNC :5900 tới lan_ip qua tunnel WireGuard" --> VPS["VPS Windows: TightVNC + phiên AutoLogon"]
flowchart LR
  ADM(["Admin browser"]) -- "HTTPS" --> FX["fxconsole /vps/{id}"]
  FX -- "write audit row" --> MY[("MySQL vps_remote_sessions")]
  FX -- "return URL ?data=token (60 s)" --> ADM
  ADM -- "HTTPS + WebSocket (new tab)" --> RP["Reverse proxy NPM / aaPanel (TLS)"]
  RP -- "HTTP" --> GC["guacamole :8080 (host :8282)"]
  GC -- "guac_internal :4822" --> GD["guacd"]
  GD -- "VNC :5900 to lan_ip through the WireGuard tunnel" --> VPS["Windows VPS: TightVNC + AutoLogon session"]

Guacamole chạy trên subdomain riêng (ví dụ guacamole.gamelau.uk) để container fxconsole không phải mang Java, cookie phiên của fxconsole không bao giờ tới Guacamole (chỉ token đi qua), và Guacamole có thể chuyển sang host khác mà không đổi fxconsole. guacd quay số tới lan_ip của VPS, nên host chạy Docker (OVH Dedicated Server 01) phải tới được LAN chứa VPS: server 01 là WireGuard client của CT WireGuard trên từng server VPS, mỗi server một tunnel (xem Hạ tầng máy chủ).

Guacamole runs on its own subdomain (for example guacamole.gamelau.uk) so the fxconsole container doesn't carry Java, fxconsole's session cookie never reaches Guacamole (only the token crosses), and Guacamole can move to another host without touching fxconsole. guacd dials the VPS lan_ip, so the Docker host (OVH Dedicated Server 01) must reach the LAN the VPS sits in: server 01 is a WireGuard client of the WireGuard CT on every VPS server, one tunnel per server (see Server infrastructure).

Luồng bấm ConnectThe Connect flow

  1. Admin bấm Connect trên /vps/{id}; React gọi POST /admin/vps/{id}/remote-session.The admin clicks Connect on /vps/{id}; React calls POST /admin/vps/{id}/remote-session.
  2. Server kiểm tra: phải là admin (403); đã cấu hình guac_json_secret_key và guac_public_url (503); secret đúng 32 ký tự hex = 16 byte (503); VPS đã báo lan_ip (422); có vnc_password và giải mã được (422).The server checks: admin only (403); guac_json_secret_key and guac_public_url configured (503); the secret is exactly 32 hex chars = 16 bytes (503); the VPS has reported lan_ip (422); vnc_password exists and decrypts (422).
  3. Ghi dòng vps_remote_sessions (token_jti UUID, vps_id, admin_id, opened_at, IP, user agent).Insert a vps_remote_sessions row (token_jti UUID, vps_id, admin_id, opened_at, IP, user agent).
  4. Dựng JSON: username = admin-{id}, expires = bây giờ + 60 s, một connection đặt tên theo tên VPS với protocol: vnc, hostname = lan_ip, port, password, color-depth: 24, clipboard hai chiều (disable-copy/paste: false).Build the JSON: username = admin-{id}, expires = now + 60 s, one connection named after the VPS with protocol: vnc, hostname = lan_ip, port, password, color-depth: 24, two-way clipboard (disable-copy/paste: false).
  5. Ký + mã hoá theo đặc tả guacamole-auth-json rồi trả {url, jti} với url = <guac_public_url>/?data=<base64>.Sign + encrypt per the guacamole-auth-json spec, then return {url, jti} where url = <guac_public_url>/?data=<base64>.
  6. Trình duyệt mở URL ở tab mới; Guacamole giải mã token, gọi guacd, vẽ desktop. Tải lại trang = bấm Connect lại = dòng audit mới.The browser opens the URL in a new tab; Guacamole decodes the token, dials guacd and renders the desktop. Reloading means a new Connect click and a new audit row.
signature = HMAC-SHA256(secret_bytes, json_string)
signed    = signature . json_string                  (raw byte concat)
encrypted = AES-128-CBC(secret_bytes, IV=zeros, signed)   (PKCS#7 padded)
token     = base64(encrypted)

Clipboard VNC chỉ là văn bản thuần (đủ để dán login), không có kéo thả file như RDP; đây là đánh đổi đã chấp nhận để giữ GUI automation của agent chạy ổn.

VNC clipboard is plain text only (fine for pasting a login), without RDP's file drop; that trade-off was accepted to keep the agent's GUI automation working.

Service trong docker-composeCompose services

guacd:
  image: guacamole/guacd:latest
  container_name: fxconsole-guacd
  networks: [guac_internal]
  restart: unless-stopped

guacamole:
  image: guacamole/guacamole:latest
  container_name: fxconsole-guacamole
  depends_on: [guacd]
  environment:
    GUACD_HOSTNAME: guacd
    GUACD_PORT: "4822"
    JSON_SECRET_KEY: ${GUAC_JSON_SECRET_KEY:-}
    WEBAPP_CONTEXT: ROOT
  networks: [guac_internal]
  ports:
    - "8282:8080"
  volumes:
    - guac_drive:/var/lib/guacamole/drive
  restart: unless-stopped
  • Chỉ guacamole được gọi guacd, qua network riêng guac_internal; guacd không có volume và không map cổng ra host.
  • Chỉ cần đặt JSON_SECRET_KEY là bật extension guacamole-auth-json trong image (Guacamole 1.5+); không cần DB của Guacamole. Giá trị phải trùng GUAC_JSON_SECRET_KEY trong .env của fxconsole.
  • WEBAPP_CONTEXT: ROOT đưa webapp về / thay vì /guacamole/, để proxy chuyển thẳng / → / mà không phải viết lại path hay cookie (nguồn gốc thường gặp của lỗi "kẹt ở login" hoặc WebSocket hỏng).
  • Volume guac_drive là chỗ tạm cho tính năng drive redirection của RDP; với VNC hiện tại controller không bật drive nên volume này nhiều khả năng không còn dùng. Nếu file tích tụ, dọn bằng docker compose exec guacamole find /var/lib/guacamole/drive -mtime +7 -delete.
  • Only guacamole may dial guacd, over the isolated guac_internal network; guacd has no volume and no host port.
  • Setting JSON_SECRET_KEY alone enables the guacamole-auth-json extension inside the image (Guacamole 1.5+); no Guacamole DB is needed. The value must equal GUAC_JSON_SECRET_KEY in fxconsole's .env.
  • WEBAPP_CONTEXT: ROOT serves the webapp at / instead of /guacamole/, so the proxy forwards / → / with no path or cookie rewriting (the usual source of "stuck on login" or broken WebSocket bugs).
  • The guac_drive volume is scratch space for RDP drive redirection; the VNC controller doesn't enable a drive, so the volume is most likely unused now. If files pile up, prune with docker compose exec guacamole find /var/lib/guacamole/drive -mtime +7 -delete.
Cổng 8282 mở trên mọi interfacePort 8282 is published on every interface

Comment trong compose nói bind vào IP LAN để container Nginx Proxy Manager (compose khác) truy cập được, nhưng "8282:8080" thực tế publish trên tất cả interface. Webapp nói HTTP thuần; token ?data= hết hạn sau 60 s nhưng vẫn chứa mật khẩu VNC đã mã hoá. Nếu host có IP public hoặc LAN không tin cậy, chặn 8282/tcp bằng ufw / iptables trừ subnet bridge của NPM. Cách sạch hơn: cho NPM tham gia network guac_internal (Option 1A bên dưới) rồi đổi lại thành 127.0.0.1:8282:8080.

The compose comment says it binds to the LAN IP so the Nginx Proxy Manager container (another compose project) can reach it, but "8282:8080" actually publishes on every interface. The webapp speaks plain HTTP; the ?data= token expires after 60 s but still carries the encrypted VNC password. If the host has a public IP or an untrusted LAN, drop 8282/tcp with ufw / iptables except from NPM's bridge subnet. The cleaner fix: join NPM to guac_internal (Option 1A below) and change the mapping back to 127.0.0.1:8282:8080.

Cấu hình (chỉ tên key)Configuration (key names only)

SettingSettingEnvHành viBehaviour
guac_json_secret_keyGUAC_JSON_SECRET_KEYKhoá AES 16 byte dạng hex (32 ký tự), tạo một lần bằng openssl rand -hex 16. Chỉ đọc trên /admin/settings để tránh lệch; nguồn gốc là env JSON_SECRET_KEY của container. Đổi: sửa phía container, rồi .env của fxconsole, rồi khởi động lại cả hai (docker compose up -d guacamole).16-byte AES key in hex (32 chars), generated once with openssl rand -hex 16. Read-only on /admin/settings to prevent drift; the container's JSON_SECRET_KEY env is the source of truth. To change: update the container side, then fxconsole's .env, then restart both (docker compose up -d guacamole).
guac_public_urlGUAC_PUBLIC_URLURL HTTPS gốc trình duyệt dùng để tới Guacamole, không có dấu / cuối. Trống = tắt nút Connect. Đọc trực tiếp mỗi lần bấm, không cần restart; giá trị trên /admin/settings thắng .env. Đổi domain: sửa field này, cấp lại chứng chỉ Let's Encrypt cho hostname mới, render lại docker/nginx-guac/guacamole.conf.template với ${GUAC_DOMAIN} mới.HTTPS base URL browsers use to reach Guacamole, with no trailing /. Empty disables the Connect button. Read live on every click, no restart; the /admin/settings value wins over .env. To rename the domain: update this field, re-issue the Let's Encrypt cert for the new hostname, and re-render docker/nginx-guac/guacamole.conf.template with the new ${GUAC_DOMAIN}.

Phía VPS: prepare-vm.ps1 lấy mật khẩu VNC riêng của VPS qua GET /api/agent/broker-config rồi cài TightVNC thành Windows Service trên cổng 5900. Admin có thể xem mật khẩu trên /vps/{id} để dùng VNC client native khi ở trong VPN, hoặc tạo lại bằng Edit VPS. Chi tiết: VPS agent.

VPS side: prepare-vm.ps1 fetches the per-VPS VNC password from GET /api/agent/broker-config and installs TightVNC as a Windows Service on port 5900. Admins can reveal the password on /vps/{id} for a native VNC client inside the VPN, or regenerate it via Edit VPS. Details: VPS agent.

Reverse proxyReverse proxy

Cổng trong hướng dẫn đã cũPort in the guide is stale

docker/nginx-guac/README.md vẫn viết container publish ở 127.0.0.1:8081. Compose hiện tại dùng 8282:8080. Thay 8081 bằng 8282 ở mọi chỗ bên dưới dùng cổng host.

docker/nginx-guac/README.md still says the container publishes on 127.0.0.1:8081. Compose now uses 8282:8080. Read 8282 wherever the steps below use the host port.

Bước 1: cho NPM tới được containerStep 1: let NPM reach the container

NPM chạy trong Docker không thấy 127.0.0.1 của host. Chọn một cách và giữ nguyên:

NPM in Docker cannot see the host's 127.0.0.1. Pick one fix and stick with it:

OptionCách làmHowForward host / port
1A (sạch nhất)(cleanest)Gắn container NPM vào network fxconsole_guac_internal (xem lệnh dưới)Attach the NPM container to fxconsole_guac_internal (commands below)fxconsole-guacamole / 8080
1Bhost.docker.internal (Docker Desktop; trên Linux thêm extra_hosts: "host.docker.internal:host-gateway" vào compose NPM)host.docker.internal (Docker Desktop; on Linux add extra_hosts: "host.docker.internal:host-gateway" to NPM's compose)host.docker.internal / cổng hosthost port
1C (cuối cùng)(last resort)Bind vào IP docker bridge, ví dụ 172.17.0.1:<port>:8080, rồi docker compose up -d guacamoleBind to the docker bridge IP, e.g. 172.17.0.1:<port>:8080, then docker compose up -d guacamole172.17.0.1 / cổng hosthost port
docker network ls | grep guac
# fxconsole_guac_internal   bridge   local
docker network connect fxconsole_guac_internal nginx-proxy-manager_app_1

Để bền vững, thêm vào compose của NPM:

To make it persistent, add to NPM's own compose:

services:
  app:
    networks:
      - default
      - fxconsole_guac_internal
networks:
  fxconsole_guac_internal:
    external: true

Không bind 0.0.0.0 nếu ufw / iptables chưa chặn cổng đó từ bên ngoài: cổng nói HTTP thuần và một token ?data= bị lộ cho phép mở desktop.

Do not bind 0.0.0.0 unless ufw / iptables drop that port from outside: it speaks plain HTTP and a leaked ?data= token opens the desktop.

Bước 2: tạo proxy host trong NPMStep 2: create the NPM proxy host

Tab / trườngTab / fieldGiá trịValue
Details · Domain Namesguacamole.yourdomain.com
Details · Schemehttp
Details · Forward Hostname / Porttheo bước 1from step 1
Details · Cache AssetsOFF
Details · Block Common ExploitsOFF (bộ lọc ăn mất URL token ?data=)(its filter eats the ?data= token URL)
Details · Websockets SupportON (tunnel Guacamole là WebSocket)(the Guacamole tunnel is WebSocket)
SSLRequest a new SSL Certificate (Let's Encrypt), Force SSL ON, HTTP/2 ON, HSTS ON, HSTS Subdomains OFF, email, đồng ý ToS. Cấp chứng chỉ mất 15–60 s, lỗi ngay nếu DNS chưa trỏ.Request a new SSL Certificate (Let's Encrypt), Force SSL ON, HTTP/2 ON, HSTS ON, HSTS Subdomains OFF, email, agree to ToS. Issuing takes 15–60 s and fails fast if DNS hasn't propagated.
AdvancedDán toàn bộ docker/nginx-guac/npm-advanced.conf: proxy_read_timeout 7200s, proxy_send_timeout 7200s, proxy_buffering off, access_log off (giữ token ngoài log). Không đặt header ở đây: NPM ghi đè ở mức location.Paste all of docker/nginx-guac/npm-advanced.conf: proxy_read_timeout 7200s, proxy_send_timeout 7200s, proxy_buffering off, access_log off (keeps the token out of logs). Don't set headers here: NPM overrides them at location level.

Bước 3: trỏ fxconsole tới URL mớiStep 3: point fxconsole at the new URL

Đặt GUAC_PUBLIC_URL=https://guacamole.yourdomain.com trong .env của fxconsole (hoặc sửa trên /admin/settings), và kiểm tra GUAC_JSON_SECRET_KEY=<secret> trùng với JSON_SECRET_KEY của container.

Set GUAC_PUBLIC_URL=https://guacamole.yourdomain.com in fxconsole's .env (or on /admin/settings), and check that GUAC_JSON_SECRET_KEY=<secret> matches the container's JSON_SECRET_KEY.

Bước 4: kiểm traStep 4: verify

Chạy theo thứ tự, dừng ở lỗi đầu tiên:

Run in order and stop at the first failure:

# 1. NPM reaches the upstream (inside the NPM container for Option 1A)
docker exec -it nginx-proxy-manager_app_1 sh
curl -I http://fxconsole-guacamole:8080
# Expect: HTTP/1.1 200 OK

# 2. End-to-end through NPM
curl -I https://guacamole.yourdomain.com
# Expect: 200 OK + strict-transport-security header

# 3. WebSocket upgrade
curl -i -N \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: dGVzdA==" -H "Sec-WebSocket-Version: 13" \
  -H "Origin: https://guacamole.yourdomain.com" \
  https://guacamole.yourdomain.com/websocket-tunnel
# Expect: HTTP/1.1 101 Switching Protocols

Nếu (3) trả 200 hoặc 400 thay vì 101, công tắc Websockets chưa ăn: lưu lại proxy host rồi thử lại. Sau đó bấm Connect trên một VPS: tab mới mở https://guacamole.yourdomain.com/#/?data=… và desktop hiện sau một hai giây.

If (3) returns 200 or 400 instead of 101, the Websockets toggle didn't take effect: re-save the proxy host and retry. Then click Connect on a VPS: a new tab opens at https://guacamole.yourdomain.com/#/?data=… and the desktop appears within a second or two.

Không dùng NPM: aaPanel / nginx trên hostWithout NPM: aaPanel / host nginx

GUAC_DOMAIN=guacamole.yourdomain.com \
  envsubst '${GUAC_DOMAIN}' \
    < guacamole.conf.template \
    > /www/server/panel/vhost/nginx/guacamole.conf
nginx -t && nginx -s reload

Rồi cấp chứng chỉ LE cho ${GUAC_DOMAIN} (UI aaPanel hoặc certbot --nginx -d guacamole.yourdomain.com) và reload nginx lần nữa. Dùng NPM thì không cần template này.

Then issue the LE cert for ${GUAC_DOMAIN} (aaPanel UI or certbot --nginx -d guacamole.yourdomain.com) and reload nginx once more. With NPM this template is unused.

Audit và dọn sessionAudit and session cleanup

Cột vps_remote_sessionsvps_remote_sessions columnÝ nghĩaMeaning
vps_id, admin_idFK tới VPS và user đã bấm ConnectFKs to the VPS and the user who clicked Connect
token_jtiUUID duy nhất cho mỗi tokenUnique UUID per token
opened_at / closed_atclosed_at hiện chỉ được điền bởi lệnh dọn (chưa có webhook ngắt kết nối)closed_at is currently filled only by the cleanup command (no disconnect webhook yet)
client_ip, user_agentIP và trình duyệt của admin lúc bấmAdmin IP and browser at click time

Lệnh fxconsole:close-stale-remote-sessions chạy mỗi giờ, đóng các dòng closed_at còn NULL sau --hours (mặc định 24). Xem trước số dòng:

fxconsole:close-stale-remote-sessions runs hourly and closes rows still NULL at closed_at after --hours (default 24). Preview the count:

docker exec fxconsole-app php artisan fxconsole:close-stale-remote-sessions --dry-run --hours=24

Log của luồng Connect nằm ở channel deployment với các khoá guac.open.success, guac.open.bad_secret, guac.open.password_decrypt_failed, guac.close_stale.done.

Connect-flow logs go to the deployment channel with keys guac.open.success, guac.open.bad_secret, guac.open.password_decrypt_failed, guac.close_stale.done.

Xử lý sự cốTroubleshooting

Triệu chứngSymptomNguyên nhân / cách sửaCause / fix
503 Guacamole is not configuredThiếu guac_json_secret_key hoặc guac_public_url.guac_json_secret_key or guac_public_url is empty.
503 … expect 16-byte hex = 32 charsSecret không đúng 32 ký tự hex.The secret is not exactly 32 hex chars.
422 VPS has not reported its LAN IP yetChờ heartbeat kế tiếp của agent.Wait for the agent's next heartbeat.
422 VNC password is not setChạy lại prepare-vm.ps1 trên VPS hoặc tạo lại mật khẩu qua Edit VPS.Re-run prepare-vm.ps1 on the VPS or rotate the password via Edit VPS.
422 VNC password is unreadableCiphertext mồ côi sau khi APP_KEY đổi; tạo lại qua Edit VPS.Ciphertext orphaned by an APP_KEY rotation; rotate via Edit VPS.
Màn đen / "Disconnected" sau vài giâyBlack screen / "Disconnected" after a few secondsWebSocket upgrade lỗi: bật Websockets Support trong NPM, lưu lại.WebSocket upgrade failing: turn on Websockets Support in NPM and re-save.
502 Bad GatewayNPM không tới được upstream: xem lại bước 1, kiểm tra docker ps có fxconsole-guacamole.NPM can't reach the upstream: re-check step 1 and that docker ps shows fxconsole-guacamole.
Kẹt ở "Connecting..."Stuck on "Connecting..."Như 502: thường do chưa làm Option 1A mà Forward Hostname vẫn là 127.0.0.1. Nếu proxy ổn, từ server 01 ping lan_ip của VPS: không được thì xem tunnel WireGuard tới server chứa VPS đó (handshake, AllowedIPs có subnet của LAN); ping được thì kiểm tra TightVNC đang chạy trên cổng 5900.Same as 502: usually Option 1A wasn't applied and Forward Hostname is still 127.0.0.1. If the proxy is fine, ping the VPS lan_ip from server 01: if that fails, check the WireGuard tunnel to the server hosting that VPS (handshake, AllowedIPs includes the LAN subnet); if it answers, check TightVNC is running on port 5900.
Vòng lặp redirect loginLogin redirect loopLệch cookie path; compose đã đặt WEBAPP_CONTEXT=ROOT. Nếu bỏ nó, NPM cần proxy_cookie_path /guacamole/ /;.Cookie path mismatch; compose already sets WEBAPP_CONTEXT=ROOT. If you override it, NPM also needs proxy_cookie_path /guacamole/ /;.
Rớt session sau ~60 s không thao tácSession drops after ~60 s idleTimeout upstream mặc định còn hiệu lực: dán lại npm-advanced.conf.Default upstream timeout still applies: re-paste npm-advanced.conf.
Token ?data= xuất hiện trong access log?data= token visible in access logsaccess_log off chưa áp: dán lại snippet Advanced.access_log off not applied: re-paste the Advanced snippet.
Cấp / gia hạn chứng chỉ LE lỗiLE cert issue / renewal failsDNS chưa trỏ về host hoặc cổng 80 bị chặn: dig guacamole.yourdomain.com, curl -I http://guacamole.yourdomain.com/.well-known/ từ bên ngoài; xem trạng thái gia hạn ở NPM → SSL Certificates.DNS not pointing at the host or port 80 blocked: dig guacamole.yourdomain.com, curl -I http://guacamole.yourdomain.com/.well-known/ from outside; check renewal status in NPM → SSL Certificates.

Ghi chú bảo mậtSecurity notes

  • Token trong URL. JSON đã ký chứa mật khẩu VNC dạng rõ trước khi mã hoá AES-128-CBC bằng GUAC_JSON_SECRET_KEY, và nó nằm trên URL nên có thể lọt vào lịch sử trình duyệt, log proxy, header Referer. Giảm thiểu đang có: hạn 60 s, access_log off trên vhost Guacamole. Bản kế hoạch đề xuất Referrer-Policy: no-referrer; nginx của fxconsole hiện gửi strict-origin-when-cross-origin (không gửi path/query sang origin khác).
  • Admin bị chiếm quyền. Session admin bị lộ vốn đã deploy / gỡ tài khoản MT5 trên cả fleet; remote desktop là tập cha nghiêm ngặt của quyền đó. Audit giúp điều tra; MFA ở Phase 3 chặn đường chiếm quyền.
  • Lưu audit. Bảng tăng tuyến tính (khoảng 10 admin × 5 session/ngày); giữ vĩnh viễn ở Phase 1, thêm dọn sau 1 năm ở Phase 3 nếu phình.
  • Độ trễ. Nếu guacd ở host khác mạng với VPS, thêm khoảng 30–50 ms; chấp nhận được cho việc admin, không liên quan tới giao dịch.
  • Token in the URL. The signed JSON holds the VNC password in clear before AES-128-CBC encryption with GUAC_JSON_SECRET_KEY, and it travels in a URL, so browser history, proxy logs and Referer headers could leak it. Mitigations in place: 60 s expiry, access_log off on the Guacamole vhost. The plan proposed Referrer-Policy: no-referrer; fxconsole's nginx currently sends strict-origin-when-cross-origin (no path/query to other origins).
  • Compromised admin. A leaked admin session can already deploy / remove MT5 accounts across the fleet; remote desktop is a strict superset. The audit log gives forensic visibility; MFA in Phase 3 closes the acquisition vector.
  • Audit retention. The table grows linearly (about 10 admins × 5 sessions/day); keep forever in Phase 1, add a 1-year prune in Phase 3 if it bloats.
  • Latency. If guacd sits on a different network from the VPSes it adds roughly 30–50 ms; fine for admin work, unrelated to trading.

Kế hoạch còn lạiRemaining roadmap

Phase 2 — PolishPhase 2 — Polish PLANNED

  1. Webhook ngắt kết nối: extension Java nhỏ trên Guacamole POST closed_at về fxconsole (hoặc đọc access log Tomcat).
  2. Kiểm tra trước: khi bấm Connect, fxconsole-app thử TCP tới cổng của VPS và báo "VPS unreachable" thay vì để Guacamole quay 30 s.
  3. Chế độ nhúng: route /vps/{id}/remote dùng iframe thay cho window.open. Phase 1 bỏ qua vì clipboard API và fullscreen API hành xử khác nhau giữa các trình duyệt khi ở trong iframe khác origin.
  1. Disconnect webhook: a small Java extension on Guacamole POSTs closed_at back to fxconsole (or scrape Tomcat access logs).
  2. Pre-flight check: on Connect, fxconsole-app runs a TCP probe to the VPS port and shows "VPS unreachable" instead of letting Guacamole spin for 30 s.
  3. Inline mode: a /vps/{id}/remote route that iframes Guacamole instead of window.open. Skipped in Phase 1 because clipboard and fullscreen APIs behave differently across browsers inside a cross-origin iframe.

Phase 3 — Chuẩn compliancePhase 3 — Compliance-grade PLANNED

  1. Ghi hình session qua recording-path của guacd, định kỳ đẩy lên storage kiểu S3, trang replay /admin/vps/{id}/remote-sessions/{n}/replay.
  2. Credentials riêng từng admin (bảng admin_vps_credentials) để audit biết admin dùng tài khoản Windows nào.
  3. Cổng MFA (TOTP / WebAuthn) trước khi cấp token.
  1. Session recording via guacd's recording-path, periodic upload to S3-compatible storage, replay page /admin/vps/{id}/remote-sessions/{n}/replay.
  2. Per-admin credentials (admin_vps_credentials table) so the audit shows which Windows account each admin used.
  3. MFA gate (TOTP / WebAuthn) before issuing the token.

Câu hỏi còn mởOpen questions

  • Ai cấp DNS và chứng chỉ cho subdomain Guacamole; có dùng chung tài khoản Cloudflare với fxconsole không?
  • Nếu gỡ Guacamole sau này: giữ bảng audit và cột credentials trên vpses cho giải pháp remote desktop khác.
  • Who provisions DNS and the certificate for the Guacamole subdomain; same Cloudflare account as fxconsole?
  • If Guacamole is retired later: keep the audit table and the credential columns on vpses for any future remote-desktop solution.