{"id":197,"date":"2026-07-28T06:16:47","date_gmt":"2026-07-28T06:16:47","guid":{"rendered":"https:\/\/www.miniamju.com\/index.php\/apache-guacamole-part-3-connecting-private-virtual-machines\/"},"modified":"2026-07-28T06:16:47","modified_gmt":"2026-07-28T06:16:47","slug":"apache-guacamole-part-3-connecting-private-virtual-machines","status":"publish","type":"post","link":"https:\/\/www.miniamju.com\/index.php\/apache-guacamole-part-3-connecting-private-virtual-machines\/","title":{"rendered":"Apache Guacamole Part 3: Connecting Private Virtual Machines"},"content":{"rendered":"<p><em>Part 3 of 3: target isolation, Windows preparation, and a validated RDP<br \/>\nconnection.<\/em><\/p>\n<p><a href=\"\/index.php\/apache-guacamole-part-1-building-the-remote-access-container\/\">Part 1<\/a><br \/>\nbuilt the private Guacamole stack and gave <code>guacd<\/code> a dedicated target network.<br \/>\n<a href=\"\/index.php\/apache-guacamole-part-2-publishing-the-gateway-through-nginx\/\">Part 2<\/a><br \/>\npublished the web application through Nginx while keeping its private listener<br \/>\nrestricted to the reverse proxy.<\/p>\n<p>This final part connects a private Windows VM through RDP. The target is added<br \/>\nto a firewall allowlist before the Guacamole connection is created, Windows<br \/>\nuses a dedicated non-administrator account, and credentials are prompted at<br \/>\nconnection time rather than stored in the shared connection profile.<\/p>\n<p>All names and addresses below are generic examples. Replace them with local<br \/>\nvalues. Do not publish real VM names, private addresses, usernames,<br \/>\ncredentials, or access assignments.<\/p>\n<pre><code class=\"language-text\">Browser -&gt; HTTPS\/Nginx -&gt; Guacamole web -&gt; guacd -&gt; approved VM:3389\n                                           |\n                                  all other targets dropped\n<\/code><\/pre>\n<blockquote>\n<p><strong>Security boundary:<\/strong> signing in to Guacamole only grants access to a<br \/>\nconnection definition. Windows still authenticates the RDP account and<br \/>\napplies its own local permissions. Neither layer replaces the other.<\/p>\n<\/blockquote>\n<h2>Pilot Design<\/h2>\n<p>The pilot uses:<\/p>\n<ul>\n<li>a non-critical Windows 11 Pro, Enterprise, or Education VM;<\/li>\n<li>RDP with Network Level Authentication;<\/li>\n<li>a dedicated, non-administrator Windows account;<\/li>\n<li>membership only in <code>Remote Desktop Users<\/code>;<\/li>\n<li>administrator-only Guacamole access during testing;<\/li>\n<li>explicit testing of stored versus user-prompted Windows credentials;<\/li>\n<li>no drive, printing, audio-input, or file-transfer redirection;<\/li>\n<li>persistent Trust on First Use certificate storage;<\/li>\n<li>a firewall rule allowing only the selected VM and RDP port.<\/li>\n<\/ul>\n<p>Guacamole RDP is an administration path. It is not a replacement for a<br \/>\nlow-latency game-streaming service and may lock or replace the active Windows<br \/>\nconsole session.<\/p>\n<h2>1. Confirm the Part 1 Network<\/h2>\n<p>Part 1 created:<\/p>\n<pre><code class=\"language-text\">network: guacamole_remote_access\nbridge: br-guac-remote\nsubnet: 172.20.0.0\/24\nguacd address: 172.20.0.2\n<\/code><\/pre>\n<p>In the Guacamole container:<\/p>\n<pre><code class=\"language-bash\">cd \/opt\/guacamole\n\ndocker network inspect guacamole_remote_access \\\n  --format &#39;{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}&#39;\n\nip -brief address show br-guac-remote\n\ndocker compose exec -T guacd id\n<\/code><\/pre>\n<p>Expected:<\/p>\n<pre><code class=\"language-text\">guacamole-guacd 172.20.0.2\/24\nbr-guac-remote has 172.20.0.1\/24\nguacd runs as its non-root image user\n<\/code><\/pre>\n<p>Stop if the network or fixed address differs from the firewall values below.<\/p>\n<h2>2. Prepare the Windows Target<\/h2>\n<p>On the Windows VM, open PowerShell as Administrator:<\/p>\n<pre><code class=\"language-powershell\">$cv = Get-ItemProperty `\n  &#39;HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion&#39;\n\n[pscustomobject]@{\n    ProductName    = $cv.ProductName\n    EditionID      = $cv.EditionID\n    DisplayVersion = $cv.DisplayVersion\n    Build          = &quot;$($cv.CurrentBuild).$($cv.UBR)&quot;\n}\n\n[pscustomobject]@{\n    RDPEnabled = (\n        Get-ItemPropertyValue `\n          &#39;HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server&#39; `\n          &#39;fDenyTSConnections&#39;\n    ) -eq 0\n\n    NLARequired = (\n        Get-ItemPropertyValue `\n          &#39;HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp&#39; `\n          &#39;UserAuthentication&#39;\n    ) -eq 1\n}\n\nGet-Service TermService |\n    Select-Object Name, Status, StartType\n\nGet-NetTCPConnection -State Listen -LocalPort 3389 `\n    -ErrorAction SilentlyContinue |\n    Select-Object LocalAddress, LocalPort, OwningProcess\n\nGet-NetFirewallRule -DisplayGroup &#39;Remote Desktop&#39; |\n    Where-Object Enabled -eq True |\n    Select-Object DisplayName, Profile, Direction, Action\n\nGet-LocalGroupMember -Group &#39;Remote Desktop Users&#39;\nGet-LocalGroupMember -Group &#39;Administrators&#39;\n<\/code><\/pre>\n<p>Required results:<\/p>\n<ul>\n<li>the Windows edition supports hosting RDP;<\/li>\n<li><code>RDPEnabled<\/code> is <code>True<\/code>;<\/li>\n<li><code>NLARequired<\/code> is <code>True<\/code>;<\/li>\n<li><code>TermService<\/code> is running;<\/li>\n<li>TCP 3389 is listening;<\/li>\n<li>the Remote Desktop firewall group is enabled;<\/li>\n<li>the dedicated RDP account is in <code>Remote Desktop Users<\/code>;<\/li>\n<li>the dedicated account is not in <code>Administrators<\/code>.<\/li>\n<\/ul>\n<p>Do not continue until those conditions are satisfied.<\/p>\n<h3>Create a Dedicated Local RDP User<\/h3>\n<p>Use a separate local Windows account for Guacamole rather than an everyday<br \/>\ndesktop, Microsoft, or administrator account.<\/p>\n<p>On Windows 11 Pro, Enterprise, or Education:<\/p>\n<ol>\n<li>Press <code>Win+R<\/code>.<\/li>\n<li>Enter <code>lusrmgr.msc<\/code> and press Enter.<\/li>\n<li>Select <strong>Users<\/strong>.<\/li>\n<li>Select <strong>Action &gt; New User<\/strong>.<\/li>\n<li>Enter a neutral username that identifies the account as an RDP account.<\/li>\n<li>Enter a strong, unique password and confirm it.<\/li>\n<li>Clear <strong>User must change password at next logon<\/strong>. NLA cannot complete a<br \/>\npassword-change workflow before the RDP session starts.<\/li>\n<li>Leave <strong>Account is disabled<\/strong> cleared.<\/li>\n<li>Leave <strong>Password never expires<\/strong> cleared unless a documented password<br \/>\nrotation process cannot be implemented.<\/li>\n<li>Select <strong>Create<\/strong>, then <strong>Close<\/strong>.<\/li>\n<\/ol>\n<p>Add only the required RDP permission:<\/p>\n<ol>\n<li>In <code>lusrmgr.msc<\/code>, select <strong>Groups<\/strong>.<\/li>\n<li>Open <strong>Remote Desktop Users<\/strong>.<\/li>\n<li>Select <strong>Add<\/strong>.<\/li>\n<li>Enter the new local username.<\/li>\n<li>Select <strong>Check Names<\/strong>, then <strong>OK<\/strong>.<\/li>\n<li>Open <strong>Administrators<\/strong> and confirm the account is not a member.<\/li>\n<\/ol>\n<p>Verify from an elevated PowerShell window:<\/p>\n<pre><code class=\"language-powershell\">Get-LocalUser |\n    Select-Object Name, Enabled, PasswordRequired, PasswordExpires\n\nGet-LocalGroupMember -Group &#39;Remote Desktop Users&#39;\nGet-LocalGroupMember -Group &#39;Administrators&#39;\n<\/code><\/pre>\n<p>The account should be enabled, require a password, appear in<br \/>\n<code>Remote Desktop Users<\/code>, and not appear in <code>Administrators<\/code>.<\/p>\n<h2>3. Add the Endpoint Firewall Policy<\/h2>\n<p>Part 2 created <code>\/usr\/local\/sbin\/guacamole-firewall<\/code> to restrict the private web<br \/>\nlistener to Nginx. Replace that script with the complete version below, which<br \/>\nretains the web restriction and adds the VM-specific <code>guacd<\/code> allowlist.<\/p>\n<p>The example values are:<\/p>\n<pre><code class=\"language-text\">Nginx proxy: 10.20.30.220\nGuacamole listener: TCP 8080\nguacd source: 172.20.0.2\nRDP target: 10.20.30.202\nRDP port: TCP 3389\n<\/code><\/pre>\n<p>Back up the current script:<\/p>\n<pre><code class=\"language-bash\">cp -a \/usr\/local\/sbin\/guacamole-firewall \\\n  \/usr\/local\/sbin\/guacamole-firewall.pre-target\n<\/code><\/pre>\n<p>Replace it:<\/p>\n<pre><code class=\"language-bash\">cat &gt; \/usr\/local\/sbin\/guacamole-firewall &lt;&lt;&#39;EOF&#39;\n#!\/bin\/sh\nset -eu\n\nPATH=\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\n\nPARENT_CHAIN=&quot;DOCKER-USER&quot;\n\nWEB_CHAIN=&quot;GUAC-WEB&quot;\nPROXY_IP=&quot;10.20.30.220&quot;\nBACKEND_PORT=&quot;8080&quot;\nLAN_INTERFACE=&quot;eth0&quot;\n\nTARGET_CHAIN=&quot;GUAC-TARGETS&quot;\nREMOTE_INTERFACE=&quot;br-guac-remote&quot;\nGUACD_IP=&quot;172.20.0.2&quot;\nRDP_TARGET=&quot;10.20.30.202&quot;\nRDP_PORT=&quot;3389&quot;\n\nwait_for_docker_chain() {\n    attempts=0\n\n    until iptables -nL &quot;$PARENT_CHAIN&quot; &gt;\/dev\/null 2&gt;&amp;1; do\n        attempts=$((attempts + 1))\n\n        if [ &quot;$attempts&quot; -ge 30 ]; then\n            echo &quot;Timed out waiting for $PARENT_CHAIN&quot; &gt;&amp;2\n            exit 1\n        fi\n\n        sleep 1\n    done\n}\n\nremove_web_jump() {\n    while iptables -C &quot;$PARENT_CHAIN&quot; \\\n        -i &quot;$LAN_INTERFACE&quot; -p tcp --dport &quot;$BACKEND_PORT&quot; \\\n        -j &quot;$WEB_CHAIN&quot; 2&gt;\/dev\/null; do\n\n        iptables -D &quot;$PARENT_CHAIN&quot; \\\n            -i &quot;$LAN_INTERFACE&quot; -p tcp --dport &quot;$BACKEND_PORT&quot; \\\n            -j &quot;$WEB_CHAIN&quot;\n    done\n}\n\nremove_target_jump() {\n    while iptables -C &quot;$PARENT_CHAIN&quot; \\\n        -i &quot;$REMOTE_INTERFACE&quot; -s &quot;$GUACD_IP&quot; \\\n        -j &quot;$TARGET_CHAIN&quot; 2&gt;\/dev\/null; do\n\n        iptables -D &quot;$PARENT_CHAIN&quot; \\\n            -i &quot;$REMOTE_INTERFACE&quot; -s &quot;$GUACD_IP&quot; \\\n            -j &quot;$TARGET_CHAIN&quot;\n    done\n}\n\nstart_rules() {\n    wait_for_docker_chain\n\n    iptables -N &quot;$WEB_CHAIN&quot; 2&gt;\/dev\/null || true\n    iptables -F &quot;$WEB_CHAIN&quot;\n\n    iptables -A &quot;$WEB_CHAIN&quot; \\\n        -p tcp -s &quot;$PROXY_IP&quot; --dport &quot;$BACKEND_PORT&quot; \\\n        -j ACCEPT\n\n    iptables -A &quot;$WEB_CHAIN&quot; \\\n        -p tcp --dport &quot;$BACKEND_PORT&quot; \\\n        -j DROP\n\n    remove_web_jump\n\n    iptables -I &quot;$PARENT_CHAIN&quot; 1 \\\n        -i &quot;$LAN_INTERFACE&quot; -p tcp --dport &quot;$BACKEND_PORT&quot; \\\n        -j &quot;$WEB_CHAIN&quot;\n\n    iptables -N &quot;$TARGET_CHAIN&quot; 2&gt;\/dev\/null || true\n    iptables -F &quot;$TARGET_CHAIN&quot;\n\n    iptables -A &quot;$TARGET_CHAIN&quot; \\\n        -p tcp -d &quot;$RDP_TARGET&quot; --dport &quot;$RDP_PORT&quot; \\\n        -m conntrack --ctstate NEW,ESTABLISHED \\\n        -j ACCEPT\n\n    iptables -A &quot;$TARGET_CHAIN&quot; -j DROP\n\n    remove_target_jump\n\n    iptables -I &quot;$PARENT_CHAIN&quot; 2 \\\n        -i &quot;$REMOTE_INTERFACE&quot; -s &quot;$GUACD_IP&quot; \\\n        -j &quot;$TARGET_CHAIN&quot;\n\n    while iptables -C &quot;$PARENT_CHAIN&quot; \\\n        -i &quot;$LAN_INTERFACE&quot; -p tcp -s &quot;$PROXY_IP&quot; \\\n        --dport &quot;$BACKEND_PORT&quot; -j ACCEPT 2&gt;\/dev\/null; do\n\n        iptables -D &quot;$PARENT_CHAIN&quot; \\\n            -i &quot;$LAN_INTERFACE&quot; -p tcp -s &quot;$PROXY_IP&quot; \\\n            --dport &quot;$BACKEND_PORT&quot; -j ACCEPT\n    done\n\n    while iptables -C &quot;$PARENT_CHAIN&quot; \\\n        -i &quot;$LAN_INTERFACE&quot; -p tcp --dport &quot;$BACKEND_PORT&quot; \\\n        -j DROP 2&gt;\/dev\/null; do\n\n        iptables -D &quot;$PARENT_CHAIN&quot; \\\n            -i &quot;$LAN_INTERFACE&quot; -p tcp --dport &quot;$BACKEND_PORT&quot; \\\n            -j DROP\n    done\n}\n\nstop_rules() {\n    if iptables -nL &quot;$PARENT_CHAIN&quot; &gt;\/dev\/null 2&gt;&amp;1; then\n        remove_target_jump\n        remove_web_jump\n    fi\n\n    if iptables -nL &quot;$TARGET_CHAIN&quot; &gt;\/dev\/null 2&gt;&amp;1; then\n        iptables -F &quot;$TARGET_CHAIN&quot;\n        iptables -X &quot;$TARGET_CHAIN&quot;\n    fi\n\n    if iptables -nL &quot;$WEB_CHAIN&quot; &gt;\/dev\/null 2&gt;&amp;1; then\n        iptables -F &quot;$WEB_CHAIN&quot;\n        iptables -X &quot;$WEB_CHAIN&quot;\n    fi\n}\n\ncase &quot;${1:-}&quot; in\n    start)\n        start_rules\n        ;;\n    stop)\n        stop_rules\n        ;;\n    *)\n        echo &quot;Usage: $0 {start|stop}&quot; &gt;&amp;2\n        exit 2\n        ;;\nesac\nEOF\n\nchmod 0750 \/usr\/local\/sbin\/guacamole-firewall\n<\/code><\/pre>\n<p>Restart the existing firewall service:<\/p>\n<pre><code class=\"language-bash\">systemctl restart guacamole-firewall.service\nsystemctl status --no-pager guacamole-firewall.service\n\niptables -nvL DOCKER-USER --line-numbers\niptables -nvL GUAC-WEB --line-numbers\niptables -nvL GUAC-TARGETS --line-numbers\n<\/code><\/pre>\n<p>Expected:<\/p>\n<pre><code class=\"language-text\">DOCKER-USER rule 1 -&gt; GUAC-WEB for eth0 TCP\/8080\nDOCKER-USER rule 2 -&gt; GUAC-TARGETS for br-guac-remote source 172.20.0.2\nGUAC-TARGETS rule 1 -&gt; ACCEPT selected VM TCP\/3389\nGUAC-TARGETS rule 2 -&gt; DROP everything else\n<\/code><\/pre>\n<h2>4. Test the Allowed Path<\/h2>\n<p>From inside <code>guacd<\/code>:<\/p>\n<pre><code class=\"language-bash\">docker compose exec -T guacd sh -c &#39;\nif command -v nc &gt;\/dev\/null 2&gt;&amp;1; then\n    nc -zvw5 10.20.30.202 3389\nelif command -v bash &gt;\/dev\/null 2&gt;&amp;1; then\n    timeout 5 bash -c &quot;&lt;\/dev\/tcp\/10.20.30.202\/3389&quot;\nelse\n    echo &quot;No TCP test utility available&quot;\n    exit 2\nfi\n&#39;\n<\/code><\/pre>\n<p>Expected:<\/p>\n<pre><code class=\"language-text\">connection to the selected VM on TCP 3389 succeeds\n<\/code><\/pre>\n<p>Check counters:<\/p>\n<pre><code class=\"language-bash\">iptables -nvL GUAC-TARGETS --line-numbers\n<\/code><\/pre>\n<p>The packet counter on the allow rule must increase.<\/p>\n<p>Test a known unapproved LAN destination and port:<\/p>\n<pre><code class=\"language-bash\">docker compose exec -T guacd sh -c &#39;\nnc -zvw3 10.20.30.1 443\n&#39;\n<\/code><\/pre>\n<p>Expected:<\/p>\n<pre><code class=\"language-text\">the connection times out or fails\nthe DROP-rule packet counter increases\n<\/code><\/pre>\n<p>Restart Docker once before creating the connection:<\/p>\n<pre><code class=\"language-bash\">systemctl restart docker\n\nsystemctl is-active docker\nsystemctl is-active guacamole-firewall.service\n\ncd \/opt\/guacamole\ndocker compose ps\n\ndocker network inspect guacamole_remote_access \\\n  --format &#39;{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}&#39;\n\niptables -nvL DOCKER-USER --line-numbers\niptables -nvL GUAC-TARGETS --line-numbers\n<\/code><\/pre>\n<p>Then repeat the allowed and denied connection tests. This confirms that Docker<br \/>\nrestores the Compose-managed network, <code>guacd<\/code> retains its fixed source address,<br \/>\nand systemd restores the target policy.<\/p>\n<h2>5. Create the Guacamole RDP Connection<\/h2>\n<p>The connection is created only after the network test succeeds. This order<br \/>\nprevents a broadly configured Guacamole connection from becoming an accidental<br \/>\nroute to systems that have not been approved.<\/p>\n<p>Sign in as the Guacamole administrator and create a new RDP connection.<\/p>\n<p>Use:<\/p>\n<table>\n<thead>\n<tr>\n<th>Field<\/th>\n<th>Pilot value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Name<\/td>\n<td>a neutral administrative label<\/td>\n<\/tr>\n<tr>\n<td>Protocol<\/td>\n<td>RDP<\/td>\n<\/tr>\n<tr>\n<td>Hostname<\/td>\n<td>the private VM address<\/td>\n<\/tr>\n<tr>\n<td>Port<\/td>\n<td><code>3389<\/code><\/td>\n<\/tr>\n<tr>\n<td>Username<\/td>\n<td>leave blank for prompting<\/td>\n<\/tr>\n<tr>\n<td>Password<\/td>\n<td>leave blank for the pilot<\/td>\n<\/tr>\n<tr>\n<td>Domain<\/td>\n<td>leave blank for prompting<\/td>\n<\/tr>\n<tr>\n<td>Security mode<\/td>\n<td>NLA<\/td>\n<\/tr>\n<tr>\n<td>Certificate handling<\/td>\n<td>Trust on First Use<\/td>\n<\/tr>\n<tr>\n<td>Drive redirection<\/td>\n<td>disabled<\/td>\n<\/tr>\n<tr>\n<td>Printing<\/td>\n<td>disabled<\/td>\n<\/tr>\n<tr>\n<td>Audio input<\/td>\n<td>disabled<\/td>\n<\/tr>\n<tr>\n<td>SFTP\/file transfer<\/td>\n<td>disabled<\/td>\n<\/tr>\n<tr>\n<td>Clipboard<\/td>\n<td>enable only if required<\/td>\n<\/tr>\n<tr>\n<td>Resize method<\/td>\n<td>display update<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Assign the connection only to the administrator during the pilot. Do not grant<br \/>\naccess to all registered users until the session, audit, and credential model<br \/>\nhave been validated.<\/p>\n<p>Keep NLA required on the Windows target. Before the first connection, verify<br \/>\nthat the persistent FreeRDP directory created in Part 1 is owned by the<br \/>\n<code>guacd<\/code> user and writable. A root-owned, read-only trust directory can cause<br \/>\nboth forced <code>NLA<\/code> and <code>Any<\/code> to fail with the generic security-negotiation<br \/>\nmessage before credentials are evaluated.<\/p>\n<p>Leave the complete authentication set blank if credentials must not be stored.<br \/>\nGuacamole will prompt for the Windows username and password when the<br \/>\nnegotiated security protocol requests them. Supplying a username while leaving<br \/>\nonly the password blank creates a partial preconfigured authenticator and can<br \/>\nprevent prompting.<\/p>\n<h2>6. Validate the Session<\/h2>\n<p>Open the connection through the public HTTPS endpoint and enter the dedicated<br \/>\nWindows username and password when prompted. For a local Windows account, use<br \/>\nthe account name in the form accepted by that host, such as<br \/>\n<code>COMPUTERNAME\\username<\/code> or <code>.\\username<\/code>.<\/p>\n<p>While the session is active, check Guacamole:<\/p>\n<pre><code class=\"language-bash\">cd \/opt\/guacamole\n\ndocker compose logs --since=10m guacd\ndocker compose logs --since=10m guacamole\n<\/code><\/pre>\n<p>Required results:<\/p>\n<ul>\n<li>no fatal connection error;<\/li>\n<li>the RDP connection reaches the selected endpoint;<\/li>\n<li>the server certificate is added to persistent FreeRDP trust on first use;<\/li>\n<li>keyboard and display updates work;<\/li>\n<li>closing the browser ends the expected Guacamole session.<\/li>\n<\/ul>\n<p>If <code>guacd<\/code> reports:<\/p>\n<pre><code class=\"language-text\">Security negotiation failed (wrong security type?)\n<\/code><\/pre>\n<p>before any authentication message, verify the FreeRDP trust-directory<br \/>\nownership from Part 1 before changing the selected RDP security mode. If the<br \/>\ndirectory is not writable by UID 1000, correct it and retry with NLA before<br \/>\nchanging Windows security policy.<\/p>\n<p>Check persistent certificate trust:<\/p>\n<pre><code class=\"language-bash\">docker compose exec -T guacd sh -c &#39;\nls -l \/home\/guacd\/.config\/freerdp\ntest -s \/home\/guacd\/.config\/freerdp\/known_hosts2 &amp;&amp;\n  echo &quot;RDP certificate trust is persistent&quot;\n&#39;\n<\/code><\/pre>\n<p>Expected:<\/p>\n<pre><code class=\"language-text\">known_hosts2 exists\nRDP certificate trust is persistent\n<\/code><\/pre>\n<p>The live tunnel will also be verified in the Nginx access log and Guacamole<br \/>\nconnection history before the connection is released to other users.<\/p>\n<p>At this point the WebSocket path deferred from Part 2 is exercised for the<br \/>\nfirst time. Loading the Guacamole dashboard alone proves ordinary HTTPS<br \/>\nauthentication; a working remote display proves that the long-lived tunnel,<br \/>\nNginx upgrade headers, <code>guacd<\/code>, endpoint firewall, RDP negotiation, and Windows<br \/>\nauthentication all work together.<\/p>\n<h2>7. Test Credential Behaviour with Another Guacamole User<\/h2>\n<p>Guacamole authorization and Windows authentication are separate:<\/p>\n<ul>\n<li>the Guacamole account controls whether the connection is visible and usable;<\/li>\n<li>Windows credentials authenticate the resulting RDP session;<\/li>\n<li>Windows credentials are not required from the user only when they are stored<br \/>\nin the connection, injected using tokens, or supplied by an external vault.<\/li>\n<\/ul>\n<p>Use two temporary connection variants to confirm the intended behaviour.<\/p>\n<h3>Shared-Credential Test<\/h3>\n<p>Clone the working connection:<\/p>\n<pre><code class=\"language-text\">RDP Pilot - Shared Credential Test\n<\/code><\/pre>\n<p>Retain the dedicated Windows username and password in this clone. Grant<br \/>\nconnection read access to a non-administrator Guacamole test user.<\/p>\n<p>Expected result:<\/p>\n<pre><code class=\"language-text\">the Guacamole test user can open the RDP session without entering Windows\ncredentials because the connection supplies the shared account\n<\/code><\/pre>\n<p>Delete this clone after the test if shared credentials are not the selected<br \/>\nproduction model.<\/p>\n<h3>Prompted-Credential Test<\/h3>\n<p>Clone the connection again:<\/p>\n<pre><code class=\"language-text\">RDP Pilot - Prompted Credential Test\n<\/code><\/pre>\n<p>Clear all three Authentication values:<\/p>\n<pre><code class=\"language-text\">Username\nPassword\nDomain\n<\/code><\/pre>\n<p>Keep NLA and Trust on First Use enabled. Grant read access to the same<br \/>\nnon-administrator Guacamole test user.<\/p>\n<p>Expected result:<\/p>\n<pre><code class=\"language-text\">Guacamole prompts for Windows credentials\nan authorized Windows account connects\nan invalid or unauthorized Windows account is rejected\n<\/code><\/pre>\n<p>This prompted model avoids storing a reusable Windows password in the<br \/>\nGuacamole connection and is the preferred model when different Guacamole users<br \/>\nshould authenticate to Windows individually.<\/p>\n<h2>Additional Information<\/h2>\n<h3>Windows Authentication Options<\/h3>\n<table>\n<thead>\n<tr>\n<th>Approach<\/th>\n<th>Guacamole compatibility<\/th>\n<th>Assessment<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Windows Hello PIN or biometric<\/td>\n<td>Not directly usable<\/td>\n<td>The PIN unlocks a device-bound Windows Hello key and is not a reusable NLA network credential<\/td>\n<\/tr>\n<tr>\n<td>Microsoft account app password<\/td>\n<td>Not documented for Windows RDP<\/td>\n<td>Avoid; it is a long-lived compatibility credential and does not provide an interactive MFA challenge<\/td>\n<\/tr>\n<tr>\n<td>Microsoft account password<\/td>\n<td>Usable by RDP<\/td>\n<td>Works as a conventional password but does not trigger Microsoft Authenticator during the NLA exchange<\/td>\n<\/tr>\n<tr>\n<td>Dedicated local Windows account and password<\/td>\n<td>Supported and predictable<\/td>\n<td>Recommended for a small self-hosted environment<\/td>\n<\/tr>\n<tr>\n<td>Guacamole password, TOTP, then Windows password<\/td>\n<td>Supported<\/td>\n<td>Provides two independent authentication boundaries<\/td>\n<\/tr>\n<tr>\n<td>Duo for Windows Logon and RDP<\/td>\n<td>Supported by Duo, subject to local testing<\/td>\n<td>Adds a second factor at Windows logon as well as any MFA protecting Guacamole<\/td>\n<\/tr>\n<tr>\n<td>Windows Hello for Business certificate<\/td>\n<td>Microsoft-supported for native RDP<\/td>\n<td>Requires managed certificate and identity infrastructure and is not equivalent to forwarding a browser user&#39;s PIN through Guacamole<\/td>\n<\/tr>\n<tr>\n<td>Microsoft Entra OpenID Connect for Guacamole<\/td>\n<td>Supported for Guacamole authentication<\/td>\n<td>Can replace or strengthen the Guacamole login, but does not automatically replace the downstream Windows RDP credential<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Recommended Windows User Profile<\/h3>\n<p>For a dedicated Guacamole RDP account:<\/p>\n<ul>\n<li>create a local account, not an administrator or everyday desktop identity;<\/li>\n<li>use a unique password stored in a password manager;<\/li>\n<li>grant membership only in <code>Remote Desktop Users<\/code>;<\/li>\n<li>keep NLA required;<\/li>\n<li>do not associate the profile with email, cloud storage, browser sync, or<br \/>\nother personal services;<\/li>\n<li>keep the profile free of personal data and unnecessary applications;<\/li>\n<li>disable drive, printer, microphone, and file-transfer redirection unless a<br \/>\nspecific workflow requires them;<\/li>\n<li>apply normal Windows patching, lockout, password-length, and audit policies;<\/li>\n<li>disable the account when access is no longer required;<\/li>\n<li>rotate the password immediately if it has been exposed or shared beyond the<br \/>\nintended administrators.<\/li>\n<\/ul>\n<p>For multiple people, prefer an individual Windows account for each person and<br \/>\nleave the Guacamole connection credentials blank so each user is prompted.<br \/>\nOne stored shared account is simpler but gives all Guacamole users the same<br \/>\nWindows identity and audit trail.<\/p>\n<h2>Outcome<\/h2>\n<p>The completed design now has three separate controls:<\/p>\n<ol>\n<li>Nginx is the only system allowed to reach the Guacamole web listener.<\/li>\n<li>The controlled <code>guacd<\/code> network can reach only approved target addresses and<br \/>\nports.<\/li>\n<li>Each Windows target still requires an authorised, non-administrator Windows<br \/>\naccount through NLA.<\/li>\n<\/ol>\n<p>The browser session, public WebSocket tunnel, persistent certificate trust,<br \/>\ntarget allowlist, and prompted Windows login have all been validated. Guacamole<br \/>\nis suitable here as an administrative remote-access tool, not as a<br \/>\nhigh-frame-rate game or video-streaming platform.<\/p>\n<h2>References<\/h2>\n<ul>\n<li><a href=\"https:\/\/guacamole.apache.org\/doc\/gug\/configuring-guacamole.html\">Apache Guacamole: Configuring Guacamole<\/a><\/li>\n<li><a href=\"https:\/\/guacamole.apache.org\/doc\/gug\/guacamole-docker.html\">Apache Guacamole: Docker deployment and persistent RDP trust<\/a><\/li>\n<li><a href=\"https:\/\/guacamole.apache.org\/doc\/gug\/duo-auth.html\">Apache Guacamole: Duo multi-factor authentication<\/a><\/li>\n<li><a href=\"https:\/\/guacamole.apache.org\/doc\/gug\/openid-auth.html\">Apache Guacamole: OpenID Connect authentication<\/a><\/li>\n<li><a href=\"https:\/\/docs.docker.com\/engine\/network\/firewall-iptables\/\">Docker: Docker with iptables<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/windows-server\/remote\/remote-desktop-services\/remotepc\/remote-desktop-allow-access\">Microsoft: Enable Remote Desktop<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/security\/identity-protection\/hello-for-business\/rdp-sign-in\">Microsoft: Remote Desktop sign-in with Windows Hello for Business<\/a><\/li>\n<li><a href=\"https:\/\/support.microsoft.com\/en-us\/accounts-billing\/manage\/how-to-get-and-use-app-passwords\">Microsoft: App passwords<\/a><\/li>\n<li><a href=\"https:\/\/duo.com\/docs\/rdp\">Duo Authentication for Windows Logon and RDP<\/a><\/li>\n<\/ul>\n<h2>Recap: Adding Another Windows Machine<\/h2>\n<p>Use this sequence for each additional Windows connection:<\/p>\n<ol>\n<li>\n<p>Reserve the VM address and confirm native RDP works.<\/p>\n<\/li>\n<li>\n<p>Keep Windows NLA enabled.<\/p>\n<\/li>\n<li>\n<p>Create a dedicated local non-administrator account through <code>lusrmgr.msc<\/code>.<\/p>\n<\/li>\n<li>\n<p>Add the account only to <code>Remote Desktop Users<\/code>.<\/p>\n<\/li>\n<li>\n<p>Add the VM address and TCP 3389 to <code>GUAC-TARGETS<\/code> or the equivalent owned<br \/>\nfirewall chain before its final drop rule.<\/p>\n<\/li>\n<li>\n<p>Restart the firewall service and test:<\/p>\n<pre><code class=\"language-text\">guacd -&gt; approved VM:3389 succeeds\nguacd -&gt; unapproved destination fails\n<\/code><\/pre>\n<\/li>\n<li>\n<p>Confirm the persistent FreeRDP directory remains owned by the <code>guacd<\/code> user,<br \/>\nhas mode <code>0700<\/code>, and is writable.<\/p>\n<\/li>\n<li>\n<p>Create the RDP connection using:<\/p>\n<pre><code class=\"language-text\">Security mode: NLA\nIgnore certificate: disabled\nTrust host certificate on first use: enabled\nDrive, printing, microphone and SFTP: disabled unless required\n<\/code><\/pre>\n<\/li>\n<li>\n<p>Leave Username, Password, and Domain blank when each user should enter<br \/>\nindividual Windows credentials.<\/p>\n<\/li>\n<li>\n<p>Grant only READ permission to the intended Guacamole users or groups.<\/p>\n<\/li>\n<li>\n<p>Test through public HTTPS and review Guacamole connection history.<\/p>\n<\/li>\n<li>\n<p>Remove temporary clones and any profile that stores unnecessary Windows<br \/>\ncredentials.<\/p>\n<\/li>\n<\/ol>\n<p>The next connection guide will cover Linux VMs. It will distinguish browser<br \/>\nSSH administration from graphical access and will cover SSH key storage,<br \/>\nhost-key pinning, SFTP restrictions, least-privilege Linux users, and the<br \/>\nadditional controls required for VNC or RDP-based Linux desktops.<\/p>\n<p><strong>Series complete:<\/strong> <a href=\"\/index.php\/apache-guacamole-part-1-building-the-remote-access-container\/\">return to Part 1<\/a><br \/>\nor <a href=\"\/index.php\/apache-guacamole-part-2-publishing-the-gateway-through-nginx\/\">review Part 2<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Connect a private Windows VM through a controlled guacd network, endpoint firewall allowlist, NLA, and a dedicated non-administrator RDP account.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"saved_in_kubio":false,"footnotes":""},"categories":[12,11],"tags":[],"class_list":["post-197","post","type-post","status-publish","format-standard","hentry","category-containers","category-virtualisation"],"_links":{"self":[{"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/posts\/197","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/comments?post=197"}],"version-history":[{"count":0,"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/posts\/197\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/media?parent=197"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/categories?post=197"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.miniamju.com\/index.php\/wp-json\/wp\/v2\/tags?post=197"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}