GoGuacamole: RDP, SSH y VNC en el navegador con un binario Go

Mascote LinuxPro tocando um telão com sessões SSH, RDP e VNC no navegador, com o logo do Go e o cachorro caramelo ao lado

Quien ya haya subido el Apache Guacamole conoce el paquete: Tomcat, el guacamole-client en Java, una extensión JDBC para la base de datos, otra para LDAP, otra para TOTP, otra para grabaciones, cada una con su archivo de configuración. El GoGuacamole es mi nuevo proyecto para sustituir esa parte por un único binario Go, con la interfaz en Vue 3 integrada, manteniendo el mismo guacd de Apache por debajo. Sigues accediendo a servidores mediante RDP, SSH y VNC desde el navegador, pero obtienes grabación de sesión, auditoría, SSO y MFA configurados en la propia interfaz, además de poder importar la base de datos de Guacamole que ya tienes.

Esta entrada cubren la versión 0.6.2 (30 de octubre de 2026): qué hace el proyecto, cómo encaja en guacd, cómo levantarlo en un minuto con una sola imagen, cómo colocarlo detrás de nginx, cómo grabar y auditar sesiones y cómo migrar desde el cliente Java. El código está en github.com/jniltinho/go-guacamole, bajo licencia MIT.

Qué es GoGuacamole

Apache Guacamole tiene dos mitades. El guacd es el demonio en C que habla RDP, SSH y VNC con los servidores y lo traduce todo al protocolo Guacamole. El guacamole-client es la aplicación web en Java que autentica al usuario, guarda las conexiones y los permisos y entrega la pantalla al navegador. GoGuacamole reescribe solo la segunda mitad:

  • Backend en Go con Echo v5 y Cobra: API REST con /api/v1, túnel WebSocket compatible con el guacamole-common-js y el protocolo Guacamole implementado directamente, incluido el handshake con guacd (por TCP o TLS).
  • Frontend en Vue 3 (Vite y Tailwind v4) integrado en el binario con embed, la misma técnica que la parte 5 de la serie Vue.js + Go con Echo v5.
  • Base de datos: SQLite por defecto, MariaDB/MySQL o PostgreSQL cuando crezca, con migraciones integradas que se ejecutan al iniciar.
  • guacd oficial: la imagen guacamole/guacd 1.6 sigue haciendo el trabajo pesado de los protocolos.

Usuarios, grupos, carpetas de conexiones, permisos heredados, historial, grabación, auditoría, inicio de sesión externo y MFA vienen listos, sin extensiones separadas. La interfaz tiene 16 temas y está en inglés y portugués de Brasil.

Painel do GoGuacamole em português com conexões SSH, RDP e VNC, atividade recente e acessos aos servidores

Arquitectura: quién habla con quién

El navegador solo se comunica con goguacamole, mediante HTTPS y WebSocket. goguacamole habla el protocolo Guacamole con guacd, que permanece en una red privada, y guacd abre la conexión RDP, SSH o VNC con el servidor de destino. El host, el usuario y la contraseña de la conexión permanecen en el servidor, cifrados con AES-256-GCM; el navegador nunca los ve.

Diagrama da arquitetura do GoGuacamole: navegador, nginx, goguacamole, guacd e servidores RDP, SSH e VNC, com identidade, banco, Redis, gravações e object storage

La apertura de una sesión sigue este camino:

  1. El navegador solicita POST /api/v1/sessions con el ID de la conexión. El servidor comprueba la sesión, las restricciones del usuario (validez de la cuenta, ventana de acceso) y el permiso efectivo de lectura.
  2. La respuesta es un ticket de un solo uso, válido durante 30 segundos, que ya reserva la plaza en caso de que la conexión tenga un límite de sesiones.
  3. El navegador abre el WebSocket /api/v1/tunnel?ticket=… enviando solo parámetros de pantalla (anchura, altura, DPI, zona horaria). El servidor valida el origen, recupera el ticket y vuelve a autorizar.
  4. goguacamole realiza el handshake con guacd usando los parámetros guardados, activa la entrada del historial (creada como pendiente cuando se emitió el ticket) y pasa a reenviar las instrucciones en ambos sentidos.

Si se revoca el permiso a mitad del proceso, el túnel se cierra al instante. Los cambios realizados por otro proceso (la CLI o el importador) se detectan en un plazo de hasta 5 segundos.

Levantándolo en un minuto: la imagen all-in-one

Desde la versión 0.6.0, cada versión publica en GHCR una imagen que reúne guacd, goguacamole y Redis en un contenedor, con SQLite en el volumen /data:

docker run -d --name goguacamole -p 8080:8080 \
  -v goguacamole-data:/data ghcr.io/jniltinho/go-guacamole:allinone

# a senha gerada do admin aparece uma única vez no log
docker logs goguacamole 2>&1 | grep 'bootstrap administrator'

Para fijar la versión, usa la etiqueta 0.6.2-allinone (o 0.6-allinone). Abra http://localhost:8080, entre como admin con la contraseña del log y el sistema pide una contraseña nueva en el primer acceso. La disponibilidad se puede comprobar mediante la API:

curl -s http://localhost:8080/ready
# {"db":"up","guacd":"up","queue":"up"}

Dentro del contenedor, supervisord mantiene los tres procesos en ejecución como el usuario guacd (uid 1000), propietario de los archivos en /data. Por eso, los comandos de la CLI se ejecutan como -u guacd:

docker exec goguacamole supervisorctl status            # guacd, redis, goguacamole
docker exec -u guacd goguacamole goguacamole user passwd admin
docker exec goguacamole goguacamole version

Este modo HTTP es para evaluación local. Para producción, consulta la lista de verificación más abajo.

Probándolo con un servidor SSH de laboratorio

Para ver una sesión real sin tocar ningún servidor, coloca GoGuacamole y un OpenSSH desechable en la misma red Docker. El guacd de la imagen all-in-one resuelve el destino por nombre de contenedor. Si el contenedor de la sección anterior sigue en ejecución, elimínalo antes (docker rm -f goguacamole), porque el laboratorio utiliza el mismo nombre y el mismo puerto:

docker network create guac-lab
docker run -d --name goguacamole --network guac-lab -p 8080:8080 \
  -v goguacamole-data:/data ghcr.io/jniltinho/go-guacamole:allinone
docker run -d --name ssh-alvo --network guac-lab \
  -e USER_NAME=demo -e USER_PASSWORD='troque-esta-senha' -e PASSWORD_ACCESS=true \
  lscr.io/linuxserver/openssh-server:latest

En la interfaz, en Conexiones → Nueva conexión, elige SSH, host ssh-alvo, puerta 2222 (la puerta de esta imagen), usuario demo y la contraseña. Antes de guardar, usa Probar conexión: GoGuacamole abre una conexión real mediante guacd con los valores del formulario e indica si el host respondió y si el inicio de sesión funcionó. En mi prueba, la contraseña correcta volvió como éxito en unos 200 ms, y la incorrecta volvió como fallo de autenticación, sin guardar nada. Después solo hay que guardar y hacer clic en Conectar: la terminal se abre en una pestaña del navegador.

Al terminar, elimina el laboratorio:

docker rm -f goguacamole ssh-alvo
docker volume rm goguacamole-data
docker network rm guac-lab

Docker Compose con servicios separados

Para un entorno duradero, el repositorio incluye un docker-compose.yml con goguacamole, guacd y Redis en contenedores separados, y perfiles para MariaDB, PostgreSQL y servidores de prueba:

git clone https://github.com/jniltinho/go-guacamole.git
cd go-guacamole
cp .env.example .env
# no .env, defina APP_ADMIN_PASSWORD (8+ caracteres); a chave pode ser gerada assim:
printf 'APP_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
make up            # app + guacd + Redis, SQLite → http://localhost:8080
Comando Qué se levanta
make up app, guacd y Redis con SQLite
make up-mariadb lo mismo, más MariaDB 11.4
make up-postgres lo mismo, más PostgreSQL 17
make up-lab objetivos de prueba SSH, VNC y RDP
make certs up-tls HTTPS nativo en https://localhost:8443 con una CA local
make down tira todo

A APP_SECRET_KEY deriva la clave que cifra las contraseñas de las conexiones. Guárdala junto con la copia de seguridad de la base de datos: sin ella, las credenciales almacenadas no se pueden recuperar. Si no defines la clave, GoGuacamole genera una secret.key en el primer inicio, y ese archivo pasa a ser igualmente crítico.

También se puede ejecutar solo el binario (Linux/amd64, en las releases) apuntando a un guacd:

docker run -d -p 127.0.0.1:4822:4822 guacamole/guacd:1.6.0
APP_ADMIN_PASSWORD='troque-agora' GUACD_ADDR=127.0.0.1:4822 ./goguacamole serve

Detrás de nginx con HTTPS

El túnel de sesión es un WebSocket que puede permanecer abierto durante horas, por lo que el proxy necesita HTTP/1.1, las cabeceras de upgrade y tiempos de espera largos. El repositorio incluye un ejemplo completo en docker/nginx/goguacamole.conf; lo esencial es esto:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

server {
    listen 443 ssl;
    http2 on;
    server_name guac.example.com;
    # ssl_certificate / ssl_certificate_key ...

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
    }

    # o túnel transmite a tela: sem buffer e com timeouts longos
    location = /api/v1/tunnel {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_buffering    off;
        proxy_read_timeout 12h;
        proxy_send_timeout 12h;
    }
}

En el lado de GoGuacamole, tres variables completan la pareja:

APP_BASE_URL=https://guac.example.com   # o host aceito como Origin do WebSocket
APP_COOKIE_SECURE=true                  # TLS termina no proxy
APP_TRUSTED_PROXIES=127.0.0.1           # só esse IP pode informar X-Forwarded-For

Con APP_TRUSTED_PROXIES claro, el registro, la auditoría y el historial registran la IP del cliente, no la de nginx, y un X-Forwarded-For forjado procedente de otro lugar se ignora. Si prefieres el Caddy, el reverse_proxy de este ya gestiona WebSocket sin configuración adicional.

Grabación de sesiones y auditoría

Aquí está buena parte del motivo del proyecto. guacd guarda la sesión en un volumen compartido y GoGuacamole se encarga del resto:

  • Vídeo de la pantalla de RDP, SSH y VNC, reproducido en el propio navegador.
  • Transcripción de SSH en texto, con cada comando y su salida, para leer o buscar.
  • Teclas pulsadas, opcional y desactivada por defecto.
  • Política por conexión: vídeo, transcripción y teclas pueden heredar el valor predeterminado, forzarse o desactivarse; el vídeo y la transcripción tienen sus propias retenciones (las teclas forman parte del vídeo).
  • Fail-closed: si la grabación requerida no está disponible, la sesión no se abre (hay excepciones temporales, auditadas).
  • Retención y retención legal: la limpieza sigue el historial, y una grabación bajo retención legal no se elimina.

Player de gravação do GoGuacamole reproduzindo uma sessão SSH no navegador

Los valores predeterminados iniciales provienen de variables de entorno (después de que un administrador guarda en Administración → Grabación, prevalece lo que está en la base de datos):

RECORDING_ENABLED=true          # padrão false; true no compose
RECORDING_SCREEN=true           # vídeo de todos os protocolos
RECORDING_TYPESCRIPT=true       # transcrição de SSH
RECORDING_INCLUDE_KEYS=false    # teclas no vídeo
RECORDING_RETENTION_DAYS=90

Las grabaciones también pueden enviarse a object storage: AWS S3, compatibles con S3 (MinIO, Ceph, R2, Wasabi), Oracle Cloud y Google Cloud Storage, mediante rclone. Cada envío se verifica mediante SHA-256, releyendo el objeto; la copia local se puede eliminar y el reproductor reproduce directamente desde el bucket. El botón Probar acceso graba, lee y elimina un objeto de prueba e informa si el bucket tiene versionado y object lock. Estas tareas se ejecutan en una cola de Redis con asynq, y el panel asynqmon está en /asynqmon, solo para administradores.

Por seguridad, la conexión con el almacenamiento de objetos pasa por un proxy interno que rechaza direcciones privadas, loopback, link-local y de metadatos de nube. Un MinIO de laboratorio en la red local debe habilitarse de forma explícita:

OBJSTORE_ALLOWED_ENDPOINTS=minio
OBJSTORE_ALLOW_HTTP=true

La auditoría registra los inicios de sesión, las sesiones y los cambios administrativos realizados desde la interfaz y la API. El panel también incluye un informe de acceso a los servidores: quién accedió a cada servidor, cuándo y durante cuánto tiempo.

Inicio de sesión: LDAP, OIDC, SAML y MFA

En el cliente Java, cada método de inicio de sesión es una extensión con su propio archivo. En GoGuacamole, todo se encuentra en Administración → Autenticación, con los secretos guardados cifrados en la base de datos y un botón de prueba para cada método:

  • LDAP / Active Directory, con los grupos del directorio asignados a grupos locales por nombre. Si administras AD o Samba 4, el AD Manager hazlo por ti.
  • OpenID Connect (authorization code con PKCE) y SAML 2.0.
  • Cabecera HTTP procedente de proxies de confianza y JSON cifrado compatible con el guacamole-auth-json.
  • TOTP con código QR y protección contra la reutilización de códigos, y Duo Universal Prompt, como segundo factor de los inicios de sesión con contraseña.

Los usuarios externos se crean en el primer inicio de sesión. Un detalle importante: el segundo factor de GoGuacamole es válido para el inicio de sesión con contraseña. En el inicio de sesión con SSO, exige MFA en tu proveedor de identidad.

Migrando desde Apache Guacamole

El importador lee la base de datos de Guacamole (MySQL o PostgreSQL) en una transacción de solo lectura, únicamente con SELECT. Empiece siempre con --dry-run:

# não grava nada importado; a origem nunca é alterada
goguacamole import guacamole --source-driver mysql \
  --dsn 'guacamole_user:senha@tcp(db-antigo:3306)/guacamole_db' --dry-run

# importação de verdade (reexecutar é idempotente; conflitos são ignorados por padrão)
goguacamole import guacamole --source-driver mysql \
  --dsn 'guacamole_user:senha@tcp(db-antigo:3306)/guacamole_db'

Un detalle: incluso en --dry-run, el comando abre la base de datos de destino de GoGuacamole y aplica las migraciones pendientes del esquema, como hace el serve. Los datos importados no se escriben.

Se importan los usuarios (con el hash y el salt, por lo que las contraseñas siguen funcionando y se migran a bcrypt en el próximo inicio de sesión), la validez y la ventana de acceso, los grupos y sus miembros, las carpetas, las conexiones con sus parámetros (secretos recifrados), los límites, los permisos del sistema que tienen equivalente (ADMINISTER, CREATE_CONNECTION, CREATE_USER e AUDIT) y los permisos de lectura de las conexiones. Los grupos anidados se aplanan. Quedan fuera, y aparecen en el informe, los perfiles de uso compartido y los permisos que no sean de lectura. Los grupos de balanceo se convierten en carpetas normales.

Al final, el importador compara, usuario por usuario, las conexiones y los privilegios que cada uno tenía en el origen y tiene en el destino, e imprime líneas ACCESS DIFF. Esas diferencias se informan, no se bloquean: por ejemplo, si ya existe en el destino una cuenta con el mismo nombre y con ADMINISTER, el informe muestra la ganancia de privilegios. La flag --accept-expansion solo decide si la lectura de los grupos de balanceo se convierte en una lectura en cada conexión secundaria. La opción sensata es ejecutar el dry-run en una copia de la base de datos de producción, revisar cada ACCESS DIFF y hacer una prueba piloto con algunos usuarios junto al cliente Java, en otro puerto, antes de cambiar el proxy.

La CLI

goguacamole serve                       # servidor HTTP(S)
goguacamole migrate up|down|status      # migrações do banco
goguacamole user create|passwd|list|disable
goguacamole import guacamole            # importação do Apache Guacamole
goguacamole config print                # configuração efetiva, segredos ocultos
goguacamole healthcheck                 # usado pelo HEALTHCHECK do Docker
goguacamole version

La configuración procede de config.toml (hay una config.toml.example en el repositorio) y de variables de entorno con prefijos como APP_, DB_, GUACD_, RECORDING_, REDIS_ e OBJSTORE_.

Cliente Java x GoGuacamole

Apache guacamole-client GoGuacamole
Ejecución Java, Tomcat y extensiones un binario Go con la interfaz integrada
Base de datos MySQL, PostgreSQL o SQL Server mediante una extensión JDBC SQLite, MariaDB/MySQL o PostgreSQL integrados
SSO y MFA extensiones separadas, configuradas en archivos integradas, configuradas y probadas desde la interfaz
Grabación grabación en disco; reproductor mediante extensión reproductor integrado, transcripciones SSH, retención, retención legal, almacenamiento de objetos
Actualización volver a desplegar el WAR y las extensiones cambiar el binario o la imagen; las migraciones se ejecutan al iniciar
guacd guacamole/guacd lo mismo guacamole/guacd (1.6)

Lista de comprobación para producción

  • APP_ENV=production: el servicio solo se inicia con TLS_ENABLED=true o APP_COOKIE_SECURE=true, con una clave definida (APP_SECRET_KEY o APP_SECRET_KEY_FILE), y rechaza TLS_SELF_SIGNED=true e GUACD_TLS_INSECURE=true. No comprueba si el proxy realmente tiene TLS: eso queda en tus manos.
  • APP_SECRET_KEY en un almacén de secretos, con copia de seguridad junto a la base de datos. Prueba la restauración en un host limpio.
  • HTTPS: TLS_ENABLED=true en el propio binario, o TLS terminado en el proxy con APP_COOKIE_SECURE=true e APP_TRUSTED_PROXIES, manteniendo el tramo proxy → goguacamole en 127.0.0.1 o en una red privada.
  • APP_BASE_URL con la URL pública.
  • guacd en una red aislada, nunca expuesto; considera GUACD_TLS=true.
  • MFA (TOTP o Duo) para los inicios de sesión con contraseña; para SSO, MFA en el proveedor.
  • Redis en red privada (o con REDIS_PASSWORD e REDIS_TLS), una base de datos por instalación.
  • Almacenamiento de objetos con un bucket o prefijo dedicado y credenciales limitadas a put/get/list/delete en él.

Límites actuales

El proyecto es nuevo, está en la serie 0.x, y conviene saber lo que aún no tiene:

  • Una réplica para los túneles: el registro de túneles, los límites y la finalización de sesión se mantienen en la memoria del proceso. Escala verticalmente. Las tareas en segundo plano y las subidas ya son seguras con varias instancias.
  • Sin compartición de sesión (entrar en una sesión activa de otro usuario) ni grupos de balanceo.
  • Grupos de usuarios planos: los grupos anidados aún no existen.
  • Binarios e imagen para Linux/amd64; otras arquitecturas requieren compilar.
  • Telnet y Kubernetes, que admite guacd, aún no están en los formularios.

O ROADMAP enumera estos elementos y otros, como métricas Prometheus, paquetes .deb/.rpm y Helm chart.

Conclusión

guacd sigue siendo la pieza adecuada para hablar de RDP, SSH y VNC. Lo que cambia GoGuacamole es lo demás: un binario, un contenedor para empezar, inicio de sesión externo y MFA sin extensiones, grabación con reproductor y transcripción, y un importador que trae la base de datos de Guacamole e informa de cada diferencia de acceso. Si ya usas Guacamole, ejecuta el importador con --dry-run en una copia de la base de datos y compara. Si estás empezando, el docker run de la imagen todo en uno basta para probar.

Las incidencias y las solicitudes de extracción son bienvenidas en el repositorio de GoGuacamole. Otros proyectos en Go aquí del blog siguen la misma idea de un único binario: go-postfixadmin, go-imapsync e mcp-wp-go. Y, para el acceso SSH directo, sin gateway, consulta claves de autenticación en SSH.