{"id":1894,"date":"2026-10-31T08:13:10","date_gmt":"2026-10-31T11:13:10","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1894"},"modified":"2026-10-31T08:13:10","modified_gmt":"2026-10-31T11:13:10","slug":"goguacamole-rdp-ssh-e-vnc-no-navegador-com-um-binario-go","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/en\/2026\/10\/goguacamole-rdp-ssh-e-vnc-no-navegador-com-um-binario-go\/","title":{"rendered":"GoGuacamole: RDP, SSH, and VNC in the browser with a Go binary"},"content":{"rendered":"<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/10\/goguacamole-rdp-ssh-vnc-navegador-go.webp\" alt=\"Mascote LinuxPro tocando um tel\u00e3o com sess\u00f5es SSH, RDP e VNC no navegador, com o logo do Go e o cachorro caramelo ao lado\" \/><\/p>\n<p>Anyone who has deployed <a href=\"\/en\/2026\/09\/apache-guacamole-acesso-remoto-com-docker-ldap-e-sso\/\">Apache Guacamole<\/a> knows the package: Tomcat, the <code data-no-translation=\"\">guacamole-client<\/code> in Java, a JDBC extension for the database, another for LDAP, another for TOTP, another for recordings, each with its own configuration file. <strong>GoGuacamole<\/strong> is my new project to replace that part with <strong>a single Go binary<\/strong>, with the Vue interface 3 built in, keeping the same <code data-no-translation=\"\">guacd<\/code> from Apache under the hood. You keep accessing servers via <strong>RDP, SSH, and VNC through the browser<\/strong>, but gain session recording, auditing, SSO, and MFA configured directly in the interface, and can import the Guacamole database you already have.<\/p>\n<p>This post covers version <strong>0.6.2<\/strong> (30 October 2026): what the project does, how it fits into guacd, how to bring it up in one minute with a single image, how to put it behind nginx, how to record and audit sessions, and how to migrate from the Java client. The code is at <a href=\"https:\/\/github.com\/jniltinho\/go-guacamole\">github.com\/jniltinho\/go-guacamole<\/a>, under the MIT license.<\/p>\n<h2>What Is GoGuacamole<\/h2>\n<p>Apache Guacamole has two halves. The <strong>guacd<\/strong> daemon is the C daemon that speaks RDP, SSH, and VNC with the servers and translates everything into the Guacamole protocol. The <strong>guacamole-client<\/strong> is the Java web application that authenticates the user, stores connections and permissions, and delivers the screen to the browser. GoGuacamole rewrites only the second half:<\/p>\n<ul>\n<li><strong>Go backend<\/strong> with Echo v5 and Cobra: REST API in <code data-no-translation=\"\">\/api\/v1<\/code>, WebSocket tunnel compatible with the <code data-no-translation=\"\">guacamole-common-js<\/code> and the Guacamole protocol implemented directly, including the handshake with guacd (over TCP or TLS).<\/li>\n<li><strong>Vue frontend 3<\/strong> (Vite and Tailwind v4) embedded in the binary using <code data-no-translation=\"\">embed<\/code>, the same technique as the <a href=\"\/en\/2026\/09\/vue-go-echo-v5-parte-5-embed-binario-unico-e-docker\/\">5 part of the Vue.js + Go with Echo v5 series<\/a>.<\/li>\n<li><strong>Database<\/strong>: SQLite by default, MariaDB\/MySQL or PostgreSQL as it grows, with embedded migrations that run at startup.<\/li>\n<li><strong>official guacd<\/strong>: the image <code data-no-translation=\"\">guacamole\/guacd<\/code> 1.6 continues doing the heavy lifting for the protocols.<\/li>\n<\/ul>\n<p>Users, groups, connection folders, inherited permissions, history, recording, auditing, external login, and MFA come ready to use, without separate extensions. The interface has 16 themes and is available in English and Brazilian Portuguese.<\/p>\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/10\/goguacamole-painel.webp\" alt=\"Painel do GoGuacamole em portugu\u00eas com conex\u00f5es SSH, RDP e VNC, atividade recente e acessos aos servidores\" \/><\/p>\n<h2>Architecture: Who Talks to Whom<\/h2>\n<p>The browser only communicates with goguacamole over HTTPS and WebSocket. goguacamole speaks the Guacamole protocol with guacd, which runs on a private network, and guacd opens the RDP, SSH, or VNC connection to the destination server. The connection's host, username, and password remain on the server, encrypted with AES-256-GCM; the browser never sees them.<\/p>\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/10\/goguacamole-arquitetura.webp\" alt=\"Diagrama da arquitetura do GoGuacamole: navegador, nginx, goguacamole, guacd e servidores RDP, SSH e VNC, com identidade, banco, Redis, grava\u00e7\u00f5es e object storage\" \/><\/p>\n<p>Opening a session follows this path:<\/p>\n<ol>\n<li>The browser requests <code data-no-translation=\"\">POST \/api\/v1\/sessions<\/code> with the connection ID. The server checks the session, the user's restrictions (account validity, access window), and the effective read permission.<\/li>\n<li>The response is a <strong>single-use ticket, valid for 30 seconds<\/strong>, that reserves the slot in case the connection has a session limit.<\/li>\n<li>The browser opens the WebSocket <code data-no-translation=\"\">\/api\/v1\/tunnel?ticket=\u2026<\/code> sending only screen parameters (width, height, DPI, time zone). The server validates the origin, redeems the ticket, and authorizes again.<\/li>\n<li>goguacamole performs the handshake with guacd using the stored parameters, enables history input (created as pending when the ticket was issued), and begins forwarding instructions in both directions.<\/li>\n<\/ol>\n<p>If the permission is revoked midway, the tunnel is closed immediately. Changes made by another process (the CLI or the importer) are detected within 5 seconds.<\/p>\n<h2>Bringing It Up in One Minute: The All-in-One Image<\/h2>\n<p>As of version 0.6.0, each release publishes to GHCR an image that combines guacd, goguacamole, and Redis in a single container, with SQLite on the volume <code data-no-translation=\"\">\/data<\/code>:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">docker run -d --name goguacamole -p 8080:8080 \\\n  -v goguacamole-data:\/data ghcr.io\/jniltinho\/go-guacamole:allinone\n\n# a senha gerada do admin aparece uma \u00fanica vez no log\ndocker logs goguacamole 2>&1 | grep 'bootstrap administrator'<\/code><\/pre>\n<p>To pin the version, use the tag <code data-no-translation=\"\">0.6.2-allinone<\/code> (or <code data-no-translation=\"\">0.6-allinone<\/code>). Open <code data-no-translation=\"\">http:\/\/localhost:8080<\/code>, enter with <code data-no-translation=\"\">admin<\/code> with the login password and the system prompts for a new password on first access. Readiness can be checked via the API:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">curl -s http:\/\/localhost:8080\/ready\n# {\"db\":\"up\",\"guacd\":\"up\",\"queue\":\"up\"}<\/code><\/pre>\n<p>Inside the container, supervisord keeps the three processes running as the user <code data-no-translation=\"\">guacd<\/code> (uid 1000), owner of the files in <code data-no-translation=\"\">\/data<\/code>. Therefore, CLI commands run as <code data-no-translation=\"\">-u guacd<\/code>:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">docker exec goguacamole supervisorctl status            # guacd, redis, goguacamole\ndocker exec -u guacd goguacamole goguacamole user passwd admin\ndocker exec goguacamole goguacamole version<\/code><\/pre>\n<p>This HTTP mode is for local evaluation. For production, see the checklist further below.<\/p>\n<h2>Testing with a Lab SSH Server<\/h2>\n<p>To see a real session without touching any server, put GoGuacamole and a disposable OpenSSH on the same Docker network. The guacd from the all-in-one image resolves the target by container name. If the container from the previous section is still running, remove it first (<code data-no-translation=\"\">docker rm -f goguacamole<\/code>), because the lab uses the same name and the same port:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">docker network create guac-lab\ndocker run -d --name goguacamole --network guac-lab -p 8080:8080 \\\n  -v goguacamole-data:\/data ghcr.io\/jniltinho\/go-guacamole:allinone\ndocker run -d --name ssh-alvo --network guac-lab \\\n  -e USER_NAME=demo -e USER_PASSWORD='troque-esta-senha' -e PASSWORD_ACCESS=true \\\n  lscr.io\/linuxserver\/openssh-server:latest<\/code><\/pre>\n<p>In the interface, go to <strong>Connections \u2192 New connection<\/strong>, choose SSH, host <code data-no-translation=\"\">ssh-alvo<\/code>, port <code data-no-translation=\"\">2222<\/code> (the image's port), user <code data-no-translation=\"\">demo<\/code> and the password. Before saving, use <strong>Test connection<\/strong>: GoGuacamole opens a real connection through guacd using the form values and reports whether the host responded and whether the login worked. In my test, the correct password returned as success in about 200 ms, and the wrong one returned as an authentication failure, without saving anything. Then just save and click <strong>Connect<\/strong>: the terminal opens in a browser tab.<\/p>\n<p>When finished, remove the lab:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">docker rm -f goguacamole ssh-alvo\ndocker volume rm goguacamole-data\ndocker network rm guac-lab<\/code><\/pre>\n<h2>Docker Compose with Separate Services<\/h2>\n<p>For a long-lasting environment, the repository provides a <code data-no-translation=\"\">docker-compose.yml<\/code> with goguacamole, guacd, and Redis in separate containers, and profiles for MariaDB, PostgreSQL, and test servers:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">git clone https:\/\/github.com\/jniltinho\/go-guacamole.git\ncd go-guacamole\ncp .env.example .env\n# no .env, defina APP_ADMIN_PASSWORD (8+ caracteres); a chave pode ser gerada assim:\nprintf 'APP_SECRET_KEY=%s\\n' \"$(openssl rand -hex 32)\" &gt;&gt; .env\nmake up            # app + guacd + Redis, SQLite \u2192 http:\/\/localhost:8080<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Command<\/th>\n<th>What it brings up<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code data-no-translation=\"\">make up<\/code><\/td>\n<td>app, guacd, and Redis with SQLite<\/td>\n<\/tr>\n<tr>\n<td><code data-no-translation=\"\">make up-mariadb<\/code><\/td>\n<td>the same, plus MariaDB 11.4<\/td>\n<\/tr>\n<tr>\n<td><code data-no-translation=\"\">make up-postgres<\/code><\/td>\n<td>the same, plus PostgreSQL 17<\/td>\n<\/tr>\n<tr>\n<td><code data-no-translation=\"\">make up-lab<\/code><\/td>\n<td>SSH, VNC, and RDP test targets<\/td>\n<\/tr>\n<tr>\n<td><code data-no-translation=\"\">make certs up-tls<\/code><\/td>\n<td>native HTTPS with <code data-no-translation=\"\">https:\/\/localhost:8443<\/code> a local CA<\/td>\n<\/tr>\n<tr>\n<td><code data-no-translation=\"\">make down<\/code><\/td>\n<td>takes everything down<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>A <code data-no-translation=\"\">APP_SECRET_KEY<\/code> derives the key that encrypts the connection passwords. <strong>Keep it together with the database backup<\/strong>: without it, the stored credentials cannot be recovered. If you do not set the key, GoGuacamole generates a <code data-no-translation=\"\">secret.key<\/code> on the first start, and that file becomes equally critical.<\/p>\n<p>You can also run just the binary (Linux\/amd64, on <a href=\"https:\/\/github.com\/jniltinho\/go-guacamole\/releases\">releases<\/a>) pointing to a guacd:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">docker run -d -p 127.0.0.1:4822:4822 guacamole\/guacd:1.6.0\nAPP_ADMIN_PASSWORD='troque-agora' GUACD_ADDR=127.0.0.1:4822 .\/goguacamole serve<\/code><\/pre>\n<h2>Behind nginx with HTTPS<\/h2>\n<p>The session tunnel is a WebSocket that may remain open for hours, so the proxy needs HTTP\/1.1, upgrade headers, and long timeouts. The repository provides a complete example in <code data-no-translation=\"\">docker\/nginx\/goguacamole.conf<\/code>; the essential part is this:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-nginx\" data-no-translation=\"\">map $http_upgrade $connection_upgrade {\n    default upgrade;\n    ''      '';\n}\n\nserver {\n    listen 443 ssl;\n    http2 on;\n    server_name guac.example.com;\n    # ssl_certificate \/ ssl_certificate_key ...\n\n    location \/ {\n        proxy_pass http:\/\/127.0.0.1:8080;\n        proxy_http_version 1.1;\n        proxy_set_header Host              $host;\n        proxy_set_header X-Real-IP         $remote_addr;\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header Upgrade           $http_upgrade;\n        proxy_set_header Connection        $connection_upgrade;\n    }\n\n    # o t\u00fanel transmite a tela: sem buffer e com timeouts longos\n    location = \/api\/v1\/tunnel {\n        proxy_pass http:\/\/127.0.0.1:8080;\n        proxy_http_version 1.1;\n        proxy_set_header Host              $host;\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header Upgrade           $http_upgrade;\n        proxy_set_header Connection        $connection_upgrade;\n        proxy_buffering    off;\n        proxy_read_timeout 12h;\n        proxy_send_timeout 12h;\n    }\n}<\/code><\/pre>\n<p>On the GoGuacamole side, three variables complete the pair:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">APP_BASE_URL=https:\/\/guac.example.com   # o host aceito como Origin do WebSocket\nAPP_COOKIE_SECURE=true                  # TLS termina no proxy\nAPP_TRUSTED_PROXIES=127.0.0.1           # s\u00f3 esse IP pode informar X-Forwarded-For<\/code><\/pre>\n<p>With <code data-no-translation=\"\">APP_TRUSTED_PROXIES<\/code> sure, the log, the audit, and the history record the client IP, not nginx's, and a <code data-no-translation=\"\">X-Forwarded-For<\/code> forged one coming from elsewhere is ignored. If you prefer the <a href=\"\/en\/2026\/10\/caddy-no-linux-servidor-web-com-https-automatico\/\">Caddy<\/a>, the <code data-no-translation=\"\">reverse_proxy<\/code> it already handles WebSocket with no extra configuration.<\/p>\n<h2>Session Recording and Auditing<\/h2>\n<p>Here is much of the reason for the project. guacd stores the session in a shared volume and GoGuacamole handles the rest:<\/p>\n<ul>\n<li><strong>Screen video<\/strong> from RDP, SSH, and VNC, played right in the browser.<\/li>\n<li><strong>SSH transcription<\/strong> in text, with every command and its output, ready to read or search.<\/li>\n<li><strong>Typed keys<\/strong>, optional and disabled by default.<\/li>\n<li><strong>Per-connection policy<\/strong>: video, transcript, and keystrokes can inherit the default, be forced, or disabled; video and transcript have separate retention periods (keystrokes are part of the video).<\/li>\n<li><strong>Fail-closed<\/strong>: if the required recording is unavailable, the session does not open (there are temporary, audited exceptions).<\/li>\n<li><strong>Retention and legal hold<\/strong>: cleanup follows the retention history, and a recording under legal hold is not deleted.<\/li>\n<\/ul>\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/10\/goguacamole-player-gravacao.webp\" alt=\"Player de grava\u00e7\u00e3o do GoGuacamole reproduzindo uma sess\u00e3o SSH no navegador\" \/><\/p>\n<p>The initial defaults come from environment variables (after an administrator saves under <strong>Administration \u2192 Recording<\/strong>, what is in the database takes precedence):<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">RECORDING_ENABLED=true          # padr\u00e3o false; true no compose\nRECORDING_SCREEN=true           # v\u00eddeo de todos os protocolos\nRECORDING_TYPESCRIPT=true       # transcri\u00e7\u00e3o de SSH\nRECORDING_INCLUDE_KEYS=false    # teclas no v\u00eddeo\nRECORDING_RETENTION_DAYS=90<\/code><\/pre>\n<p>Recordings can also be sent to <strong>object storage<\/strong>: AWS S3, S3-compatible solutions (MinIO, Ceph, R2, Wasabi), Oracle Cloud, and Google Cloud Storage via rclone. Each upload is verified by SHA-256 by reading the object back; the local copy can be removed, and the player streams directly from the bucket. The button <strong>Test access<\/strong> writes, reads, and deletes a test object and reports whether the bucket has versioning and object lock. These tasks run in a queue in Redis with <a href=\"\/en\/2026\/09\/asynq-filas-de-tarefas-em-go-com-redis-e-asynqmon\/\">asynq<\/a>, and the asynqmon panel is available at <code data-no-translation=\"\">\/asynqmon<\/code>, only to administrators.<\/p>\n<p>For security, the connection to the object storage goes through an internal proxy that rejects private addresses, loopback, link-local, and cloud metadata addresses. A lab MinIO instance on the local network must be explicitly allowed:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">OBJSTORE_ALLOWED_ENDPOINTS=minio\nOBJSTORE_ALLOW_HTTP=true<\/code><\/pre>\n<p>The audit log records logins, sessions, and administrative changes made through the interface and the API. The panel also has a server access report: who accessed which server, when, and for how long.<\/p>\n<h2>Login: LDAP, OIDC, SAML, and MFA<\/h2>\n<p>In the Java client, each login method is an extension with its own file. In GoGuacamole, everything is under <strong>Administration \u2192 Authentication<\/strong>, with secrets stored encrypted in the database and a test button for each method:<\/p>\n<ul>\n<li><strong>LDAP \/ Active Directory<\/strong>, with directory groups mapped to local groups by name. If you manage AD or Samba 4, the <a href=\"\/en\/2026\/10\/ad-manager-active-directory-e-samba-4-pelo-navegador-desktop-e-cli\/\">AD Manager<\/a> pairs with it.<\/li>\n<li><strong>OpenID Connect<\/strong> (authorization code with PKCE) and <strong>SAML 2.0<\/strong>.<\/li>\n<li><strong>HTTP header<\/strong> coming from trusted proxies and <strong>encrypted JSON<\/strong> compatible with the <code data-no-translation=\"\">guacamole-auth-json<\/code>.<\/li>\n<li><strong>TOTP<\/strong> with QR code and protection against code reuse, and <strong>Duo Universal Prompt<\/strong>, as the second factor for password logins.<\/li>\n<\/ul>\n<p>External users are created on first login. One important detail: Guacamole's second factor applies to password login. For SSO login, enforce MFA in your identity provider.<\/p>\n<h2>Migrating from Apache Guacamole<\/h2>\n<p>The importer reads the Guacamole database (MySQL or PostgreSQL) in a read-only transaction, using only <code data-no-translation=\"\">SELECT<\/code>. Always start with <code data-no-translation=\"\">--dry-run<\/code>:<\/p>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\"># n\u00e3o grava nada importado; a origem nunca \u00e9 alterada\ngoguacamole import guacamole --source-driver mysql \\\n  --dsn 'guacamole_user:senha@tcp(db-antigo:3306)\/guacamole_db' --dry-run\n\n# importa\u00e7\u00e3o de verdade (reexecutar \u00e9 idempotente; conflitos s\u00e3o ignorados por padr\u00e3o)\ngoguacamole import guacamole --source-driver mysql \\\n  --dsn 'guacamole_user:senha@tcp(db-antigo:3306)\/guacamole_db'<\/code><\/pre>\n<p>One detail: even when <code data-no-translation=\"\">--dry-run<\/code>, the command opens GoGuacamole\u2019s target database and applies any pending schema migrations, as does the <code data-no-translation=\"\">serve<\/code>. The imported data is not written, however.<\/p>\n<p>Users come over (with the hash and salt, so <strong>the passwords continue to work<\/strong> and are migrated to bcrypt on the next login), validity and access window, groups and members, folders, connections with parameters (re-encrypted secrets), limits, the system permissions that have an equivalent (<code data-no-translation=\"\">ADMINISTER<\/code>, <code data-no-translation=\"\">CREATE_CONNECTION<\/code>, <code data-no-translation=\"\">CREATE_USER<\/code> and <code data-no-translation=\"\">AUDIT<\/code>) and the connection read permissions. Nested groups are flattened. The following are left out and appear in the report: sharing profiles and permissions other than read permissions. Load-balancing groups become regular folders.<\/p>\n<p>At the end, the importer compares, user by user, the connections and privileges each user had in the source and has in the destination, and prints lines <code data-no-translation=\"\">ACCESS DIFF<\/code>. These differences are <strong>reported, not blocked<\/strong>: for example, if an account with the same name and with <code data-no-translation=\"\">ADMINISTER<\/code>, the report shows the privilege gain. The flag <code data-no-translation=\"\">--accept-expansion<\/code> only determines whether load-balancing group reads become reads on each child connection. The sensible approach is to run the dry run on a copy of the production database, review each <code data-no-translation=\"\">ACCESS DIFF<\/code> and pilot it with a few users side by side with the Java client, on another port, before switching the proxy.<\/p>\n<h2>The CLI<\/h2>\n<pre data-no-translation=\"\"><code class=\"language-bash\" data-no-translation=\"\">goguacamole serve                       # servidor HTTP(S)\ngoguacamole migrate up|down|status      # migra\u00e7\u00f5es do banco\ngoguacamole user create|passwd|list|disable\ngoguacamole import guacamole            # importa\u00e7\u00e3o do Apache Guacamole\ngoguacamole config print                # configura\u00e7\u00e3o efetiva, segredos ocultos\ngoguacamole healthcheck                 # usado pelo HEALTHCHECK do Docker\ngoguacamole version<\/code><\/pre>\n<p>The configuration comes from <code data-no-translation=\"\">config.toml<\/code> (there is a <code data-no-translation=\"\">config.toml.example<\/code> in the repository) and environment variables with prefixes such as <code data-no-translation=\"\">APP_<\/code>, <code data-no-translation=\"\">DB_<\/code>, <code data-no-translation=\"\">GUACD_<\/code>, <code data-no-translation=\"\">RECORDING_<\/code>, <code data-no-translation=\"\">REDIS_<\/code> and <code data-no-translation=\"\">OBJSTORE_<\/code>.<\/p>\n<h2>Java client vs. GoGuacamole<\/h2>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>Apache Guacamole client<\/th>\n<th>GoGuacamole<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Execution<\/td>\n<td>Java, Tomcat and extensions<\/td>\n<td>a Go binary with the embedded interface<\/td>\n<\/tr>\n<tr>\n<td>Database<\/td>\n<td>MySQL, PostgreSQL or SQL Server via JDBC extension<\/td>\n<td>embedded SQLite, MariaDB\/MySQL or PostgreSQL<\/td>\n<\/tr>\n<tr>\n<td>SSO and MFA<\/td>\n<td>separate extensions, configured in files<\/td>\n<td>embedded, configured, and tested through the interface<\/td>\n<\/tr>\n<tr>\n<td>Recording<\/td>\n<td>recording to disk; player via extension<\/td>\n<td>embedded player, SSH transcripts, retention, legal hold, object storage<\/td>\n<\/tr>\n<tr>\n<td>Update<\/td>\n<td>redeploy the WAR and extensions<\/td>\n<td>replace the binary or image; migrations run at startup<\/td>\n<\/tr>\n<tr>\n<td>guacd<\/td>\n<td><code data-no-translation=\"\">guacamole\/guacd<\/code><\/td>\n<td>the same <code data-no-translation=\"\">guacamole\/guacd<\/code> (1.6)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Production checklist<\/h2>\n<ul>\n<li><code data-no-translation=\"\">APP_ENV=production<\/code>: the service only starts with <code data-no-translation=\"\">TLS_ENABLED=true<\/code> or <code data-no-translation=\"\">APP_COOKIE_SECURE=true<\/code>, with a defined key (<code data-no-translation=\"\">APP_SECRET_KEY<\/code> or <code data-no-translation=\"\">APP_SECRET_KEY_FILE<\/code>), and refuses <code data-no-translation=\"\">TLS_SELF_SIGNED=true<\/code> and <code data-no-translation=\"\">GUACD_TLS_INSECURE=true<\/code>. It does not verify whether the proxy actually has TLS: that is up to you.<\/li>\n<li><code data-no-translation=\"\">APP_SECRET_KEY<\/code> in a secrets vault, with a backup alongside the database. Test the restore on a clean host.<\/li>\n<li>HTTPS: <code data-no-translation=\"\">TLS_ENABLED=true<\/code> in the binary itself, or with TLS terminated on the proxy, <code data-no-translation=\"\">APP_COOKIE_SECURE=true<\/code> and <code data-no-translation=\"\">APP_TRUSTED_PROXIES<\/code>, keeping the proxy \u2192 goguacamole section in <code data-no-translation=\"\">127.0.0.1<\/code> or on a private network.<\/li>\n<li><code data-no-translation=\"\">APP_BASE_URL<\/code> with the public URL.<\/li>\n<li>guacd on an isolated network, never exposed; consider <code data-no-translation=\"\">GUACD_TLS=true<\/code>.<\/li>\n<li>MFA (TOTP or Duo) for password logins; for SSO, MFA at the provider.<\/li>\n<li>Redis on a private network (or with <code data-no-translation=\"\">REDIS_PASSWORD<\/code> and <code data-no-translation=\"\">REDIS_TLS<\/code>), one database per installation.<\/li>\n<li>Object storage with a bucket or dedicated prefix and credentials limited to put\/get\/list\/delete on it.<\/li>\n<\/ul>\n<h2>Current limitations<\/h2>\n<p>The project is new, is in the 0.x series, and it is worth knowing what it does not yet have:<\/p>\n<ul>\n<li><strong>A replica for the tunnels<\/strong>: the tunnel registry, limits, and session closures are kept in the process's memory. Scale vertically. Background tasks and uploads are already safe with multiple instances.<\/li>\n<li><strong>No session sharing<\/strong> (joining an active session of another user) and no load-balancing groups.<\/li>\n<li><strong>Flat user groups<\/strong>: nested groups do not exist yet.<\/li>\n<li><strong>Binaries and image for Linux\/amd64<\/strong>; other architectures require compiling.<\/li>\n<li>Telnet and Kubernetes, which guacd supports, are not yet in the forms.<\/li>\n<\/ul>\n<p>The <a href=\"https:\/\/github.com\/jniltinho\/go-guacamole\/blob\/main\/ROADMAP.md\">ROADMAP<\/a> list those items and others, such as Prometheus metrics, .deb\/.rpm packages, and Helm charts.<\/p>\n<h2>Conclusion<\/h2>\n<p>guacd remains the right piece for handling RDP, SSH, and VNC. What GoGuacamole changes is everything else: a binary, a container to get started, external login and MFA without extensions, recording with a player and transcription, and an importer that brings over the Guacamole database and reports every access difference. If you already use Guacamole, run the importer with <code data-no-translation=\"\">--dry-run<\/code> on a copy of the database and compare. If you are just starting, the <code data-no-translation=\"\">docker run<\/code> from the all-in-one image is enough to test.<\/p>\n<p>Issues and pull requests are welcome on the <a href=\"https:\/\/github.com\/jniltinho\/go-guacamole\">GoGuacamole repository<\/a>. Other Go projects here on the blog follow the same single-binary idea: <a href=\"\/en\/2026\/09\/go-postfixadmin\/\">go-postfixadmin<\/a>, <a href=\"\/en\/2026\/09\/go-imapsync\/\">go-imapsync<\/a> and <a href=\"\/en\/2026\/09\/mcp-wp-go-wordpress-por-mcp-sem-ssh\/\">mcp-wp-go<\/a>. And, for direct SSH access without a gateway, see <a href=\"\/en\/2017\/04\/ssh-sem-senha\/\">SSH authentication keys<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>GoGuacamole replaces the Apache Guacamole Java client with a Go binary featuring a Vue interface 3: RDP, SSH, and VNC in the browser, session recording, auditing, SSO, MFA, and Guacamole database import.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[46,2,111,120],"tags":[557,156,58,556,555,546,203,506,547],"class_list":["post-1894","post","type-post","status-publish","format-standard","hentry","category-devops","category-linux","category-opensource","category-servidores","tag-acesso-remoto","tag-docker","tag-go","tag-goguacamole","tag-guacamole","tag-rdp","tag-self-hosted","tag-ssh","tag-vnc"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1894","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/comments?post=1894"}],"version-history":[{"count":1,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1894\/revisions"}],"predecessor-version":[{"id":1895,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1894\/revisions\/1895"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/media?parent=1894"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/categories?post=1894"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/tags?post=1894"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}